11import asyncio
2+ import base64
23import collections .abc
34import concurrent .futures
45import datetime
56import json
67import logging
8+ import re
79import threading
810import uuid
911from typing import Any , Coroutine
@@ -197,6 +199,81 @@ def _find_agent_text_recursive(obj: Any) -> str:
197199 return "\n \n " .join (texts )
198200
199201
202+ def _find_json_objects (
203+ text : str ,
204+ ) -> collections .abc .Iterable [dict [str , Any ]]:
205+ """Finds top-level JSON objects starting with {"id": in text."""
206+ decoder = json .JSONDecoder ()
207+ for match in re .finditer (r'\{"id":' , text ):
208+ start = match .start ()
209+ try :
210+ obj , _ = decoder .raw_decode (text , start )
211+ if isinstance (obj , dict ):
212+ yield obj
213+ except json .JSONDecodeError :
214+ continue
215+
216+
217+ def _extract_tool_details_from_token (
218+ token_str : str ,
219+ ) -> list [dict [str , Any ]]:
220+ """Extracts tool execution details from A2A conversation token."""
221+ if not token_str :
222+ return []
223+
224+ cleaned_token_str = re .sub (r"[^A-Za-z0-9+/=]" , "" , token_str )
225+
226+ try :
227+ decoded_bytes = base64 .b64decode (cleaned_token_str )
228+ decoded_str = decoded_bytes .decode ("utf-8" , errors = "ignore" )
229+ except (ValueError , UnicodeDecodeError ) as e :
230+ logger .exception ("Failed to decode token: %s" , e )
231+ return []
232+
233+ calls = {}
234+ responses = {}
235+
236+ for js in _find_json_objects (decoded_str ):
237+ content = js .get ("content" , {})
238+ if not isinstance (content , dict ):
239+ continue
240+ parts = content .get ("parts" )
241+ if not isinstance (parts , list ):
242+ continue
243+ for part in parts :
244+ if not isinstance (part , dict ):
245+ continue
246+ if "functionCall" in part :
247+ fc = part ["functionCall" ]
248+ fc_id = fc .get ("id" )
249+ if fc_id :
250+ calls [fc_id ] = {
251+ "id" : fc_id ,
252+ "name" : fc ["name" ],
253+ "params" : fc .get ("args" , {}),
254+ "output" : {},
255+ "fail" : 0 ,
256+ }
257+ if "functionResponse" in part :
258+ fr = part ["functionResponse" ]
259+ fr_id = fr .get ("id" )
260+ if fr_id :
261+ responses [fr_id ] = fr .get ("response" , {})
262+
263+ # Match calls and responses to determine failure and store output
264+ for fr_id , resp_payload in responses .items ():
265+ if fr_id in calls :
266+ calls [fr_id ]["output" ] = resp_payload
267+ if isinstance (resp_payload , dict ):
268+ if resp_payload .get ("error" ) or resp_payload .get ("errors" ):
269+ calls [fr_id ]["fail" ] = 1
270+ elif isinstance (resp_payload , str ):
271+ if "error" in resp_payload .lower ():
272+ calls [fr_id ]["fail" ] = 1
273+
274+ return list (calls .values ())
275+
276+
200277class DataEngineeringAgentGenerator (QueryGenerator ):
201278 """Data Engineering Agent (DEA) Query Generator using the A2A SDK."""
202279
@@ -261,7 +338,12 @@ def generate_internal(self, prompt: EvalDeaRequest) -> EvalDeaRequest:
261338 )
262339
263340 try :
264- prompt .generated_nl_response = self .run_async (coro )
341+ reply_text , new_token = self .run_async (coro )
342+ prompt .generated_nl_response = reply_text
343+ if new_token :
344+ all_tools = _extract_tool_details_from_token (new_token )
345+ prompt .this_turn_tool_details = all_tools
346+ prompt .accumulated_tools = list ({t ["name" ] for t in all_tools })
265347 except Exception :
266348 logger .exception ("A2A SDK messaging error" )
267349 raise
@@ -272,7 +354,7 @@ async def _run_client(
272354 prompt : str ,
273355 conversation_id : str | None ,
274356 target_workspace : str ,
275- ) -> str :
357+ ) -> tuple [ str , str ] :
276358 """Core asynchronous A2A SDK connection loop."""
277359 # Configure Client in standard Non-Streaming Mode
278360 config = ClientConfig (
@@ -326,11 +408,13 @@ async def _run_client(
326408 }
327409
328410 # Handle ConversationToken state memory thread-safely
329- token = ""
411+ conversation_token = ""
330412 with self ._token_lock :
331- token = self ._conversation_token_cache .get (conversation_id , "" )
332- if token :
333- message_req .metadata [CONVERSATION_TOKEN_URI ] = token
413+ conversation_token = self ._conversation_token_cache .get (
414+ conversation_id , ""
415+ )
416+ if conversation_token :
417+ message_req .metadata [CONVERSATION_TOKEN_URI ] = conversation_token
334418
335419 context = ClientCallContext (
336420 timeout = 300.0 ,
@@ -340,7 +424,7 @@ async def _run_client(
340424 )
341425
342426 reply_text = ""
343- new_token = ""
427+ new_conversation_token = ""
344428
345429 try :
346430 async for resp in client .send_message (
@@ -358,19 +442,23 @@ async def _run_client(
358442 resp .HasField ("task" )
359443 and CONVERSATION_TOKEN_URI in resp .task .metadata
360444 ):
361- new_token = resp .task .metadata [CONVERSATION_TOKEN_URI ]
445+ new_conversation_token = resp .task .metadata [
446+ CONVERSATION_TOKEN_URI
447+ ]
362448 except Exception as e :
363449 self ._log_api_error_details (e )
364450 raise
365451 finally :
366452 await client .close ()
367453
368454 # Cache the new token thread-safely
369- if new_token :
455+ if new_conversation_token :
370456 with self ._token_lock :
371- self ._conversation_token_cache [conversation_id ] = new_token
457+ self ._conversation_token_cache [
458+ conversation_id
459+ ] = new_conversation_token
372460
373- return reply_text .strip ()
461+ return reply_text .strip (), new_conversation_token
374462
375463 @staticmethod
376464 def run_async (coro : Coroutine [Any , Any , Any ]) -> Any :
0 commit comments