Skip to content

Commit 523333b

Browse files
seanzhougooglecopybara-github
authored andcommitted
fix: Add response schema for agent tool function declaration even when it's return None
PiperOrigin-RevId: 783610166
1 parent 3f9f773 commit 523333b

File tree

5 files changed

+467
-2
lines changed

5 files changed

+467
-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(

src/google/adk/tools/agent_tool.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def populate_name(cls, data: Any) -> Any:
6161
@override
6262
def _get_declaration(self) -> types.FunctionDeclaration:
6363
from ..agents.llm_agent import LlmAgent
64+
from ..utils.variant_utils import GoogleLLMVariant
6465

6566
if isinstance(self.agent, LlmAgent) and self.agent.input_schema:
6667
result = _automatic_function_calling_util.build_function_declaration(
@@ -80,6 +81,17 @@ def _get_declaration(self) -> types.FunctionDeclaration:
8081
description=self.agent.description,
8182
name=self.name,
8283
)
84+
85+
# Set response schema for non-GEMINI_API variants
86+
if self._api_variant != GoogleLLMVariant.GEMINI_API:
87+
# Determine response type based on agent's output schema
88+
if isinstance(self.agent, LlmAgent) and self.agent.output_schema:
89+
# Agent has structured output schema - response is an object
90+
result.response = types.Schema(type=types.Type.OBJECT)
91+
else:
92+
# Agent returns text - response is a string
93+
result.response = types.Schema(type=types.Type.STRING)
94+
8395
result.name = self.name
8496
return result
8597

tests/unittests/tools/test_agent_tool.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from google.adk.agents import SequentialAgent
1717
from google.adk.agents.callback_context import CallbackContext
1818
from google.adk.tools.agent_tool import AgentTool
19+
from google.adk.utils.variant_utils import GoogleLLMVariant
20+
from google.genai import types
1921
from google.genai.types import Part
2022
from pydantic import BaseModel
2123
from pytest import mark
@@ -209,3 +211,147 @@ class CustomOutput(BaseModel):
209211
# The second request is the tool agent request.
210212
assert mock_model.requests[1].config.response_schema == CustomOutput
211213
assert mock_model.requests[1].config.response_mime_type == 'application/json'
214+
215+
216+
@mark.parametrize(
217+
'env_variables',
218+
[
219+
'VERTEX', # Test VERTEX_AI variant
220+
],
221+
indirect=True,
222+
)
223+
def test_agent_tool_response_schema_no_output_schema_vertex_ai():
224+
"""Test AgentTool with no output schema has string response schema for VERTEX_AI."""
225+
tool_agent = Agent(
226+
name='tool_agent',
227+
model=testing_utils.MockModel.create(responses=['test response']),
228+
)
229+
230+
agent_tool = AgentTool(agent=tool_agent)
231+
declaration = agent_tool._get_declaration()
232+
233+
assert declaration.name == 'tool_agent'
234+
assert declaration.parameters.type == 'OBJECT'
235+
assert declaration.parameters.properties['request'].type == 'STRING'
236+
# Should have string response schema for VERTEX_AI
237+
assert declaration.response is not None
238+
assert declaration.response.type == types.Type.STRING
239+
240+
241+
@mark.parametrize(
242+
'env_variables',
243+
[
244+
'VERTEX', # Test VERTEX_AI variant
245+
],
246+
indirect=True,
247+
)
248+
def test_agent_tool_response_schema_with_output_schema_vertex_ai():
249+
"""Test AgentTool with output schema has object response schema for VERTEX_AI."""
250+
251+
class CustomOutput(BaseModel):
252+
custom_output: str
253+
254+
tool_agent = Agent(
255+
name='tool_agent',
256+
model=testing_utils.MockModel.create(responses=['test response']),
257+
output_schema=CustomOutput,
258+
)
259+
260+
agent_tool = AgentTool(agent=tool_agent)
261+
declaration = agent_tool._get_declaration()
262+
263+
assert declaration.name == 'tool_agent'
264+
# Should have object response schema for VERTEX_AI when output_schema exists
265+
assert declaration.response is not None
266+
assert declaration.response.type == types.Type.OBJECT
267+
268+
269+
@mark.parametrize(
270+
'env_variables',
271+
[
272+
'GOOGLE_AI', # Test GEMINI_API variant
273+
],
274+
indirect=True,
275+
)
276+
def test_agent_tool_response_schema_gemini_api():
277+
"""Test AgentTool with GEMINI_API variant has no response schema."""
278+
279+
class CustomOutput(BaseModel):
280+
custom_output: str
281+
282+
tool_agent = Agent(
283+
name='tool_agent',
284+
model=testing_utils.MockModel.create(responses=['test response']),
285+
output_schema=CustomOutput,
286+
)
287+
288+
agent_tool = AgentTool(agent=tool_agent)
289+
declaration = agent_tool._get_declaration()
290+
291+
assert declaration.name == 'tool_agent'
292+
# GEMINI_API should not have response schema
293+
assert declaration.response is None
294+
295+
296+
@mark.parametrize(
297+
'env_variables',
298+
[
299+
'VERTEX', # Test VERTEX_AI variant
300+
],
301+
indirect=True,
302+
)
303+
def test_agent_tool_response_schema_with_input_schema_vertex_ai():
304+
"""Test AgentTool with input and output schemas for VERTEX_AI."""
305+
306+
class CustomInput(BaseModel):
307+
custom_input: str
308+
309+
class CustomOutput(BaseModel):
310+
custom_output: str
311+
312+
tool_agent = Agent(
313+
name='tool_agent',
314+
model=testing_utils.MockModel.create(responses=['test response']),
315+
input_schema=CustomInput,
316+
output_schema=CustomOutput,
317+
)
318+
319+
agent_tool = AgentTool(agent=tool_agent)
320+
declaration = agent_tool._get_declaration()
321+
322+
assert declaration.name == 'tool_agent'
323+
assert declaration.parameters.type == 'OBJECT'
324+
assert declaration.parameters.properties['custom_input'].type == 'STRING'
325+
# Should have object response schema for VERTEX_AI when output_schema exists
326+
assert declaration.response is not None
327+
assert declaration.response.type == types.Type.OBJECT
328+
329+
330+
@mark.parametrize(
331+
'env_variables',
332+
[
333+
'VERTEX', # Test VERTEX_AI variant
334+
],
335+
indirect=True,
336+
)
337+
def test_agent_tool_response_schema_with_input_schema_no_output_vertex_ai():
338+
"""Test AgentTool with input schema but no output schema for VERTEX_AI."""
339+
340+
class CustomInput(BaseModel):
341+
custom_input: str
342+
343+
tool_agent = Agent(
344+
name='tool_agent',
345+
model=testing_utils.MockModel.create(responses=['test response']),
346+
input_schema=CustomInput,
347+
)
348+
349+
agent_tool = AgentTool(agent=tool_agent)
350+
declaration = agent_tool._get_declaration()
351+
352+
assert declaration.name == 'tool_agent'
353+
assert declaration.parameters.type == 'OBJECT'
354+
assert declaration.parameters.properties['custom_input'].type == 'STRING'
355+
# Should have string response schema for VERTEX_AI when no output_schema
356+
assert declaration.response is not None
357+
assert declaration.response.type == types.Type.STRING

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

0 commit comments

Comments
 (0)