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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +430 -184
app.py CHANGED
@@ -1,26 +1,18 @@
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,21 +24,58 @@ logging.basicConfig(
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",
43
  collection_name: str = "markdown_docs",
44
- default_top_k: int = 5,
45
  openai_model: str = "gpt-4o-mini",
46
  gemini_model: str = "gemini-1.5-flash",
47
  temperature: float = 0.3,
48
- max_tokens: int = 1000,
49
- system_name: str = "Document RAG System"):
 
50
  self.local_dir = local_dir
51
  self.embedding_model = embedding_model
52
  self.collection_name = collection_name
@@ -56,15 +85,20 @@ class Config:
56
  self.temperature = temperature
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,6 +110,7 @@ class Config:
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,33 +120,59 @@ class Config:
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,8 +182,22 @@ class EmbeddingEngine:
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,13 +205,33 @@ class EmbeddingEngine:
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,15 +241,19 @@ class VectorStoreManager:
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,15 +262,18 @@ class VectorStoreManager:
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,42 +287,76 @@ class VectorStoreManager:
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,6 +364,15 @@ class VectorStoreManager:
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,19 +383,31 @@ class VectorStoreManager:
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,28 +415,57 @@ class VectorStoreManager:
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,14 +486,27 @@ class RAGSystem:
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,44 +514,91 @@ class RAGSystem:
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,70 +609,130 @@ class RAGSystem:
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:
371
  error_msg = f"Error generating response with OpenAI: {str(e)}"
372
  logger.error(error_msg)
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:
403
  error_msg = f"Error generating response with Gemini: {str(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,145 +744,85 @@ class RAGSystem:
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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.1.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
+ context_limit (int): Maximum characters to include in context
66
  """
67
+
68
  def __init__(self,
69
  local_dir: str = "./chroma_db",
70
  embedding_model: str = "all-MiniLM-L6-v2",
71
  collection_name: str = "markdown_docs",
72
+ default_top_k: int = 8, # Increased from 5 to 8 for more context
73
  openai_model: str = "gpt-4o-mini",
74
  gemini_model: str = "gemini-1.5-flash",
75
  temperature: float = 0.3,
76
+ max_tokens: int = 2000, # Increased from 1000 to 2000 for more comprehensive responses
77
+ system_name: str = "Document RAG System",
78
+ context_limit: int = 16000): # Increased context limit for more comprehensive context
79
  self.local_dir = local_dir
80
  self.embedding_model = embedding_model
81
  self.collection_name = collection_name
 
85
  self.temperature = temperature
86
  self.max_tokens = max_tokens
87
  self.system_name = system_name
88
+ self.context_limit = context_limit
89
 
90
+ # Create local directory if it doesn't exist
91
  os.makedirs(local_dir, exist_ok=True)
92
+
93
  logger.info(f"Initialized configuration: {self.__dict__}")
94
 
95
  def to_dict(self) -> Dict[str, Any]:
96
+ """Convert configuration to dictionary for serialization"""
97
  return self.__dict__
98
 
99
  @classmethod
100
  def from_file(cls, config_path: str) -> 'Config':
101
+ """Load configuration from JSON file"""
102
  try:
103
  with open(config_path, 'r') as f:
104
  config_dict = json.load(f)
 
110
  return cls()
111
 
112
  def save_to_file(self, config_path: str) -> bool:
113
+ """Save configuration to JSON file"""
114
  try:
115
  with open(config_path, 'w') as f:
116
  json.dump(self.to_dict(), f, indent=2)
 
120
  logger.error(f"Failed to save configuration to {config_path}: {e}")
121
  return False
122
 
 
123
  class EmbeddingEngine:
124
  """
