ragunath-ravi commited on
Commit
02fc469
Β·
verified Β·
1 Parent(s): 2655c55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +397 -578
app.py CHANGED
@@ -1,145 +1,133 @@
1
  import gradio as gr
2
  import os
3
- import tempfile
4
- import uuid
5
- from datetime import datetime
6
- from typing import List, Dict, Any, Optional
7
  import json
 
8
  import asyncio
9
- from dataclasses import dataclass, asdict
 
10
  import logging
11
 
12
- # Document processing imports
13
- import PyPDF2
14
- import pandas as pd
15
- from docx import Document
16
- from pptx import Presentation
17
- import markdown
18
-
19
- # ML/AI imports
20
  from langchain.text_splitter import RecursiveCharacterTextSplitter
21
  from langchain.embeddings import HuggingFaceEmbeddings
22
  from langchain.vectorstores import FAISS
23
- from langchain.schema import Document as LCDocument
24
- from huggingface_hub import InferenceClient
 
 
 
 
 
 
25
 
26
- # Setup logging
27
  logging.basicConfig(level=logging.INFO)
28
  logger = logging.getLogger(__name__)
29
 
30
- # Get HF token from environment
31
- HF_TOKEN = os.getenv('hf_token')
 
 
 
 
 
 
 
 
32
 
33
- # MCP Message Structure
34
- @dataclass
35
  class MCPMessage:
36
- sender: str
37
- receiver: str
38
- type: str
39
- trace_id: str
40
- payload: Dict[str, Any]
41
- timestamp: str = None
42
-
43
- def __post_init__(self):
44
- if self.timestamp is None:
45
- self.timestamp = datetime.now().isoformat()
46
 
47
  def to_dict(self):
48
- return asdict(self)
49
-
50
- # MCP Communication Layer
51
- class MCPCommunicator:
 
 
 
 
 
 
 
52
  def __init__(self):
53
- self.message_queue = asyncio.Queue()
54
  self.subscribers = {}
55
 
56
- async def send_message(self, message: MCPMessage):
57
- logger.info(f"MCP: {message.sender} -> {message.receiver}: {message.type}")
58
- await self.message_queue.put(message)
 
 
 
 
 
 
59
 
60
- async def receive_message(self, agent_name: str) -> MCPMessage:
61
- while True:
62
- message = await self.message_queue.get()
63
- if message.receiver == agent_name:
64
- return message
65
- # Re-queue if not for this agent
66
- await self.message_queue.put(message)
67
 
68
- # Global MCP instance
69
- mcp = MCPCommunicator()
70
 
71
- # Base Agent Class
72
- class BaseAgent:
73
- def __init__(self, name: str):
74
- self.name = name
75
- self.mcp = mcp
76
-
77
- async def send_mcp_message(self, receiver: str, msg_type: str, payload: Dict[str, Any], trace_id: str):
78
- message = MCPMessage(
79
- sender=self.name,
80
- receiver=receiver,
81
- type=msg_type,
82
- trace_id=trace_id,
83
- payload=payload
84
- )
85
- await self.mcp.send_message(message)
86
 
87
- async def receive_mcp_message(self) -> MCPMessage:
88
- return await self.mcp.receive_message(self.name)
89
-
90
- # Document Ingestion Agent
91
- class IngestionAgent(BaseAgent):
92
- def __init__(self):
93
- super().__init__("IngestionAgent")
94
  self.text_splitter = RecursiveCharacterTextSplitter(
95
  chunk_size=1000,
96
- chunk_overlap=200,
97
- length_function=len,
98
  )
99
 
 
 
 
 
 
100
  def parse_pdf(self, file_path: str) -> str:
101
- """Parse PDF file and extract text"""
102
  try:
103
  with open(file_path, 'rb') as file:
104
  pdf_reader = PyPDF2.PdfReader(file)
105
  text = ""
106
  for page in pdf_reader.pages:
107
- text += page.extract_text() + "\n"
108
  return text
109
  except Exception as e:
110
  logger.error(f"Error parsing PDF: {e}")
111
  return ""
112
 
113
- def parse_docx(self, file_path: str) -> str:
114
- """Parse DOCX file and extract text"""
115
- try:
116
- doc = Document(file_path)
117
- text = ""
118
- for paragraph in doc.paragraphs:
119
- text += paragraph.text + "\n"
120
- return text
121
- except Exception as e:
122
- logger.error(f"Error parsing DOCX: {e}")
123
- return ""
124
-
125
  def parse_pptx(self, file_path: str) -> str:
126
- """Parse PPTX file and extract text"""
127
  try:
128
  prs = Presentation(file_path)
129
  text = ""
130
- for slide_num, slide in enumerate(prs.slides, 1):
131
- text += f"Slide {slide_num}:\n"
132
  for shape in slide.shapes:
133
  if hasattr(shape, "text"):
134
  text += shape.text + "\n"
135
- text += "\n"
136
  return text
137
  except Exception as e:
138
  logger.error(f"Error parsing PPTX: {e}")
139
  return ""
140
 
141
  def parse_csv(self, file_path: str) -> str:
142
- """Parse CSV file and convert to text"""
143
  try:
144
  df = pd.read_csv(file_path)
145
  return df.to_string()
@@ -147,584 +135,415 @@ class IngestionAgent(BaseAgent):
147
  logger.error(f"Error parsing CSV: {e}")
148
  return ""
149
 
150
- def parse_txt_md(self, file_path: str) -> str:
151
- """Parse TXT or MD file"""
 
 
 
 
 
 
 
 
 
 
 
 
152
  try:
153
  with open(file_path, 'r', encoding='utf-8') as file:
