1616import os
1717import io
1818import base64
19+ from typing import Any
1920
2021from pypdf import PdfReader
2122from docx import Document
23+ from bindu .utils .logging import get_logger
24+
25+ logger = get_logger ("examples.document_analyzer" )
2226
2327load_dotenv ()
2428
2529# Define LLM agent
2630agent = Agent (
27- instructions = """
31+ instructions = """
2832You are an advanced document analysis assistant.
2933
3034Your job is to analyze uploaded documents and answer the user's prompt
4246- If the prompt asks for summary, provide concise bullet points
4347- Do not hallucinate information outside the document
4448""" ,
45- model = OpenRouter (
46- id = "arcee-ai/trinity-large-preview:free" ,
49+ model = OpenRouter (
50+ id = "arcee-ai/trinity-large-preview:free" ,
4751 api_key = os .getenv ("OPENROUTER_API_KEY" ),
4852 ),
4953)
5054
55+
5156# Document Parsing
5257def extract_text_from_pdf (file_bytes ):
5358 """Extract text from pdf bytes"""
@@ -67,10 +72,16 @@ def extract_text_from_pdf(file_bytes):
6772
6873 return "\n " .join (text )
6974
75+
7076def extract_text_from_docx (file_bytes ):
7177 """Extract text from docx bytes"""
72- doc = Document (io .BytesIO (file_bytes ))
73- return "\n " .join ([p .text for p in doc .paragraphs ])
78+ try :
79+ doc = Document (io .BytesIO (file_bytes ))
80+ return "\n " .join ([p .text for p in doc .paragraphs ])
81+ except Exception as e :
82+ logger .error (f"Error extracting DOCX text: { e } " )
83+ return ""
84+
7485
7586def extract_document_text (file_bytes , mime_type ):
7687 """Parse document according to their mime type"""
@@ -84,6 +95,7 @@ def extract_document_text(file_bytes, mime_type):
8495
8596 raise ValueError (f"Unsupported file type: { mime_type } " )
8697
98+
8799# FilePart processing
88100def get_file_bytes (part ):
89101 """Extract file bytes from FilePart"""
@@ -98,39 +110,38 @@ def get_file_bytes(part):
98110
99111 if isinstance (data , str ):
100112 import base64
113+
101114 return base64 .b64decode (data )
102115
103116 return data
104117
105- # Handler
106- def handler (messages : list [dict ]):
107- """
108- Receives task.history — a list of A2A Message objects.
109- Each message has: role, parts[], kind, messageId, contextId, taskId
110- Each part has: kind="text"|"file", and either text or file.bytes+mimeType
111- """
112- if not messages :
113- return "No messages received."
114- import json
115- print ("DEBUG messages:" , json .dumps (messages , indent = 2 , default = str ))
116118
117- prompt = ""
118- extracted_docs = []
119+ # Handler
120+ def _collect_prompt_and_documents (
121+ messages : list [dict [str , Any ]],
122+ ) -> tuple [str , list [str ], list [str ]]:
123+ """Support both raw A2A messages and runtime chat-format messages."""
124+ prompt_parts : list [str ] = []
125+ extracted_docs : list [str ] = []
126+ errors : list [str ] = []
119127
120128 for msg in messages :
121- # if a role is provided, only process user messages; treat missing
122- # roles as coming from the user so that tests/clients without a role
123- # field still work.
124129 role = msg .get ("role" )
125130 if role is not None and role != "user" :
126131 continue
127132
128- # be defensive: parts could be None or omitted
133+ # Runtime path: manifest worker passes chat-format messages.
134+ content = msg .get ("content" )
135+ if isinstance (content , str ) and content .strip ():
136+ prompt_parts .append (content )
137+
138+ # Compatibility path: raw A2A messages with parts.
129139 parts = msg .get ("parts" ) or []
130140 for part in parts :
131141 if part .get ("kind" ) == "text" :
132- prompt = part .get ("text" , "" )
133-
142+ text = part .get ("text" , "" )
143+ if text :
144+ prompt_parts .append (text )
134145 elif part .get ("kind" ) == "file" :
135146 try :
136147 file_info = part .get ("file" , {})
@@ -147,30 +158,46 @@ def handler(messages: list[dict]):
147158 )
148159 doc_text = extract_document_text (file_bytes , mime_type )
149160 extracted_docs .append (doc_text )
150-
151161 except Exception as e :
152- extracted_docs .append (f"Error processing file: { str (e )} " )
162+ errors .append (str (e ))
163+
164+ return "\n " .join (prompt_parts ).strip (), extracted_docs , errors
165+
166+
167+ def handler (messages : list [dict ]):
168+ """
169+ Receives task.history — a list of A2A Message objects.
170+ Each message has: role, parts[], kind, messageId, contextId, taskId
171+ Each part has: kind="text"|"file", and either text or file.bytes+mimeType
172+ """
173+ if not messages :
174+ return "No messages received."
175+ prompt , extracted_docs , errors = _collect_prompt_and_documents (messages )
153176
154177 if not extracted_docs :
178+ if errors :
179+ return "Failed to process documents:\n " + "\n " .join (errors )
155180 return "No valid document found in the messages."
156181
157182 combined_document = "\n \n " .join (extracted_docs )
158- result = agent .run (input = f"""
183+ result = agent .run (
184+ input = f"""
159185User Prompt:
160186{ prompt }
161187
162188Document Content:
163189{ combined_document }
164190
165191Provide analysis based on the prompt.
166- """ )
192+ """
193+ )
167194 return result
168195
169196
170197# Bindu config
171198config = {
172- "author" : "vyomrohila@gmail.com" ,
173- "name" : "document_analyzer_agent" ,
199+ "author" : "vyomrohila@gmail.com" ,
200+ "name" : "document_analyzer_agent" ,
174201 "description" : "AI agent that analyzes uploaded PDF or DOCX documents based on a user prompt." ,
175202 "deployment" : {
176203 "url" : "http://localhost:3773" ,
0 commit comments