125
+ Handle embeddings with a lightweight model.
126
+
127
+ This class manages the embedding model used to convert text to vector
128
+ representations for semantic search.
129
+
130
+ Attributes:
131
+ model (SentenceTransformer): The loaded embedding model
132
+ model_name (str): Name of the successfully loaded model
133
+ vector_size (int): Dimension of the embedding vectors
134
+ device (str): Device used for inference ('cuda' or 'cpu')
135
  """
136
+
137
  def __init__(self, model_name="all-MiniLM-L6-v2"):
138
+ """
139
+ Initialize the embedding engine with the specified model.
140
+
141
+ Args:
142
+ model_name (str): Name of the embedding model to load
143
+
144
+ Raises:
145
+ SystemExit: If no embedding model could be loaded
146
+ """
147
+ # Use GPU if available
148
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
149
  logger.info(f"Using device for embeddings: {self.device}")
150
 
151
+ # Try multiple model options in order of preference
152
  model_options = [
153
  model_name,
154
+ "all-MiniLM-L6-v2", # Good balance of speed and quality
155
+ "paraphrase-MiniLM-L3-v2", # Faster but less accurate
156
+ "all-mpnet-base-v2" # Higher quality but larger model
157
  ]
158
+
159
  self.model = None
160
 
161
+ # Try each model in order until one works
162
  for model_option in model_options:
163
  try:
164
  logger.info(f"Attempting to load embedding model: {model_option}")
165
  self.model = SentenceTransformer(model_option)
166
+
167
+ # Move model to device
168
  self.model.to(self.device)
169
+
170
  logger.info(f"Successfully loaded embedding model: {model_option}")
171
  self.model_name = model_option
172
  self.vector_size = self.model.get_sentence_embedding_dimension()
173
  logger.info(f"Embedding vector size: {self.vector_size}")
174
  break
175
+
176
  except Exception as e:
177
  logger.warning(f"Failed to load embedding model {model_option}: {str(e)}")
178
 
 
182
  raise SystemExit(error_msg)
183
 
184
  def embed(self, texts: List[str]) -> np.ndarray:
185
+ """
186
+ Generate embeddings for a list of texts.
187
+
188
+ Args:
189
+ texts (List[str]): List of texts to embed
190
+
191
+ Returns:
192
+ np.ndarray: Array of embeddings
193
+
194
+ Raises:
195
+ ValueError: If the input is invalid
196
+ RuntimeError: If embedding fails
197
+ """
198
  if not texts:
199
  raise ValueError("Cannot embed empty list of texts")
200
+
201
  try:
202
  embeddings = self.model.encode(texts, convert_to_numpy=True)
203
  return embeddings
 
205
  logger.error(f"Error generating embeddings: {e}")
206
  raise RuntimeError(f"Failed to generate embeddings: {e}")
207
 
 
208
  class VectorStoreManager:
209
  """
