From 33ac8380adfff46ed8a7d518ae6f27345027c074 Mon Sep 17 00:00:00 2001 From: "Xiang (Sean) Zhou" Date: Wed, 16 Jul 2025 23:59:04 -0700 Subject: [PATCH] fix: Set response schema for function that returns None PiperOrigin-RevId: 784053725 --- .../tools/_automatic_function_calling_util.py | 22 ++- .../tools/test_build_function_declaration.py | 117 ++++++++++++ .../tools/test_from_function_with_options.py | 172 ++++++++++++++++++ 3 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 tests/unittests/tools/test_from_function_with_options.py diff --git a/src/google/adk/tools/_automatic_function_calling_util.py b/src/google/adk/tools/_automatic_function_calling_util.py index 89f0d33c8..3a26862ea 100644 --- a/src/google/adk/tools/_automatic_function_calling_util.py +++ b/src/google/adk/tools/_automatic_function_calling_util.py @@ -20,7 +20,6 @@ from typing import Any from typing import Callable from typing import Dict -from typing import Literal from typing import Optional from typing import Union @@ -329,7 +328,26 @@ def from_function_with_options( return declaration return_annotation = inspect.signature(func).return_annotation - if return_annotation is inspect._empty: + + # Handle functions with no return annotation or that return None + if ( + return_annotation is inspect._empty + or return_annotation is None + or return_annotation is type(None) + ): + # Create a response schema for None/null return + return_value = inspect.Parameter( + 'return_value', + inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=None, + ) + declaration.response = ( + _function_parameter_parse_util._parse_schema_from_parameter( + variant, + return_value, + func.__name__, + ) + ) return declaration return_value = inspect.Parameter( diff --git a/tests/unittests/tools/test_build_function_declaration.py b/tests/unittests/tools/test_build_function_declaration.py index eb95a6e3b..e0e29ee49 100644 --- a/tests/unittests/tools/test_build_function_declaration.py +++ b/tests/unittests/tools/test_build_function_declaration.py @@ -17,6 +17,8 @@ from google.adk.tools import _automatic_function_calling_util from google.adk.tools.agent_tool import ToolContext +from google.adk.utils.variant_utils import GoogleLLMVariant +from google.genai import types # TODO: crewai requires python 3.10 as minimum # from crewai_tools import FileReadTool from pydantic import BaseModel @@ -262,3 +264,118 @@ def simple_function(input_str: List[CustomInput]) -> str: # assert function_decl.name == 'directory_read_tool' # assert function_decl.parameters.type == 'OBJECT' # assert function_decl.parameters.properties['file_path'].type == 'STRING' + + +def test_function_no_return_annotation_gemini_api(): + """Test function with no return annotation using GEMINI_API variant.""" + + def function_no_return(param: str): + """A function with no return annotation.""" + return None + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=function_no_return, variant=GoogleLLMVariant.GEMINI_API + ) + + assert function_decl.name == 'function_no_return' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['param'].type == 'STRING' + # GEMINI_API should not have response schema + assert function_decl.response is None + + +def test_function_no_return_annotation_vertex_ai(): + """Test function with no return annotation using VERTEX_AI variant.""" + + def function_no_return(param: str): + """A function with no return annotation.""" + return None + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=function_no_return, variant=GoogleLLMVariant.VERTEX_AI + ) + + assert function_decl.name == 'function_no_return' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for None return + assert function_decl.response is not None + assert function_decl.response.type == types.Type.NULL + + +def test_function_explicit_none_return_vertex_ai(): + """Test function with explicit None return annotation using VERTEX_AI variant.""" + + def function_none_return(param: str) -> None: + """A function that explicitly returns None.""" + pass + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=function_none_return, variant=GoogleLLMVariant.VERTEX_AI + ) + + assert function_decl.name == 'function_none_return' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for explicit None return + assert function_decl.response is not None + assert function_decl.response.type == types.Type.NULL + + +def test_function_explicit_none_return_gemini_api(): + """Test function with explicit None return annotation using GEMINI_API variant.""" + + def function_none_return(param: str) -> None: + """A function that explicitly returns None.""" + pass + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=function_none_return, variant=GoogleLLMVariant.GEMINI_API + ) + + assert function_decl.name == 'function_none_return' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['param'].type == 'STRING' + # GEMINI_API should not have response schema + assert function_decl.response is None + + +def test_function_regular_return_type_vertex_ai(): + """Test function with regular return type using VERTEX_AI variant.""" + + def function_string_return(param: str) -> str: + """A function that returns a string.""" + return param + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=function_string_return, variant=GoogleLLMVariant.VERTEX_AI + ) + + assert function_decl.name == 'function_string_return' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for string return + assert function_decl.response is not None + assert function_decl.response.type == types.Type.STRING + + +def test_transfer_to_agent_like_function(): + """Test a function similar to transfer_to_agent that caused the original issue.""" + + def transfer_to_agent(agent_name: str, tool_context: ToolContext): + """Transfer the question to another agent.""" + tool_context.actions.transfer_to_agent = agent_name + + function_decl = _automatic_function_calling_util.build_function_declaration( + func=transfer_to_agent, + ignore_params=['tool_context'], + variant=GoogleLLMVariant.VERTEX_AI, + ) + + assert function_decl.name == 'transfer_to_agent' + assert function_decl.parameters.type == 'OBJECT' + assert function_decl.parameters.properties['agent_name'].type == 'STRING' + assert 'tool_context' not in function_decl.parameters.properties + # This should now have a response schema for VERTEX_AI variant + assert function_decl.response is not None + assert function_decl.response.type == types.Type.NULL diff --git a/tests/unittests/tools/test_from_function_with_options.py b/tests/unittests/tools/test_from_function_with_options.py new file mode 100644 index 000000000..328eefab3 --- /dev/null +++ b/tests/unittests/tools/test_from_function_with_options.py @@ -0,0 +1,172 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + +from google.adk.tools import _automatic_function_calling_util +from google.adk.utils.variant_utils import GoogleLLMVariant +from google.genai import types + + +def test_from_function_with_options_no_return_annotation_gemini(): + """Test from_function_with_options with no return annotation for GEMINI_API.""" + + def test_function(param: str): + """A test function with no return annotation.""" + return None + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.GEMINI_API + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # GEMINI_API should not have response schema + assert declaration.response is None + + +def test_from_function_with_options_no_return_annotation_vertex(): + """Test from_function_with_options with no return annotation for VERTEX_AI.""" + + def test_function(param: str): + """A test function with no return annotation.""" + return None + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.VERTEX_AI + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for None return + assert declaration.response is not None + assert declaration.response.type == types.Type.NULL + + +def test_from_function_with_options_explicit_none_return_vertex(): + """Test from_function_with_options with explicit None return for VERTEX_AI.""" + + def test_function(param: str) -> None: + """A test function that explicitly returns None.""" + pass + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.VERTEX_AI + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for explicit None return + assert declaration.response is not None + assert declaration.response.type == types.Type.NULL + + +def test_from_function_with_options_explicit_none_return_gemini(): + """Test from_function_with_options with explicit None return for GEMINI_API.""" + + def test_function(param: str) -> None: + """A test function that explicitly returns None.""" + pass + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.GEMINI_API + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # GEMINI_API should not have response schema + assert declaration.response is None + + +def test_from_function_with_options_string_return_vertex(): + """Test from_function_with_options with string return for VERTEX_AI.""" + + def test_function(param: str) -> str: + """A test function that returns a string.""" + return param + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.VERTEX_AI + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for string return + assert declaration.response is not None + assert declaration.response.type == types.Type.STRING + + +def test_from_function_with_options_dict_return_vertex(): + """Test from_function_with_options with dict return for VERTEX_AI.""" + + def test_function(param: str) -> Dict[str, str]: + """A test function that returns a dict.""" + return {'result': param} + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.VERTEX_AI + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for dict return + assert declaration.response is not None + assert declaration.response.type == types.Type.OBJECT + + +def test_from_function_with_options_int_return_vertex(): + """Test from_function_with_options with int return for VERTEX_AI.""" + + def test_function(param: str) -> int: + """A test function that returns an int.""" + return 42 + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.VERTEX_AI + ) + + assert declaration.name == 'test_function' + assert declaration.parameters.type == 'OBJECT' + assert declaration.parameters.properties['param'].type == 'STRING' + # VERTEX_AI should have response schema for int return + assert declaration.response is not None + assert declaration.response.type == types.Type.INTEGER + + +def test_from_function_with_options_no_params(): + """Test from_function_with_options with no parameters.""" + + def test_function() -> None: + """A test function with no parameters that returns None.""" + pass + + declaration = _automatic_function_calling_util.from_function_with_options( + test_function, GoogleLLMVariant.VERTEX_AI + ) + + assert declaration.name == 'test_function' + # No parameters should result in no parameters field or empty parameters + assert ( + declaration.parameters is None + or len(declaration.parameters.properties) == 0 + ) + # VERTEX_AI should have response schema for None return + assert declaration.response is not None + assert declaration.response.type == types.Type.NULL