Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
#
|
32 |
-
HF_TOKEN = os.getenv('
|
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.
|
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 |
-
|
146 |
-
|
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:
|
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
|
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 |
-
|
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 |
-
|
285 |
-
|
|
|
286 |
async def generate_response(self, query: str, context_chunks: List[Dict], trace_id: str) -> str:
|
287 |
-
"""Generate response using LLM
|
288 |
try:
|
289 |
-
|
290 |
-
|
291 |
-
response
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
|
|
297 |
)
|
298 |
-
|
299 |
-
|
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
|
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.
|
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
|
386 |
if not files:
|
387 |
return "β Please upload at least one file."
|
388 |
-
|
389 |
-
#
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
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
|
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
|
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__":
|