210
+ Manage Chroma vector store operations - upload, query, etc.
211
+
212
+ This class provides an interface to the ChromaDB vector database,
213
+ handling document storage, retrieval, and management.
214
+
215
+ Attributes:
216
+ config (Config): Configuration parameters
217
+ client (chromadb.PersistentClient): ChromaDB client
218
+ collection (chromadb.Collection): The active ChromaDB collection
219
+ embedding_engine (EmbeddingEngine): Engine for generating embeddings
220
  """
221
+
222
  def __init__(self, config: Config):
223
+ """
224
+ Initialize the vector store manager.
225
+
226
+ Args:
227
+ config (Config): Configuration parameters
228
+
229
+ Raises:
230
+ SystemExit: If the vector store cannot be initialized
231
+ """
232
  self.config = config
233
+
234
+ # Initialize Chroma client (local persistence)
235
  logger.info(f"Initializing Chroma at {config.local_dir}")
236
  try:
237
  self.client = chromadb.PersistentClient(path=config.local_dir)
 
241
  logger.critical(error_msg)
242
  raise SystemExit(error_msg)
243
 
244
+ # Get or create collection
245
  try:
246
+ # Initialize embedding model
247
  logger.info("Loading embedding model...")
248
  self.embedding_engine = EmbeddingEngine(config.embedding_model)
249
  logger.info(f"Using embedding model: {self.embedding_engine.model_name}")
250
 
251
+ # Create embedding function
252
  sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
253
  model_name=self.embedding_engine.model_name
254
  )
255
 
256
+ # Try to get existing collection or create a new one
257
  try:
258
  self.collection = self.client.get_collection(
259
  name=config.collection_name,
 
262
  logger.info(f"Using existing collection: {config.collection_name}")
263
  except Exception as e:
264
  logger.warning(f"Error getting collection: {e}")
265
+ # Attempt to get a list of available collections
266
  collections = self.client.list_collections()
267
  if collections:
268
  logger.info(f"Available collections: {[c.name for c in collections]}")
269
+ # Use the first available collection if any
270
  self.collection = self.client.get_collection(
271
  name=collections[0].name,
272
  embedding_function=sentence_transformer_ef
273
  )
274
  logger.info(f"Using collection: {collections[0].name}")
275
  else:
276
+ # Create new collection if none exist
277
  self.collection = self.client.create_collection(
278
  name=config.collection_name,
279
  embedding_function=sentence_transformer_ef,
 
287
  raise SystemExit(error_msg)
288
 
289
  def query(self, query_text: str, n_results: int = 5) -> List[Dict]:
290
+ """
291
+ Query the vector store with a text query.
292
+
293
+ Args:
294
+ query_text (str): The query text
295
+ n_results (int): Number of results to return
296
+
297
+ Returns:
298
+ List[Dict]: List of results with document text, metadata, and similarity score
299
+ """
300
  if not query_text.strip():
301
  logger.warning("Empty query received")
302
  return []
303
+
304
  try:
305
  logger.info(f"Querying vector store with: '{query_text[:50]}...' (top {n_results})")
306
+
307
+ # Query the collection
308
  search_results = self.collection.query(
309
  query_texts=[query_text],
310
  n_results=n_results,
311
  include=["documents", "metadatas", "distances"]
312
  )
313
+
314
+ # Format results
315
  results = []
316
  if search_results["documents"] and len(search_results["documents"][0]) > 0:
317
  for i in range(len(search_results["documents"][0])):
318
  results.append({
319
  'document': search_results["documents"][0][i],
320
  'metadata': search_results["metadatas"][0][i] if search_results["metadatas"] else {},
321
+ 'score': 1.0 - search_results["distances"][0][i], # Convert distance to similarity
322
  'distance': search_results["distances"][0][i]
323
  })
324
+
325
  logger.info(f"Found {len(results)} results for query")
326
  else:
327
  logger.info("No results found for query")
328
+
329
  return results
330
  except Exception as e:
331
  logger.error(f"Error querying collection: {e}")
332
  logger.debug(traceback.format_exc())
333
  return []
334
 
335
+ def add_document(self,
336
+ document: str,
337
+ doc_id: str,
338
+ metadata: Dict[str, Any]) -> bool:
339
+ """
340
+ Add a document to the vector store.
341
+
342
+ Args:
343
+ document (str): The document text
344
+ doc_id (str): Unique identifier for the document
345
+ metadata (Dict[str, Any]): Metadata about the document
346
+
347
+ Returns:
348
+ bool: True if successful, False otherwise
349
+ """
350
  try:
351
  logger.info(f"Adding document '{doc_id}' to vector store")
352
+
353
+ # Add the document to the collection
354
  self.collection.add(
355
  documents=[document],
356
  ids=[doc_id],
357
  metadatas=[metadata]
358
  )
359
+
360
  logger.info(f"Successfully added document '{doc_id}'")
361
  return True
362
  except Exception as e:
 
364
  return False
365
 
366
  def delete_document(self, doc_id: str) -> bool:
367
+ """
368
+ Delete a document from the vector store.
369
+
370
+ Args:
371
+ doc_id (str): ID of the document to delete
372
+
373
+ Returns:
374
+ bool: True if successful, False otherwise
375
+ """
376
  try:
377
  logger.info(f"Deleting document '{doc_id}' from vector store")
378
  self.collection.delete(ids=[doc_id])
 
383
  return False
384
 
385
  def get_statistics(self) -> Dict[str, Any]:
386
+ """
387
+ Get statistics about the vector store.
388
+
389
+ Returns:
390
+ Dict[str, Any]: Statistics about the vector store
391
+ """
392
  stats = {
393
  'collection_name': self.config.collection_name,
394
  'embedding_model': self.embedding_engine.model_name,
395
  'embedding_dimensions': self.embedding_engine.vector_size,
396
  'device': self.embedding_engine.device
397
  }
398
+
399
  try:
400
+ # Get collection count
401
  collection_count = self.collection.count()
402
  stats['total_documents'] = collection_count
403
+
404
+ # Get unique metadata values
405
  if collection_count > 0:
406
  try:
407
+ # Get a sample of document metadata
408
  sample_results = self.collection.get(limit=min(collection_count, 100))
409
  if sample_results and 'metadatas' in sample_results and sample_results['metadatas']:
410
+ # Count unique files if filename exists in metadata
411
  filenames = set()
412
  for metadata in sample_results['metadatas']:
413
  if 'filename' in metadata:
 
415
  stats['unique_files'] = len(filenames)
416
  except Exception as e:
417
  logger.warning(f"Error getting metadata statistics: {e}")
418
+
419
  logger.info(f"Vector store statistics: {stats}")
420
  except Exception as e:
421
  logger.error(f"Error getting statistics: {e}")
422
  stats['error'] = str(e)
423
+
424
  return stats
425
 
 
426
  class RAGSystem:
427
  """
