jzou19950715 commited on
Commit
ca322ba
·
verified ·
1 Parent(s): 78841ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -428
app.py CHANGED
@@ -1,18 +1,26 @@
1
  import os
2
  import sys
3
  import logging
4
- from pathlib import Path
5
  import json
6
- from datetime import datetime
7
- from typing import List, Dict, Any, Optional, Tuple, Union
8
  import traceback
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- # Configure detailed logging with file output
11
  LOG_DIR = "logs"
12
  os.makedirs(LOG_DIR, exist_ok=True)
13
  log_file = os.path.join(LOG_DIR, f"rag_system_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
14
 
15
- # Set up root logger with both file and console handlers
16
  logging.basicConfig(
17
  level=logging.INFO,
18
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -24,46 +32,11 @@ logging.basicConfig(
24
  logger = logging.getLogger("rag_system")
25
  logger.info(f"Starting RAG system. Log file: {log_file}")
26
 
27
- # Importing necessary libraries with error handling
28
- try:
29
- import torch
30
- import numpy as np
31
- from sentence_transformers import SentenceTransformer
32
- import chromadb
33
- from chromadb.utils import embedding_functions
34
- import gradio as gr
35
- from openai import OpenAI
36
- import google.generativeai as genai
37
- logger.info("All required libraries successfully imported")
38
- except ImportError as e:
39
- logger.critical(f"Failed to import required libraries: {e}")
40
- print(f"ERROR: Missing required libraries. Please install with: pip install -r requirements.txt")
41
- print(f"Specific error: {e}")
42
- sys.exit(1)
43
-
44
- # Version info for tracking
45
- VERSION = "1.0.0"
46
- logger.info(f"RAG System Version: {VERSION}")
47
-
48
  class Config:
49
  """
50
  Configuration for vector store and RAG system.
51
-
52
- This class centralizes all configuration parameters for the application,
53
- making it easier to modify settings and ensure consistency.
54
-
55
- Attributes:
56
- local_dir (str): Directory for ChromaDB persistence
57
- embedding_model (str): Name of the embedding model to use
58
- collection_name (str): Name of the ChromaDB collection
59
- default_top_k (int): Default number of results to return
60
- openai_model (str): Default OpenAI model to use
61
- gemini_model (str): Default Gemini model to use
62
- temperature (float): Temperature setting for LLM generation
63
- max_tokens (int): Maximum tokens for LLM response
64
- system_name (str): Name of the system for UI
65
  """
66
-
67
  def __init__(self,
68
  local_dir: str = "./chroma_db",
69
  embedding_model: str = "all-MiniLM-L6-v2",
@@ -84,18 +57,14 @@ class Config:
84
  self.max_tokens = max_tokens
85
  self.system_name = system_name
86
 
87
- # Create local directory if it doesn't exist
88
  os.makedirs(local_dir, exist_ok=True)
89
-
90
  logger.info(f"Initialized configuration: {self.__dict__}")
91
 
92
  def to_dict(self) -> Dict[str, Any]:
93
- """Convert configuration to dictionary for serialization"""
94
  return self.__dict__
95
 
96
  @classmethod
97
  def from_file(cls, config_path: str) -> 'Config':
98
- """Load configuration from JSON file"""
99
  try:
100
  with open(config_path, 'r') as f:
101
  config_dict = json.load(f)
@@ -107,7 +76,6 @@ class Config:
107
  return cls()
108
 
109
  def save_to_file(self, config_path: str) -> bool:
110
- """Save configuration to JSON file"""
111
  try:
112
  with open(config_path, 'w') as f:
113
  json.dump(self.to_dict(), f, indent=2)
@@ -117,59 +85,33 @@ class Config:
117
  logger.error(f"Failed to save configuration to {config_path}: {e}")
118
  return False
119
 
 
120
  class EmbeddingEngine:
121
  """
122
- Handle embeddings with a lightweight model.
123
-
124
- This class manages the embedding model used to convert text to vector
125
- representations for semantic search.
126
-
127
- Attributes:
128
- model (SentenceTransformer): The loaded embedding model
129
- model_name (str): Name of the successfully loaded model
130
- vector_size (int): Dimension of the embedding vectors
131
- device (str): Device used for inference ('cuda' or 'cpu')
132
  """
133
-
134
  def __init__(self, model_name="all-MiniLM-L6-v2"):
135
- """
136
- Initialize the embedding engine with the specified model.
137
-
138
- Args:
139
- model_name (str): Name of the embedding model to load
140
-
141
- Raises:
142
- SystemExit: If no embedding model could be loaded
143
- """
144
- # Use GPU if available
145
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
146
  logger.info(f"Using device for embeddings: {self.device}")
147
 
148
- # Try multiple model options in order of preference
149
  model_options = [
150
  model_name,
151
- "all-MiniLM-L6-v2", # Good balance of speed and quality
152
- "paraphrase-MiniLM-L3-v2", # Faster but less accurate
153
- "all-mpnet-base-v2" # Higher quality but larger model
154
  ]
155
-
156
  self.model = None
157
 
158
- # Try each model in order until one works
159
  for model_option in model_options:
160
  try:
161
  logger.info(f"Attempting to load embedding model: {model_option}")
162
  self.model = SentenceTransformer(model_option)
163
-
164
- # Move model to device
165
  self.model.to(self.device)
166
-
167
  logger.info(f"Successfully loaded embedding model: {model_option}")
168
  self.model_name = model_option
169
  self.vector_size = self.model.get_sentence_embedding_dimension()
170
  logger.info(f"Embedding vector size: {self.vector_size}")
171
  break
172
-
173
  except Exception as e:
174
  logger.warning(f"Failed to load embedding model {model_option}: {str(e)}")
175
 
@@ -179,22 +121,8 @@ class EmbeddingEngine:
179
  raise SystemExit(error_msg)
180
 
181
  def embed(self, texts: List[str]) -> np.ndarray:
182
- """
183
- Generate embeddings for a list of texts.
184
-
185
- Args:
186
- texts (List[str]): List of texts to embed
187
-
188
- Returns:
189
- np.ndarray: Array of embeddings
190
-
191
- Raises:
192
- ValueError: If the input is invalid
193
- RuntimeError: If embedding fails
194
- """
195
  if not texts:
196
  raise ValueError("Cannot embed empty list of texts")
197
-
198
  try:
199
  embeddings = self.model.encode(texts, convert_to_numpy=True)
200
  return embeddings
@@ -202,33 +130,13 @@ class EmbeddingEngine:
202
  logger.error(f"Error generating embeddings: {e}")
203
  raise RuntimeError(f"Failed to generate embeddings: {e}")
204
 
 
205
  class VectorStoreManager:
206
  """
207
- Manage Chroma vector store operations - upload, query, etc.
208
-
209
- This class provides an interface to the ChromaDB vector database,
210
- handling document storage, retrieval, and management.
211
-
212
- Attributes:
213
- config (Config): Configuration parameters
214
- client (chromadb.PersistentClient): ChromaDB client
215
- collection (chromadb.Collection): The active ChromaDB collection
216
- embedding_engine (EmbeddingEngine): Engine for generating embeddings
217
  """
218
-
219
  def __init__(self, config: Config):
220
- """
221
- Initialize the vector store manager.
222
-
223
- Args:
224
- config (Config): Configuration parameters
225
-
226
- Raises:
227
- SystemExit: If the vector store cannot be initialized
228
- """
229
  self.config = config
230
-
231
- # Initialize Chroma client (local persistence)
232
  logger.info(f"Initializing Chroma at {config.local_dir}")
233
  try:
234
  self.client = chromadb.PersistentClient(path=config.local_dir)
@@ -238,19 +146,15 @@ class VectorStoreManager:
238
  logger.critical(error_msg)
239
  raise SystemExit(error_msg)
240
 
241
- # Get or create collection
242
  try:
243
- # Initialize embedding model
244
  logger.info("Loading embedding model...")
245
  self.embedding_engine = EmbeddingEngine(config.embedding_model)
246
  logger.info(f"Using embedding model: {self.embedding_engine.model_name}")
247
 
248
- # Create embedding function
249
  sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
250
  model_name=self.embedding_engine.model_name
251
  )
252
 
253
- # Try to get existing collection or create a new one
254
  try:
255
  self.collection = self.client.get_collection(
256
  name=config.collection_name,
@@ -259,18 +163,15 @@ class VectorStoreManager:
259
  logger.info(f"Using existing collection: {config.collection_name}")
260
  except Exception as e:
261
  logger.warning(f"Error getting collection: {e}")
262
- # Attempt to get a list of available collections
263
  collections = self.client.list_collections()
264
  if collections:
265
  logger.info(f"Available collections: {[c.name for c in collections]}")
266
- # Use the first available collection if any
267
  self.collection = self.client.get_collection(
268
  name=collections[0].name,
269
  embedding_function=sentence_transformer_ef
270
  )
271
  logger.info(f"Using collection: {collections[0].name}")
272
  else:
273
- # Create new collection if none exist
274
  self.collection = self.client.create_collection(
275
  name=config.collection_name,
276
  embedding_function=sentence_transformer_ef,
@@ -284,76 +185,42 @@ class VectorStoreManager:
284
  raise SystemExit(error_msg)
285
 
286
  def query(self, query_text: str, n_results: int = 5) -> List[Dict]:
287
- """
288
- Query the vector store with a text query.
289
-
290
- Args:
291
- query_text (str): The query text
292
- n_results (int): Number of results to return
293
-
294
- Returns:
295
- List[Dict]: List of results with document text, metadata, and similarity score
296
- """
297
  if not query_text.strip():
298
  logger.warning("Empty query received")
299
  return []
300
-
301
  try:
302
  logger.info(f"Querying vector store with: '{query_text[:50]}...' (top {n_results})")
303
-
304
- # Query the collection
305
  search_results = self.collection.query(
306
  query_texts=[query_text],
307
  n_results=n_results,
308
- include=["documents", "metadatas", "distances", "embeddings"]
309
  )
310
-
311
- # Format results
312
  results = []
313
  if search_results["documents"] and len(search_results["documents"][0]) > 0:
314
  for i in range(len(search_results["documents"][0])):
315
  results.append({
316
  'document': search_results["documents"][0][i],
317
  'metadata': search_results["metadatas"][0][i] if search_results["metadatas"] else {},
318
- 'score': 1.0 - search_results["distances"][0][i], # Convert distance to similarity
319
  'distance': search_results["distances"][0][i]
320
  })
321
-
322
  logger.info(f"Found {len(results)} results for query")
323
  else:
324
  logger.info("No results found for query")
325
-
326
  return results
327
  except Exception as e:
328
  logger.error(f"Error querying collection: {e}")
329
  logger.debug(traceback.format_exc())
330
  return []
331
 
332
- def add_document(self,
333
- document: str,
334
- doc_id: str,
335
- metadata: Dict[str, Any]) -> bool:
336
- """
337
- Add a document to the vector store.
338
-
339
- Args:
340
- document (str): The document text
341
- doc_id (str): Unique identifier for the document
342
- metadata (Dict[str, Any]): Metadata about the document
343
-
344
- Returns:
345
- bool: True if successful, False otherwise
346
- """
347
  try:
348
  logger.info(f"Adding document '{doc_id}' to vector store")
349
-
350
- # Add the document to the collection
351
  self.collection.add(
352
  documents=[document],
353
  ids=[doc_id],
354
  metadatas=[metadata]
355
  )
356
-
357
  logger.info(f"Successfully added document '{doc_id}'")
358
  return True
359
  except Exception as e:
@@ -361,15 +228,6 @@ class VectorStoreManager:
361
  return False
362
 
363
  def delete_document(self, doc_id: str) -> bool:
364
- """
365
- Delete a document from the vector store.
366
-
367
- Args:
368
- doc_id (str): ID of the document to delete
369
-
370
- Returns:
371
- bool: True if successful, False otherwise
372
- """
373
  try:
374
  logger.info(f"Deleting document '{doc_id}' from vector store")
375
  self.collection.delete(ids=[doc_id])
@@ -380,31 +238,19 @@ class VectorStoreManager:
380
  return False
381
 
382
  def get_statistics(self) -> Dict[str, Any]:
383
- """
384
- Get statistics about the vector store.
385
-
386
- Returns:
387
- Dict[str, Any]: Statistics about the vector store
388
- """
389
  stats = {
390
  'collection_name': self.config.collection_name,
391
  'embedding_model': self.embedding_engine.model_name,
392
  'embedding_dimensions': self.embedding_engine.vector_size,
393
  'device': self.embedding_engine.device
394
  }
395
-
396
  try:
397
- # Get collection count
398
  collection_count = self.collection.count()
399
  stats['total_documents'] = collection_count
400
-
401
- # Get unique metadata values
402
  if collection_count > 0:
403
  try:
404
- # Get a sample of document metadata
405
  sample_results = self.collection.get(limit=min(collection_count, 100))
406
  if sample_results and 'metadatas' in sample_results and sample_results['metadatas']:
407
- # Count unique files if filename exists in metadata
408
  filenames = set()
409
  for metadata in sample_results['metadatas']:
410
  if 'filename' in metadata:
@@ -412,57 +258,28 @@ class VectorStoreManager:
412
  stats['unique_files'] = len(filenames)
413
  except Exception as e:
414
  logger.warning(f"Error getting metadata statistics: {e}")
415
-
416
  logger.info(f"Vector store statistics: {stats}")
417
  except Exception as e:
418
  logger.error(f"Error getting statistics: {e}")
419
  stats['error'] = str(e)
420
-
421
  return stats
422
 
 
423
  class RAGSystem:
424
  """
425
- Retrieval-Augmented Generation with multiple LLM providers.
426
-
427
- This class handles the RAG workflow: retrieval of relevant documents,
428
- formatting context, and generating responses with different LLM providers.
429
-
430
- Attributes:
431
- vector_store (VectorStoreManager): Manager for vector store operations
432
- openai_client (Optional[OpenAI]): OpenAI client
433
- gemini_configured (bool): Whether Gemini API is configured
434
- config (Config): Configuration parameters
435
  """
436
-
437
  def __init__(self, vector_store: VectorStoreManager, config: Config):
438
- """
439
- Initialize the RAG system.
440
-
441
- Args:
442
- vector_store (VectorStoreManager): Vector store manager
443
- config (Config): Configuration parameters
444
- """
445
  self.vector_store = vector_store
446
  self.config = config
447
  self.openai_client = None
448
  self.gemini_configured = False
449
-
450
  logger.info("Initialized RAG system")
451
 
452
  def setup_openai(self, api_key: str) -> bool:
453
- """
454
- Set up OpenAI client with API key.
455
-
456
- Args:
457
- api_key (str): OpenAI API key
458
-
459
- Returns:
460
- bool: True if successful, False otherwise
461
- """
462
  if not api_key.strip():
463
  logger.warning("Empty OpenAI API key provided")
464
  return False
465
-
466
  try:
467
  logger.info("Setting up OpenAI client")
468
  self.openai_client = OpenAI(api_key=api_key)
@@ -483,27 +300,14 @@ class RAGSystem:
483
  return False
484
 
485
  def setup_gemini(self, api_key: str) -> bool:
486
- """
487
- Set up Gemini with API key.
488
-
489
- Args:
490
- api_key (str): Google AI API key
491
-
492
- Returns:
493
- bool: True if successful, False otherwise
494
- """
495
  if not api_key.strip():
496
  logger.warning("Empty Gemini API key provided")
497
  return False
498
-
499
  try:
500
  logger.info("Setting up Gemini client")
501
  genai.configure(api_key=api_key)
502
-
503
- # Test the API key with a simple request
504
  model = genai.GenerativeModel(self.config.gemini_model)
505
  response = model.generate_content("Test connection")
506
-
507
  self.gemini_configured = True
508
  logger.info("Gemini client configured successfully")
509
  return True
@@ -511,83 +315,44 @@ class RAGSystem:
511
  logger.error(f"Error configuring Gemini: {e}")
512
  self.gemini_configured = False
513
  return False
514
-
515
  def format_context(self, documents: List[Dict]) -> str:
516
- """
517
- Format retrieved documents into context for the LLM.
518
-
519
- Args:
520
- documents (List[Dict]): List of retrieved documents
521
-
522
- Returns:
523
- str: Formatted context for the LLM
524
- """
525
  if not documents:
526
  logger.warning("No documents provided for context formatting")
527
  return "No relevant documents found."
528
-
529
  logger.info(f"Formatting {len(documents)} documents for context")
530
  context_parts = []
531
-
532
  for i, doc in enumerate(documents):
533
  metadata = doc['metadata']
534
- # Extract document metadata in a robust way
535
  title = metadata.get('title', metadata.get('filename', 'Unknown document'))
536
  source = metadata.get('source', metadata.get('path', 'Unknown source'))
537
  date = metadata.get('date', metadata.get('created_at', 'Unknown date'))
538
-
539
- # Format header with metadata
540
  header = f"Document {i+1} - {title}"
541
  if source != 'Unknown source':
542
  header += f" (Source: {source})"
543
  if date != 'Unknown date':
544
  header += f" (Date: {date})"
545
-
546
- # For readability, limit length of context document
547
  doc_text = doc['document']
548
- if len(doc_text) > 8000: # Limit long documents in context
549
  doc_text = doc_text[:8000] + "... [Document truncated for context]"
550
-
551
  context_parts.append(f"{header}:\n{doc_text}\n")
552
-
553
  full_context = "\n".join(context_parts)
554
  logger.info(f"Created context with {len(full_context)} characters")
555
-
556
  return full_context
557
-
558
  def generate_response_openai(self, query: str, context: str) -> str:
559
- """
560
- Generate a response using OpenAI model with context.
561
-
562
- Args:
563
- query (str): User query
564
- context (str): Formatted document context
565
-
566
- Returns:
567
- str: Generated response
568
- """
569
  if not self.openai_client:
570
  logger.warning("OpenAI API key not configured for response generation")
571
- return "Error: OpenAI API key not configured. Please enter an API key in the API key field."
572
 
573
- system_prompt = """
574
- You are a helpful, detailed, and accurate assistant that answers questions based on the context provided.
575
- Follow these guidelines:
576
-
577
- 1. Use ONLY the information from the context to answer the user's question.
578
- 2. If the context doesn't contain the information needed, say so clearly and do your best to deduce and infer the answer.
579
- 3. Always cite the specific documents from the context that you used in your answer by referencing their number (e.g., "According to Document 1...").
580
- 4. Organize your response in a clear, structured format with headings where appropriate.
581
- 5. Use the best practices of writings.
582
- 6. If the information in different documents conflicts, acknowledge this and explain the different perspectives.
583
- 7. Be specific and detailed in your answers, focusing on accuracy over brevity.
584
- 8. Aim to be educational and informative in your tone.
585
- 9. You aim to write between 300-500 words of comprehensive answer to user question.
586
- """
587
 
588
  try:
589
- logger.info(f"Generating response with OpenAI ({self.config.openai_model})")
590
-
591
  start_time = datetime.now()
592
  response = self.openai_client.chat.completions.create(
593
  model=self.config.openai_model,
@@ -598,10 +363,8 @@ class RAGSystem:
598
  temperature=self.config.temperature,
599
  max_tokens=self.config.max_tokens,
600
  )
601
-
602
  generation_time = (datetime.now() - start_time).total_seconds()
603
  response_text = response.choices[0].message.content
604
-
605
  logger.info(f"Generated response with OpenAI in {generation_time:.2f} seconds")
606
  return response_text
607
  except Exception as e:
@@ -610,63 +373,30 @@ class RAGSystem:
610
  return f"Error: {error_msg}"
611
 
612
  def generate_response_gemini(self, query: str, context: str) -> str:
613
- """
614
- Generate a response using Gemini with context.
615
-
616
- Args:
617
- query (str): User query
618
- context (str): Formatted document context
619
-
620
- Returns:
621
- str: Generated response
622
- """
623
  if not self.gemini_configured:
624
  logger.warning("Gemini API key not configured for response generation")
625
- return "Error: Google AI API key not configured. Please enter an API key in the API key field."
626
 
627
- prompt = f"""
628
- You are a highly supportive and insightful assistant dedicated to providing clear, helpful, and well-structured answers based on the given context. Your goal is to ensure the user receives a thorough, encouraging, and informative response that directly addresses their question.
629
-
630
- **Guidelines for Your Response:**
631
- - Use ONLY the information from the **context** to form a detailed and well-reasoned answer.
632
- - If the context lacks sufficient information, state it clearly while offering general insights or related knowledge.
633
- - Cite specific sections from the context by referring to document numbers (e.g., "According to Document 1...").
634
- - Maintain a **friendly, professional, and supportive** tone that encourages user engagement.
635
- - Aim for **clarity and depth**, breaking down complex ideas into easy-to-understand explanations.
636
- - Organize your response with headings and sections if appropriate.
637
- - Do not make up information or use knowledge outside of the provided context.
638
- - If information in different documents conflicts, explain the different perspectives.
639
-
640
- **Context:**
641
- {context}
642
 
643
- **User's Question:**
644
- {query}
645
-
646
- **Your Response:**
647
- """
648
-
649
  try:
650
- logger.info(f"Generating response with Gemini ({self.config.gemini_model})")
651
-
652
  start_time = datetime.now()
653
  model = genai.GenerativeModel(self.config.gemini_model)
654
-
655
  generation_config = {
656
  "temperature": self.config.temperature,
657
  "max_output_tokens": self.config.max_tokens,
658
  "top_p": 0.9,
659
  "top_k": 40
660
  }
661
-
662
- response = model.generate_content(
663
- prompt,
664
- generation_config=generation_config
665
- )
666
-
667
  generation_time = (datetime.now() - start_time).total_seconds()
668
  response_text = response.text
669
-
670
  logger.info(f"Generated response with Gemini in {generation_time:.2f} seconds")
671
  return response_text
672
  except Exception as e:
@@ -674,50 +404,29 @@ class RAGSystem:
674
  logger.error(error_msg)
675
  return f"Error: {error_msg}"
676
 
677
- def query_and_generate(self,
678
- query: str,
679
- n_results: int = 5,
680
- model: str = "openai") -> Tuple[str, str]:
681
- """
682
- Retrieve relevant documents and generate a response using the specified model.
683
-
684
- Args:
685
- query (str): User query
686
- n_results (int): Number of documents to retrieve
687
- model (str): Model provider to use ('openai' or 'gemini')
688
-
689
- Returns:
690
- Tuple[str, str]: (Generated response, Search results)
691
- """
692
  if not query.strip():
693
  logger.warning("Empty query received")
694
  return "Please enter a question to get a response.", "No search performed."
695
 
696
- logger.info(f"Processing query: '{query[:50]}...' with {model} model")
697
-
698
- # Query vector store
699
  documents = self.vector_store.query(query, n_results=n_results)
700
 
701
- # Format search results
702
  formatted_results = []
703
  for i, res in enumerate(documents):
704
  metadata = res['metadata']
705
  title = metadata.get('title', metadata.get('filename', 'Unknown'))
706
- preview = res['document'][:500] + '...' if len(res['document']) > 500 else res['document']
707
- formatted_results.append(f"**Result {i+1}** (Similarity: {res['score']:.2f})\n"
708
- f"**Source:** {title}\n"
709
- f"**Preview:**\n{preview}\n\n---\n")
710
-
711
  search_output_text = "\n".join(formatted_results) if formatted_results else "No results found."
712
 
713
  if not documents:
714
  logger.warning("No relevant documents found")
715
  return "No relevant documents found to answer your question.", search_output_text
716
 
717
- # Format context
718
  context = self.format_context(documents)
719
 
720
- # Generate response with the appropriate model
721
  if model == "openai":
722
  response = self.generate_response_openai(query, context)
723
  elif model == "gemini":
@@ -729,114 +438,145 @@ class RAGSystem:
729
 
730
  return response, search_output_text
731
 
 
732
  def get_db_stats(vector_store: VectorStoreManager) -> str:
733
- """
734
- Function to get vector store statistics.
735
-
736
- Args:
737
- vector_store (VectorStoreManager): Vector store manager
738
-
739
- Returns:
740
- str: Formatted statistics string
741
- """
742
  try:
743
  stats = vector_store.get_statistics()
744
  total_docs = stats.get('total_documents', 0)
745
  unique_files = stats.get('unique_files', 'Unknown')
746
  model = stats.get('embedding_model', 'Unknown')
747
  device = stats.get('device', 'Unknown')
748
-
749
- stats_text = [
750
- f"Total documents: {total_docs}",
751
- f"Unique files: {unique_files}",
752
- f"Embedding model: {model}",
753
  f"Device: {device}"
754
- ]
755
-
756
- return "\n".join(stats_text)
757
  except Exception as e:
758
  logger.error(f"Error getting statistics: {e}")
759
  return "Error getting database statistics"
760
 
 
761
  def main():
762
- """Main function to run the RAG application"""
763
- print(f"Starting {CONFIG_FILE_PATH}Document RAG System v{VERSION}")
764
- print(f"Log file: {log_file}")
765
-
766
- # Path for configuration file
767
  CONFIG_FILE_PATH = "rag_config.json"
 
 
768
 
769
- # Try to load configuration from file, or use defaults
770
  if os.path.exists(CONFIG_FILE_PATH):
771
  config = Config.from_file(CONFIG_FILE_PATH)
772
  else:
773
- config = Config(
774
- local_dir="./chroma_db", # Store Chroma files in dedicated directory
775
- collection_name="markdown_docs"
776
- )
777
- # Save default configuration
778
  config.save_to_file(CONFIG_FILE_PATH)
779
 
780
  try:
781
- # Initialize vector store manager with existing collection
782
  vector_store = VectorStoreManager(config)
783
-
784
- # Initialize RAG system without API keys initially
785
  rag_system = RAGSystem(vector_store, config)
786
-
787
- # Create the Gradio interface
788
- with gr.Blocks(title=config.system_name) as app:
789
- gr.Markdown(f"# {config.system_name} v{VERSION}")
790
- gr.Markdown("Retrieve and generate answers from your documents using AI")
791
-
792
- with gr.Row():
793
- with gr.Column(scale=1):
794
- # API Keys and model selection
795
- with gr.Box():
796
- gr.Markdown("### LLM Configuration")
797
- model_choice = gr.Radio(
798
- choices=["openai", "gemini"],
799
- value="openai",
800
- label="Choose LLM Provider",
801
- info=f"Select which model to use ({config.openai_model} or {config.gemini_model})"
802
- )
803
-
804
- api_key_input = gr.Textbox(
805
- label="API Key",
806
- placeholder="Enter your API key here...",
807
- type="password",
808
- info="Your API key is not stored between sessions"
809
- )
810
-
811
- save_key_button = gr.Button("Save API Key", variant="primary")
812
- api_status = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
- # Search controls
815
- with gr.Box():
816
- gr.Markdown("### Search Settings")
817
- num_results = gr.Slider(
818
- minimum=1,
819
- maximum=20,
820
- value=15,
821
- step=1,
822
- label="Number of documents to retrieve",
823
- info="Higher values may provide more context but slower responses"
824
- )
825
-
826
- temperature_slider = gr.Slider(
827
- minimum=0.0,
828
- maximum=1.0,
829
- value=config.temperature,
830
- step=0.05,
831
- label="Temperature",
832
- info="Lower values = more factual, higher values = more creative"
833
- )
834
-
835
- max_tokens_slider = gr.Slider(
836
- minimum=100,
837
- maximum=4000,
838
- value=config.max_tokens,
839
- step=100,
840
- label="Max Output Tokens",
841
- info="Maximum length of generated response"
842
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import sys
3
  import logging
 
4
  import json
 
 
5
  import traceback
6
+ from datetime import datetime
7
+ from typing import List, Dict, Any, Optional, Tuple
8
+
9
+ # Third-party libraries
10
+ import torch
11
+ import numpy as np
12
+ from sentence_transformers import SentenceTransformer
13
+ import chromadb
14
+ from chromadb.utils import embedding_functions
15
+ import gradio as gr
16
+ from openai import OpenAI
17
+ import google.generativeai as genai
18
 
19
+ # ----------------- Logging Configuration -----------------
20
  LOG_DIR = "logs"
21
  os.makedirs(LOG_DIR, exist_ok=True)
22
  log_file = os.path.join(LOG_DIR, f"rag_system_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
23
 
 
24
  logging.basicConfig(
25
  level=logging.INFO,
26
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
 
32
  logger = logging.getLogger("rag_system")
33
  logger.info(f"Starting RAG system. Log file: {log_file}")
34
 
35
+ # ----------------- Configuration Class -----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  class Config:
37
  """
38
  Configuration for vector store and RAG system.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  """
 
40
  def __init__(self,
41
  local_dir: str = "./chroma_db",
42
  embedding_model: str = "all-MiniLM-L6-v2",
 
57
  self.max_tokens = max_tokens
58
  self.system_name = system_name
59
 
 
60
  os.makedirs(local_dir, exist_ok=True)
 
61
  logger.info(f"Initialized configuration: {self.__dict__}")
62
 
63
  def to_dict(self) -> Dict[str, Any]:
 
64
  return self.__dict__
65
 
66
  @classmethod
67
  def from_file(cls, config_path: str) -> 'Config':
 
68
  try:
69
  with open(config_path, 'r') as f:
70
  config_dict = json.load(f)
 
76
  return cls()
77
 
78
  def save_to_file(self, config_path: str) -> bool:
 
79
  try:
80
  with open(config_path, 'w') as f:
81
  json.dump(self.to_dict(), f, indent=2)
 
85
  logger.error(f"Failed to save configuration to {config_path}: {e}")
86
  return False
87
 
88
+ # ----------------- Embedding Engine -----------------
89
  class EmbeddingEngine:
90
  """
91
+ Handles text embeddings using a lightweight model.
 
 
 
 
 
 
 
 
 
92
  """
 
93
  def __init__(self, model_name="all-MiniLM-L6-v2"):
 
 
 
 
 
 
 
 
 
 
94
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
95
  logger.info(f"Using device for embeddings: {self.device}")
96
 
 
97
  model_options = [
98
  model_name,
99
+ "all-MiniLM-L6-v2",
100
+ "paraphrase-MiniLM-L3-v2",
101
+ "all-mpnet-base-v2"
102
  ]
 
103
  self.model = None
104
 
 
105
  for model_option in model_options:
106
  try:
107
  logger.info(f"Attempting to load embedding model: {model_option}")
108
  self.model = SentenceTransformer(model_option)
 
 
109
  self.model.to(self.device)
 
110
  logger.info(f"Successfully loaded embedding model: {model_option}")
111
  self.model_name = model_option
112
  self.vector_size = self.model.get_sentence_embedding_dimension()
113
  logger.info(f"Embedding vector size: {self.vector_size}")
114
  break
 
115
  except Exception as e:
116
  logger.warning(f"Failed to load embedding model {model_option}: {str(e)}")
117
 
 
121
  raise SystemExit(error_msg)
122
 
123
  def embed(self, texts: List[str]) -> np.ndarray:
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if not texts:
125
  raise ValueError("Cannot embed empty list of texts")
 
126
  try:
127
  embeddings = self.model.encode(texts, convert_to_numpy=True)
128
  return embeddings
 
130
  logger.error(f"Error generating embeddings: {e}")
131
  raise RuntimeError(f"Failed to generate embeddings: {e}")
132
 
133
+ # ----------------- Vector Store Manager -----------------
134
  class VectorStoreManager:
135
  """
136
+ Manages Chroma vector store operations.
 
 
 
 
 
 
 
 
 
137
  """
 
138
  def __init__(self, config: Config):
 
 
 
 
 
 
 
 
 
139
  self.config = config
 
 
140
  logger.info(f"Initializing Chroma at {config.local_dir}")
141
  try:
142
  self.client = chromadb.PersistentClient(path=config.local_dir)
 
146
  logger.critical(error_msg)
147
  raise SystemExit(error_msg)
148
 
 
149
  try:
 
150
  logger.info("Loading embedding model...")
151
  self.embedding_engine = EmbeddingEngine(config.embedding_model)
152
  logger.info(f"Using embedding model: {self.embedding_engine.model_name}")
153
 
 
154
  sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
155
  model_name=self.embedding_engine.model_name
156
  )
157
 
 
158
  try:
159
  self.collection = self.client.get_collection(
160
  name=config.collection_name,
 
163
  logger.info(f"Using existing collection: {config.collection_name}")
164
  except Exception as e:
165
  logger.warning(f"Error getting collection: {e}")
 
166
  collections = self.client.list_collections()
167
  if collections:
168
  logger.info(f"Available collections: {[c.name for c in collections]}")
 
169
  self.collection = self.client.get_collection(
170
  name=collections[0].name,
171
  embedding_function=sentence_transformer_ef
172
  )
173
  logger.info(f"Using collection: {collections[0].name}")
174
  else:
 
175
  self.collection = self.client.create_collection(
176
  name=config.collection_name,
177
  embedding_function=sentence_transformer_ef,
 
185
  raise SystemExit(error_msg)
186
 
187
  def query(self, query_text: str, n_results: int = 5) -> List[Dict]:
 
 
 
 
 
 
 
 
 
 
188
  if not query_text.strip():
189
  logger.warning("Empty query received")
190
  return []
 
191
  try:
192
  logger.info(f"Querying vector store with: '{query_text[:50]}...' (top {n_results})")
 
 
193
  search_results = self.collection.query(
194
  query_texts=[query_text],
195
  n_results=n_results,
196
+ include=["documents", "metadatas", "distances"]
197
  )
 
 
198
  results = []
199
  if search_results["documents"] and len(search_results["documents"][0]) > 0:
200
  for i in range(len(search_results["documents"][0])):
201
  results.append({
202
  'document': search_results["documents"][0][i],
203
  'metadata': search_results["metadatas"][0][i] if search_results["metadatas"] else {},
204
+ 'score': 1.0 - search_results["distances"][0][i], # convert distance to similarity
205
  'distance': search_results["distances"][0][i]
206
  })
 
207
  logger.info(f"Found {len(results)} results for query")
208
  else:
209
  logger.info("No results found for query")
 
210
  return results
211
  except Exception as e:
212
  logger.error(f"Error querying collection: {e}")
213
  logger.debug(traceback.format_exc())
214
  return []
215
 
216
+ def add_document(self, document: str, doc_id: str, metadata: Dict[str, Any]) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  try:
218
  logger.info(f"Adding document '{doc_id}' to vector store")
 
 
219
  self.collection.add(
220
  documents=[document],
221
  ids=[doc_id],
222
  metadatas=[metadata]
223
  )
 
224
  logger.info(f"Successfully added document '{doc_id}'")
225
  return True
226
  except Exception as e:
 
228
  return False
229
 
230
  def delete_document(self, doc_id: str) -> bool:
 
 
 
 
 
 
 
 
 
231
  try:
232
  logger.info(f"Deleting document '{doc_id}' from vector store")
233
  self.collection.delete(ids=[doc_id])
 
238
  return False
239
 
240
  def get_statistics(self) -> Dict[str, Any]:
 
 
 
 
 
 
241
  stats = {
242
  'collection_name': self.config.collection_name,
243
  'embedding_model': self.embedding_engine.model_name,
244
  'embedding_dimensions': self.embedding_engine.vector_size,
245
  'device': self.embedding_engine.device
246
  }
 
247
  try:
 
248
  collection_count = self.collection.count()
249
  stats['total_documents'] = collection_count
 
 
250
  if collection_count > 0:
251
  try:
 
252
  sample_results = self.collection.get(limit=min(collection_count, 100))
253
  if sample_results and 'metadatas' in sample_results and sample_results['metadatas']:
 
254
  filenames = set()
255
  for metadata in sample_results['metadatas']:
256
  if 'filename' in metadata:
 
258
  stats['unique_files'] = len(filenames)
259
  except Exception as e:
260
  logger.warning(f"Error getting metadata statistics: {e}")
 
261
  logger.info(f"Vector store statistics: {stats}")
262
  except Exception as e:
263
  logger.error(f"Error getting statistics: {e}")
264
  stats['error'] = str(e)
 
265
  return stats
266
 
267
+ # ----------------- RAG System -----------------
268
  class RAGSystem:
269
  """
270
+ Handles the Retrieval-Augmented Generation workflow.
 
 
 
 
 
 
 
 
 
271
  """
 
272
  def __init__(self, vector_store: VectorStoreManager, config: Config):
 
 
 
 
 
 
 
273
  self.vector_store = vector_store
274
  self.config = config
275
  self.openai_client = None
276
  self.gemini_configured = False
 
277
  logger.info("Initialized RAG system")
278
 
279
  def setup_openai(self, api_key: str) -> bool:
 
 
 
 
 
 
 
 
 
280
  if not api_key.strip():
281
  logger.warning("Empty OpenAI API key provided")
282
  return False
 
283
  try:
284
  logger.info("Setting up OpenAI client")
285
  self.openai_client = OpenAI(api_key=api_key)
 
300
  return False
301
 
302
  def setup_gemini(self, api_key: str) -> bool:
 
 
 
 
 
 
 
 
 
303
  if not api_key.strip():
304
  logger.warning("Empty Gemini API key provided")
305
  return False
 
306
  try:
307
  logger.info("Setting up Gemini client")
308
  genai.configure(api_key=api_key)
 
 
309
  model = genai.GenerativeModel(self.config.gemini_model)
310
  response = model.generate_content("Test connection")
 
311
  self.gemini_configured = True
312
  logger.info("Gemini client configured successfully")
313
  return True
 
315
  logger.error(f"Error configuring Gemini: {e}")
316
  self.gemini_configured = False
317
  return False
318
+
319
  def format_context(self, documents: List[Dict]) -> str:
 
 
 
 
 
 
 
 
 
320
  if not documents:
321
  logger.warning("No documents provided for context formatting")
322
  return "No relevant documents found."
 
323
  logger.info(f"Formatting {len(documents)} documents for context")
324
  context_parts = []
 
325
  for i, doc in enumerate(documents):
326
  metadata = doc['metadata']
 
327
  title = metadata.get('title', metadata.get('filename', 'Unknown document'))
328
  source = metadata.get('source', metadata.get('path', 'Unknown source'))
329
  date = metadata.get('date', metadata.get('created_at', 'Unknown date'))
 
 
330
  header = f"Document {i+1} - {title}"
331
  if source != 'Unknown source':
332
  header += f" (Source: {source})"
333
  if date != 'Unknown date':
334
  header += f" (Date: {date})"
 
 
335
  doc_text = doc['document']
336
+ if len(doc_text) > 8000:
337
  doc_text = doc_text[:8000] + "... [Document truncated for context]"
 
338
  context_parts.append(f"{header}:\n{doc_text}\n")
 
339
  full_context = "\n".join(context_parts)
340
  logger.info(f"Created context with {len(full_context)} characters")
 
341
  return full_context
342
+
343
  def generate_response_openai(self, query: str, context: str) -> str:
 
 
 
 
 
 
 
 
 
 
344
  if not self.openai_client:
345
  logger.warning("OpenAI API key not configured for response generation")
346
+ return "Error: OpenAI API key not configured. Please enter an API key."
347
 
348
+ system_prompt = (
349
+ "You are a knowledgeable assistant that answers questions based solely on the provided context. "
350
+ "Use clear headings and cite the document numbers where the information is found. "
351
+ "If the context lacks the needed details, say so and suggest what additional details might help."
352
+ )
 
 
 
 
 
 
 
 
 
353
 
354
  try:
355
+ logger.info(f"Generating response with OpenAI using model {self.config.openai_model}")
 
356
  start_time = datetime.now()
357
  response = self.openai_client.chat.completions.create(
358
  model=self.config.openai_model,
 
363
  temperature=self.config.temperature,
364
  max_tokens=self.config.max_tokens,
365
  )
 
366
  generation_time = (datetime.now() - start_time).total_seconds()
367
  response_text = response.choices[0].message.content
 
368
  logger.info(f"Generated response with OpenAI in {generation_time:.2f} seconds")
369
  return response_text
370
  except Exception as e:
 
373
  return f"Error: {error_msg}"
374
 
375
  def generate_response_gemini(self, query: str, context: str) -> str:
 
 
 
 
 
 
 
 
 
 
376
  if not self.gemini_configured:
377
  logger.warning("Gemini API key not configured for response generation")
378
+ return "Error: Gemini API key not configured. Please enter an API key."
379
 
380
+ prompt = (
381
+ "You are an insightful assistant who provides detailed, well-organized answers based solely on the provided context. "
382
+ "Answer the question below by clearly citing document numbers where applicable. "
383
+ "If there is insufficient context, indicate what further details would be needed.\n\n"
384
+ f"Context:\n{context}\n\nQuestion: {query}\n\nAnswer:"
385
+ )
 
 
 
 
 
 
 
 
 
386
 
 
 
 
 
 
 
387
  try:
388
+ logger.info(f"Generating response with Gemini using model {self.config.gemini_model}")
 
389
  start_time = datetime.now()
390
  model = genai.GenerativeModel(self.config.gemini_model)
 
391
  generation_config = {
392
  "temperature": self.config.temperature,
393
  "max_output_tokens": self.config.max_tokens,
394
  "top_p": 0.9,
395
  "top_k": 40
396
  }
397
+ response = model.generate_content(prompt, generation_config=generation_config)
 
 
 
 
 
398
  generation_time = (datetime.now() - start_time).total_seconds()
399
  response_text = response.text
 
400
  logger.info(f"Generated response with Gemini in {generation_time:.2f} seconds")
401
  return response_text
402
  except Exception as e:
 
404
  logger.error(error_msg)
405
  return f"Error: {error_msg}"
406
 
407
+ def query_and_generate(self, query: str, n_results: int = 5, model: str = "openai") -> Tuple[str, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  if not query.strip():
409
  logger.warning("Empty query received")
410
  return "Please enter a question to get a response.", "No search performed."
411
 
412
+ logger.info(f"Processing query: '{query[:50]}...' using {model} model")
 
 
413
  documents = self.vector_store.query(query, n_results=n_results)
414
 
415
+ # Format retrieval details (hidden by default in the UI)
416
  formatted_results = []
417
  for i, res in enumerate(documents):
418
  metadata = res['metadata']
419
  title = metadata.get('title', metadata.get('filename', 'Unknown'))
420
+ preview = res['document'][:300] + '...' if len(res['document']) > 300 else res['document']
421
+ formatted_results.append(f"**Document {i+1}**\nSource: {title}\nPreview:\n{preview}\n")
 
 
 
422
  search_output_text = "\n".join(formatted_results) if formatted_results else "No results found."
423
 
424
  if not documents:
425
  logger.warning("No relevant documents found")
426
  return "No relevant documents found to answer your question.", search_output_text
427
 
 
428
  context = self.format_context(documents)
429
 
 
430
  if model == "openai":
431
  response = self.generate_response_openai(query, context)
432
  elif model == "gemini":
 
438
 
439
  return response, search_output_text
440
 
441
+ # ----------------- Utility Function -----------------
442
  def get_db_stats(vector_store: VectorStoreManager) -> str:
 
 
 
 
 
 
 
 
 
443
  try:
444
  stats = vector_store.get_statistics()
445
  total_docs = stats.get('total_documents', 0)
446
  unique_files = stats.get('unique_files', 'Unknown')
447
  model = stats.get('embedding_model', 'Unknown')
448
  device = stats.get('device', 'Unknown')
449
+ stats_text = (
450
+ f"Total documents: {total_docs}\n"
451
+ f"Unique files: {unique_files}\n"
452
+ f"Embedding model: {model}\n"
 
453
  f"Device: {device}"
454
+ )
455
+ return stats_text
 
456
  except Exception as e:
457
  logger.error(f"Error getting statistics: {e}")
458
  return "Error getting database statistics"
459
 
460
+ # ----------------- Main Application -----------------
461
  def main():
462
+ # Define configuration file path before usage
 
 
 
 
463
  CONFIG_FILE_PATH = "rag_config.json"
464
+ print(f"Starting Document RAG System v1.0.0")
465
+ print(f"Log file: {log_file}")
466
 
467
+ # Load configuration from file or use defaults
468
  if os.path.exists(CONFIG_FILE_PATH):
469
  config = Config.from_file(CONFIG_FILE_PATH)
470
  else:
471
+ config = Config(local_dir="./chroma_db", collection_name="markdown_docs")
 
 
 
 
472
  config.save_to_file(CONFIG_FILE_PATH)
473
 
474
  try:
 
475
  vector_store = VectorStoreManager(config)
 
 
476
  rag_system = RAGSystem(vector_store, config)
477
+ except Exception as e:
478
+ print(f"Error initializing system: {e}")
479
+ sys.exit(1)
480
+
481
+ # ----------------- Gradio Callback Functions -----------------
482
+ def save_api_key(model_choice: str, api_key: str):
483
+ if model_choice == "openai":
484
+ success = rag_system.setup_openai(api_key)
485
+ return "OpenAI API key saved and configured successfully." if success else "Error configuring OpenAI API key."
486
+ elif model_choice == "gemini":
487
+ success = rag_system.setup_gemini(api_key)
488
+ return "Gemini API key saved and configured successfully." if success else "Error configuring Gemini API key."
489
+ else:
490
+ return "Unknown model choice."
491
+
492
+ def process_query(query: str, model_choice: str, n_results: int, temperature: float, max_tokens: int):
493
+ # Update configuration parameters based on slider values
494
+ config.temperature = temperature
495
+ config.max_tokens = max_tokens
496
+ response_text, search_details = rag_system.query_and_generate(query, n_results=n_results, model=model_choice)
497
+ return response_text, search_details
498
+
499
+ # ----------------- Gradio Interface -----------------
500
+ with gr.Blocks(title=config.system_name) as app:
501
+ gr.Markdown(f"# {config.system_name} v1.0.0")
502
+ gr.Markdown("Retrieve answers from your documents with AI-powered retrieval and generation.")
503
+
504
+ with gr.Row():
505
+ with gr.Column(scale=1):
506
+ with gr.Box():
507
+ gr.Markdown("### LLM Configuration")
508
+ model_choice = gr.Radio(
509
+ choices=["openai", "gemini"],
510
+ value="openai",
511
+ label="Select LLM Provider",
512
+ info="Choose between OpenAI and Gemini models."
513
+ )
514
+ api_key_input = gr.Textbox(
515
+ label="API Key",
516
+ placeholder="Enter your API key here...",
517
+ type="password",
518
+ info="Your API key is not stored between sessions."
519
+ )
520
+ save_key_button = gr.Button("Save API Key", variant="primary")
521
+ api_status = gr.Markdown("")
522
 
523
+ with gr.Box():
524
+ gr.Markdown("### Search Settings")
525
+ n_results_slider = gr.Slider(
526
+ minimum=1,
527
+ maximum=20,
528
+ value=config.default_top_k,
529
+ step=1,
530
+ label="Documents to Retrieve",
531
+ info="Number of documents for context."
532
+ )
533
+ temperature_slider = gr.Slider(
534
+ minimum=0.0,
535
+ maximum=1.0,
536
+ value=config.temperature,
537
+ step=0.05,
538
+ label="Response Temperature",
539
+ info="Lower values yield more factual responses."
540
+ )
541
+ max_tokens_slider = gr.Slider(
542
+ minimum=100,
543
+ maximum=4000,
544
+ value=config.max_tokens,
545
+ step=100,
546
+ label="Max Output Tokens",
547
+ info="Maximum tokens in generated response."
548
+ )
549
+
550
+ with gr.Column(scale=2):
551
+ with gr.Box():
552
+ gr.Markdown("### Ask a Question")
553
+ query_input = gr.Textbox(
554
+ label="Your Question",
555
+ placeholder="Enter your question here..."
556
+ )
557
+ submit_button = gr.Button("Submit")
558
+ with gr.Box():
559
+ answer_output = gr.Markdown(label="Answer")
560
+ with gr.Accordion("View Document Retrieval Details (hidden)", open=False):
561
+ retrieval_output = gr.Markdown(label="Retrieval Details")
562
+
563
+ # Set up callbacks
564
+ save_key_button.click(
565
+ save_api_key,
566
+ inputs=[model_choice, api_key_input],
567
+ outputs=api_status
568
+ )
569
+
570
+ submit_button.click(
571
+ process_query,
572
+ inputs=[query_input, model_choice, n_results_slider, temperature_slider, max_tokens_slider],
573
+ outputs=[answer_output, retrieval_output]
574
+ )
575
+
576
+ with gr.Accordion("View Database Statistics", open=False):
577
+ db_stats = gr.Markdown(get_db_stats(vector_store))
578
+
579
+ app.launch()
580
+
581
+ if __name__ == "__main__":
582
+ main()