154
- content = file.read()
155
- # If markdown, convert to plain text
156
- if file_path.lower().endswith('.md'):
157
- content = markdown.markdown(content)
158
- return content
159
  except Exception as e:
160
- logger.error(f"Error parsing TXT/MD: {e}")
161
  return ""
162
 
163
- async def process_documents(self, files: List[str], trace_id: str) -> List[LCDocument]:
164
- """Process uploaded documents and return chunked documents"""
165
- all_documents = []
 
166
 
167
  for file_path in files:
168
  file_ext = os.path.splitext(file_path)[1].lower()
169
- filename = os.path.basename(file_path)
170
 
171
- # Parse based on file extension
172
  if file_ext == '.pdf':
173
- content = self.parse_pdf(file_path)
174
- elif file_ext == '.docx':
175
- content = self.parse_docx(file_path)
176
  elif file_ext == '.pptx':
177
- content = self.parse_pptx(file_path)
178
  elif file_ext == '.csv':
179
- content = self.parse_csv(file_path)
 
 
180
  elif file_ext in ['.txt', '.md']:
181
- content = self.parse_txt_md(file_path)
182
  else:
183
  logger.warning(f"Unsupported file type: {file_ext}")
184
  continue
185
 
186
- if content.strip():
187
- # Split content into chunks
188
- chunks = self.text_splitter.split_text(content)
189
-
190
- # Create LangChain documents
191
- for i, chunk in enumerate(chunks):
192
- doc = LCDocument(
193
- page_content=chunk,
194
- metadata={
195
- "source": filename,
196
- "chunk_id": i,
197
- "file_type": file_ext
198
- }
199
- )
200
- all_documents.append(doc)
201
 
202
- return all_documents
203
-
204
- # Retrieval Agent
205
- class RetrievalAgent(BaseAgent):
206
- def __init__(self):
207
- super().__init__("RetrievalAgent")
208
- self.embeddings = HuggingFaceEmbeddings(
209
- model_name="sentence-transformers/all-MiniLM-L6-v2"
210
  )
 
 
 
 
 
 
 
 
 
211
  self.vector_store = None
212
 
213
- async def create_vector_store(self, documents: List[LCDocument], trace_id: str):
214
- """Create vector store from documents"""
215
- try:
216
- if documents:
217
- self.vector_store = FAISS.from_documents(documents, self.embeddings)
218
- logger.info(f"Created vector store with {len(documents)} documents")
219
- else:
220
- logger.warning("No documents to create vector store")
221
- except Exception as e:
222
- logger.error(f"Error creating vector store: {e}")
223
 
224
- async def retrieve_relevant_chunks(self, query: str, k: int = 5, trace_id: str = None) -> List[Dict]:
225
- """Retrieve relevant chunks for a query"""
226
- if not self.vector_store:
227
- return []
228
 
229
- try:
230
- # Similarity search
231
- docs = self.vector_store.similarity_search(query, k=k)
232
-
233
- # Format results
234
- results = []
235
- for doc in docs:
236
- results.append({
237
- "content": doc.page_content,
238
- "source": doc.metadata.get("source", "Unknown"),
239
- "chunk_id": doc.metadata.get("chunk_id", 0),
240
- "file_type": doc.metadata.get("file_type", "Unknown")
241
- })
242
-
243
- return results
244
- except Exception as e:
245
- logger.error(f"Error retrieving chunks: {e}")
246
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
- # LLM Response Agent
249
- class LLMResponseAgent(BaseAgent):
250
- def __init__(self):
251
- super().__init__("LLMResponseAgent")
252
- self.client = InferenceClient(
253
- model="meta-llama/Llama-3.1-8B",
254
- token=HF_TOKEN
255
- )
 
 
 
 
256
 
257
- def format_prompt(self, query: str, context_chunks: List[Dict]) -> str:
258
- """Format prompt with context and query"""
259
- context_text = "\n\n".join([
260
- f"Source: {chunk['source']}\nContent: {chunk['content']}"
261
- for chunk in context_chunks
262
- ])
263
 
264
- prompt = f"""Based on the following context from uploaded documents, please answer the user's question.
 
 
 
 
265
 
266
  Context:
267
  {context_text}
268
 
269
  Question: {query}
270
 
271
- Please provide a comprehensive answer based on the context above. If the context doesn't contain enough information to fully answer the question, please mention what information is available and what might be missing.
272
-
273
  Answer:"""
274
-
275
- return prompt
276
-
277
- async def generate_response(self, query: str, context_chunks: List[Dict], trace_id: str) -> str:
278
- """Generate response using LLM"""
279
  try:
280
- prompt = self.format_prompt(query, context_chunks)
281
-
282
- # Generate response using HuggingFace Inference
283
- response = self.client.text_generation(
284
  prompt,
285
  max_new_tokens=512,
286
  temperature=0.7,
287
- do_sample=True,
288
- return_full_text=False
289
  )
290
 
291
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  except Exception as e:
293
  logger.error(f"Error generating response: {e}")
294
- return f"I apologize, but I encountered an error while generating the response: {str(e)}"
295
 
296
- # Coordinator Agent
297
- class CoordinatorAgent(BaseAgent):
298
- def __init__(self):
299
- super().__init__("CoordinatorAgent")
300
- self.ingestion_agent = IngestionAgent()
301
- self.retrieval_agent = RetrievalAgent()
302
- self.llm_agent = LLMResponseAgent()
303
- self.documents_processed = False
304
 