428
+ Retrieval-Augmented Generation with multiple LLM providers.
429
+
430
+ This class handles the RAG workflow: retrieval of relevant documents,
431
+ formatting context, and generating responses with different LLM providers.
432
+
433
+ Attributes:
434
+ vector_store (VectorStoreManager): Manager for vector store operations
435
+ openai_client (Optional[OpenAI]): OpenAI client
436
+ gemini_configured (bool): Whether Gemini API is configured
437
+ config (Config): Configuration parameters
438
  """
439
+
440
  def __init__(self, vector_store: VectorStoreManager, config: Config):
441
+ """
442
+ Initialize the RAG system.
443
+
444
+ Args:
445
+ vector_store (VectorStoreManager): Vector store manager
446
+ config (Config): Configuration parameters
447
+ """
448
  self.vector_store = vector_store
449
  self.config = config
450
  self.openai_client = None
451
  self.gemini_configured = False
452
+
453
  logger.info("Initialized RAG system")
454
 
455
  def setup_openai(self, api_key: str) -> bool:
456
+ """
457
+ Set up OpenAI client with API key.
458
+
459
+ Args:
460
+ api_key (str): OpenAI API key
461
+
462
+ Returns:
463
+ bool: True if successful, False otherwise
464
+ """
465
  if not api_key.strip():
466
  logger.warning("Empty OpenAI API key provided")
467
  return False
468
+
469
  try:
470
  logger.info("Setting up OpenAI client")
471
  self.openai_client = OpenAI(api_key=api_key)
 
486
  return False
487
 
488
  def setup_gemini(self, api_key: str) -> bool:
489
+ """
490
+ Set up Gemini with API key.
491
+
492
+ Args:
493
+ api_key (str): Google AI API key
494
+
495
+ Returns:
496
+ bool: True if successful, False otherwise
497
+ """
498
  if not api_key.strip():
499
  logger.warning("Empty Gemini API key provided")
500
  return False
501
+
502
  try:
503
  logger.info("Setting up Gemini client")
504
  genai.configure(api_key=api_key)
505
+
506
+ # Test the API key with a simple request
507
  model = genai.GenerativeModel(self.config.gemini_model)
508
  response = model.generate_content("Test connection")
509
+
510
  self.gemini_configured = True
511
  logger.info("Gemini client configured successfully")
512
  return True
 
514
  logger.error(f"Error configuring Gemini: {e}")
515
  self.gemini_configured = False
516
  return False
517
+
518
  def format_context(self, documents: List[Dict]) -> str:
519
+ """
520
+ Format retrieved documents into context for the LLM.
521
+
522
+ Args:
523
+ documents (List[Dict]): List of retrieved documents
524
+
525
+ Returns:
526
+ str: Formatted context for the LLM
527
+ """
528
  if not documents:
529
  logger.warning("No documents provided for context formatting")
530
  return "No relevant documents found."
531
+
532
  logger.info(f"Formatting {len(documents)} documents for context")
533
  context_parts = []
534
+
535
  for i, doc in enumerate(documents):
536
  metadata = doc['metadata']
537
+ # Extract document metadata in a robust way
538
  title = metadata.get('title', metadata.get('filename', 'Unknown document'))
539
+
540
+ # Format header with just essential metadata for cleaner context
541
  header = f"Document {i+1} - {title}"
542
+
543
+ # For readability, limit length of context document (using config value)
 
 
544
  doc_text = doc['document']
545
+ if len(doc_text) > (self.config.context_limit // len(documents)):
546
+ # Divide context limit among the documents
547
+ max_length = self.config.context_limit // len(documents)
548
+ doc_text = doc_text[:max_length] + "... [Document truncated for brevity]"
549
+
550
  context_parts.append(f"{header}:\n{doc_text}\n")
551
+
552
  full_context = "\n".join(context_parts)
553
  logger.info(f"Created context with {len(full_context)} characters")
554
+
555
  return full_context
556
+
557
  def generate_response_openai(self, query: str, context: str) -> str:
558
+ """
559
+ Generate a response using OpenAI model with context.
560
+
561
+ Args:
562
+ query (str): User query
563
+ context (str): Formatted document context
564
+
565
+ Returns:
566
+ str: Generated response
567
+ """
568
  if not self.openai_client:
569
  logger.warning("OpenAI API key not configured for response generation")
570
+ return "Please configure an OpenAI API key to use this feature. Enter your API key in the field and click 'Save API Key'."
571
 
572
+ # Improved system prompt for better, more comprehensive responses
573
+ system_prompt = """
574
+ You are an exceptionally helpful, clear, and friendly AI research assistant. Your goal is to provide comprehensive, well-structured, and insightful answers based on the provided document context.
575
+
576
+ Guidelines for your response:
577
+
578
+ 1. USE ONLY the information contained in the provided context documents to form your answer. If the context doesn't contain enough information to provide a complete answer, acknowledge this limitation clearly.
579
+
580
+ 2. Always provide well-structured, detailed responses between 300-500 words that thoroughly address the user's question.
581
+
582
+ 3. Format your response with clear headings, bullet points, or numbered lists when appropriate to enhance readability.
583
+
584
+ 4. Cite your sources by referring to the document numbers (e.g., "According to Document 1...") to support your claims.
585
+
586
+ 5. Use a friendly, conversational, and supportive tone that makes complex information accessible.
587
+
588
+ 6. If different documents offer conflicting information, acknowledge these differences and present both perspectives without bias.
589
+
590
+ 7. When appropriate, organize information into logical categories or chronological order to improve clarity.
591
+
592
+ 8. Use examples from the documents to illustrate key points when available.
593
+
594
+ 9. Conclude with a brief summary of the main points if the answer is complex.
595
+
596
+ 10. Remember to stay focused on the user's specific question while providing sufficient context for complete understanding.
597
+ """
598
 
599
  try:
600
+ logger.info(f"Generating response with OpenAI ({self.config.openai_model})")
601
+
602
  start_time = datetime.now()
603
  response = self.openai_client.chat.completions.create(
604
  model=self.config.openai_model,
 
609
  temperature=self.config.temperature,
610
  max_tokens=self.config.max_tokens,
611
  )
612
+
613
  generation_time = (datetime.now() - start_time).total_seconds()
614
  response_text = response.choices[0].message.content
615
+
616
  logger.info(f"Generated response with OpenAI in {generation_time:.2f} seconds")
617
  return response_text
618
  except Exception as e:
619
  error_msg = f"Error generating response with OpenAI: {str(e)}"
620
  logger.error(error_msg)
621
+ return f"I encountered an error while generating your response. Please try again or check your API key. Error details: {str(e)}"
622
 
623
  def generate_response_gemini(self, query: str, context: str) -> str:
624
+ """
625
+ Generate a response using Gemini with context.
626
+
627
+ Args:
628
+ query (str): User query
629
+ context (str): Formatted document context
630
+
631
+ Returns:
632
+ str: Generated response
633
+ """
634
  if not self.gemini_configured:
635
  logger.warning("Gemini API key not configured for response generation")
636
+ return "Please configure a Google AI API key to use this feature. Enter your API key in the field and click 'Save API Key'."
637
 
638
+ # Improved Gemini prompt for more comprehensive and user-friendly responses
639
+ prompt = f"""
640
+ You are a knowledgeable and friendly research assistant who excels at providing clear, comprehensive, and well-structured responses. Your goal is to help users understand complex information from documents in an accessible way.
641
+
642
+ **Guidelines for Your Response:**
643
+
644
+ - Create a detailed, well-organized response of approximately 300-500 words that thoroughly addresses the user's question.
645
+ - Use ONLY information from the provided context documents.
646
+ - Structure your answer with clear paragraphs, and use headings, bullet points, or numbered lists when appropriate.
647
+ - Maintain a friendly, conversational tone that makes information accessible and engaging.
648
+ - When citing information, reference specific documents by number (e.g., "As mentioned in Document 2...").
649
+ - If the context doesn't contain enough information for a complete answer, acknowledge this limitation while providing what you can from the available context.
650
+ - If documents contain conflicting information, present both perspectives fairly.
651
+ - Conclude with a brief summary if the topic is complex.
652
 
