VimalrajS04 commited on
Commit
bddaf46
Β·
0 Parent(s):

The main interface created with gradio

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