ragunath-ravi commited on
Commit
5391728
Β·
verified Β·
1 Parent(s): d4655d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +132 -139
app.py CHANGED
@@ -8,7 +8,6 @@ import json
8
  import asyncio
9
  from dataclasses import dataclass, asdict
10
  import logging
11
- import sys
12
 
13
  # Document processing imports
14
  import PyPDF2
@@ -28,14 +27,8 @@ from huggingface_hub import InferenceClient
28
  logging.basicConfig(level=logging.INFO)
29
  logger = logging.getLogger(__name__)
30
 
31
- # --- Get HF token from environment and perform a crucial check ---
32
- HF_TOKEN = os.getenv('HF_TOKEN')
33
-
34
- if HF_TOKEN is None:
35
- logger.error("FATAL ERROR: HuggingFace token (HF_TOKEN) environment variable is not set.")
36
- logger.error("Please set the HF_TOKEN environment variable before running the application.")
37
- sys.exit("HuggingFace token (HF_TOKEN) is not set. Exiting.")
38
-
39
 
40
  # MCP Message Structure
41
  @dataclass
@@ -46,11 +39,11 @@ class MCPMessage:
46
  trace_id: str
47
  payload: Dict[str, Any]
48
  timestamp: str = None
49
-
50
  def __post_init__(self):
51
  if self.timestamp is None:
52
  self.timestamp = datetime.now().isoformat()
53
-
54
  def to_dict(self):
55
  return asdict(self)
56
 
@@ -59,18 +52,18 @@ class MCPCommunicator:
59
  def __init__(self):
60
  self.message_queue = asyncio.Queue()
61
  self.subscribers = {}
62
-
63
  async def send_message(self, message: MCPMessage):
64
  logger.info(f"MCP: {message.sender} -> {message.receiver}: {message.type}")
65
  await self.message_queue.put(message)
66
-
67
  async def receive_message(self, agent_name: str) -> MCPMessage:
68
  while True:
69
  message = await self.message_queue.get()
70
  if message.receiver == agent_name:
71
  return message
72
  # Re-queue if not for this agent
73
- await self.mcp.put(message) # Corrected: Use mcp.put instead of message_queue.put directly
74
 
75
  # Global MCP instance
76
  mcp = MCPCommunicator()
@@ -80,7 +73,7 @@ class BaseAgent:
80
  def __init__(self, name: str):
81
  self.name = name
82
  self.mcp = mcp
83
-
84
  async def send_mcp_message(self, receiver: str, msg_type: str, payload: Dict[str, Any], trace_id: str):
85
  message = MCPMessage(
86
  sender=self.name,
@@ -90,7 +83,7 @@ class BaseAgent:
90
  payload=payload
91
  )
92
  await self.mcp.send_message(message)
93
-
94
  async def receive_mcp_message(self) -> MCPMessage:
95
  return await self.mcp.receive_message(self.name)
96
 
@@ -103,7 +96,7 @@ class IngestionAgent(BaseAgent):
103
  chunk_overlap=200,
104
  length_function=len,
105
  )
106
-
107
  def parse_pdf(self, file_path: str) -> str:
108
  """Parse PDF file and extract text"""
109
  try:
@@ -116,7 +109,7 @@ class IngestionAgent(BaseAgent):
116
  except Exception as e:
117
  logger.error(f"Error parsing PDF: {e}")
118
  return ""
119
-
120
  def parse_docx(self, file_path: str) -> str:
121
  """Parse DOCX file and extract text"""
122
  try:
@@ -128,7 +121,7 @@ class IngestionAgent(BaseAgent):
128
  except Exception as e:
129
  logger.error(f"Error parsing DOCX: {e}")
130
  return ""
131
-
132
  def parse_pptx(self, file_path: str) -> str:
133
  """Parse PPTX file and extract text"""
134
  try:
@@ -142,9 +135,9 @@ class IngestionAgent(BaseAgent):
142
  text += "\n"
143
  return text
