Skip to content

Commit 551c5bd

Browse files
jsondaicopybara-github
authored andcommitted
fix: GenAI Client(evals) - Strip ADK-injected params from plain callable tools
PiperOrigin-RevId: 932761778
1 parent 958e3eb commit 551c5bd

3 files changed

Lines changed: 163 additions & 18 deletions

File tree

agentplatform/_genai/types/evals.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,48 @@ def _get_tool_declarations_from_agent(agent: Any) -> genai_types.ToolListUnion:
9696
tool_declarations.append({"function_declarations": [declaration]})
9797
continue
9898

99-
tool_declarations.append(
100-
{
101-
"function_declarations": [
102-
genai_types.FunctionDeclaration.from_callable_with_api_option(
103-
callable=tool
104-
)
105-
]
106-
}
107-
)
99+
declaration = AgentConfig._get_declaration_from_callable(tool)
100+
if declaration is not None:
101+
tool_declarations.append({"function_declarations": [declaration]})
108102
return tool_declarations
109103

104+
@staticmethod
105+
def _get_declaration_from_callable(
106+
tool: Any,
107+
) -> Optional[genai_types.FunctionDeclaration]:
108+
"""Builds a function declaration for a plain callable tool.
109+
110+
ADK agents store plain Python functions in `agent.tools` and only wrap
111+
them in `FunctionTool` lazily at runtime. Such functions often take
112+
ADK-injected parameters (e.g. `tool_context: ToolContext`) that the
113+
generic `google-genai` schema generator rejects. When google-adk is
114+
available, wrap the callable in ADK's `FunctionTool` so its declaration
115+
logic strips those injected parameters. Otherwise, fall back to the
116+
generic generator.
117+
118+
Args:
119+
tool: A plain callable tool from an agent's `tools` list.
120+
121+
Returns:
122+
The function declaration for the tool, or None if the tool has no
123+
declaration.
124+
"""
125+
# pylint: disable=g-import-not-at-top,protected-access
126+
# The returned FunctionDeclaration may populate either `parameters` or
127+
# `parameters_json_schema` depending on the installed google-adk version
128+
# and the JSON_SCHEMA_FOR_FUNC_DECL feature flag (default-on in adk>=2.2).
129+
# A future adk major version will drop `parameters`, so downstream
130+
# consumers must handle both fields.
131+
try:
132+
from google.adk.tools.function_tool import FunctionTool
133+
134+
return FunctionTool(func=tool)._get_declaration() # type: ignore[no-any-return]
135+
except ImportError:
136+
pass
137+
return genai_types.FunctionDeclaration.from_callable_with_api_option(
138+
callable=tool
139+
)
140+
110141
@classmethod
111142
def from_agent(cls, agent: Any) -> "AgentConfig":
112143
"""Creates an AgentConfig from an ADK agent.

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#
1515
# pylint: disable=protected-access,bad-continuation,
1616
import base64
17+
import builtins
1718
import importlib
1819
import json
1920
import os
@@ -6012,6 +6013,88 @@ def my_search_tool(query: str) -> str:
60126013
mock_function_declaration
60136014
]
60146015

6016+
def test_load_from_agent_plain_callable_wraps_in_adk_function_tool(self):
6017+
"""Plain callables with ToolContext params are declared via ADK FunctionTool."""
6018+
6019+
def memorize(key: str, value: str, tool_context: "ToolContext"): # noqa: F821
6020+
tool_context.state[key] = value
6021+
return {"status": "ok"}
6022+
6023+
mock_declaration = mock.Mock(spec=genai_types.FunctionDeclaration)
6024+
mock_function_tool_cls = mock.MagicMock()
6025+
mock_function_tool_cls.return_value._get_declaration.return_value = (
6026+
mock_declaration
6027+
)
6028+
mock_modules = {
6029+
"google.adk": mock.MagicMock(),
6030+
"google.adk.tools": mock.MagicMock(),
6031+
"google.adk.tools.function_tool": mock.MagicMock(
6032+
FunctionTool=mock_function_tool_cls
6033+
),
6034+
}
6035+
6036+
mock_agent = mock.Mock()
6037+
mock_agent.name = "mock_agent"
6038+
mock_agent.instruction = "mock instruction"
6039+
mock_agent.description = "mock description"
6040+
mock_agent.tools = [memorize]
6041+
mock_agent.sub_agents = []
6042+
6043+
with (
6044+
mock.patch.object(
6045+
genai_types.FunctionDeclaration, "from_callable_with_api_option"
6046+
) as mock_from_callable,
6047+
mock.patch.dict(sys.modules, mock_modules),
6048+
):
6049+
agent_info = agentplatform_genai_types.evals.AgentInfo.load_from_agent(
6050+
agent=mock_agent,
6051+
)
6052+
6053+
assert agent_info.agents["mock_agent"].tools[0].function_declarations == [
6054+
mock_declaration
6055+
]
6056+
mock_function_tool_cls.assert_called_once_with(func=memorize)
6057+
mock_from_callable.assert_not_called()
6058+
6059+
def test_load_from_agent_plain_callable_falls_back_without_adk(self):
6060+
"""When google-adk is unavailable, plain callables use from_callable."""
6061+
6062+
def my_plain_tool(query: str) -> str:
6063+
return query
6064+
6065+
mock_declaration = mock.Mock(spec=genai_types.FunctionDeclaration)
6066+
6067+
mock_agent = mock.Mock()
6068+
mock_agent.name = "mock_agent"
6069+
mock_agent.instruction = "mock instruction"
6070+
mock_agent.description = "mock description"
6071+
mock_agent.tools = [my_plain_tool]
6072+
mock_agent.sub_agents = []
6073+
6074+
real_import = builtins.__import__
6075+
6076+
def _no_adk_import(name, *args, **kwargs):
6077+
if name == "google.adk.tools.function_tool":
6078+
raise ImportError("google-adk not installed")
6079+
return real_import(name, *args, **kwargs)
6080+
6081+
with (
6082+
mock.patch.object(
6083+
genai_types.FunctionDeclaration,
6084+
"from_callable_with_api_option",
6085+
return_value=mock_declaration,
6086+
) as mock_from_callable,
6087+
mock.patch.object(builtins, "__import__", side_effect=_no_adk_import),
6088+
):
6089+
agent_info = agentplatform_genai_types.evals.AgentInfo.load_from_agent(
6090+
agent=mock_agent,
6091+
)
6092+
6093+
assert agent_info.agents["mock_agent"].tools[0].function_declarations == [
6094+
mock_declaration
6095+
]
6096+
mock_from_callable.assert_called_once_with(callable=my_plain_tool)
6097+
60156098

60166099
class TestValidateDatasetAgentData:
60176100
"""Unit tests for the _validate_dataset_agent_data function."""

vertexai/_genai/types/evals.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,48 @@ def _get_tool_declarations_from_agent(agent: Any) -> genai_types.ToolListUnion:
9696
tool_declarations.append({"function_declarations": [declaration]})
9797
continue
9898

99-
tool_declarations.append(
100-
{
101-
"function_declarations": [
102-
genai_types.FunctionDeclaration.from_callable_with_api_option(
103-
callable=tool
104-
)
105-
]
106-
}
107-
)
99+
declaration = AgentConfig._get_declaration_from_callable(tool)
100+
if declaration is not None:
101+
tool_declarations.append({"function_declarations": [declaration]})
108102
return tool_declarations
109103

104+
@staticmethod
105+
def _get_declaration_from_callable(
106+
tool: Any,
107+
) -> Optional[genai_types.FunctionDeclaration]:
108+
"""Builds a function declaration for a plain callable tool.
109+
110+
ADK agents store plain Python functions in `agent.tools` and only wrap
111+
them in `FunctionTool` lazily at runtime. Such functions often take
112+
ADK-injected parameters (e.g. `tool_context: ToolContext`) that the
113+
generic `google-genai` schema generator rejects. When google-adk is
114+
available, wrap the callable in ADK's `FunctionTool` so its declaration
115+
logic strips those injected parameters. Otherwise, fall back to the
116+
generic generator.
117+
118+
Args:
119+
tool: A plain callable tool from an agent's `tools` list.
120+
121+
Returns:
122+
The function declaration for the tool, or None if the tool has no
123+
declaration.
124+
"""
125+
# pylint: disable=g-import-not-at-top,protected-access
126+
# The returned FunctionDeclaration may populate either `parameters` or
127+
# `parameters_json_schema` depending on the installed google-adk version
128+
# and the JSON_SCHEMA_FOR_FUNC_DECL feature flag (default-on in adk>=2.2).
129+
# A future adk major version will drop `parameters`, so downstream
130+
# consumers must handle both fields.
131+
try:
132+
from google.adk.tools.function_tool import FunctionTool
133+
134+
return FunctionTool(func=tool)._get_declaration() # type: ignore[no-any-return]
135+
except ImportError:
136+
pass
137+
return genai_types.FunctionDeclaration.from_callable_with_api_option(
138+
callable=tool
139+
)
140+
110141
@classmethod
111142
def from_agent(cls, agent: Any) -> "AgentConfig":
112143
"""Creates an AgentConfig from an ADK agent.

0 commit comments

Comments
 (0)