653
+ **Context Documents:**
654
+ {context}
655
+
656
+ **User's Question:**
657
+ {query}
658
+
659
+ **Your Response:**
660
+ """
661
+
662
  try:
663
+ logger.info(f"Generating response with Gemini ({self.config.gemini_model})")
664
+
665
  start_time = datetime.now()
666
  model = genai.GenerativeModel(self.config.gemini_model)
667
+
668
  generation_config = {
669
  "temperature": self.config.temperature,
670
  "max_output_tokens": self.config.max_tokens,
671
  "top_p": 0.9,
672
  "top_k": 40
673
  }
674
+
675
+ response = model.generate_content(
676
+ prompt,
677
+ generation_config=generation_config
678
+ )
679
+
680
  generation_time = (datetime.now() - start_time).total_seconds()
681
  response_text = response.text
682
+
683
  logger.info(f"Generated response with Gemini in {generation_time:.2f} seconds")
684
  return response_text
685
  except Exception as e:
686
  error_msg = f"Error generating response with Gemini: {str(e)}"
687
  logger.error(error_msg)
688
+ return f"I encountered an error while generating your response. Please try again or check your API key. Error details: {str(e)}"
689
 
690
+ def query_and_generate(self,
691
+ query: str,
692
+ n_results: int = 5,
693
+ model: str = "openai") -> Tuple[str, str]:
694
+ """
695
+ Retrieve relevant documents and generate a response using the specified model.
696
+
697
+ Args:
698
+ query (str): User query
699
+ n_results (int): Number of documents to retrieve
700
+ model (str): Model provider to use ('openai' or 'gemini')
701
+
702
+ Returns:
703
+ Tuple[str, str]: (Generated response, Search results)
704
+ """
705
  if not query.strip():
706
  logger.warning("Empty query received")
707
  return "Please enter a question to get a response.", "No search performed."
708
 
709
+ logger.info(f"Processing query: '{query[:50]}...' with {model} model")
710
+
711
+ # Query vector store
712
  documents = self.vector_store.query(query, n_results=n_results)
713
 
714
+ # Format search results (for logs and hidden UI component)
715
+ # We'll format this in a way that's more useful for reference but not shown in UI
716
  formatted_results = []
717
  for i, res in enumerate(documents):
718
  metadata = res['metadata']
719
  title = metadata.get('title', metadata.get('filename', 'Unknown'))
720
+ score = res['score']
721
+
722
+ # Only include a very brief preview for reference
723
+ preview = res['document'][:100] + '...' if len(res['document']) > 100 else res['document']
724
+ formatted_results.append(f"Document {i+1}: {title} (Relevance: {score:.2f})")
725
+
726
+ search_output_text = "\n".join(formatted_results) if formatted_results else "No relevant documents found."
727
 
728
  if not documents:
729
  logger.warning("No relevant documents found")
730
+ return "I couldn't find relevant information in the knowledge base to answer your question. Could you try rephrasing your question or ask about a different topic?", search_output_text
731
 
732
+ # Format context
733
  context = self.format_context(documents)
734
 
735
+ # Generate response with the appropriate model
736
  if model == "openai":
737
  response = self.generate_response_openai(query, context)
738
  elif model == "gemini":
 
744
 
745
  return response, search_output_text
746
 
 
747
  def get_db_stats(vector_store: VectorStoreManager) -> str:
748
+ """
749
+ Function to get vector store statistics.
750
+
751
+ Args:
752
+ vector_store (VectorStoreManager): Vector store manager
753
+
754
+ Returns:
755
+ str: Formatted statistics string
756
+ """
757
  try:
758
  stats = vector_store.get_statistics()
759
  total_docs = stats.get('total_documents', 0)
760
+
761
+ stats_text = f"Documents in knowledge base: {total_docs}"
 
 
 
 
 
 
 
762
  return stats_text
763
  except Exception as e:
764
  logger.error(f"Error getting statistics: {e}")
765
  return "Error getting database statistics"
766
 
 
767
  def main():
768
+ """Main function to run the RAG application"""
769
+ # Path for configuration file
770
  CONFIG_FILE_PATH = "rag_config.json"
 
 
771
 
772
+ # Try to load configuration from file, or use defaults
773
  if os.path.exists(CONFIG_FILE_PATH):
774
  config = Config.from_file(CONFIG_FILE_PATH)
775
  else:
776
+ config = Config(
777
+ local_dir="./chroma_db", # Store Chroma files in dedicated directory
778
+ collection_name="markdown_docs"
779
+ )
780
+ # Save default configuration
781
  config.save_to_file(CONFIG_FILE_PATH)
782
 
783
+ print(f"Starting Document Knowledge Assistant v{VERSION}")
784
+ print(f"Log file: {log_file}")
785
+
786
  try:
787
+ # Initialize vector store manager with existing collection
788
  vector_store = VectorStoreManager(config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
 
790
+ # Initialize RAG system without API keys initially
791
+ rag_system = RAGSystem(vector_store, config)
792
 
793
+ # Custom CSS for better UI
794
+ custom_css = """
795
+ .gradio-container {
796
+ max-width: 1200px;
797
+ margin: auto;
798
+ }
799
+ .gr-prose h1 {
800
+ font-size: 2.5rem;
801
+ margin-bottom: 1rem;
802
+ color: #1a5276;
803
+ }
804
+ .gr-prose h3 {
805
+ font-size: 1.25rem;
806
+ font-weight: 600;
807
+ margin-top: 1rem;
808
+ margin-bottom: 0.5rem;
809
+ color: #2874a6;
810
+ }
811
+ .container {
812
+ margin: 0 auto;
813
+ padding: 2rem;
814
+ }
815
+ .gr-box {
816
+ border-radius: 8px;
817
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
818
+ padding: 1rem;
819
+ margin-bottom: 1rem;
820
+ background-color: #f9f9f9;
821
+ }
822
+ .footer {
823
+ text-align: center;
824
+ font-size: 0.8rem;
825
+ color: #666;
826
+ margin-top: 2rem;
827
+ }
828
+ """