144
  except Exception as e:
145
- logger.error(f"Error parsing PPTX: {e}")
146
- return ""
147
-
148
  def parse_csv(self, file_path: str) -> str:
149
  """Parse CSV file and convert to text"""
150
  try:
@@ -153,7 +146,7 @@ class IngestionAgent(BaseAgent):
153
  except Exception as e:
154
  logger.error(f"Error parsing CSV: {e}")
155
  return ""
156
-
157
  def parse_txt_md(self, file_path: str) -> str:
158
  """Parse TXT or MD file"""
159
  try:
@@ -166,15 +159,15 @@ class IngestionAgent(BaseAgent):
166
  except Exception as e:
167
  logger.error(f"Error parsing TXT/MD: {e}")
168
  return ""
169
-
170
  async def process_documents(self, files: List[str], trace_id: str) -> List[LCDocument]:
171
  """Process uploaded documents and return chunked documents"""
172
  all_documents = []
173
-
174
- for file_path in files: # file_path is already the path to the temporary file
175
  file_ext = os.path.splitext(file_path)[1].lower()
176
  filename = os.path.basename(file_path)
177
-
178
  # Parse based on file extension
179
  if file_ext == '.pdf':
180
  content = self.parse_pdf(file_path)
@@ -189,11 +182,11 @@ class IngestionAgent(BaseAgent):
189
  else:
190
  logger.warning(f"Unsupported file type: {file_ext}")
191
  continue
192
-
193
  if content.strip():
194
  # Split content into chunks
195
  chunks = self.text_splitter.split_text(content)
196
-
197
  # Create LangChain documents
198
  for i, chunk in enumerate(chunks):
199
  doc = LCDocument(
@@ -205,7 +198,7 @@ class IngestionAgent(BaseAgent):
205
  }
206
  )
207
  all_documents.append(doc)
208
-
209
  return all_documents
210
 
211
  # Retrieval Agent
@@ -216,7 +209,7 @@ class RetrievalAgent(BaseAgent):
216
  model_name="sentence-transformers/all-MiniLM-L6-v2"
217
  )
218
  self.vector_store = None
219
-
220
  async def create_vector_store(self, documents: List[LCDocument], trace_id: str):
221
  """Create vector store from documents"""
222
  try:
@@ -227,16 +220,16 @@ class RetrievalAgent(BaseAgent):
227
  logger.warning("No documents to create vector store")
228
  except Exception as e:
229
  logger.error(f"Error creating vector store: {e}")
230
-
231
  async def retrieve_relevant_chunks(self, query: str, k: int = 5, trace_id: str = None) -> List[Dict]:
232
  """Retrieve relevant chunks for a query"""
233
  if not self.vector_store:
234
  return []
235
-
236
  try:
237
  # Similarity search
238
  docs = self.vector_store.similarity_search(query, k=k)
239
-
240
  # Format results
241
  results = []
242
  for doc in docs:
@@ -246,7 +239,7 @@ class RetrievalAgent(BaseAgent):
246
  "chunk_id": doc.metadata.get("chunk_id", 0),
247
  "file_type": doc.metadata.get("file_type", "Unknown")
248
  })
249
-
250
  return results
251
  except Exception as e:
252
  logger.error(f"Error retrieving chunks: {e}")
@@ -260,18 +253,15 @@ class LLMResponseAgent(BaseAgent):
260
  model="meta-llama/Llama-3.1-8B-Instruct",
261
  token=HF_TOKEN
262
  )
263
-
264
- def format_prompt_for_conversational(self, query: str, context_chunks: List[Dict]) -> str:
265
- """
266
- Format prompt with context and query as a single 'user' input
267
- suitable for a conversational model.
268
- """
269
  context_text = "\n\n".join([
270
  f"Source: {chunk['source']}\nContent: {chunk['content']}"
271
  for chunk in context_chunks
272
  ])
