Skip to content

fix: Set response schema for function that returns None #1993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/google/adk/tools/_automatic_function_calling_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
117 changes: 117 additions & 0 deletions tests/unittests/tools/test_build_function_declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
172 changes: 172 additions & 0 deletions tests/unittests/tools/test_from_function_with_options.py
Original file line number Diff line number Diff line change
@@ -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