305
- async def process_documents(self, files: List[str]) -> str:
306
- """Orchestrate document processing"""
307
- trace_id = str(uuid.uuid4())
308
-
309
- try:
310
- # Step 1: Ingestion
311
- await self.send_mcp_message(
312
- "IngestionAgent",
313
- "DOCUMENT_INGESTION_REQUEST",
314
- {"files": files},
315
- trace_id
316
- )
317
-
318
- documents = await self.ingestion_agent.process_documents(files, trace_id)
319
-
320
- await self.send_mcp_message(
321
- "RetrievalAgent",
322
- "VECTOR_STORE_CREATE_REQUEST",
323
- {"documents": len(documents)},
324
- trace_id
325
- )
326
-
327
- # Step 2: Create vector store
328
- await self.retrieval_agent.create_vector_store(documents, trace_id)
329
-
330
- self.documents_processed = True
331
-
332
- return f"Successfully processed {len(documents)} document chunks from {len(files)} files."
333
-
334
- except Exception as e:
335
- logger.error(f"Error in document processing: {e}")
336
- return f"Error processing documents: {str(e)}"
337
 
338
- async def answer_query(self, query: str) -> tuple[str, List[Dict]]:
339
- """Orchestrate query answering"""
340
- if not self.documents_processed:
341
- return "Please upload and process documents first.", []
 
 
 
 
 
 
 
342
 
343
- trace_id = str(uuid.uuid4())
344
 
345
- try:
346
- # Step 1: Retrieval
347
- await self.send_mcp_message(
348
- "RetrievalAgent",
349
- "RETRIEVAL_REQUEST",
350
- {"query": query},
351
- trace_id
352
- )
353
-
354
- context_chunks = await self.retrieval_agent.retrieve_relevant_chunks(query, k=5, trace_id=trace_id)
355
-
356
- # Step 2: LLM Response
357
- await self.send_mcp_message(
358
- "LLMResponseAgent",
359
- "LLM_GENERATION_REQUEST",
360
- {"query": query, "context_chunks": len(context_chunks)},
361
- trace_id
362
- )
363
-
364
- response = await self.llm_agent.generate_response(query, context_chunks, trace_id)
365
-
366
- return response, context_chunks
367
-
368
- except Exception as e:
369
- logger.error(f"Error in query processing: {e}")
370
- return f"Error processing query: {str(e)}", []
371
-
372
- # Global coordinator instance
373
- coordinator = CoordinatorAgent()
374
-
375
- async def process_files(files):
376
- """Process uploaded files"""
377
- if not files:
378
- return "❌ Please upload at least one file."
379
 
380
- # Save uploaded files to temporary directory
381
- file_paths = []
382
- for file in files:
383
- # Handle file path - Gradio returns file path as string
384
- if hasattr(file, 'name'):
385
- file_path = file.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  else:
387
- file_path = str(file)
388
- file_paths.append(file_path)
389
-
390
- result = await coordinator.process_documents(file_paths)
391
-
392
- return result
393
-
394
- async def answer_question(query, history):
395
- """Answer user question"""
396
- if not query.strip():
397
- return history, ""
398
-
399
- response, context_chunks = await coordinator.answer_query(query)
400
-
401
- # Format response with sources
402
- if context_chunks:
403
- sources = "\n\n**Sources:**\n"
404
- for i, chunk in enumerate(context_chunks[:3], 1): # Show top 3 sources
405
- sources += f"{i}. {chunk['source']} (Chunk {chunk['chunk_id']})\n"
406
- response += sources
407
-
408
- # Add to chat history
409
- history.append((query, response))
410
-
411
- return history, ""
412
-
413
- # Custom CSS
414
- custom_css = """
415
- /* Main container styling */
416
- .gradio-container {
417
- max-width: 1200px !important;
418
- margin: 0 auto !important;
419
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
420
- }
421
-
422
- /* Header styling */
423
- .header-container {
424
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
425
- color: white !important;
426
- padding: 2rem !important;
427
- border-radius: 15px !important;
428
- margin-bottom: 2rem !important;
429
- text-align: center !important;
430
- box-shadow: 0 8px 32px rgba(0,0,0,0.1) !important;
431
- }
432
-
433
- .header-title {
434
- font-size: 2.5rem !important;
435
- font-weight: 700 !important;
436
- margin-bottom: 0.5rem !important;
437
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3) !important;
438
- }
439
-
440
- .header-subtitle {
441
- font-size: 1.2rem !important;
442
- opacity: 0.9 !important;
443
- font-weight: 300 !important;
444
- }
445
-
446
- /* Tab styling */
447
- .tab-nav {
448
- background: white !important;
449
- border-radius: 12px !important;
450
- box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important;
451
- padding: 0.5rem !important;
452
- margin-bottom: 1rem !important;
453
- }
454
-
455
- /* Card styling */
456
- .setup-card, .upload-card, .chat-card {
457
- background: white !important;
458
- border-radius: 15px !important;
459
- padding: 2rem !important;
460
- box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important;
461
- border: 1px solid #e1e5e9 !important;
462
- margin-bottom: 1.5rem !important;
463
- }
464
-
465
- /* Button styling */
466
- .primary-button {
467
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
468
- color: white !important;
469
- border: none !important;
470
- border-radius: 10px !important;
471
- padding: 0.75rem 2rem !important;
472
- font-weight: 600 !important;
473
- transition: all 0.3s ease !important;
474
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
475
- }
476
-
477
- .primary-button:hover {
478
- transform: translateY(-2px) !important;
479
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
480
- }
481
 
482
- /* Chat interface styling */
483
- .chat-container {
484
- max-height: 600px !important;
485
- overflow-y: auto !important;
486
- background: #f8f9fa !important;
487
- border-radius: 15px !important;
488
- padding: 1rem !important;
489
- border: 1px solid #e1e5e9 !important;
490
- }
491
 
492
- /* Input styling */
493
- .input-container input, .input-container textarea {
494
- border: 2px solid #e1e5e9 !important;
495
- border-radius: 10px !important;
496
- padding: 0.75rem 1rem !important;
497
- font-size: 1rem !important;
498
- transition: border-color 0.3s ease !important;
499
- }
500
-
501
- .input-container input:focus, .input-container textarea:focus {
502
- border-color: #667eea !important;
503
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
504
- outline: none !important;
505
- }
506
-
507
- /* Status indicators */
508
- .status-success {
509
- color: #28a745 !important;
510
- background: #d4edda !important;
511
- padding: 0.75rem 1rem !important;
512
- border-radius: 8px !important;
513
- border: 1px solid #c3e6cb !important;
514
- margin: 1rem 0 !important;
515
- }
516
-
517
- .status-error {
518
- color: #dc3545 !important;
519
- background: #f8d7da !important;
520
- padding: 0.75rem 1rem !important;
521
- border-radius: 8px !important;
522
- border: 1px solid #f5c6cb !important;
523
- margin: 1rem 0 !important;
524
- }
525
-
526
- /* File upload styling */
527
- .file-upload {
528
- border: 2px dashed #667eea !important;
529
- border-radius: 15px !important;
530
- padding: 2rem !important;
531
- text-align: center !important;
532
- background: #f8f9ff !important;
533
- transition: all 0.3s ease !important;
534
- }
535
-
536
- .file-upload:hover {
537
- border-color: #764ba2 !important;
538
- background: #f0f4ff !important;
539
- }
540
-
541
- /* Architecture diagram container */
542
- .architecture-container {
543
- background: white !important;
544
- border-radius: 15px !important;
545
- padding: 2rem !important;
546
- margin: 1rem 0 !important;
547
- box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important;
548
- text-align: center !important;
549
- }
550
-
551
- /* Responsive design */
552
- @media (max-width: 768px) {
553
- .header-title {
554
- font-size: 2rem !important;
555
- }
556
-
557
- .setup-card, .upload-card, .chat-card {
558
- padding: 1.5rem !important;
559
- }
560
- }
561
-
562
- /* Animation for loading states */
563
- @keyframes pulse {
564
- 0% { opacity: 1; }
565
- 50% { opacity: 0.5; }
566
- 100% { opacity: 1; }
567
- }
568
-
569
- .loading {
570
- animation: pulse 1.5s ease-in-out infinite !important;
571
- }
572
- """
573
-
574
- # Create Gradio Interface
575
  def create_interface():