273
-
274
- prompt_as_user_input = f"""Based on the following context from uploaded documents, please answer the user's question.
275
 
276
  Context:
277
  {context_text}
@@ -281,29 +271,26 @@ Question: {query}
281
  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.
282
 
283
  Answer:"""
284
- return prompt_as_user_input
285
-
 
286
  async def generate_response(self, query: str, context_chunks: List[Dict], trace_id: str) -> str:
287
- """Generate response using LLM via the conversational task."""
288
  try:
289
- formatted_input = self.format_prompt_for_conversational(query, context_chunks)
290
-
291
- response = self.client.conversational(
292
- inputs=formatted_input,
293
- parameters={
294
- "temperature": 0.7,
295
- "max_new_tokens": 512,
296
- }
 
297
  )
298
-
299
- if response.generated_responses:
300
- return response.generated_responses[0]
301
- else:
302
- logger.warning("LLM generated an empty response via conversational API.")
303
- return "I apologize, the model did not generate a response."
304
-
305
  except Exception as e:
306
- logger.error(f"Error generating response with conversational LLM: {e}")
307
  return f"I apologize, but I encountered an error while generating the response: {str(e)}"
308
 
309
  # Coordinator Agent
@@ -314,66 +301,70 @@ class CoordinatorAgent(BaseAgent):
314
  self.retrieval_agent = RetrievalAgent()
315
  self.llm_agent = LLMResponseAgent()
316
  self.documents_processed = False
317
-
318
  async def process_documents(self, files: List[str]) -> str:
319
  """Orchestrate document processing"""
320
  trace_id = str(uuid.uuid4())
321
-
322
  try:
 
323
  await self.send_mcp_message(
324
- "IngestionAgent",
325
- "DOCUMENT_INGESTION_REQUEST",
326
- {"files": files},
327
  trace_id
328
  )
329
-
330
  documents = await self.ingestion_agent.process_documents(files, trace_id)
331
-
332
  await self.send_mcp_message(
333
- "RetrievalAgent",
334
- "VECTOR_STORE_CREATE_REQUEST",
335
- {"documents": len(documents)},
336
  trace_id
337
  )
338
-
 
339
  await self.retrieval_agent.create_vector_store(documents, trace_id)
340
-
341
  self.documents_processed = True
342
-
343
  return f"Successfully processed {len(documents)} document chunks from {len(files)} files."
344
-
345
  except Exception as e:
346
  logger.error(f"Error in document processing: {e}")
347
  return f"Error processing documents: {str(e)}"
348
-
349
  async def answer_query(self, query: str) -> tuple[str, List[Dict]]:
350
  """Orchestrate query answering"""
351
  if not self.documents_processed:
352
  return "Please upload and process documents first.", []
353
-
354
  trace_id = str(uuid.uuid4())
355
-
356
  try:
 
357
  await self.send_mcp_message(
358
- "RetrievalAgent",
359
- "RETRIEVAL_REQUEST",
360
- {"query": query},
361
  trace_id
362
  )
363
-
364
- context_chunks = await self.retrierieval_agent.retrieve_relevant_chunks(query, k=5, trace_id=trace_id)
365
-
 
366
  await self.send_mcp_message(
367
- "LLMResponseAgent",
368
- "LLM_GENERATION_REQUEST",
369
- {"query": query, "context_chunks": len(context_chunks)},
370
  trace_id
371
  )
372
-
373
  response = await self.llm_agent.generate_response(query, context_chunks, trace_id)
374
-
375
  return response, context_chunks
376
-
377
  except Exception as e:
378
  logger.error(f"Error in query processing: {e}")
379
  return f"Error processing query: {str(e)}", []
@@ -382,40 +373,44 @@ class CoordinatorAgent(BaseAgent):
382
  coordinator = CoordinatorAgent()
383
 
384
  async def process_files(files):
385
- """Process uploaded files (already temporary file paths from Gradio)"""
386
  if not files:
387
  return "❌ Please upload at least one file."
