Skip to content

Commit a4dfdf4

Browse files
committed
Using Agent Cards skills instead of just RemoteA2aAgent description
1 parent 2f4f561 commit a4dfdf4

File tree

2 files changed

+180
-74
lines changed

2 files changed

+180
-74
lines changed

src/google/adk/flows/llm_flows/agent_transfer.py

Lines changed: 129 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@
3030
from ._base_llm_processor import BaseLlmRequestProcessor
3131

3232
if typing.TYPE_CHECKING:
33+
from a2a.types import AgentCard
34+
3335
from ...agents import BaseAgent
3436
from ...agents import LlmAgent
37+
from ...agents.remote_a2a_agent import RemoteA2aAgent
3538

3639

3740
class _AgentTransferLlmRequestProcessor(BaseLlmRequestProcessor):
@@ -50,11 +53,11 @@ async def run_async(
5053
if not transfer_targets:
5154
return
5255

53-
llm_request.append_instructions([
54-
_build_target_agents_instructions(
55-
invocation_context.agent, transfer_targets
56-
)
57-
])
56+
# Build instructions asynchronously to support A2A agent card resolution
57+
instructions = await _build_target_agents_instructions(
58+
invocation_context.agent, transfer_targets
59+
)
60+
llm_request.append_instructions([instructions])
5861

5962
transfer_to_agent_tool = FunctionTool(func=transfer_to_agent)
6063
tool_context = ToolContext(invocation_context)
@@ -69,19 +72,114 @@ async def run_async(
6972
request_processor = _AgentTransferLlmRequestProcessor()
7073

7174

75+
def _build_target_agent_info_from_card(
76+
target_agent: RemoteA2aAgent, agent_card: AgentCard
77+
) -> str:
78+
"""Build rich agent info from A2A Agent Card.
79+
80+
Args:
81+
target_agent: The RemoteA2aAgent instance
82+
agent_card: The resolved A2A Agent Card
83+
84+
Returns:
85+
Formatted string with detailed agent information from the card,
86+
optimized for LLM consumption when selecting subagents.
87+
"""
88+
info_parts = []
89+
90+
# Start with a clear header for the agent
91+
info_parts.append(f'### Agent: {target_agent.name}')
92+
93+
# Include both RemoteA2aAgent description and agent card description
94+
# This provides both the locally-configured context and the remote agent's self-description
95+
descriptions = []
96+
if target_agent.description:
97+
descriptions.append(f'Description: {target_agent.description}')
98+
if agent_card.description and agent_card.description != target_agent.description:
99+
descriptions.append(f'Agent card description: {agent_card.description}')
100+
101+
if descriptions:
102+
info_parts.append('\n'.join(descriptions))
103+
104+
# Add skills in a structured, LLM-friendly format
105+
if agent_card.skills:
106+
info_parts.append('\nSkills:')
107+
for skill in agent_card.skills:
108+
# Format: "- skill_name: description (tags: tag1, tag2)"
109+
skill_parts = [f' - **{skill.name}**']
110+
if skill.description:
111+
skill_parts.append(f': {skill.description}')
112+
if skill.tags:
113+
skill_parts.append(f' [Tags: {", ".join(skill.tags)}]')
114+
info_parts.append(''.join(skill_parts))
115+
116+
return '\n'.join(info_parts)
117+
118+
119+
async def _build_target_agents_info_async(target_agent: BaseAgent) -> str:
120+
"""Build agent info, using A2A Agent Card if available.
121+
122+
Args:
123+
target_agent: The agent to build info for
124+
125+
Returns:
126+
Formatted string with agent information
127+
"""
128+
from ...agents.remote_a2a_agent import RemoteA2aAgent
129+
130+
# Check if this is a RemoteA2aAgent and ensure it's resolved
131+
if isinstance(target_agent, RemoteA2aAgent):
132+
try:
133+
# Ensure the agent card is resolved
134+
await target_agent._ensure_resolved()
135+
136+
# If we have an agent card, use it to build rich info
137+
if target_agent._agent_card:
138+
return _build_target_agent_info_from_card(
139+
target_agent, target_agent._agent_card
140+
)
141+
except Exception:
142+
# If resolution fails, fall through to default behavior
143+
pass
144+
145+
# Fallback to original behavior for non-A2A agents or if card unavailable
146+
return _build_target_agents_info(target_agent)
147+
148+
72149
def _build_target_agents_info(target_agent: BaseAgent) -> str:
73-
return f"""
74-
Agent name: {target_agent.name}
75-
Agent description: {target_agent.description}
76-
"""
150+
"""Build basic agent info (fallback for non-A2A agents).
151+
152+
Args:
153+
target_agent: The agent to build info for
154+
155+
Returns:
156+
Formatted string with basic agent information, matching the enhanced format
157+
for consistency with A2A agent cards.
158+
"""
159+
info_parts = [f'### Agent: {target_agent.name}']
160+
161+
if target_agent.description:
162+
info_parts.append(f'Description: {target_agent.description}')
163+
164+
return '\n'.join(info_parts)
77165

78166

79167
line_break = '\n'
80168

81169

82-
def _build_target_agents_instructions(
170+
async def _build_target_agents_instructions(
83171
agent: LlmAgent, target_agents: list[BaseAgent]
84172
) -> str:
173+
"""Build instructions for agent transfer with detailed agent information.
174+
175+
Args:
176+
agent: The current agent
177+
target_agents: List of agents that can be transferred to
178+
179+
Returns:
180+
Formatted instructions string with agent transfer information,
181+
optimized for LLM decision-making about which subagent to use.
182+
"""
85183
# Build list of available agent names for the NOTE
86184
# target_agents already includes parent agent if applicable, so no need to add it again
87185
available_agent_names = [target_agent.name for target_agent in target_agents]
@@ -94,27 +192,36 @@ def _build_target_agents_instructions(
94192
f'`{name}`' for name in available_agent_names
95193
)
96194

195+
# Build agent info asynchronously to support A2A agent card resolution
196+
agent_info_list = []
197+
for target_agent in target_agents:
198+
agent_info = await _build_target_agents_info_async(target_agent)
199+
agent_info_list.append(agent_info)
200+
201+
# Create a separator for visual clarity
202+
agents_section = '\n\n'.join(agent_info_list)
203+
97204
si = f"""
98-
You have a list of other agents to transfer to:
205+
## Available Agents for Transfer
206+
207+
You can delegate tasks to the following specialized agents. Carefully review each agent's description and skills to determine the best match for the user's request.
208+
209+
{agents_section}
210+
211+
## Decision Criteria
99212
100-
{line_break.join([
101-
_build_target_agents_info(target_agent) for target_agent in target_agents
102-
])}
213+
1. **Assess your own capability**: If you are the best agent to handle this request based on your own description and capabilities, answer it directly.
103214
104-
If you are the best to answer the question according to your description, you
105-
can answer it.
215+
2. **Consider specialized agents**: If another agent has more relevant skills or expertise for this request, call the `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function to transfer to that agent. Match the user's needs with the agent's skills and descriptions above.
106216
107-
If another agent is better for answering the question according to its
108-
description, call `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function to transfer the
109-
question to that agent. When transferring, do not generate any text other than
110-
the function call.
217+
3. **When transferring**: Only call the function - do not generate any additional text.
111218
112-
**NOTE**: the only available agents for `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function are {formatted_agent_names}.
219+
**IMPORTANT**: The only valid agent names for `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` are: {formatted_agent_names}
113220
"""
114221

115222
if agent.parent_agent and not agent.disallow_transfer_to_parent:
116223
si += f"""
117-
If neither you nor the other agents are best for the question, transfer to your parent agent {agent.parent_agent.name}.
224+
4. **Escalate to parent**: If neither you nor the specialized agents are suitable for this request, transfer to your parent agent `{agent.parent_agent.name}` for broader assistance.
118225
"""
119226
return si
120227

tests/unittests/flows/llm_flows/test_agent_transfer_system_instructions.py

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -102,44 +102,41 @@ async def test_agent_transfer_includes_sorted_agent_names_in_system_instructions
102102

103103
# The NOTE should contain agents in alphabetical order: sub-agents + parent + peers
104104
expected_content = """\
105+
## Available Agents for Transfer
105106
106-
You have a list of other agents to transfer to:
107+
You can delegate tasks to the following specialized agents. Carefully review each agent's description and skills to determine the best match for the user's request.
107108
109+
### Agent: z_agent
110+
Description: Last agent
108111
109-
Agent name: z_agent
110-
Agent description: Last agent
112+
### Agent: a_agent
113+
Description: First agent
111114
115+
### Agent: m_agent
116+
Description: Middle agent
112117
113-
Agent name: a_agent
114-
Agent description: First agent
118+
### Agent: parent_agent
119+
Description: Parent agent
115120
121+
### Agent: peer_agent
122+
Description: Peer agent
116123
117-
Agent name: m_agent
118-
Agent description: Middle agent
124+
## Decision Criteria
119125
126+
1. **Assess your own capability**: If you are the best agent to handle this request based on your own description and capabilities, answer it directly.
120127
121-
Agent name: parent_agent
122-
Agent description: Parent agent
128+
2. **Consider specialized agents**: If another agent has more relevant skills or expertise for this request, call the `transfer_to_agent` function to transfer to that agent. Match the user's needs with the agent's skills and descriptions above.
123129
130+
3. **When transferring**: Only call the function - do not generate any additional text.
124131
125-
Agent name: peer_agent
126-
Agent description: Peer agent
127-
128-
129-
If you are the best to answer the question according to your description, you
130-
can answer it.
131-
132-
If another agent is better for answering the question according to its
133-
description, call `transfer_to_agent` function to transfer the
134-
question to that agent. When transferring, do not generate any text other than
135-
the function call.
136-
137-
**NOTE**: the only available agents for `transfer_to_agent` function are `a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`.
138-
139-
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
132+
**IMPORTANT**: The only valid agent names for `transfer_to_agent` are: `a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`
133+
"""
140134

141135
assert expected_content in instructions
142136

137+
# Also verify the parent escalation instruction is present
138+
assert '4. **Escalate to parent**: If neither you nor the specialized agents are suitable for this request, transfer to your parent agent `parent_agent` for broader assistance.' in instructions
139+
143140

144141
@pytest.mark.asyncio
145142
async def test_agent_transfer_system_instructions_without_parent():
@@ -177,30 +174,32 @@ async def test_agent_transfer_system_instructions_without_parent():
177174

178175
# Direct multiline string assertion showing the exact expected content
179176
expected_content = """\
177+
## Available Agents for Transfer
180178
181-
You have a list of other agents to transfer to:
179+
You can delegate tasks to the following specialized agents. Carefully review each agent's description and skills to determine the best match for the user's request.
182180
181+
### Agent: agent1
182+
Description: First sub-agent
183183
184-
Agent name: agent1
185-
Agent description: First sub-agent
184+
### Agent: agent2
185+
Description: Second sub-agent
186186
187+
## Decision Criteria
187188
188-
Agent name: agent2
189-
Agent description: Second sub-agent
189+
1. **Assess your own capability**: If you are the best agent to handle this request based on your own description and capabilities, answer it directly.
190190
191+
2. **Consider specialized agents**: If another agent has more relevant skills or expertise for this request, call the `transfer_to_agent` function to transfer to that agent. Match the user's needs with the agent's skills and descriptions above.
191192
192-
If you are the best to answer the question according to your description, you
193-
can answer it.
193+
3. **When transferring**: Only call the function - do not generate any additional text.
194194
195-
If another agent is better for answering the question according to its
196-
description, call `transfer_to_agent` function to transfer the
197-
question to that agent. When transferring, do not generate any text other than
198-
the function call.
199-
200-
**NOTE**: the only available agents for `transfer_to_agent` function are `agent1`, `agent2`."""
195+
**IMPORTANT**: The only valid agent names for `transfer_to_agent` are: `agent1`, `agent2`
196+
"""
201197

202198
assert expected_content in instructions
203199

200+
# Verify no parent escalation instruction is present
201+
assert 'Escalate to parent' not in instructions
202+
204203

205204
@pytest.mark.asyncio
206205
async def test_agent_transfer_simplified_parent_instructions():
@@ -236,32 +235,32 @@ async def test_agent_transfer_simplified_parent_instructions():
236235

237236
# Direct multiline string assertion showing the exact expected content
238237
expected_content = """\
238+
## Available Agents for Transfer
239239
240-
You have a list of other agents to transfer to:
241-
240+
You can delegate tasks to the following specialized agents. Carefully review each agent's description and skills to determine the best match for the user's request.
242241
243-
Agent name: sub_agent
244-
Agent description: Sub agent
242+
### Agent: sub_agent
243+
Description: Sub agent
245244
245+
### Agent: parent_agent
246+
Description: Parent agent
246247
247-
Agent name: parent_agent
248-
Agent description: Parent agent
248+
## Decision Criteria
249249
250+
1. **Assess your own capability**: If you are the best agent to handle this request based on your own description and capabilities, answer it directly.
250251
251-
If you are the best to answer the question according to your description, you
252-
can answer it.
252+
2. **Consider specialized agents**: If another agent has more relevant skills or expertise for this request, call the `transfer_to_agent` function to transfer to that agent. Match the user's needs with the agent's skills and descriptions above.
253253
254-
If another agent is better for answering the question according to its
255-
description, call `transfer_to_agent` function to transfer the
256-
question to that agent. When transferring, do not generate any text other than
257-
the function call.
254+
3. **When transferring**: Only call the function - do not generate any additional text.
258255
259-
**NOTE**: the only available agents for `transfer_to_agent` function are `parent_agent`, `sub_agent`.
260-
261-
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
256+
**IMPORTANT**: The only valid agent names for `transfer_to_agent` are: `parent_agent`, `sub_agent`
257+
"""
262258

263259
assert expected_content in instructions
264260

261+
# Also verify the parent escalation instruction is present
262+
assert '4. **Escalate to parent**: If neither you nor the specialized agents are suitable for this request, transfer to your parent agent `parent_agent` for broader assistance.' in instructions
263+
265264

266265
@pytest.mark.asyncio
267266
async def test_agent_transfer_no_instructions_when_no_transfer_targets():

0 commit comments

Comments
 (0)