576
- with gr.Blocks(css=custom_css, title="πŸ€– Agentic RAG Chatbot") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  gr.HTML("""
578
- <div class="header-container">
579
- <h1 class="header-title">πŸ€– Agentic RAG Chatbot</h1>
580
- <p class="header-subtitle">Multi-Format Document QA using Model Context Protocol (MCP)</p>
581
- </div>
582
  """)
583
 
584
- with gr.Tabs() as tabs:
585
- # Upload Tab
586
- with gr.TabItem("πŸ“ Upload Documents", elem_classes=["tab-nav"]):
587
- gr.HTML("""
588
- <div class="upload-card">
589
- <h3>πŸ“„ Document Upload</h3>
590
- <p>Upload your documents in any supported format: PDF, DOCX, PPTX, CSV, TXT, or Markdown.</p>
591
- </div>
592
- """)
593
 
594
  file_upload = gr.File(
595
- label="Choose Files",
596
  file_count="multiple",
597
- file_types=[".pdf", ".docx", ".pptx", ".csv", ".txt", ".md"],
598
- elem_classes=["file-upload"]
599
- )
600
-
601
- upload_button = gr.Button(
602
- "Process Documents",
603
- variant="primary",
604
- elem_classes=["primary-button"]
605
  )
606
 
607
  upload_status = gr.Textbox(
608
- label="Processing Status",
609
  interactive=False,
610
- elem_classes=["input-container"]
611
  )
612
-
613
- # Chat Tab
614
- with gr.TabItem("πŸ’¬ Chat", elem_classes=["tab-nav"]):
615
- gr.HTML("""
616
- <div class="chat-card">
617
- <h3>πŸ—¨οΈ Ask Questions</h3>
618
- <p>Ask questions about your uploaded documents. The AI will provide answers based on the document content.</p>
619
- </div>
 
 
 
 
 
 
 
 
620
  """)
 
 
 
621
 
622
  chatbot = gr.Chatbot(
623
- label="Conversation",
624
- height=400,
625
- elem_classes=["chat-container"]
 
626
  )
627
 
628
  with gr.Row():
629
- query_input = gr.Textbox(
630
- label="Your Question",
631
- placeholder="What are the key findings in the document?",
632
- elem_classes=["input-container"]
633
- )
634
- ask_button = gr.Button(
635
- "Ask",
636
- variant="primary",
637
- elem_classes=["primary-button"]
638
  )
 
639
 
640
  gr.Examples(
641
  examples=[
642
- "What are the main topics covered in the documents?",
643
  "Can you summarize the key findings?",
644
- "What are the important metrics mentioned?",
645
  "What recommendations are provided?",
 
646
  ],
647
- inputs=query_input,
648
- label="Example Questions"
649
  )