388
-
389
- # Gradio's gr.File component with default type="filepath" already provides
390
- # the temporary file paths. We just need to pass them to the ingestion agent.
391
- # The 'files' variable here is already a list of strings (filepaths).
392
- file_paths_for_ingestion = [file.name for file in files] # Extract the actual path string
393
-
394
- result = await coordinator.process_documents(file_paths_for_ingestion)
395
-
396
- # Cleanup is handled by Gradio's tmp directory cleanup, but explicit removal is also fine.
397
- # No need to manually create temp files or read content.
398
-
 
 
399
  return result
400
 
401
  async def answer_question(query, history):
402
  """Answer user question"""
403
  if not query.strip():
404
  return history, ""
405
-
406
  response, context_chunks = await coordinator.answer_query(query)
407
-
 
408
  if context_chunks:
409
  sources = "\n\n**Sources:**\n"
410
- for i, chunk in enumerate(context_chunks[:3], 1):
411
  sources += f"{i}. {chunk['source']} (Chunk {chunk['chunk_id']})\n"
412
  response += sources
413
-
 
414
  history.append((query, response))
415
-
416
  return history, ""
417
 
418
- # Custom CSS (unchanged)
419
  custom_css = """
420
  /* Main container styling */
421
  .gradio-container {
@@ -458,7 +453,7 @@ custom_css = """
458
  }
459
 
460
  /* Card styling */
461
- .upload-card, .chat-card {
462
  background: white !important;
463
  border-radius: 15px !important;
464
  padding: 2rem !important;
@@ -558,8 +553,8 @@ custom_css = """
558
  .header-title {
559
  font-size: 2rem !important;
560
  }
561
-
562
- .upload-card, .chat-card {
563
  padding: 1.5rem !important;
564
  }
565
  }
@@ -585,9 +580,9 @@ def create_interface():
585
  <p class="header-subtitle">Multi-Format Document QA using Model Context Protocol (MCP)</p>
586
  </div>
587
  """)
588
-
589
  with gr.Tabs() as tabs:
590
- # Upload Tab (now the first tab)
591
  with gr.TabItem("πŸ“ Upload Documents", elem_classes=["tab-nav"]):
592
  gr.HTML("""
593
  <div class="upload-card">
@@ -595,28 +590,26 @@ def create_interface():
595
  <p>Upload your documents in any supported format: PDF, DOCX, PPTX, CSV, TXT, or Markdown.</p>
596
  </div>
597
  """)
598
-
599
  file_upload = gr.File(
600
  label="Choose Files",
601
  file_count="multiple",
602
  file_types=[".pdf", ".docx", ".pptx", ".csv", ".txt", ".md"],
603
- # type="filepath" is the default, but explicitly setting it helps clarify
604
- type="filepath",
605
  elem_classes=["file-upload"]
606
  )
607
-
608
  upload_button = gr.Button(
609
- "Process Documents",
610
  variant="primary",
611
  elem_classes=["primary-button"]
612
  )
613
-
614
  upload_status = gr.Textbox(
615
  label="Processing Status",
616
  interactive=False,
617
  elem_classes=["input-container"]
618
  )
619
-
620
  # Chat Tab
621
  with gr.TabItem("πŸ’¬ Chat", elem_classes=["tab-nav"]):
622
  gr.HTML("""
@@ -625,13 +618,13 @@ def create_interface():
625
  <p>Ask questions about your uploaded documents. The AI will provide answers based on the document content.</p>
626
  </div>
627
  """)
628
-
629
  chatbot = gr.Chatbot(
630
  label="Conversation",
631
  height=400,
632
  elem_classes=["chat-container"]
633
  )
634
-
635
  with gr.Row():
636
  query_input = gr.Textbox(
637
  label="Your Question",
@@ -639,11 +632,11 @@ def create_interface():
639
  elem_classes=["input-container"]
640
  )
641
  ask_button = gr.Button(
642
- "Ask",
643
  variant="primary",
644
  elem_classes=["primary-button"]
645
  )
