Skip to content

Commit 4a42d0d

Browse files
qieqieplusxuanyang15
authored andcommitted
feat: Add enum constraint to agent_name for transfer_to_agent
Merge #2437 Current implementation of `transfer_to_agent` doesn't enforce strict constraints on agent names, we could use JSON Schema's enum definition to implement stricter constraints. Co-authored-by: Xuan Yang <xygoogle@google.com> COPYBARA_INTEGRATE_REVIEW=#2437 from qieqieplus:main 052e8e7 PiperOrigin-RevId: 836410397
1 parent 728abe4 commit 4a42d0d

File tree

6 files changed

+286
-33
lines changed

6 files changed

+286
-33
lines changed

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@
2424
from ...agents.invocation_context import InvocationContext
2525
from ...events.event import Event
2626
from ...models.llm_request import LlmRequest
27-
from ...tools.function_tool import FunctionTool
2827
from ...tools.tool_context import ToolContext
29-
from ...tools.transfer_to_agent_tool import transfer_to_agent
28+
from ...tools.transfer_to_agent_tool import TransferToAgentTool
3029
from ._base_llm_processor import BaseLlmRequestProcessor
3130

3231
if typing.TYPE_CHECKING:
@@ -50,13 +49,18 @@ async def run_async(
5049
if not transfer_targets:
5150
return
5251

52+
transfer_to_agent_tool = TransferToAgentTool(
53+
agent_names=[agent.name for agent in transfer_targets]
54+
)
55+
5356
llm_request.append_instructions([
5457
_build_target_agents_instructions(
55-
invocation_context.agent, transfer_targets
58+
transfer_to_agent_tool.name,
59+
invocation_context.agent,
60+
transfer_targets,
5661
)
5762
])
5863

59-
transfer_to_agent_tool = FunctionTool(func=transfer_to_agent)
6064
tool_context = ToolContext(invocation_context)
6165
await transfer_to_agent_tool.process_llm_request(
6266
tool_context=tool_context, llm_request=llm_request
@@ -80,10 +84,13 @@ def _build_target_agents_info(target_agent: BaseAgent) -> str:
8084

8185

8286
def _build_target_agents_instructions(
83-
agent: LlmAgent, target_agents: list[BaseAgent]
87+
tool_name: str,
88+
agent: LlmAgent,
89+
target_agents: list[BaseAgent],
8490
) -> str:
8591
# Build list of available agent names for the NOTE
86-
# target_agents already includes parent agent if applicable, so no need to add it again
92+
# target_agents already includes parent agent if applicable,
93+
# so no need to add it again
8794
available_agent_names = [target_agent.name for target_agent in target_agents]
8895

8996
# Sort for consistency
@@ -101,15 +108,16 @@ def _build_target_agents_instructions(
101108
_build_target_agents_info(target_agent) for target_agent in target_agents
102109
])}
103110
104-
If you are the best to answer the question according to your description, you
105-
can answer it.
111+
If you are the best to answer the question according to your description,
112+
you can answer it.
106113
107114
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.
115+
description, call `{tool_name}` function to transfer the question to that
116+
agent. When transferring, do not generate any text other than the function
117+
call.
111118
112-
**NOTE**: the only available agents for `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function are {formatted_agent_names}.
119+
**NOTE**: the only available agents for `{tool_name}` function are
120+
{formatted_agent_names}.
113121
"""
114122

115123
if agent.parent_agent and not agent.disallow_transfer_to_parent:
@@ -119,9 +127,6 @@ def _build_target_agents_instructions(
119127
return si
120128

121129

122-
_TRANSFER_TO_AGENT_FUNCTION_NAME = transfer_to_agent.__name__
123-
124-
125130
def _get_transfer_targets(agent: LlmAgent) -> list[BaseAgent]:
126131
from ...agents.llm_agent import LlmAgent
127132

src/google/adk/tools/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from .preload_memory_tool import preload_memory_tool as preload_memory
3838
from .tool_context import ToolContext
3939
from .transfer_to_agent_tool import transfer_to_agent
40+
from .transfer_to_agent_tool import TransferToAgentTool
4041
from .url_context_tool import url_context
4142
from .vertex_ai_search_tool import VertexAiSearchTool
4243

@@ -75,6 +76,10 @@
7576
'preload_memory': ('.preload_memory_tool', 'preload_memory_tool'),
7677
'ToolContext': ('.tool_context', 'ToolContext'),
7778
'transfer_to_agent': ('.transfer_to_agent_tool', 'transfer_to_agent'),
79+
'TransferToAgentTool': (
80+
'.transfer_to_agent_tool',
81+
'TransferToAgentTool',
82+
),
7883
'url_context': ('.url_context_tool', 'url_context'),
7984
'VertexAiSearchTool': ('.vertex_ai_search_tool', 'VertexAiSearchTool'),
8085
'MCPToolset': ('.mcp_tool.mcp_toolset', 'MCPToolset'),

src/google/adk/tools/transfer_to_agent_tool.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import Optional
18+
19+
from google.genai import types
20+
from typing_extensions import override
21+
22+
from .function_tool import FunctionTool
1723
from .tool_context import ToolContext
1824

1925

@@ -23,7 +29,61 @@ def transfer_to_agent(agent_name: str, tool_context: ToolContext) -> None:
2329
This tool hands off control to another agent when it's more suitable to
2430
answer the user's question according to the agent's description.
2531
32+
Note:
33+
For most use cases, you should use TransferToAgentTool instead of this
34+
function directly. TransferToAgentTool provides additional enum constraints
35+
that prevent LLMs from hallucinating invalid agent names.
36+
2637
Args:
2738
agent_name: the agent name to transfer to.
2839
"""
2940
tool_context.actions.transfer_to_agent = agent_name
41+
42+
43+
class TransferToAgentTool(FunctionTool):
44+
"""A specialized FunctionTool for agent transfer with enum constraints.
45+
46+
This tool enhances the base transfer_to_agent function by adding JSON Schema
47+
enum constraints to the agent_name parameter. This prevents LLMs from
48+
hallucinating invalid agent names by restricting choices to only valid agents.
49+
50+
Attributes:
51+
agent_names: List of valid agent names that can be transferred to.
52+
"""
53+
54+
def __init__(
55+
self,
56+
agent_names: list[str],
57+
):
58+
"""Initialize the TransferToAgentTool.
59+
60+
Args:
61+
agent_names: List of valid agent names that can be transferred to.
62+
"""
63+
super().__init__(func=transfer_to_agent)
64+
self._agent_names = agent_names
65+
66+
@override
67+
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
68+
"""Add enum constraint to the agent_name parameter.
69+
70+
Returns:
71+
FunctionDeclaration with enum constraint on agent_name parameter.
72+
"""
73+
function_decl = super()._get_declaration()
74+
if not function_decl:
75+
return function_decl
76+
77+
# Handle parameters (types.Schema object)
78+
if function_decl.parameters:
79+
agent_name_schema = function_decl.parameters.properties.get('agent_name')
80+
if agent_name_schema:
81+
agent_name_schema.enum = self._agent_names
82+
83+
# Handle parameters_json_schema (dict)
84+
if function_decl.parameters_json_schema:
85+
properties = function_decl.parameters_json_schema.get('properties', {})
86+
if 'agent_name' in properties:
87+
properties['agent_name']['enum'] = self._agent_names
88+
89+
return function_decl

tests/unittests/flows/llm_flows/test_agent_transfer_system_instructions.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,16 @@ async def test_agent_transfer_includes_sorted_agent_names_in_system_instructions
126126
Agent description: Peer agent
127127
128128
129-
If you are the best to answer the question according to your description, you
130-
can answer it.
129+
If you are the best to answer the question according to your description,
130+
you can answer it.
131131
132132
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.
133+
description, call `transfer_to_agent` function to transfer the question to that
134+
agent. When transferring, do not generate any text other than the function
135+
call.
136136
137-
**NOTE**: the only available agents for `transfer_to_agent` function are `a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`.
137+
**NOTE**: the only available agents for `transfer_to_agent` function are
138+
`a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`.
138139
139140
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
140141

@@ -189,15 +190,16 @@ async def test_agent_transfer_system_instructions_without_parent():
189190
Agent description: Second sub-agent
190191
191192
192-
If you are the best to answer the question according to your description, you
193-
can answer it.
193+
If you are the best to answer the question according to your description,
194+
you can answer it.
194195
195196
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.
197+
description, call `transfer_to_agent` function to transfer the question to that
198+
agent. When transferring, do not generate any text other than the function
199+
call.
199200
200-
**NOTE**: the only available agents for `transfer_to_agent` function are `agent1`, `agent2`."""
201+
**NOTE**: the only available agents for `transfer_to_agent` function are
202+
`agent1`, `agent2`."""
201203

202204
assert expected_content in instructions
203205

@@ -248,15 +250,16 @@ async def test_agent_transfer_simplified_parent_instructions():
248250
Agent description: Parent agent
249251
250252
251-
If you are the best to answer the question according to your description, you
252-
can answer it.
253+
If you are the best to answer the question according to your description,
254+
you can answer it.
253255
254256
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.
257+
description, call `transfer_to_agent` function to transfer the question to that
258+
agent. When transferring, do not generate any text other than the function
259+
call.
258260
259-
**NOTE**: the only available agents for `transfer_to_agent` function are `parent_agent`, `sub_agent`.
261+
**NOTE**: the only available agents for `transfer_to_agent` function are
262+
`parent_agent`, `sub_agent`.
260263
261264
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
262265

tests/unittests/tools/test_build_function_declaration.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,19 @@ def transfer_to_agent(agent_name: str, tool_context: ToolContext):
411411
# Changed: Now uses Any type instead of NULL for no return annotation
412412
assert function_decl.response is not None
413413
assert function_decl.response.type is None # Any type maps to None in schema
414+
415+
416+
def test_transfer_to_agent_tool_with_enum_constraint():
417+
"""Test TransferToAgentTool adds enum constraint to agent_name."""
418+
from google.adk.tools.transfer_to_agent_tool import TransferToAgentTool
419+
420+
agent_names = ['agent_a', 'agent_b', 'agent_c']
421+
tool = TransferToAgentTool(agent_names=agent_names)
422+
423+
function_decl = tool._get_declaration()
424+
425+
assert function_decl.name == 'transfer_to_agent'
426+
assert function_decl.parameters.type == 'OBJECT'
427+
assert function_decl.parameters.properties['agent_name'].type == 'STRING'
428+
assert function_decl.parameters.properties['agent_name'].enum == agent_names
429+
assert 'tool_context' not in function_decl.parameters.properties

0 commit comments

Comments
 (0)