650
-
651
- # Architecture Tab
652
- with gr.TabItem("πŸ—οΈ Architecture", elem_classes=["tab-nav"]):
653
- gr.HTML("""
654
- <div class="architecture-container">
655
- <h3>πŸ›οΈ System Architecture</h3>
656
- <p>This system uses an agentic architecture with Model Context Protocol (MCP) for inter-agent communication.</p>
657
- </div>
658
- """)
659
-
660
- gr.Markdown("""
661
- ## πŸ”„ Agent Flow Diagram
662
-
663
- ```
664
- User Upload β†’ CoordinatorAgent β†’ IngestionAgent β†’ RetrievalAgent β†’ LLMResponseAgent
665
- ↓ ↓ ↓ ↓ ↓
666
- Documents MCP Messages Text Chunks Vector Store Final Response
667
- ```
668
-
669
- ## πŸ€– Agent Descriptions
670
-
671
- - **CoordinatorAgent**: Orchestrates the entire workflow and manages MCP communication
672
- - **IngestionAgent**: Parses and preprocesses documents (PDF, DOCX, PPTX, CSV, TXT, MD)
673
- - **RetrievalAgent**: Handles embeddings and semantic retrieval using FAISS
674
- - **LLMResponseAgent**: Generates final responses using Llama-3.1-8B-Instruct
675
-
676
- ## πŸ”— Tech Stack
677
-
678
- - **Frontend**: Gradio with custom CSS
679
- - **LLM**: Meta Llama-3.1-8B-Instruct (via HuggingFace Inference)
680
- - **Embeddings**: sentence-transformers/all-MiniLM-L6-v2
681
- - **Vector Store**: FAISS
682
- - **Document Processing**: PyPDF2, python-docx, python-pptx, pandas
683
- - **Framework**: LangChain for document handling
684
-
685
- ## πŸ“¨ MCP Message Example
686
-
687
- ```json
688
- {
689
- "sender": "RetrievalAgent",
690
- "receiver": "LLMResponseAgent",
691
- "type": "RETRIEVAL_RESULT",
692
- "trace_id": "rag-457",
693
- "payload": {
694
- "retrieved_context": ["Revenue increased by 25%", "Q1 KPIs exceeded targets"],
695
- "query": "What were the Q1 KPIs?"
696
- },
697
- "timestamp": "2025-07-21T10:30:00Z"
698
- }
699
- ```
700
- """)
701
 
702
  # Event handlers
703
- upload_button.click(
704
- fn=process_files,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
  inputs=[file_upload],
706
  outputs=[upload_status]
707
  )
708
 
709
- ask_button.click(
710
- fn=answer_question,
711
- inputs=[query_input, chatbot],
712
- outputs=[chatbot, query_input]
 
713
  )
714
 
715
- query_input.submit(
716
- fn=answer_question,
717
- inputs=[query_input, chatbot],
718
- outputs=[chatbot, query_input]
 
719
  )
720
 
721
- return demo
722
 
 
723
  if __name__ == "__main__":
724
  demo = create_interface()
725
  demo.launch(
726
  share=True,
727
  server_name="0.0.0.0",
728
- server_port=7860,
729
- show_api=False
730
  )
 
1
  import gradio as gr
2
  import os
 
 
 
 
3
  import json
4
+ import uuid
5
  import asyncio
6
+ from datetime import datetime
7
+ from typing import List, Dict, Any, Optional, Generator
8
  import logging
9
 
10
+ # Import required libraries
11
+ from huggingface_hub import InferenceClient
 
 
 
 
 
 
12
  from langchain.text_splitter import RecursiveCharacterTextSplitter
13
  from langchain.embeddings import HuggingFaceEmbeddings
14
  from langchain.vectorstores import FAISS
15
+ from langchain.docstore.document import Document
16
+
17
+ # Import document parsers
18
+ import PyPDF2
19
+ from pptx import Presentation
20
+ import pandas as pd
21
+ from docx import Document as DocxDocument
22
+ import io
23
 
24
+ # Configure logging
25
  logging.basicConfig(level=logging.INFO)
26
  logger = logging.getLogger(__name__)
27
 
28
+ # Get HuggingFace token from environment
29
+ HF_TOKEN = os.getenv("hf_token")
30
+ if not HF_TOKEN:
31
+ raise ValueError("HuggingFace token not found in environment variables")
32
+
33
+ # Initialize HuggingFace Inference Client
34
+ client = InferenceClient(model="meta-llama/Llama-3.1-8B-Instruct", token=HF_TOKEN)
35
+
36
+ # Initialize embeddings
37
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
38
 
 
 
39
  class MCPMessage:
40
+ """Model Context Protocol Message Structure"""
41
+ def __init__(self, sender: str, receiver: str, msg_type: str,
42
+ trace_id: str = None, payload: Dict = None):
43
+ self.sender = sender
44
+ self.receiver = receiver
45
+ self.type = msg_type
46
+ self.trace_id = trace_id or str(uuid.uuid4())
47
+ self.payload = payload or {}
48
+ self.timestamp = datetime.now().isoformat()
 
49
 
50
  def to_dict(self):
51
+ return {
52
+ "sender": self.sender,
53
+ "receiver": self.receiver,
54
+ "type": self.type,
55
+ "trace_id": self.trace_id,
56
+ "payload": self.payload,
57
+ "timestamp": self.timestamp
58
+ }
59
+
60
+ class MessageBus:
61
+ """In-memory message bus for MCP communication"""
62
  def __init__(self):
63
+ self.messages = []
64
  self.subscribers = {}
65
 
66
+ def publish(self, message: MCPMessage):
67
+ """Publish message to the bus"""
68
+ self.messages.append(message)
69
+ logger.info(f"Message published: {message.sender} -> {message.receiver} [{message.type}]")
70
+
71
+ # Notify subscribers
72
+ if message.receiver in self.subscribers:
73
+ for callback in self.subscribers[message.receiver]:
74
+ callback(message)
75
 