646
-
647
  gr.Examples(
648
  examples=[
649
  "What are the main topics covered in the documents?",
@@ -654,7 +647,7 @@ def create_interface():
654
  inputs=query_input,
655
  label="Example Questions"
656
  )
657
-
658
  # Architecture Tab
659
  with gr.TabItem("πŸ—οΈ Architecture", elem_classes=["tab-nav"]):
660
  gr.HTML("""
@@ -663,38 +656,38 @@ def create_interface():
663
  <p>This system uses an agentic architecture with Model Context Protocol (MCP) for inter-agent communication.</p>
664
  </div>
665
  """)
666
-
667
  gr.Markdown("""
668
  ## πŸ”„ Agent Flow Diagram
669
-
670
  ```
671
  User Upload β†’ CoordinatorAgent β†’ IngestionAgent β†’ RetrievalAgent β†’ LLMResponseAgent
672
  ↓ ↓ ↓ ↓ ↓
673
  Documents MCP Messages Text Chunks Vector Store Final Response
674
  ```
675
-
676
  ## πŸ€– Agent Descriptions
677
-
678
  - **CoordinatorAgent**: Orchestrates the entire workflow and manages MCP communication
679
  - **IngestionAgent**: Parses and preprocesses documents (PDF, DOCX, PPTX, CSV, TXT, MD)
680
  - **RetrievalAgent**: Handles embeddings and semantic retrieval using FAISS
681
  - **LLMResponseAgent**: Generates final responses using Llama-3.1-8B-Instruct
682
-
683
  ## πŸ”— Tech Stack
684
-
685
  - **Frontend**: Gradio with custom CSS
686
  - **LLM**: Meta Llama-3.1-8B-Instruct (via HuggingFace Inference)
687
  - **Embeddings**: sentence-transformers/all-MiniLM-L6-v2
688
  - **Vector Store**: FAISS
689
  - **Document Processing**: PyPDF2, python-docx, python-pptx, pandas
690
  - **Framework**: LangChain for document handling
691
-
692
  ## πŸ“¨ MCP Message Example
693
-
694
  ```json
695
  {
696
  "sender": "RetrievalAgent",
697
- "receiver": "LLMResponseAgent",
698
  "type": "RETRIEVAL_RESULT",
699
  "trace_id": "rag-457",
700
  "payload": {
@@ -705,26 +698,26 @@ def create_interface():
705
  }
706
  ```
707
  """)
708
-
709
  # Event handlers
710
  upload_button.click(
711
  fn=process_files,
712
  inputs=[file_upload],
713
  outputs=[upload_status]
714
  )
715
-
716
  ask_button.click(
717
  fn=answer_question,
718
  inputs=[query_input, chatbot],
719
  outputs=[chatbot, query_input]
720
  )
721
-
722
  query_input.submit(
723
  fn=answer_question,
724
  inputs=[query_input, chatbot],
725
  outputs=[chatbot, query_input]
726
  )
727
-
728
  return demo
729
 
730
  if __name__ == "__main__":
 
8
  import asyncio
9
  from dataclasses import dataclass, asdict
10
  import logging
 
11
 
12
  # Document processing imports
13
  import PyPDF2
 
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
 
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
 
 
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()
 
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,
 
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
 
 
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:
 
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:
 
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:
 
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:
 
146
  except Exception as e:
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:
 
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)
 
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(
 
198
  }
199
  )
200
  all_documents.append(doc)
201
+
202
  return all_documents
203
 
204
  # Retrieval Agent
 
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:
 
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:
 
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}")
 
253
  model="meta-llama/Llama-3.1-8B-Instruct",
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}
 
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
 
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)}", []
 
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 {
 
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;
 
553
  .header-title {
554
  font-size: 2rem !important;
555
  }
556
+
557
+ .setup-card, .upload-card, .chat-card {
558
  padding: 1.5rem !important;
559
  }
560
  }
 
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">
 
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("""
 
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",
 
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?",
 
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("""
 
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": {
 
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__":