@@ -107,151 +107,160 @@ def setup_routes(self, app):
107107 import threading
108108 from queue import Queue
109109 from flask import request , Response , jsonify
110-
111- # Register the tasks/stream endpoint
110+ import time
111+ import traceback
112+ from contextlib import contextmanager
113+
114+ STREAM_TIMEOUT = 300
115+ QUEUE_CHECK_INTERVAL = 0.01
116+
117+ @contextmanager
118+ def managed_thread (target_func , daemon = True ):
119+ thread = threading .Thread (target = target_func )
120+ thread .daemon = daemon
121+ thread .start ()
122+ try :
123+ yield thread
124+ finally :
125+ pass
126+
127+ def create_sse_event (event_type , data ):
128+ if event_type :
129+ return f"event: { event_type } \n data: { json .dumps (data )} \n \n "
130+ return f"data: { json .dumps (data )} \n \n "
131+
132+ def log_with_context (message , task_id = None , level = "info" ):
133+ log_context = {"task_id" : task_id } if task_id else {}
134+ log_data = {"message" : message , "level" : level , "context" : log_context }
135+ print (f"{ level .upper ()} : { message } " )
136+ return log_data
137+
138+ async def process_task_stream (task , queue , done_event , error_event , error_message ):
139+ task_id = task .id if task else "unknown"
140+ task_stream = None
141+ try :
142+ task_stream = self .tasks_send_subscribe (task )
143+ index = 0
144+ last_task_update = None
145+
146+ async for task_update in task_stream :
147+ last_task_update = task_update
148+ update_dict = task_update .to_dict ()
149+ queue .put ({
150+ "task" : update_dict ,
151+ "index" : index ,
152+ "append" : True
153+ })
154+ index += 1
155+
156+ if last_task_update :
157+ final_dict = last_task_update .to_dict ()
158+ if isinstance (final_dict .get ("status" ), dict ):
159+ final_dict ["status" ]["state" ] = "completed"
160+ queue .put ({
161+ "task" : final_dict ,
162+ "index" : index ,
163+ "append" : True ,
164+ "lastChunk" : True
165+ })
166+
167+ except asyncio .CancelledError :
168+ error_message [0 ] = "Task streaming cancelled"
169+ error_event .set ()
170+ except Exception as e :
171+ error_message [0 ] = str (e )
172+ error_event .set ()
173+ finally :
174+ done_event .set ()
175+ if hasattr (task_stream , 'aclose' ) and callable (task_stream .aclose ):
176+ try :
177+ await task_stream .aclose ()
178+ except Exception as e :
179+ log_with_context (f"Error closing task stream: { e } " , task_id , "error" )
180+
181+ def run_task_stream (task , queue , done_event , error_event , error_message ):
182+ task_id = task .id if task else "unknown"
183+ try :
184+ loop = asyncio .new_event_loop ()
185+ asyncio .set_event_loop (loop )
186+ main_task = loop .create_task (process_task_stream (task , queue , done_event , error_event , error_message ))
187+ try :
188+ loop .run_until_complete (main_task )
189+ except Exception as e :
190+ error_message [0 ] = f"Event loop error: { str (e )} "
191+ error_event .set ()
192+ finally :
193+ pending = asyncio .all_tasks (loop )
194+ for pending_task in pending :
195+ pending_task .cancel ()
196+ if pending :
197+ loop .run_until_complete (asyncio .gather (* pending , return_exceptions = True ))
198+ loop .close ()
199+ except Exception as e :
200+ error_message [0 ] = f"Thread setup error: { str (e )} "
201+ error_event .set ()
202+ done_event .set ()
203+
112204 @app .route ("/a2a/tasks/stream" , methods = ["POST" ])
113205 def handle_task_streaming ():
114- """Handle task streaming requests."""
206+ task = None
115207 try :
116208 data = request .json
117- print (f"Task streaming request received: { json .dumps (data )[:100 ]} ..." )
118-
119- # Parse the task
120- from python_a2a .models .task import Task
121209 if "task" in data :
122210 task = Task .from_dict (data ["task" ])
123211 else :
124212 task = Task .from_dict (data )
125-
126- # Check if tasks_send_subscribe is implemented
213+
127214 if not hasattr (self , 'tasks_send_subscribe' ):
128215 return jsonify ({"error" : "This agent does not support task streaming" }), 405
129-
130- # Set up streaming response
216+
131217 def generate ():
132- """Generator for streaming server-sent events."""
133- # Create a thread and asyncio event loop
134218 queue = Queue ()
135219 done_event = threading .Event ()
136-
137- def run_task_stream ():
138- """Run the task streaming in a dedicated thread."""
139- async def process_task_stream ():
140- """Process the task stream."""
220+ error_event = threading .Event ()
221+ error_message = [None ]
222+ task_id = task .id if task else "unknown"
223+
224+ with managed_thread (lambda : run_task_stream (task , queue , done_event , error_event , error_message )):
225+ yield create_sse_event (None , {"message" : "Task streaming established" })
226+
227+ deadline = time .time () + STREAM_TIMEOUT
228+ sent_last_chunk = False
229+
230+ while (not done_event .is_set () or not queue .empty ()) and time .time () < deadline :
231+ if error_event .is_set ():
232+ yield create_sse_event ("error" , {"error" : error_message [0 ] or "Unknown error" })
233+ break
234+
141235 try :
142- # Get the task stream generator
143- task_stream = self .tasks_send_subscribe (task )
144-
145- # Process each task update
146- index = 0
147- async for task_update in task_stream :
148- # Convert task to dict
149- update_dict = task_update .to_dict ()
150-
151- # Add metadata for streaming
152- update_data = {
153- "task" : update_dict ,
154- "index" : index ,
155- "append" : True
156- }
157-
158- # Put in queue
159- queue .put (update_data )
160- print (f"Put task update { index } in queue" )
161- index += 1
162-
163- # Signal completion
164- queue .put ({
165- "task" : task_update .to_dict (),
166- "index" : index ,
167- "append" : True ,
168- "lastUpdate" : True
169- })
170- print ("Task streaming complete" )
171-
236+ if not queue .empty ():
237+ update = queue .get (block = False )
238+ yield create_sse_event (None , update )
239+ if update .get ("lastChunk" , False ):
240+ sent_last_chunk = True
241+ break
242+ else :
243+ time .sleep (QUEUE_CHECK_INTERVAL )
172244 except Exception as e :
173- # Log the error
174- print (f"Error in task streaming: { str (e )} " )
175- import traceback
176- traceback .print_exc ()
177-
178- # Put error in queue
179- queue .put ({"error" : str (e )})
180-
181- finally :
182- # Signal we're done
183- done_event .set ()
184-
185- # Create a new event loop
186- loop = asyncio .new_event_loop ()
187- asyncio .set_event_loop (loop )
188-
189- # Run the streaming process
190- try :
191- loop .run_until_complete (process_task_stream ())
192- finally :
193- loop .close ()
194-
195- # Start the streaming thread
196- thread = threading .Thread (target = run_task_stream )
197- thread .daemon = True
198- thread .start ()
199-
200- # Yield initial SSE comment
201- yield f": Task streaming established\n \n "
202-
203- # Process queue items until done
204- import time
205- timeout = time .time () + 60 # 60-second timeout
206-
207- while not done_event .is_set () and time .time () < timeout :
208- try :
209- # Check for update in queue
210- if not queue .empty ():
211- update = queue .get (block = False )
212-
213- # Check if it's an error
214- if "error" in update :
215- error_event = f"event: error\n data: { json .dumps (update )} \n \n "
216- yield error_event
217- break
218-
219- # Format as SSE event
220- data_event = f"data: { json .dumps (update )} \n \n "
221- yield data_event
222-
223- # Check if it's the last update
224- if update .get ("lastUpdate" , False ):
225- break
226- else :
227- # No data yet, sleep briefly
228- time .sleep (0.01 )
229- except Exception as e :
230- # Error
231- print (f"Error in queue processing: { e } " )
232- error_event = f"event: error\n data: { json .dumps ({'error' : str (e )})} \n \n "
233- yield error_event
234- break
235-
236- # If timed out
237- if time .time () >= timeout and not done_event .is_set ():
238- error_event = f"event: error\n data: { json .dumps ({'error' : 'Task streaming timed out' })} \n \n "
239- yield error_event
240-
241- # Create the SSE response
245+ yield create_sse_event ("error" , {"error" : str (e )})
246+ break
247+
248+ if time .time () >= deadline and not done_event .is_set ():
249+ yield create_sse_event ("error" , {"error" : "Task streaming timed out" })
250+
242251 response = Response (generate (), mimetype = "text/event-stream" )
243- response .headers ["Cache-Control" ] = "no-cache"
244- response .headers ["Connection" ] = "keep-alive"
245- response .headers ["X-Accel-Buffering" ] = "no"
252+ response .headers .update ({
253+ "Cache-Control" : "no-cache" ,
254+ "Connection" : "keep-alive" ,
255+ "X-Accel-Buffering" : "no" ,
256+ "Transfer-Encoding" : "chunked"
257+ })
246258 return response
247-
259+
248260 except Exception as e :
249- # Log the exception
250- print (f"Exception in task streaming handler: { str (e )} " )
251- import traceback
261+ task_id = task .id if task else None
262+ log_with_context (f"Exception in task streaming handler: { str (e )} " , task_id , "error" )
252263 traceback .print_exc ()
253-
254- # Return error
255264 return jsonify ({"error" : str (e )}), 500
256265
257266 def handle_message (self , message : Message ) -> Message :
@@ -356,6 +365,21 @@ async def tasks_send_subscribe(self, task: Task) -> AsyncGenerator[Task, None]:
356365 print (f"[Server] Processing task { task_id } " )
357366 print (f"[Server] Query: { query [:50 ]} ..." )
358367
368+ task .status = TaskStatus (state = TaskState .SUBMITTED )
369+
370+ print (f"[Server] Task { task_id } : Yielding SUBMITTED state" )
371+ yield Task (
372+ id = task .id ,
373+ status = TaskStatus (
374+ state = task .status .state ,
375+ message = task .status .message .copy () if task .status .message else None ,
376+ timestamp = task .status .timestamp
377+ ),
378+ message = task .message ,
379+ session_id = task .session_id ,
380+ artifacts = task .artifacts .copy () if task .artifacts else []
381+ )
382+
359383 # Update task status to waiting (analogous to in_progress)
360384 task .status = TaskStatus (state = TaskState .WAITING )
361385
@@ -652,9 +676,15 @@ async def run_task(self, query: str) -> Dict[str, Any]:
652676 try :
653677 # Stream task updates
654678 async for task_update in self .client .tasks_send_subscribe (task ):
679+ print (f"task update is :{ task_update } " )
655680 # Track updates
656681 self .updates_received += 1
657682
683+ print (f"Raw update { self .updates_received } artifacts:" )
684+ for i , artifact in enumerate (task_update .artifacts or []):
685+ print (f" Artifact { i } type: { artifact .get ('type' , 'MISSING' )} " )
686+ print (f" Artifact { i } raw: { json .dumps (artifact )[:200 ]} ..." )
687+
658688 # Store latest update
659689 latest_update = task_update
660690
@@ -705,6 +735,7 @@ async def _process_task_update(self, task: Task) -> None:
705735 print (status_line )
706736
707737 # Process artifacts
738+ print (f"Task for processing artifact: { task } " )
708739 artifacts = task .artifacts or []
709740 for artifact in artifacts :
710741 await self ._process_artifact (artifact )
@@ -766,6 +797,16 @@ async def _process_artifact(self, artifact: Dict[str, Any]) -> None:
766797 print (f"\n { CYAN } Partial Result:{ RESET } " )
767798 print (f"{ text } " )
768799
800+ elif "parts" in artifact and isinstance (artifact ["parts" ], list ):
801+ # Handle artifacts with parts but no type
802+ text = ""
803+ for part in artifact ["parts" ]:
804+ if isinstance (part , dict ) and part .get ("type" ) == "text" :
805+ text += part .get ("text" , "" )
806+ if text :
807+ print (f"\n { CYAN } Partial Result:{ RESET } " )
808+ print (f"{ text } " )
809+
769810 elif artifact_type == "text" :
770811 # Simple text artifact
771812 if "parts" in artifact :
0 commit comments