76
+ def subscribe(self, agent_name: str, callback):
77
+ """Subscribe agent to receive messages"""
78
+ if agent_name not in self.subscribers:
79
+ self.subscribers[agent_name] = []
80
+ self.subscribers[agent_name].append(callback)
 
 
81
 
82
+ # Global message bus
83
+ message_bus = MessageBus()
84
 
85
+ class IngestionAgent:
86
+ """Agent responsible for document parsing and preprocessing"""
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ def __init__(self, message_bus: MessageBus):
89
+ self.name = "IngestionAgent"
90
+ self.message_bus = message_bus
91
+ self.message_bus.subscribe(self.name, self.handle_message)
 
 
 
92
  self.text_splitter = RecursiveCharacterTextSplitter(
93
  chunk_size=1000,
94
+ chunk_overlap=200
 
95
  )
96
 
97
+ def handle_message(self, message: MCPMessage):
98
+ """Handle incoming MCP messages"""
99
+ if message.type == "INGESTION_REQUEST":
100
+ self.process_documents(message)
101
+
102
  def parse_pdf(self, file_path: str) -> str:
103
+ """Parse PDF document"""
104
  try:
105
  with open(file_path, 'rb') as file:
106
  pdf_reader = PyPDF2.PdfReader(file)
107
  text = ""
108
  for page in pdf_reader.pages:
109
+ text += page.extract_text()
110
  return text
111
  except Exception as e:
112
  logger.error(f"Error parsing PDF: {e}")
113
  return ""
114
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  def parse_pptx(self, file_path: str) -> str:
116
+ """Parse PPTX document"""
117
  try:
118
  prs = Presentation(file_path)
119
  text = ""
120
+ for slide in prs.slides:
 
121
  for shape in slide.shapes:
122
  if hasattr(shape, "text"):
123
  text += shape.text + "\n"
 
124
  return text
125
  except Exception as e:
126
  logger.error(f"Error parsing PPTX: {e}")
127
  return ""
128
 
129
  def parse_csv(self, file_path: str) -> str:
130
+ """Parse CSV document"""
131
  try:
132
  df = pd.read_csv(file_path)
133
  return df.to_string()
 
135
  logger.error(f"Error parsing CSV: {e}")
136
  return ""
137
 
138
+ def parse_docx(self, file_path: str) -> str:
139
+ """Parse DOCX document"""
140
+ try:
141
+ doc = DocxDocument(file_path)
142
+ text = ""
143
+ for paragraph in doc.paragraphs:
144
+ text += paragraph.text + "\n"
145
+ return text
146
+ except Exception as e:
147
+ logger.error(f"Error parsing DOCX: {e}")
148
+ return ""
149
+
150
+ def parse_txt(self, file_path: str) -> str:
151
+ """Parse TXT/Markdown document"""
152
  try:
153
  with open(file_path, 'r', encoding='utf-8') as file:
154
+ return file.read()
 
 
 
 
155
  except Exception as e:
156
+ logger.error(f"Error parsing TXT: {e}")
157
  return ""
158
 
159
+ def process_documents(self, message: MCPMessage):
160
+ """Process uploaded documents"""
161
+ files = message.payload.get("files", [])
162
+ processed_docs = []
163
 
164
  for file_path in files:
165
  file_ext = os.path.splitext(file_path)[1].lower()
 
166
 
167
+ # Parse document based on file type
168
  if file_ext == '.pdf':
169
+ text = self.parse_pdf(file_path)
 
 
170
  elif file_ext == '.pptx':
171
+ text = self.parse_pptx(file_path)
172
  elif file_ext == '.csv':
173
+ text = self.parse_csv(file_path)
174
+ elif file_ext == '.docx':
175
+ text = self.parse_docx(file_path)
176
  elif file_ext in ['.txt', '.md']:
177
+ text = self.parse_txt(file_path)
178
  else:
179
  logger.warning(f"Unsupported file type: {file_ext}")
180
  continue
181
 
182
+ if text:
183
+ # Split text into chunks
184
+ chunks = self.text_splitter.split_text(text)
185
+ docs = [Document(page_content=chunk, metadata={"source": file_path})
186
+ for chunk in chunks]
187
+ processed_docs.extend(docs)
 
 
 
 
 
 
 
 
 
188
 
189
+ # Send processed documents to RetrievalAgent
190
+ response = MCPMessage(
191
+ sender=self.name,
192
+ receiver="RetrievalAgent",
193
+ msg_type="INGESTION_COMPLETE",
194
+ trace_id=message.trace_id,
195
+ payload={"documents": processed_docs}
 
196
  )
197
+ self.message_bus.publish(response)
198
+
199
+ class RetrievalAgent:
200
+ """Agent responsible for embedding and semantic retrieval"""
201
+
202
+ def __init__(self, message_bus: MessageBus):
203
+ self.name = "RetrievalAgent"
204
+ self.message_bus = message_bus
205
+ self.message_bus.subscribe(self.name, self.handle_message)
206
  self.vector_store = None
207
 
208
+ def handle_message(self, message: MCPMessage):
209
+ """Handle incoming MCP messages"""
210
+ if message.type == "INGESTION_COMPLETE":
211
+ self.create_vector_store(message)
212
+ elif message.type == "RETRIEVAL_REQUEST":
213
+ self.retrieve_context(message)
 
 
 
 
214
 
215
+ def create_vector_store(self, message: MCPMessage):
216
+ """Create vector store from processed documents"""
217
+ documents = message.payload.get("documents", [])
 
218
 
