Skip to content

Commit 86a4487

Browse files
seanzhougooglecopybara-github
authored andcommitted
fix: Annotate response type as None for transfer_to_agent tool and set empty Schema as response schema when tool has no response annotation
1. if a function has no return type annotation, we should treat it as returning any type 2. we use empty schema (with `type` as None) to indicate no type constraints and this is already supported by model server PiperOrigin-RevId: 789808104
1 parent faadef1 commit 86a4487

7 files changed

+242
-16
lines changed

src/google/adk/tools/_automatic_function_calling_util.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,28 @@ def from_function_with_options(
329329

330330
return_annotation = inspect.signature(func).return_annotation
331331

332-
# Handle functions with no return annotation or that return None
332+
# Handle functions with no return annotation
333+
if return_annotation is inspect._empty:
334+
# Functions with no return annotation can return any type
335+
return_value = inspect.Parameter(
336+
'return_value',
337+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
338+
annotation=typing.Any,
339+
)
340+
declaration.response = (
341+
_function_parameter_parse_util._parse_schema_from_parameter(
342+
variant,
343+
return_value,
344+
func.__name__,
345+
)
346+
)
347+
return declaration
348+
349+
# Handle functions that explicitly return None
333350
if (
334-
return_annotation is inspect._empty
335-
or return_annotation is None
351+
return_annotation is None
336352
or return_annotation is type(None)
353+
or (isinstance(return_annotation, str) and return_annotation == 'None')
337354
):
338355
# Create a response schema for None/null return
339356
return_value = inspect.Parameter(

src/google/adk/tools/_function_parameter_parse_util.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
list: types.Type.ARRAY,
3939
dict: types.Type.OBJECT,
4040
None: types.Type.NULL,
41+
# TODO requested google GenAI SDK to add a Type.ANY and do the mapping on
42+
# their side, once new enum is added, replace the below one with
43+
# Any: types.Type.ANY
44+
Any: None,
4145
}
4246

4347
logger = logging.getLogger('google_adk.' + __name__)

src/google/adk/tools/transfer_to_agent_tool.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
from .tool_context import ToolContext
1618

1719

18-
def transfer_to_agent(agent_name: str, tool_context: ToolContext):
20+
def transfer_to_agent(agent_name: str, tool_context: ToolContext) -> None:
1921
"""Transfer the question to another agent.
2022
2123
This tool hands off control to another agent when it's more suitable to

tests/unittests/flows/llm_flows/test_agent_transfer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_auto_to_single():
8989
('sub_agent_1', 'response1'),
9090
]
9191

92-
# root_agent should still be the current agent, becaues sub_agent_1 is single.
92+
# root_agent should still be the current agent, because sub_agent_1 is single.
9393
assert testing_utils.simplify_events(runner.run('test2')) == [
9494
('root_agent', 'response2'),
9595
]
@@ -140,7 +140,7 @@ def test_auto_to_auto_to_single():
140140
def test_auto_to_sequential():
141141
response = [
142142
transfer_call_part('sub_agent_1'),
143-
# sub_agent_1 responds directly instead of transfering.
143+
# sub_agent_1 responds directly instead of transferring.
144144
'response1',
145145
'response2',
146146
'response3',
@@ -189,7 +189,7 @@ def test_auto_to_sequential():
189189
def test_auto_to_sequential_to_auto():
190190
response = [
191191
transfer_call_part('sub_agent_1'),
192-
# sub_agent_1 responds directly instead of transfering.
192+
# sub_agent_1 responds directly instead of transferring.
193193
'response1',
194194
transfer_call_part('sub_agent_1_2_1'),
195195
'response2',
@@ -250,7 +250,7 @@ def test_auto_to_sequential_to_auto():
250250
def test_auto_to_loop():
251251
response = [
252252
transfer_call_part('sub_agent_1'),
253-
# sub_agent_1 responds directly instead of transfering.
253+
# sub_agent_1 responds directly instead of transferring.
254254
'response1',
255255
'response2',
256256
'response3',

tests/unittests/tools/test_build_function_declaration.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,10 @@ def function_no_return(param: str):
298298
assert function_decl.name == 'function_no_return'
299299
assert function_decl.parameters.type == 'OBJECT'
300300
assert function_decl.parameters.properties['param'].type == 'STRING'
301-
# VERTEX_AI should have response schema for None return
301+
# VERTEX_AI should have response schema for functions with no return annotation
302+
# Changed: Now uses Any type instead of NULL for no return annotation
302303
assert function_decl.response is not None
303-
assert function_decl.response.type == types.Type.NULL
304+
assert function_decl.response.type is None # Any type maps to None in schema
304305

305306

306307
def test_function_explicit_none_return_vertex_ai():
@@ -359,8 +360,8 @@ def function_string_return(param: str) -> str:
359360
assert function_decl.response.type == types.Type.STRING
360361

361362

362-
def test_transfer_to_agent_like_function():
363-
"""Test a function similar to transfer_to_agent that caused the original issue."""
363+
def test_fucntion_with_no_response_annotations():
364+
"""Test a function that has no response annotations."""
364365

365366
def transfer_to_agent(agent_name: str, tool_context: ToolContext):
366367
"""Transfer the question to another agent."""
@@ -376,6 +377,7 @@ def transfer_to_agent(agent_name: str, tool_context: ToolContext):
376377
assert function_decl.parameters.type == 'OBJECT'
377378
assert function_decl.parameters.properties['agent_name'].type == 'STRING'
378379
assert 'tool_context' not in function_decl.parameters.properties
379-
# This should now have a response schema for VERTEX_AI variant
380+
# This function has no return annotation, so it gets Any type instead of NULL
381+
# Changed: Now uses Any type instead of NULL for no return annotation
380382
assert function_decl.response is not None
381-
assert function_decl.response.type == types.Type.NULL
383+
assert function_decl.response.type is None # Any type maps to None in schema

tests/unittests/tools/test_from_function_with_options.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Any
1516
from typing import Dict
1617

1718
from google.adk.tools import _automatic_function_calling_util
@@ -51,9 +52,10 @@ def test_function(param: str):
5152
assert declaration.name == 'test_function'
5253
assert declaration.parameters.type == 'OBJECT'
5354
assert declaration.parameters.properties['param'].type == 'STRING'
54-
# VERTEX_AI should have response schema for None return
55+
# VERTEX_AI should have response schema for functions with no return annotation
56+
# Changed: Now uses Any type instead of NULL for no return annotation
5557
assert declaration.response is not None
56-
assert declaration.response.type == types.Type.NULL
58+
assert declaration.response.type is None # Any type maps to None in schema
5759

5860

5961
def test_from_function_with_options_explicit_none_return_vertex():
@@ -150,6 +152,26 @@ def test_function(param: str) -> int:
150152
assert declaration.response.type == types.Type.INTEGER
151153

152154

155+
def test_from_function_with_options_any_annotation_vertex():
156+
"""Test from_function_with_options with Any type annotation for VERTEX_AI."""
157+
158+
def test_function(param: Any) -> Any:
159+
"""A test function that uses Any type annotations."""
160+
return param
161+
162+
declaration = _automatic_function_calling_util.from_function_with_options(
163+
test_function, GoogleLLMVariant.VERTEX_AI
164+
)
165+
166+
assert declaration.name == 'test_function'
167+
assert declaration.parameters.type == 'OBJECT'
168+
# Any type should map to None in schema (TYPE_UNSPECIFIED behavior)
169+
assert declaration.parameters.properties['param'].type is None
170+
# VERTEX_AI should have response schema for Any return
171+
assert declaration.response is not None
172+
assert declaration.response.type is None # Any type maps to None in schema
173+
174+
153175
def test_from_function_with_options_no_params():
154176
"""Test from_function_with_options with no parameters."""
155177

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Any
18+
from typing import Dict
19+
20+
from google.adk.tools import _automatic_function_calling_util
21+
from google.adk.utils.variant_utils import GoogleLLMVariant
22+
from google.genai import types
23+
24+
25+
def test_string_annotation_none_return_vertex():
26+
"""Test function with string annotation 'None' return for VERTEX_AI."""
27+
28+
def test_function(_param: str) -> None:
29+
"""A test function that returns None with string annotation."""
30+
pass
31+
32+
declaration = _automatic_function_calling_util.from_function_with_options(
33+
test_function, GoogleLLMVariant.VERTEX_AI
34+
)
35+
36+
assert declaration.name == 'test_function'
37+
assert declaration.parameters.type == 'OBJECT'
38+
assert declaration.parameters.properties['_param'].type == 'STRING'
39+
# VERTEX_AI should have response schema for None return (stored as string)
40+
assert declaration.response is not None
41+
assert declaration.response.type == types.Type.NULL
42+
43+
44+
def test_string_annotation_none_return_gemini():
45+
"""Test function with string annotation 'None' return for GEMINI_API."""
46+
47+
def test_function(_param: str) -> None:
48+
"""A test function that returns None with string annotation."""
49+
pass
50+
51+
declaration = _automatic_function_calling_util.from_function_with_options(
52+
test_function, GoogleLLMVariant.GEMINI_API
53+
)
54+
55+
assert declaration.name == 'test_function'
56+
assert declaration.parameters.type == 'OBJECT'
57+
assert declaration.parameters.properties['_param'].type == 'STRING'
58+
# GEMINI_API should not have response schema
59+
assert declaration.response is None
60+
61+
62+
def test_string_annotation_str_return_vertex():
63+
"""Test function with string annotation 'str' return for VERTEX_AI."""
64+
65+
def test_function(_param: str) -> str:
66+
"""A test function that returns a string with string annotation."""
67+
return _param
68+
69+
declaration = _automatic_function_calling_util.from_function_with_options(
70+
test_function, GoogleLLMVariant.VERTEX_AI
71+
)
72+
73+
assert declaration.name == 'test_function'
74+
assert declaration.parameters.type == 'OBJECT'
75+
assert declaration.parameters.properties['_param'].type == 'STRING'
76+
# VERTEX_AI should have response schema for string return (stored as string)
77+
assert declaration.response is not None
78+
assert declaration.response.type == types.Type.STRING
79+
80+
81+
def test_string_annotation_int_return_vertex():
82+
"""Test function with string annotation 'int' return for VERTEX_AI."""
83+
84+
def test_function(_param: str) -> int:
85+
"""A test function that returns an int with string annotation."""
86+
return 42
87+
88+
declaration = _automatic_function_calling_util.from_function_with_options(
89+
test_function, GoogleLLMVariant.VERTEX_AI
90+
)
91+
92+
assert declaration.name == 'test_function'
93+
assert declaration.parameters.type == 'OBJECT'
94+
assert declaration.parameters.properties['_param'].type == 'STRING'
95+
# VERTEX_AI should have response schema for int return (stored as string)
96+
assert declaration.response is not None
97+
assert declaration.response.type == types.Type.INTEGER
98+
99+
100+
def test_string_annotation_dict_return_vertex():
101+
"""Test function with string annotation Dict return for VERTEX_AI."""
102+
103+
def test_function(_param: str) -> Dict[str, str]:
104+
"""A test function that returns a dict with string annotation."""
105+
return {'result': _param}
106+
107+
declaration = _automatic_function_calling_util.from_function_with_options(
108+
test_function, GoogleLLMVariant.VERTEX_AI
109+
)
110+
111+
assert declaration.name == 'test_function'
112+
assert declaration.parameters.type == 'OBJECT'
113+
assert declaration.parameters.properties['_param'].type == 'STRING'
114+
# VERTEX_AI should have response schema for dict return (stored as string)
115+
assert declaration.response is not None
116+
assert declaration.response.type == types.Type.OBJECT
117+
118+
119+
def test_string_annotation_any_return_vertex():
120+
"""Test function with string annotation 'Any' return for VERTEX_AI."""
121+
122+
def test_function(_param: Any) -> Any:
123+
"""A test function that uses Any type with string annotations."""
124+
return _param
125+
126+
declaration = _automatic_function_calling_util.from_function_with_options(
127+
test_function, GoogleLLMVariant.VERTEX_AI
128+
)
129+
130+
assert declaration.name == 'test_function'
131+
assert declaration.parameters.type == 'OBJECT'
132+
# Any type should map to None in schema (TYPE_UNSPECIFIED behavior)
133+
assert declaration.parameters.properties['_param'].type is None
134+
# VERTEX_AI should have response schema for Any return (stored as string)
135+
assert declaration.response is not None
136+
assert declaration.response.type is None # Any type maps to None in schema
137+
138+
139+
def test_string_annotation_mixed_parameters_vertex():
140+
"""Test function with mixed string annotations for parameters."""
141+
142+
def test_function(str_param: str, int_param: int, any_param: Any) -> str:
143+
"""A test function with mixed parameter types as string annotations."""
144+
return f'{str_param}-{int_param}-{any_param}'
145+
146+
declaration = _automatic_function_calling_util.from_function_with_options(
147+
test_function, GoogleLLMVariant.VERTEX_AI
148+
)
149+
150+
assert declaration.name == 'test_function'
151+
assert declaration.parameters.type == 'OBJECT'
152+
assert declaration.parameters.properties['str_param'].type == 'STRING'
153+
assert declaration.parameters.properties['int_param'].type == 'INTEGER'
154+
assert declaration.parameters.properties['any_param'].type is None # Any type
155+
# VERTEX_AI should have response schema for string return (stored as string)
156+
assert declaration.response is not None
157+
assert declaration.response.type == types.Type.STRING
158+
159+
160+
def test_string_annotation_no_params_vertex():
161+
"""Test function with no parameters but string annotation return."""
162+
163+
def test_function() -> str:
164+
"""A test function with no parameters that returns string (string annotation)."""
165+
return 'hello'
166+
167+
declaration = _automatic_function_calling_util.from_function_with_options(
168+
test_function, GoogleLLMVariant.VERTEX_AI
169+
)
170+
171+
assert declaration.name == 'test_function'
172+
# No parameters should result in no parameters field or empty parameters
173+
assert (
174+
declaration.parameters is None
175+
or len(declaration.parameters.properties) == 0
176+
)
177+
# VERTEX_AI should have response schema for string return (stored as string)
178+
assert declaration.response is not None
179+
assert declaration.response.type == types.Type.STRING

0 commit comments

Comments
 (0)