VimalrajS04 commited on
Commit
b1c0d34
Β·
unverified Β·
1 Parent(s): 85efc9b

Integreted with new db

Browse files
Files changed (1) hide show
  1. main.py +815 -0
main.py ADDED
@@ -0,0 +1,815 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from doctr.models import ocr_predictor
3
+ from PIL import Image
4
+ import numpy as np
5
+ import torch
6
+ from langchain_community.embeddings import HuggingFaceEmbeddings
7
+ # from langchain_community.vectorstores import FAISS Β # No longer needed
8
+
9
+ from langchain_core.documents import Document
10
+ import os
11
+ from groq import Groq
12
+ import base64
13
+ from io import BytesIO
14
+ import fitz # PyMuPDF
15
+ from pathlib import Path
16
+ import time
17
+ # import shutil Β # No longer needed
18
+
19
+ # -------------------------------
20
+ # 1️⃣ Load OCR + Embedding Models + Groq Client
21
+ # -------------------------------
22
+ device = "cuda" if torch.cuda.is_available() else "cpu"
23
+ ocr_model = ocr_predictor(pretrained=True).to(device)
24
+ embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
25
+
26
+ # Initialize Groq client
27
+ groq_client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
28
+
29
+ # Model configurations
30
+ VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
31
+ LLM_MODEL = "llama-3.3-70b-versatile"
32
+
33
+ # -------------------------------
34
+ # 1b ☁️ NEW: Qdrant Cloud Configuration
35
+ # -------------------------------
36
+ # Using the credentials you provided
37
+ QDRANT_URL = "https://bdf142ef-7e2a-433b-87a0-301ff303e3af.us-east4-0.gcp.cloud.qdrant.io:6333"
38
+ # API Key is loaded from environment variable for security
39
+ QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY")
40
+ COLLECTION_NAME = "multimodal_rag_store"
41
+
42
+ # NEW IMPORTS for QDRANT
43
+ from langchain_qdrant import Qdrant
44
+ from qdrant_client import QdrantClient
45
+
46
+ # -------------------------------
47
+ # 2️⃣ Helper: Check if image has substantial text
48
+ # -------------------------------
49
+ def has_substantial_text(text, min_words=10):
50
+ """
51
+ Determines if OCR extracted enough text to consider it a text-based image.
52
+ """
53
+ words = text.split()
54
+ return len(words) >= min_words
55
+
56
+
57
+ # -------------------------------
58
+ # 3️⃣ IMPROVED: Vision Analysis using Groq Llama 4 Scout
59
+ # -------------------------------
60
+ # ... (This function is identical, no changes needed) ...
61
+ def analyze_image_with_vision(img_path=None, img_bytes=None, pil_image=None, max_retries=3):
62
+ """
63
+ Uses Groq's Llama 4 Scout vision model to analyze images (graphs, charts, pictures).
64
+ Returns a descriptive summary optimized for chart and graph detection.
65
+ """
66
+ for attempt in range(max_retries):
67
+ try:
68
+ # Read image data
69
+ if pil_image:
70
+ buffered = BytesIO()
71
+ pil_image.save(buffered, format="PNG")
72
+ img_data = buffered.getvalue()
73
+ img_format = "png"
74
+ elif img_path:
75
+ with open(img_path, "rb") as img_file:
76
+ img_data = img_file.read()
77
+ img_format = img_path.lower().split('.')[-1]
78
+ elif img_bytes:
79
+ img_data = img_bytes
80
+ img_format = "png"
81
+ else:
82
+ return ""
83
+
84
+ base64_image = base64.b64encode(img_data).decode('utf-8')
85
+ if img_format == 'jpg':
86
+ img_format = 'jpeg'
87
+
88
+ vision_prompt = """Analyze this image carefully and provide a detailed description:
89
+
90
+ 1. IDENTIFY THE TYPE: Is this a chart, graph, table, diagram, photograph, or text document?
91
+
92
+ 2. IF IT'S A CHART/GRAPH/TABLE:
93
+ - Specify the exact type (bar chart, pie chart, line graph, scatter plot, table, etc.)
94
+ - List ALL categories/labels shown
95
+ - Describe the data values and trends
96
+ - Mention axis labels, title, legend if present
97
+ - Highlight key insights or patterns
98
+
99
+ 3. IF IT'S A PHOTOGRAPH/DIAGRAM:
100
+ - Describe what you see in detail
101
+ - Identify key objects, people, or concepts
102
+ - Note any text visible in the image
103
+
104
+ 4. IF IT'S A TEXT DOCUMENT:
105
+ - Summarize the main content and structure
106
+
107
+ Provide a comprehensive description suitable for semantic search. Be specific and detailed."""
108
+
109
+ chat_completion = groq_client.chat.completions.create(
110
+ messages=[
111
+ {
112
+ "role": "user",
113
+ "content": [
114
+ {"type": "text", "text": vision_prompt},
115
+ {
116
+ "type": "image_url",
117
+ "image_url": {
118
+ "url": f"data:image/{img_format};base64,{base64_image}"
119
+ }
120
+ }
121
+ ]
122
+ }
123
+ ],
124
+ model=VISION_MODEL,
125
+ temperature=0.2,
126
+ max_tokens=1500,
127
+ )
128
+
129
+ summary = chat_completion.choices[0].message.content
130
+ if summary and len(summary.strip()) > 30:
131
+ return summary
132
+ else:
133
+ if attempt < max_retries - 1:
134
+ time.sleep(1)
135
+ continue
136
+ return ""
137
+
138
+ except Exception as e:
139
+ error_msg = str(e)
140
+ if "model_not_found" in error_msg or "not available" in error_msg:
141
+ print(f"❌ Vision model '{VISION_MODEL}' not available!")
142
+ return ""
143
+ else:
144
+ if attempt < max_retries - 1:
145
+ time.sleep(2)
146
+ continue
147
+ return ""
148
+
149
+ return ""
150
+
151
+
152
+ # -------------------------------
153
+ # 4️⃣ Smart OCR/Vision Extraction for Images
154
+ # -------------------------------
155
+ # ... (This function is identical, no changes needed) ...
156
+ def extract_text_from_image(img_path):
157
+ """
158
+ Intelligently extracts content from images:
159
+ - Uses OCR for text-heavy images
160
+ - Uses Vision model for graphs, charts, and pictures
161
+ """
162
+ try:
163
+ image = Image.open(img_path).convert("RGB")
164
+ image_np = np.array(image)
165
+ result = ocr_model([image_np])
166
+
167
+ text = []
168
+ for page in result.pages:
169
+ for block in page.blocks:
170
+ for line in block.lines:
171
+ line_text = " ".join([word.value for word in line.words])
172
+ text.append(line_text)
173
+
174
+ ocr_text = "\n".join(text)
175
+
176
+ if has_substantial_text(ocr_text, min_words=10):
177
+ print(f"πŸ“„ {os.path.basename(img_path)}: Using OCR (text document)")
178
+ return ocr_text
179
+ else:
180
+ print(f"πŸ–ΌοΈ {os.path.basename(img_path)}: Using Vision Model (graph/chart/picture)")
181
+ vision_summary = analyze_image_with_vision(img_path=img_path)
182
+ return vision_summary if vision_summary else ocr_text
183
+
184
+ except Exception as e:
185
+ print(f"❌ Error processing {img_path}: {e}")
186
+ return ""
187
+
188
+
189
+ # -------------------------------
190
+ # 5️⃣ Extract Text from Plain Text Files
191
+ # -------------------------------
192
+ # ... (This function is identical, no changes needed) ...
193
+ def extract_text_from_txt(file_path):
194
+ """
195
+ Extracts text from plain text files (.txt, .md, etc.)
196
+ """
197
+ try:
198
+ with open(file_path, 'r', encoding='utf-8') as f:
199
+ text = f.read()
200
+ print(f"πŸ“ {os.path.basename(file_path)}: Extracted text document")
201
+ return text
202
+ except Exception as e:
203
+ print(f"❌ Error reading text file {file_path}: {e}")
204
+ return ""
205
+
206
+
207
+ # -------------------------------
208
+ # 6️⃣ ENHANCED: Extract Content from PDFs with Vision Analysis
209
+ # -------------------------------
210
+ # ... (This function is identical, no changes needed) ...
211
+ def extract_content_from_pdf(pdf_path):
212
+ """
213
+ Extracts content from PDFs with comprehensive vision analysis
214
+ """
215
+ try:
216
+ doc = fitz.open(pdf_path)
217
+ all_content = []
218
+
219
+ for page_num, page in enumerate(doc, 1):
220
+ page_content = []
221
+
222
+ # Extract text
223
+ text = page.get_text()
224
+ if text.strip():
225
+ page_content.append(f"[Page {page_num} - Text Content]\n{text}")
226
+ print(f"πŸ“„ {os.path.basename(pdf_path)} (Page {page_num}): Extracted text ({len(text)} chars)")
227
+
228
+ # Render entire page as image for vision analysis
229
+ print(f"πŸ”„ {os.path.basename(pdf_path)} (Page {page_num}): Rendering page for vision analysis...")
230
+ try:
231
+ mat = fitz.Matrix(2, 2)
232
+ pix = page.get_pixmap(matrix=mat)
233
+ img_data = pix.tobytes("png")
234
+ page_image = Image.open(BytesIO(img_data)).convert("RGB")
235
+
236
+ print(f"πŸ” {os.path.basename(pdf_path)} (Page {page_num}): Analyzing with {VISION_MODEL}...")
237
+ vision_analysis = analyze_image_with_vision(pil_image=page_image)
238
+
239
+ if vision_analysis and len(vision_analysis.strip()) > 30:
240
+ vision_section = f"[Page {page_num} - Visual Analysis]\n{vision_analysis}"
241
+ page_content.append(vision_section)
242
+ print(f"βœ… {os.path.basename(pdf_path)} (Page {page_num}): Vision analysis complete")
243
+
244
+ except Exception as e:
245
+ print(f"❌ Error rendering page {page_num}: {e}")
246
+
247
+ # Extract embedded images
248
+ image_list = page.get_images(full=True)
249
+ for img_index, img_info in enumerate(image_list, 1):
250
+ try:
251
+ xref = img_info[0]
252
+ base_image = doc.extract_image(xref)
253
+ image_bytes = base_image["image"]
254
+ image = Image.open(BytesIO(image_bytes)).convert("RGB")
255
+ image_np = np.array(image)
256
+
257
+ result = ocr_model([image_np])
258
+ ocr_text = []
259
+ for ocr_page in result.pages:
260
+ for block in ocr_page.blocks:
261
+ for line in block.lines:
262
+ line_text = " ".join([word.value for word in line.words])
263
+ ocr_text.append(line_text)
264
+
265
+ extracted_text = "\n".join(ocr_text)
266
+
267
+ if has_substantial_text(extracted_text, min_words=10):
268
+ page_content.append(f"[Page {page_num} - Embedded Image {img_index} OCR]\n{extracted_text}")
269
+ else:
270
+ vision_summary = analyze_image_with_vision(img_bytes=image_bytes)
271
+ if vision_summary:
272
+ page_content.append(
273
+ f"[Page {page_num} - Embedded Image {img_index} Analysis]\n{vision_summary}")
274
+
275
+ except Exception as e:
276
+ print(f"❌ Error processing embedded image {img_index}: {e}")
277
+ continue
278
+
279
+ if page_content:
280
+ combined_page = "\n\n---SECTION BREAK---\n\n".join(page_content)
281
+ all_content.append(combined_page)
282
+
283
+ doc.close()
284
+ final_content = "\n\n---PAGE BREAK---\n\n".join(all_content)
285
+ return final_content
286
+
287
+ except Exception as e:
288
+ print(f"❌ Error processing PDF {pdf_path}: {e}")
289
+ return ""
290
+
291
+
292
+ # -------------------------------
293
+ # 7️⃣ Process All Document Types
294
+ # -------------------------------
295
+ # ... (This function is identical, no changes needed) ...
296
+ def create_documents_from_folder(folder_path):
297
+ """
298
+ Process all supported file types in a folder and create documents.
299
+ """
300
+ docs = []
301
+
302
+ for root, dirs, files in os.walk(folder_path):
303
+ for filename in files:
304
+ full_path = os.path.join(root, filename)
305
+ file_ext = filename.lower().split('.')[-1]
306
+
307
+ print(f"\n{'=' * 60}")
308
+ print(f"Processing: {filename}")
309
+ print(f"{'=' * 60}")
310
+
311
+ text = ""
312
+
313
+ if file_ext in ["jpg", "jpeg", "png"]:
314
+ text = extract_text_from_image(full_path)
315
+ elif file_ext in ["txt", "md"]:
316
+ text = extract_text_from_txt(full_path)
317
+ elif file_ext == "pdf":
318
+ text = extract_content_from_pdf(full_path)
319
+ else:
320
+ print(f"⏭️ Skipping unsupported file: {filename}")
321
+ continue
322
+
323
+ if text.strip():
324
+ relative_path = os.path.relpath(full_path, folder_path)
325
+ doc = Document(
326
+ page_content=text,
327
+ metadata={
328
+ "source": relative_path,
329
+ "filename": filename,
330
+ "file_type": file_ext,
331
+ # This captures the *file modification time* for local files
332
+ "upload_timestamp": os.path.getmtime(full_path)
333
+ }
334
+ )
335
+ docs.append(doc)
336
+ print(f"βœ… Added {filename} to documents ({len(text)} chars)")
337
+ else:
338
+ print(f"⚠️ Skipping {filename} - no content extracted")
339
+
340
+ return docs
341
+
342
+
343
+ # -------------------------------
344
+ # 8️⃣ ☁️ NEW: Build or Update QDRANT Store
345
+ # -------------------------------
346
+ # ... (This function is identical, no changes needed) ...
347
+ def build_or_update_qdrant_store(folder_path):
348
+ """
349
+ Builds a new Qdrant store in the cloud, deleting any old one.
350
+ """
351
+ print("\n" + "=" * 60)
352
+ print("πŸ”„ STARTING DOCUMENT PROCESSING FOR QDRANT")
353
+ print("=" * 60)
354
+
355
+ docs = create_documents_from_folder(folder_path) # This part is the same
356
+
357
+ if not docs:
358
+ print("\n⚠️ No valid documents found!")
359
+ return None
360
+
361
+ print(f"\nβœ… Successfully processed {len(docs)} documents")
362
+ print(f"☁️ Uploading documents to Qdrant Cloud collection: {COLLECTION_NAME}...")
363
+
364
+ # This command connects, deletes the old collection (if it exists),
365
+ # and uploads all the new documents.
366
+ try:
367
+ vector_store = Qdrant.from_documents(
368
+ docs,
369
+ embedding_model,
370
+ url=QDRANT_URL,
371
+ api_key=QDRANT_API_KEY,
372
+ collection_name=COLLECTION_NAME,
373
+ force_recreate=True # This matches the old "delete and rebuild" logic
374
+ )
375
+ print(f"βœ… Successfully created/updated Qdrant collection: {COLLECTION_NAME}")
376
+ return vector_store
377
+ except Exception as e:
378
+ print(f"❌ Error connecting or uploading to Qdrant: {e}")
379
+ print("Please check your QDRANT_URL and QDRANT_API_KEY")
380
+ return None
381
+
382
+
383
+ # -------------------------------
384
+ # 9️⃣ ☁️ NEW: Query QDRANT Function with Chart-Aware Re-ranking
385
+ # -------------------------------
386
+ # ... (This function is identical, no changes needed) ...
387
+ def query_qdrant_store(query_text, k=3):
388
+ """
389
+ Query the QDRANT store and return top-k relevant documents.
390
+ """
391
+ try:
392
+ # 1. Create the Qdrant client
393
+ client = QdrantClient(
394
+ url=QDRANT_URL,
395
+ api_key=QDRANT_API_KEY,
396
+ timeout=20 # Increased timeout for cloud connection
397
+ )
398
+
399
+ # 2. Instantiate the LangChain vector store object
400
+ vector_store = Qdrant(
401
+ client=client,
402
+ collection_name=COLLECTION_NAME,
403
+ embeddings=embedding_model
404
+ )
405
+ print(f"βœ… Connected to Qdrant collection: {COLLECTION_NAME}")
406
+
407
+ except Exception as e:
408
+ print(f"❌ Error connecting to Qdrant: {e}")
409
+ return []
410
+
411
+ initial_k = k * 3
412
+ # This part is identical to the old code!
413
+ results = vector_store.similarity_search_with_score(query_text, k=initial_k)
414
+
415
+ # Check if query is asking about visual content
416
+ visual_query_keywords = ['chart', 'graph', 'bar', 'pie', 'plot', 'diagram', 'table', 'visual', 'visualization']
417
+ is_visual_query = any(keyword in query_text.lower() for keyword in visual_query_keywords)
418
+
419
+ if is_visual_query:
420
+ print(f"πŸ” Detected visual content query - applying smart re-ranking...")
421
+ reranked_results = []
422
+
423
+ for doc, score in results:
424
+ boost = 0.0
425
+
426
+ if "**Type:**" in doc.page_content or "Visual Analysis]" in doc.page_content:
427
+ visual_content = doc.page_content.lower()
428
+
429
+ if 'bar chart' in query_text.lower() or 'bar graph' in query_text.lower():
430
+ if 'bar chart' in visual_content or 'bar graph' in visual_content:
431
+ boost += 1.0
432
+
433
+ elif 'pie chart' in query_text.lower():
434
+ if 'pie chart' in visual_content:
435
+ boost += 1.0
436
+
437
+ elif 'line graph' in query_text.lower() or 'line chart' in query_text.lower():
438
+ if 'line graph' in visual_content or 'line chart' in visual_content:
439
+ boost += 1.0
440
+
441
+ elif any(kw in query_text.lower() for kw in ['chart', 'graph', 'visualization']):
442
+ if any(kw in visual_content for kw in ['chart', 'graph', 'plot', 'diagram']):
443
+ boost += 0.5
444
+ else:
445
+ boost += 0.2
446
+
447
+ adjusted_score = score - boost
448
+ reranked_results.append((doc, adjusted_score, score))
449
+
450
+ reranked_results.sort(key=lambda x: x[1])
451
+ results = [(doc, adj_score) for doc, adj_score, _ in reranked_results[:k]]
452
+ else:
453
+ results = results[:k]
454
+
455
+ retrieved_docs = []
456
+ for doc, score in results:
457
+ retrieved_docs.append({
458
+ "source": doc.metadata['source'],
459
+ "content": doc.page_content,
460
+ "score": score,
461
+ "metadata": doc.metadata
462
+ })
463
+
464
+ return retrieved_docs
465
+
466
+
467
+ # -------------------------------
468
+ # πŸ†• 10️⃣ Answer Question using Llama 3.3 70B (MODIFIED)
469
+ # -------------------------------
470
+ def answer_question_with_llm(query_text, retrieved_docs, max_tokens=1000): # Reduced max_tokens for brevity
471
+ """
472
+ Uses Llama-3.3-70b-versatile to answer questions based on retrieved documents.
473
+ **MODIFIED** for shorter answers and to read metadata (like upload time).
474
+ """
475
+ if not retrieved_docs:
476
+ return "❌ No relevant documents found to answer your question."
477
+
478
+ # Prepare context from retrieved documents
479
+ context_parts = []
480
+ for i, doc in enumerate(retrieved_docs, 1):
481
+ source = doc['source']
482
+ content = doc['content']
483
+ metadata = doc['metadata'] # Get metadata
484
+
485
+ # Format timestamp
486
+ timestamp = metadata.get('upload_timestamp')
487
+ readable_time = "N/A"
488
+ if timestamp:
489
+ try:
490
+ # Use time.ctime() for a simple, human-readable string
491
+ readable_time = time.ctime(float(timestamp))
492
+ except (ValueError, TypeError):
493
+ readable_time = str(timestamp) # Fallback
494
+
495
+ metadata_str = (
496
+ f"Source: {source}\n"
497
+ f"File Type: {metadata.get('file_type', 'N/A')}\n"
498
+ f"Uploaded/Modified: {readable_time}"
499
+ )
500
+
501
+ # Truncate very long content to fit within token limits
502
+ max_content_length = 2500 # Kept it reasonably long for context
503
+ if len(content) > max_content_length:
504
+ content = content[:max_content_length] + "...[truncated]"
505
+
506
+ context_parts.append(
507
+ f"--- Document {i} ---\n"
508
+ f"[METADATA]:\n{metadata_str}\n\n"
509
+ f"[CONTENT]:\n{content}\n"
510
+ )
511
+
512
+ context = "\n".join(context_parts)
513
+
514
+ # Construct the prompt (MODIFIED FOR BREVITY)
515
+ system_prompt = """You are a concise AI assistant. Answer the user's question *only* using the provided documents.
516
+ - Be brief and to the point.
517
+ - The documents include `[METADATA]` and `[CONTENT]`.
518
+ - Use the metadata to answer questions about file details (like upload time, source, or file type).
519
+ - If the answer is not in the documents or metadata, simply state 'That information is not available in the documents.'"""
520
+
521
+ user_prompt = f"""DOCUMENTS:
522
+ {context}
523
+
524
+ QUESTION: {query_text}
525
+
526
+ ANSWER: (Provide a concise answer based *only* on the documents)"""
527
+
528
+ try:
529
+ print(f"\nπŸ€– Generating answer with {LLM_MODEL}...")
530
+
531
+ response = groq_client.chat.completions.create(
532
+ model=LLM_MODEL,
533
+ messages=[
534
+ {"role": "system", "content": system_prompt},
535
+ {"role": "user", "content": user_prompt}
536
+ ],
537
+ temperature=0.2, # Lowered for more factual, less creative answers
538
+ max_tokens=max_tokens,
539
+ top_p=0.9,
540
+ )
541
+
542
+ answer = response.choices[0].message.content
543
+ return answer
544
+
545
+ except Exception as e:
546
+ print(f"❌ Error calling LLM: {e}")
547
+ return f"❌ Error generating answer: {str(e)}"
548
+
549
+
550
+ # -------------------------------
551
+ # πŸ†• 11️⃣ Complete RAG Pipeline
552
+ # -------------------------------
553
+ # ... (This function is identical, no changes needed) ...
554
+ def ask_question(query_text, k=3, show_sources=True):
555
+ """
556
+ Complete RAG pipeline: retrieves relevant documents and generates an answer.
557
+ (This function is kept for logic, but the Gradio app will use a wrapper)
558
+ """
559
+ print("\n" + "=" * 80)
560
+ print(f"❓ QUESTION: {query_text}")
561
+ print("=" * 80)
562
+
563
+ # Step 1: Retrieve relevant documents
564
+ print("\nπŸ“š Retrieving relevant documents from Qdrant...")
565
+ retrieved_docs = query_qdrant_store(query_text, k=k) # <-- CHANGED
566
+
567
+ if not retrieved_docs:
568
+ print("❌ No relevant documents found.")
569
+ return
570
+
571
+ # ... (Rest of function is identical) ...
572
+ if show_sources:
573
+ print(f"\nπŸ“„ Retrieved {len(retrieved_docs)} relevant documents:")
574
+ for i, doc in enumerate(retrieved_docs, 1):
575
+ print(f" {i}. {doc['source']} (score: {doc['score']:.4f})")
576
+
577
+ answer = answer_question_with_llm(query_text, retrieved_docs)
578
+ # ... (print statements) ...
579
+ return {
580
+ "query": query_text,
581
+ "answer": answer,
582
+ "sources": retrieved_docs
583
+ }
584
+
585
+
586
+ # -------------------------------
587
+ # πŸ†• 12️⃣ ☁️ NEW: GRADIO: Process Uploaded Files to QDRANT
588
+ # -------------------------------
589
+ def process_uploaded_files(file_list):
590
+ """
591
+ Processes a list of files uploaded via Gradio, adds them to the
592
+ QDRANT CLOUD index.
593
+ """
594
+ if not file_list:
595
+ return "No files uploaded. Please upload files first."
596
+
597
+ print("\n" + "=" * 60)
598
+ print("NEW UPLOAD DETECTED: Processing files...")
599
+ print("=" * 60)
600
+
601
+ # --- This whole section is identical ---
602
+ docs = []
603
+ for file_obj in file_list:
604
+ # file_obj.name is the temporary path where Gradio stored the file
605
+ full_path = file_obj.name
606
+ filename = os.path.basename(full_path)
607
+ file_ext = filename.lower().split('.')[-1]
608
+
609
+ print(f"\nProcessing uploaded file: {filename}")
610
+
611
+ text = ""
612
+ if file_ext in ["jpg", "jpeg", "png"]:
613
+ text = extract_text_from_image(full_path)
614
+ elif file_ext in ["txt", "md"]:
615
+ text = extract_text_from_txt(full_path)
616
+ elif file_ext == "pdf":
617
+ text = extract_content_from_pdf(full_path)
618
+ else:
619
+ print(f"⏭️ Skipping unsupported file: {filename}")
620
+ continue
621
+
622
+ if text.strip():
623
+ # Use filename as source for metadata
624
+ doc = Document(
625
+ page_content=text,
626
+ metadata={
627
+ "source": filename, # Using filename as source
628
+ "filename": filename,
629
+ "file_type": file_ext,
630
+ "upload_timestamp": time.time() # This captures the *moment of upload*
631
+ }
632
+ )
633
+ docs.append(doc)
634
+ print(f"βœ… Added {filename} to documents ({len(text)} chars)")
635
+ else:
636
+ print(f"⚠️ Skipping {filename} - no content extracted")
637
+ # --- End of identical section ---
638
+
639
+ if not docs:
640
+ print("\n⚠️ No valid documents processed from upload.")
641
+ return "⚠️ No valid documents were processed from the upload."
642
+
643
+ # --- NEW QDRANT UPLOAD LOGIC (APPENDS to DB) ---
644
+ try:
645
+ print(f"\n☁️ Connecting to Qdrant to add {len(docs)} new documents...")
646
+
647
+ # 1. Create the client
648
+ client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
649
+
650
+ # 2. Instantiate the LangChain vector store object
651
+ vector_store = Qdrant(
652
+ client=client,
653
+ collection_name=COLLECTION_NAME,
654
+ embeddings=embedding_model
655
+ )
656
+
657
+ # 3. Add the new documents (this appends to the existing collection)
658
+ vector_store.add_documents(docs)
659
+
660
+ success_message = f"βœ… Successfully processed and added {len(docs)} new document(s) to the cloud knowledge base."
661
+ print(f"\n{success_message}")
662
+ return success_message
663
+
664
+ except Exception as e:
665
+ print(f"❌ Error adding documents to Qdrant: {e}")
666
+ return f"❌ Error updating vector store: {e}"
667
+
668
+
669
+ # -------------------------------
670
+ # βœ… Example Usage (NOW WITH GRADIO TABS!)
671
+ # -------------------------------
672
+ if __name__ == "__main__":
673
+ folder = "data" # Your folder with images, PDFs, and text files
674
+ # faiss_store_path = "faiss_multimodal_store" # <-- No longer needed
675
+
676
+ # Check Groq API key
677
+ if not os.environ.get("GROQ_API_KEY"):
678
+ print("\n" + "=" * 60)
679
+ print("⚠️ WARNING: GROQ_API_KEY not set!")
680
+ print("=" * 60)
681
+ print("Please set it with: export GROQ_API_KEY='your-key-here'")
682
+ print("=" * 60 + "\n")
683
+ exit(1)
684
+
685
+ # --- NEW: Check Qdrant API Key ---
686
+ if not QDRANT_API_KEY:
687
+ print("\n" + "=" * 60)
688
+ print("⚠️ WARNING: QDRANT_API_KEY not set!")
689
+ print("=" * 60)
690
+ print("Please set it with: export QDRANT_API_KEY='your-key-here'")
691
+ print("=" * 60 + "\n")
692
+ exit(1)
693
+
694
+ print(f"βœ… Groq API Key found")
695
+ print(f"βœ… Qdrant API Key found")
696
+ print(f"βœ… Vision Model: {VISION_MODEL}")
697
+ print(f"βœ… LLM Model: {LLM_MODEL}\n")
698
+
699
+ # ---
700
+ # πŸš€ ALWAYS REBUILD THE CLOUD DATABASE ON STARTUP
701
+ # ---
702
+ print("\n" + "=" * 60)
703
+ print(f"πŸ”„ Always rebuilding Qdrant collection '{COLLECTION_NAME}' from '{folder}'...")
704
+ print("=" * 60)
705
+
706
+ # Remove the old FAISS store logic
707
+ # This one function now handles deleting the old cloud collection
708
+ # and uploading the new one.
709
+ build_or_update_qdrant_store(folder)
710
+
711
+
712
+ # ---
713
+ # πŸš€ GRADIO INTERFACE SECTION (WITH TABS)
714
+ # ---
715
+
716
+ def gradio_ask_question(query_text, k=3):
717
+ """
718
+ A wrapper function for Gradio that calls the RAG pipeline
719
+ and formats the output as a single string.
720
+ """
721
+ print("\n" + "=" * 80)
722
+ print(f"❓ GRADIO QUERY: {query_text}")
723
+ print("=" * 80)
724
+
725
+ # Step 1: Retrieve relevant documents (using NEW Qdrant function)
726
+ print("\nπŸ“š Retrieving relevant documents from Qdrant...")
727
+ retrieved_docs = query_qdrant_store(query_text, k=k) # <-- CHANGED
728
+
729
+ if not retrieved_docs:
730
+ print("❌ No relevant documents found.")
731
+ return "❌ No relevant documents found to answer your question."
732
+
733
+ print(f"\nπŸ“„ Retrieved {len(retrieved_docs)} relevant documents:")
734
+ for i, doc in enumerate(retrieved_docs, 1):
735
+ # Show the new metadata in the console log
736
+ print(f" {i}. {doc['source']} (Score: {doc['score']:.4f})")
737
+ print(f" Metadata: {doc['metadata']}")
738
+
739
+
740
+ # Step 2: Generate answer with LLM (using your existing function)
741
+ answer = answer_question_with_llm(query_text, retrieved_docs)
742
+
743
+ # Step 3: Format the response for the chat bubble
744
+ sources_md = "\n\n---\n**πŸ“š Sources Used:**\n"
745
+ for i, doc in enumerate(retrieved_docs, 1):
746
+ sources_md += f"* **{doc['source']}** (Score: {doc['score']:.4f})\n"
747
+
748
+ final_response = answer + sources_md
749
+
750
+ print("\n" + "=" * 80)
751
+ print("πŸ’‘ ANSWER (for Gradio):")
752
+ print(final_response)
753
+ print("=" * 80)
754
+
755
+ return final_response
756
+
757
+
758
+ def chat_response_func(message, history):
759
+ """
760
+ The main function that Gradio's ChatInterface will call.
761
+ """
762
+ return gradio_ask_question(message, k=3)
763
+
764
+
765
+ print("\n" + "=" * 80)
766
+ print("πŸš€ LAUNCHING GRADIO INTERFACE WITH TABS...")
767
+ print("=" * 80)
768
+ print("Visit the URL in your terminal (usually http://127.0.0.1:7860) to chat or upload.")
769
+ print("=" * 80)
770
+
771
+ # Create the Gradio UI using Blocks for tabs
772
+ with gr.Blocks(theme="soft") as demo:
773
+ gr.Markdown("# 🧠 Multimodal RAG System (Powered by Qdrant Cloud)")
774
+
775
+ with gr.Tabs():
776
+ # --- CHAT TAB ---
777
+ with gr.TabItem("Chat with Documents"):
778
+ gr.ChatInterface(
779
+ fn=chat_response_func,
780
+ title="Multimodal RAG Chat",
781
+ description="Ask questions about your documents (PDFs, images, text). The system uses Llama 4 Scout for vision and Llama 3.3 70B for answers.",
782
+ examples=[
783
+ "What documents contain bar charts?",
784
+ "Summarize the information about pollution",
785
+ "What are the key findings in the environmental report?",
786
+ "Describe the graphs showing water quality"
787
+ ],
788
+ )
789
+
790
+ # --- UPLOAD TAB ---
791
+ with gr.TabItem("Upload New Documents"):
792
+ gr.Markdown("Upload new PDF, image, or text files to add them to the knowledge base.")
793
+
794
+ file_uploader = gr.File(
795
+ label="Upload Documents",
796
+ file_count="multiple",
797
+ file_types=["image", ".pdf", ".txt", ".md"],
798
+ interactive=True
799
+ )
800
+
801
+ upload_button = gr.Button("Process and Add Documents", variant="primary")
802
+
803
+ status_output = gr.Markdown("Status: Ready to upload new documents.")
804
+
805
+ # Connect the upload button to the processing function
806
+ upload_button.click(
807
+ fn=process_uploaded_files, # This now calls the Qdrant upload function
808
+ inputs=[file_uploader],
809
+ outputs=[status_output]
810
+ )
811
+
812
+ # Launch the app
813
+ demo.launch()
814
+
815
+ print("\nπŸ‘‹ Interface closed. Goodbye!")