219
+ if documents:
220
+ try:
221
+ self.vector_store = FAISS.from_documents(documents, embeddings)
222
+ logger.info(f"Vector store created with {len(documents)} documents")
223
+
224
+ # Notify completion
225
+ response = MCPMessage(
226
+ sender=self.name,
227
+ receiver="CoordinatorAgent",
228
+ msg_type="VECTORSTORE_READY",
229
+ trace_id=message.trace_id,
230
+ payload={"status": "ready"}
231
+ )
232
+ self.message_bus.publish(response)
233
+ except Exception as e:
234
+ logger.error(f"Error creating vector store: {e}")
235
+
236
+ def retrieve_context(self, message: MCPMessage):
237
+ """Retrieve relevant context for a query"""
238
+ query = message.payload.get("query", "")
239
+ k = message.payload.get("k", 3)
240
+
241
+ if self.vector_store and query:
242
+ try:
243
+ docs = self.vector_store.similarity_search(query, k=k)
244
+ context = [{"content": doc.page_content, "source": doc.metadata.get("source", "")}
245
+ for doc in docs]
246
+
247
+ response = MCPMessage(
248
+ sender=self.name,
249
+ receiver="LLMResponseAgent",
250
+ msg_type="CONTEXT_RESPONSE",
251
+ trace_id=message.trace_id,
252
+ payload={
253
+ "query": query,
254
+ "retrieved_context": context,
255
+ "top_chunks": [doc.page_content for doc in docs]
256
+ }
257
+ )
258
+ self.message_bus.publish(response)
259
+ except Exception as e:
260
+ logger.error(f"Error retrieving context: {e}")
261
 
262
+ class LLMResponseAgent:
263
+ """Agent responsible for generating LLM responses"""
264
+
265
+ def __init__(self, message_bus: MessageBus):
266
+ self.name = "LLMResponseAgent"
267
+ self.message_bus = message_bus
268
+ self.message_bus.subscribe(self.name, self.handle_message)
269
+
270
+ def handle_message(self, message: MCPMessage):
271
+ """Handle incoming MCP messages"""
272
+ if message.type == "CONTEXT_RESPONSE":
273
+ self.generate_response(message)
274
 
275
+ def generate_response(self, message: MCPMessage):
276
+ """Generate response using retrieved context"""
277
+ query = message.payload.get("query", "")
278
+ context = message.payload.get("retrieved_context", [])
 
 
279
 
280
+ # Build prompt with context
281
+ context_text = "\n\n".join([f"Source: {ctx['source']}\nContent: {ctx['content']}"
282
+ for ctx in context])
283
+
284
+ prompt = f"""Based on the following context, please answer the user's question accurately and comprehensively.
285
 
286
  Context:
287
  {context_text}
288
 
289
  Question: {query}
290
 
 
 
291
  Answer:"""
292
+
 
 
 
 
293
  try:
294
+ # Generate streaming response
295
+ response_stream = client.text_generation(
 
 
296
  prompt,
297
  max_new_tokens=512,
298
  temperature=0.7,
299
+ stream=True
 
300
  )
301
 
302
+ # Send streaming response
303
+ response = MCPMessage(
304
+ sender=self.name,
305
+ receiver="CoordinatorAgent",
306
+ msg_type="LLM_RESPONSE_STREAM",
307
+ trace_id=message.trace_id,
308
+ payload={
309
+ "query": query,
310
+ "response_stream": response_stream,
311
+ "context": context
312
+ }
313
+ )
314
+ self.message_bus.publish(response)
315
+
316
  except Exception as e:
317
  logger.error(f"Error generating response: {e}")
 
318
 
319
+ class CoordinatorAgent:
320
+ """Coordinator agent that orchestrates the entire workflow"""
 
 
 
 
 
 
321
 
322
+ def __init__(self, message_bus: MessageBus):
323
+ self.name = "CoordinatorAgent"
324
+ self.message_bus = message_bus
325
+ self.message_bus.subscribe(self.name, self.handle_message)
326
+ self.current_response_stream = None
327
+ self.vector_store_ready = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ def handle_message(self, message: MCPMessage):
330
+ """Handle incoming MCP messages"""
331
+ if message.type == "VECTORSTORE_READY":
332
+ self.vector_store_ready = True
333
+ elif message.type == "LLM_RESPONSE_STREAM":
334
+ self.current_response_stream = message.payload.get("response_stream")
335
+
336
+ def process_files(self, files):
337
+ """Process uploaded files"""
338
+ if not files:
339
+ return "No files uploaded."
340
 
341
+ file_paths = [file.name for file in files]
342
 
343
+ # Send ingestion request
344
+ message = MCPMessage(
345
+ sender=self.name,
346
+ receiver="IngestionAgent",
347
+ msg_type="INGESTION_REQUEST",
348
+ payload={"files": file_paths}
349
+ )
350
+ self.message_bus.publish(message)
351
+
352
+ return f"Processing {len(files)} files: {', '.join([os.path.basename(fp) for fp in file_paths])}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
+ def handle_query(self, query: str, history: List):
355
+ """Handle user query and return streaming response"""
356
+ if not self.vector_store_ready:
357
+ yield "Please upload and process documents first."
358
+ return
359
+
360
+ # Send retrieval request
361
+ message = MCPMessage(
362
+ sender=self.name,
363
+ receiver="RetrievalAgent",
364
+ msg_type="RETRIEVAL_REQUEST",
365
+ payload={"query": query}
366
+ )
367
+ self.message_bus.publish(message)
368
+
369
+ # Wait for response and stream
370
+ import time
371
+ timeout = 10 # seconds
372
+ start_time = time.time()
373
+
374
+ while not self.current_response_stream and (time.time() - start_time) < timeout:
375
+ time.sleep(0.1)
376
+
377
+ if self.current_response_stream:
378
+ partial_response = ""
379
+ try:
380
+ for token in self.current_response_stream:
381
+ if token:
382
+ partial_response += token
383
+ yield partial_response
384
+ time.sleep(0.05) # Simulate streaming delay
385
+ except Exception as e:
386
+ yield f"Error generating response: {e}"
387
+ finally:
388
+ self.current_response_stream = None
389
  else:
