Skip to content

Commit 33ac838

Browse files
seanzhougooglecopybara-github
authored andcommitted
fix: Set response schema for function that returns None
PiperOrigin-RevId: 784053725
1 parent 31fa5d9 commit 33ac838

File tree

3 files changed

+309
-2
lines changed

3 files changed

+309
-2
lines changed

src/google/adk/tools/_automatic_function_calling_util.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from typing import Any
2121
from typing import Callable
2222
from typing import Dict
23-
from typing import Literal
2423
from typing import Optional
2524
from typing import Union
2625

@@ -329,7 +328,26 @@ def from_function_with_options(
329328
return declaration
330329

331330
return_annotation = inspect.signature(func).return_annotation
332-
if return_annotation is inspect._empty:
331+
332+
# Handle functions with no return annotation or that return None
333+
if (
334+
return_annotation is inspect._empty
335+
or return_annotation is None
336+
or return_annotation is type(None)
337+
):
338+
# Create a response schema for None/null return
339+
return_value = inspect.Parameter(
340+
'return_value',
341+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
342+
annotation=None,
343+
)
344+
declaration.response = (
345+
_function_parameter_parse_util._parse_schema_from_parameter(
346+
variant,
347+
return_value,
348+
func.__name__,
349+
)
350+
)
333351
return declaration
334352

335353
return_value = inspect.Parameter(

tests/unittests/tools/test_build_function_declaration.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
from google.adk.tools import _automatic_function_calling_util
1919
from google.adk.tools.agent_tool import ToolContext
20+
from google.adk.utils.variant_utils import GoogleLLMVariant
21+
from google.genai import types
2022
# TODO: crewai requires python 3.10 as minimum
2123
# from crewai_tools import FileReadTool
2224
from pydantic import BaseModel
@@ -262,3 +264,118 @@ def simple_function(input_str: List[CustomInput]) -> str:
262264
# assert function_decl.name == 'directory_read_tool'
263265
# assert function_decl.parameters.type == 'OBJECT'
264266
# assert function_decl.parameters.properties['file_path'].type == 'STRING'
267+
268+
269+
def test_function_no_return_annotation_gemini_api():
270+
"""Test function with no return annotation using GEMINI_API variant."""
271+
272+
def function_no_return(param: str):
273+
"""A function with no return annotation."""
274+
return None
275+
276+
function_decl = _automatic_function_calling_util.build_function_declaration(
277+
func=function_no_return, variant=GoogleLLMVariant.GEMINI_API
278+
)
279+
280+
assert function_decl.name == 'function_no_return'
281+
assert function_decl.parameters.type == 'OBJECT'
282+
assert function_decl.parameters.properties['param'].type == 'STRING'
283+
# GEMINI_API should not have response schema
284+
assert function_decl.response is None
285+
286+
287+
def test_function_no_return_annotation_vertex_ai():
288+
"""Test function with no return annotation using VERTEX_AI variant."""
289+
290+
def function_no_return(param: str):
291+
"""A function with no return annotation."""
292+
return None
293+
294+
function_decl = _automatic_function_calling_util.build_function_declaration(
295+
func=function_no_return, variant=GoogleLLMVariant.VERTEX_AI
296+
)
297+
298+
assert function_decl.name == 'function_no_return'
299+
assert function_decl.parameters.type == 'OBJECT'
300+
assert function_decl.parameters.properties['param'].type == 'STRING'
301+
# VERTEX_AI should have response schema for None return
302+
assert function_decl.response is not None
303+
assert function_decl.response.type == types.Type.NULL
304+
305+
306+
def test_function_explicit_none_return_vertex_ai():
307+
"""Test function with explicit None return annotation using VERTEX_AI variant."""
308+
309+
def function_none_return(param: str) -> None:
310+
"""A function that explicitly returns None."""
311+
pass
312+
313+
function_decl = _automatic_function_calling_util.build_function_declaration(
314+
func=function_none_return, variant=GoogleLLMVariant.VERTEX_AI
315+
)
316+
317+
assert function_decl.name == 'function_none_return'
318+
assert function_decl.parameters.type == 'OBJECT'
319+
assert function_decl.parameters.properties['param'].type == 'STRING'
320+
# VERTEX_AI should have response schema for explicit None return
321+
assert function_decl.response is not None
322+
assert function_decl.response.type == types.Type.NULL
323+
324+
325+
def test_function_explicit_none_return_gemini_api():
326+
"""Test function with explicit None return annotation using GEMINI_API variant."""
327+
328+
def function_none_return(param: str) -> None:
329+
"""A function that explicitly returns None."""
330+
pass
331+
332+
function_decl = _automatic_function_calling_util.build_function_declaration(
333+
func=function_none_return, variant=GoogleLLMVariant.GEMINI_API
334+
)
335+
336+
assert function_decl.name == 'function_none_return'
337+
assert function_decl.parameters.type == 'OBJECT'
338+
assert function_decl.parameters.properties['param'].type == 'STRING'
339+
# GEMINI_API should not have response schema
340+
assert function_decl.response is None
341+
342+
343+
def test_function_regular_return_type_vertex_ai():
344+
"""Test function with regular return type using VERTEX_AI variant."""
345+
346+
def function_string_return(param: str) -> str:
347+
"""A function that returns a string."""
348+
return param
349+
350+
function_decl = _automatic_function_calling_util.build_function_declaration(
351+
func=function_string_return, variant=GoogleLLMVariant.VERTEX_AI
352+
)
353+
354+
assert function_decl.name == 'function_string_return'
355+
assert function_decl.parameters.type == 'OBJECT'
356+
assert function_decl.parameters.properties['param'].type == 'STRING'
357+
# VERTEX_AI should have response schema for string return
358+
assert function_decl.response is not None
359+
assert function_decl.response.type == types.Type.STRING
360+
361+
362+
def test_transfer_to_agent_like_function():
363+
"""Test a function similar to transfer_to_agent that caused the original issue."""
364+
365+
def transfer_to_agent(agent_name: str, tool_context: ToolContext):
366+
"""Transfer the question to another agent."""
367+
tool_context.actions.transfer_to_agent = agent_name
368+
369+
function_decl = _automatic_function_calling_util.build_function_declaration(
370+
func=transfer_to_agent,
371+
ignore_params=['tool_context'],
372+
variant=GoogleLLMVariant.VERTEX_AI,
373+
)
374+
375+
assert function_decl.name == 'transfer_to_agent'
376+
assert function_decl.parameters.type == 'OBJECT'
377+
assert function_decl.parameters.properties['agent_name'].type == 'STRING'
378+
assert 'tool_context' not in function_decl.parameters.properties
379+
# This should now have a response schema for VERTEX_AI variant
380+
assert function_decl.response is not None
381+
assert function_decl.response.type == types.Type.NULL
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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 typing import Dict
16+
17+
from google.adk.tools import _automatic_function_calling_util
18+
from google.adk.utils.variant_utils import GoogleLLMVariant
19+
from google.genai import types
20+
21+
22+
def test_from_function_with_options_no_return_annotation_gemini():
23+
"""Test from_function_with_options with no return annotation for GEMINI_API."""
24+
25+
def test_function(param: str):
26+
"""A test function with no return annotation."""
27+
return None
28+
29+
declaration = _automatic_function_calling_util.from_function_with_options(
30+
test_function, GoogleLLMVariant.GEMINI_API
31+
)
32+
33+
assert declaration.name == 'test_function'
34+
assert declaration.parameters.type == 'OBJECT'
35+
assert declaration.parameters.properties['param'].type == 'STRING'
36+
# GEMINI_API should not have response schema
37+
assert declaration.response is None
38+
39+
40+
def test_from_function_with_options_no_return_annotation_vertex():
41+
"""Test from_function_with_options with no return annotation for VERTEX_AI."""
42+
43+
def test_function(param: str):
44+
"""A test function with no return annotation."""
45+
return None
46+
47+
declaration = _automatic_function_calling_util.from_function_with_options(
48+
test_function, GoogleLLMVariant.VERTEX_AI
49+
)
50+
51+
assert declaration.name == 'test_function'
52+
assert declaration.parameters.type == 'OBJECT'
53+
assert declaration.parameters.properties['param'].type == 'STRING'
54+
# VERTEX_AI should have response schema for None return
55+
assert declaration.response is not None
56+
assert declaration.response.type == types.Type.NULL
57+
58+
59+
def test_from_function_with_options_explicit_none_return_vertex():
60+
"""Test from_function_with_options with explicit None return for VERTEX_AI."""
61+
62+
def test_function(param: str) -> None:
63+
"""A test function that explicitly returns None."""
64+
pass
65+
66+
declaration = _automatic_function_calling_util.from_function_with_options(
67+
test_function, GoogleLLMVariant.VERTEX_AI
68+
)
69+
70+
assert declaration.name == 'test_function'
71+
assert declaration.parameters.type == 'OBJECT'
72+
assert declaration.parameters.properties['param'].type == 'STRING'
73+
# VERTEX_AI should have response schema for explicit None return
74+
assert declaration.response is not None
75+
assert declaration.response.type == types.Type.NULL
76+
77+
78+
def test_from_function_with_options_explicit_none_return_gemini():
79+
"""Test from_function_with_options with explicit None return for GEMINI_API."""
80+
81+
def test_function(param: str) -> None:
82+
"""A test function that explicitly returns None."""
83+
pass
84+
85+
declaration = _automatic_function_calling_util.from_function_with_options(
86+
test_function, GoogleLLMVariant.GEMINI_API
87+
)
88+
89+
assert declaration.name == 'test_function'
90+
assert declaration.parameters.type == 'OBJECT'
91+
assert declaration.parameters.properties['param'].type == 'STRING'
92+
# GEMINI_API should not have response schema
93+
assert declaration.response is None
94+
95+
96+
def test_from_function_with_options_string_return_vertex():
97+
"""Test from_function_with_options with string return for VERTEX_AI."""
98+
99+
def test_function(param: str) -> str:
100+
"""A test function that returns a string."""
101+
return param
102+
103+
declaration = _automatic_function_calling_util.from_function_with_options(
104+
test_function, GoogleLLMVariant.VERTEX_AI
105+
)
106+
107+
assert declaration.name == 'test_function'
108+
assert declaration.parameters.type == 'OBJECT'
109+
assert declaration.parameters.properties['param'].type == 'STRING'
110+
# VERTEX_AI should have response schema for string return
111+
assert declaration.response is not None
112+
assert declaration.response.type == types.Type.STRING
113+
114+
115+
def test_from_function_with_options_dict_return_vertex():
116+
"""Test from_function_with_options with dict return for VERTEX_AI."""
117+
118+
def test_function(param: str) -> Dict[str, str]:
119+
"""A test function that returns a dict."""
120+
return {'result': param}
121+
122+
declaration = _automatic_function_calling_util.from_function_with_options(
123+
test_function, GoogleLLMVariant.VERTEX_AI
124+
)
125+
126+
assert declaration.name == 'test_function'
127+
assert declaration.parameters.type == 'OBJECT'
128+
assert declaration.parameters.properties['param'].type == 'STRING'
129+
# VERTEX_AI should have response schema for dict return
130+
assert declaration.response is not None
131+
assert declaration.response.type == types.Type.OBJECT
132+
133+
134+
def test_from_function_with_options_int_return_vertex():
135+
"""Test from_function_with_options with int return for VERTEX_AI."""
136+
137+
def test_function(param: str) -> int:
138+
"""A test function that returns an int."""
139+
return 42
140+
141+
declaration = _automatic_function_calling_util.from_function_with_options(
142+
test_function, GoogleLLMVariant.VERTEX_AI
143+
)
144+
145+
assert declaration.name == 'test_function'
146+
assert declaration.parameters.type == 'OBJECT'
147+
assert declaration.parameters.properties['param'].type == 'STRING'
148+
# VERTEX_AI should have response schema for int return
149+
assert declaration.response is not None
150+
assert declaration.response.type == types.Type.INTEGER
151+
152+
153+
def test_from_function_with_options_no_params():
154+
"""Test from_function_with_options with no parameters."""
155+
156+
def test_function() -> None:
157+
"""A test function with no parameters that returns None."""
158+
pass
159+
160+
declaration = _automatic_function_calling_util.from_function_with_options(
161+
test_function, GoogleLLMVariant.VERTEX_AI
162+
)
163+
164+
assert declaration.name == 'test_function'
165+
# No parameters should result in no parameters field or empty parameters
166+
assert (
167+
declaration.parameters is None
168+
or len(declaration.parameters.properties) == 0
169+
)
170+
# VERTEX_AI should have response schema for None return
171+
assert declaration.response is not None
172+
assert declaration.response.type == types.Type.NULL

0 commit comments

Comments
 (0)