main
py 134 lines 4.95 KB
Raw
1 from typing import Any
2
3 from helpers.tool import Tool, Response
4 from helpers.print_style import PrintStyle
5 from helpers.fasta2a_client import connect_to_agent, is_client_available
6
7
8 A2A_EMPTY_RESPONSE_ERROR = (
9 "A2A chat failed: the remote task completed but no assistant text was found. "
10 "Expected final.result.history to include an assistant message with a text "
11 "part, or a text artifact/status message. Treat this as a failed remote "
12 "response, not success."
13 )
14
15
16 def _session_key(agent_url: str) -> str:
17 """Keep root and explicit /a2a URLs in the same conversation cache."""
18 normalized = agent_url.rstrip("/")
19 if normalized.endswith("/a2a"):
20 return normalized[:-4].rstrip("/")
21 return normalized
22
23
24 def _text_from_part(part: Any) -> str:
25 if not isinstance(part, dict):
26 return ""
27 for key in ("text", "content"):
28 value = part.get(key)
29 if isinstance(value, str) and value.strip():
30 return value.strip()
31 return ""
32
33
34 def _text_from_message(message: Any) -> str:
35 if isinstance(message, str):
36 return message.strip()
37 if not isinstance(message, dict):
38 return ""
39
40 parts = message.get("parts")
41 if isinstance(parts, list):
42 texts = [_text_from_part(part) for part in parts]
43 text = "\n".join(text for text in texts if text)
44 if text:
45 return text
46
47 for key in ("text", "content", "message", "output"):
48 value = message.get(key)
49 if isinstance(value, str) and value.strip():
50 return value.strip()
51
52 return ""
53
54
55 def _extract_latest_assistant_text(task_response: Any) -> str:
56 if not isinstance(task_response, dict):
57 return ""
58
59 result = task_response.get("result", task_response)
60 if not isinstance(result, dict):
61 return ""
62
63 history = result.get("history")
64 if isinstance(history, list):
65 for message in reversed(history):
66 if isinstance(message, dict) and message.get("role") == "user":
67 continue
68 text = _text_from_message(message)
69 if text:
70 return text
71
72 status = result.get("status")
73 if isinstance(status, dict):
74 text = _text_from_message(status.get("message"))
75 if text:
76 return text
77
78 artifacts = result.get("artifacts")
79 if isinstance(artifacts, list):
80 for artifact in reversed(artifacts):
81 text = _text_from_message(artifact)
82 if text:
83 return text
84
85 return _text_from_message(result)
86
87
88 class A2AChatTool(Tool):
89 """Communicate with another FastA2A-compatible agent."""
90
91 async def execute(self, **kwargs):
92 if not is_client_available():
93 return Response(message="FastA2A client not available on this instance.", break_loop=False)
94
95 agent_url: str | None = kwargs.get("agent_url") # required
96 user_message: str | None = kwargs.get("message") # required
97 attachments = kwargs.get("attachments", None) # optional list[str]
98 reset = bool(kwargs.get("reset", False))
99 if not agent_url or not isinstance(agent_url, str):
100 return Response(message="agent_url argument missing", break_loop=False)
101 if not user_message or not isinstance(user_message, str):
102 return Response(message="message argument missing", break_loop=False)
103
104 # Retrieve or create session cache on the Agent instance
105 sessions: dict[str, str] = self.agent.get_data("_a2a_sessions") or {}
106 cache_key = _session_key(agent_url)
107
108 # Handle reset flag: start fresh conversation
109 if reset and cache_key in sessions:
110 sessions.pop(cache_key, None)
111
112 context_id = None if reset else sessions.get(cache_key)
113 try:
114 async with await connect_to_agent(agent_url) as conn:
115 task_resp = await conn.send_message(user_message, attachments=attachments, context_id=context_id)
116 task_id = task_resp.get("result", {}).get("id") # type: ignore[index]
117 if not task_id:
118 return Response(message="Remote agent failed to create task.", break_loop=False)
119 final = await conn.wait_for_completion(task_id)
120 new_context_id = final["result"].get("context_id") # type: ignore[index]
121 if isinstance(new_context_id, str):
122 sessions[cache_key] = new_context_id
123 # persist back to agent data
124 self.agent.set_data("_a2a_sessions", sessions)
125 assistant_text = _extract_latest_assistant_text(final)
126 if not assistant_text:
127 return Response(
128 message=A2A_EMPTY_RESPONSE_ERROR,
129 break_loop=False,
130 )
131 return Response(message=assistant_text, break_loop=False)
132 except Exception as e:
133 PrintStyle.error(f"A2A chat error: {e}")
134 return Response(message=f"A2A chat error: {e}", break_loop=False)