390
+ yield "Timeout: No response received from LLM agent."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
392
+ # Initialize agents
393
+ ingestion_agent = IngestionAgent(message_bus)
394
+ retrieval_agent = RetrievalAgent(message_bus)
395
+ llm_response_agent = LLMResponseAgent(message_bus)
396
+ coordinator_agent = CoordinatorAgent(message_bus)
 
 
 
 
397
 
398
+ # Gradio Interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  def create_interface():
400
+ """Create Gradio interface"""
401
+
402
+ with gr.Blocks(
403
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
404
+ css="""
405
+ .gradio-container {
406
+ max-width: 1200px !important;
407
+ }
408
+ .header-text {
409
+ text-align: center;
410
+ color: #667eea;
411
+ font-size: 2.5em;
412
+ font-weight: bold;
413
+ margin-bottom: 10px;
414
+ }
415
+ .subheader-text {
416
+ text-align: center;
417
+ color: #666;
418
+ font-size: 1.2em;
419
+ margin-bottom: 20px;
420
+ }
421
+ .upload-section {
422
+ border: 2px dashed #667eea;
423
+ border-radius: 10px;
424
+ padding: 20px;
425
+ margin: 10px 0;
426
+ }
427
+ .chat-container {
428
+ height: 500px;
429
+ }
430
+ """,
431
+ title="πŸ€– Agentic RAG Chatbot"
432
+ ) as iface:
433
+
434
+ # Header
435
  gr.HTML("""
436
+ <div class="header-text">πŸ€– Agentic RAG Chatbot</div>
437
+ <div class="subheader-text">Multi-Format Document QA with Model Context Protocol (MCP)</div>
 
 
438
  """)
439
 
440
+ with gr.Row():
441
+ with gr.Column(scale=1):
442
+ gr.Markdown("## πŸ“ Document Upload")
 
 
 
 
 
 
443
 
444
  file_upload = gr.File(
 
445
  file_count="multiple",
446
+ file_types=[".pdf", ".pptx", ".csv", ".docx", ".txt", ".md"],
447
+ label="Upload Documents (PDF, PPTX, CSV, DOCX, TXT, MD)",
448
+ elem_classes=["upload-section"]
 
 
 
 
 
449
  )
450
 
451
  upload_status = gr.Textbox(
452
+ label="Upload Status",
453
  interactive=False,
454
+ max_lines=3
455
  )
456
+
457
+ process_btn = gr.Button(
458
+ "πŸ”„ Process Documents",
459
+ variant="primary",
460
+ size="lg"
461
+ )
462
+
463
+ gr.Markdown("## πŸ—οΈ Architecture Info")
464
+ gr.Markdown("""
465
+ **Agents:**
466
+ - πŸ”„ IngestionAgent: Document parsing
467
+ - πŸ” RetrievalAgent: Semantic search
468
+ - πŸ€– LLMResponseAgent: Response generation
469
+ - 🎯 CoordinatorAgent: Workflow orchestration
470
+
471
+ **MCP Communication:** Structured message passing between agents
472
  """)
473
+
474
+ with gr.Column(scale=2):
475
+ gr.Markdown("## πŸ’¬ Chat Interface")
476
 
477
  chatbot = gr.Chatbot(
478
+ height=500,
479
+ elem_classes=["chat-container"],
480
+ show_copy_button=True,
481
+ bubble_full_width=False
482
  )
483
 
484
  with gr.Row():
485
+ msg = gr.Textbox(
486
+ label="Ask a question about your documents...",
487
+ placeholder="What are the key findings in the uploaded documents?",
488
+ scale=4,
489
+ submit=True
 
 
 
 
490
  )
491
+ submit_btn = gr.Button("Send πŸš€", scale=1, variant="primary")
492
 
493
  gr.Examples(
494
  examples=[
495
+ "What are the main topics discussed in the documents?",
496
  "Can you summarize the key findings?",
497
+ "What metrics or KPIs are mentioned?",
498
  "What recommendations are provided?",
499
+ "Are there any trends or patterns identified?"
500
  ],
501
+ inputs=msg
 
502
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
  # Event handlers
505
+ def process_files_handler(files):
506
+ return coordinator_agent.process_files(files)
507
+
508
+ def respond(message, history):
509
+ if message.strip():
510
+ # Add user message to history
511
+ history.append([message, ""])
512
+
513
+ # Get streaming response
514
+ for response in coordinator_agent.handle_query(message, history):
515
+ history[-1][1] = response
516
+ yield history, ""
517
+ else:
518
+ yield history, message
519
+
520
+ process_btn.click(
521
+ process_files_handler,
522
  inputs=[file_upload],
523
  outputs=[upload_status]
524
  )
525
 
526
+ submit_btn.click(
527
+ respond,
528
+ inputs=[msg, chatbot],
529
+ outputs=[chatbot, msg],
530
+ show_progress=True
531
  )
532
 
533
+ msg.submit(
534
+ respond,
535
+ inputs=[msg, chatbot],
536
+ outputs=[chatbot, msg],
537
+ show_progress=True
538
  )
539
 
540
+ return iface
541
 
542
+ # Launch the application
543
  if __name__ == "__main__":
544
  demo = create_interface()
545
  demo.launch(
546
  share=True,
547
  server_name="0.0.0.0",
548
+ server_port=7860
 
549
  )