Skip to content

Commit 27816f3

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Add detailed agent tool definitions
FUTURE_COPYBARA_INTEGRATE_REVIEW=#6967 from googleapis:release-please--branches--main 4a0d1d2 PiperOrigin-RevId: 948478465
1 parent 3335750 commit 27816f3

7 files changed

Lines changed: 472 additions & 142 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Copyright 2026 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+
"""Built-in tool catalog for Gemini Agent evaluation display.
16+
17+
The Gemini Agents API (``GET agents/{id}``) returns each tool as a bare type
18+
discriminator (e.g. ``{"type": "code_execution"}``) with no parameter schema
19+
or description. The authoritative, full-fidelity expansion lives server-side
20+
in ``cloud/ai/platform/evaluation/utils/interaction_converter.py``.
21+
22+
This module is a **display-only duplicate** of that server catalog, kept here
23+
so ``show()`` can render tools with full names, descriptions, and parameter
24+
schemas without a server round-trip.
25+
26+
**If the server catalog changes, this SDK-side copy must be updated to match.**
27+
"""
28+
29+
from typing import Any, Optional
30+
31+
from google.genai import types as genai_types
32+
33+
34+
def _str_schema(description: str) -> genai_types.Schema:
35+
return genai_types.Schema(type="STRING", description=description)
36+
37+
38+
# Maps a built-in Gemini Agent tool type to the concrete FunctionDeclarations
39+
# the agent actually exposes for that type.
40+
#
41+
# Source of truth: interaction_converter.py, _BUILTIN_TOOL_FUNCTION_DECLARATIONS
42+
BUILTIN_TOOL_DECLARATIONS: dict[str, list[genai_types.FunctionDeclaration]] = {
43+
"code_execution": [
44+
genai_types.FunctionDeclaration(
45+
name="run_command",
46+
description=(
47+
"Runs a shell command on the sandbox VM. If the command does"
48+
" not complete within WaitMsBeforeAsync, it is sent to the"
49+
" background and a CommandId is returned for use with"
50+
" command_status."
51+
),
52+
parameters=genai_types.Schema(
53+
type="OBJECT",
54+
properties={
55+
"CommandLine": _str_schema("The shell command to run."),
56+
"Cwd": _str_schema(
57+
"The current working directory for the command."
58+
),
59+
"WaitMsBeforeAsync": genai_types.Schema(
60+
type="INTEGER",
61+
description=(
62+
"Milliseconds to wait for the command to complete"
63+
" before sending it to the background. Default:"
64+
" 10000."
65+
),
66+
),
67+
"SafeToAutoRun": genai_types.Schema(
68+
type="BOOLEAN",
69+
description=(
70+
"Whether the command is safe to auto-run without"
71+
" user approval."
72+
),
73+
),
74+
},
75+
required=["CommandLine", "Cwd"],
76+
),
77+
),
78+
],
79+
"filesystem": [
80+
genai_types.FunctionDeclaration(
81+
name="view_file",
82+
description="Reads the content of a workspace file.",
83+
),
84+
genai_types.FunctionDeclaration(
85+
name="create_file",
86+
description="Writes content to a new or existing file.",
87+
),
88+
genai_types.FunctionDeclaration(
89+
name="edit_file",
90+
description="Replaces a specific block of text in a file.",
91+
),
92+
genai_types.FunctionDeclaration(
93+
name="list_dir",
94+
description="Lists the files in a directory.",
95+
),
96+
genai_types.FunctionDeclaration(
97+
name="delete_file",
98+
description="Removes a file from the workspace.",
99+
),
100+
genai_types.FunctionDeclaration(
101+
name="move_file",
102+
description="Renames or moves a file.",
103+
),
104+
],
105+
}
106+
107+
108+
# Sandbox-environment orchestration tool declarations.
109+
#
110+
# Source of truth: interaction_converter.py, _SANDBOX_FUNCTION_DECLARATIONS
111+
SANDBOX_DECLARATIONS: list[genai_types.FunctionDeclaration] = [
112+
genai_types.FunctionDeclaration(
113+
name="provision_sandbox",
114+
description="Provisions a sandbox environment.",
115+
parameters=genai_types.Schema(
116+
type="OBJECT",
117+
properties={
118+
"display_name": _str_schema(
119+
"The display name of the sandbox environment."
120+
),
121+
"poll_creation_lro": genai_types.Schema(
122+
type="BOOLEAN",
123+
description=(
124+
"Whether to poll the creation long-running operation."
125+
),
126+
),
127+
},
128+
),
129+
),
130+
genai_types.FunctionDeclaration(
131+
name="load_sandbox",
132+
description="Loads a previously provisioned sandbox environment.",
133+
parameters=genai_types.Schema(
134+
type="OBJECT",
135+
properties={
136+
"reasoning_engine_resource_name": _str_schema(
137+
"The resource name of the reasoning engine. Format:"
138+
" projects/{project}/locations/{location}/reasoningEngines/{id}"
139+
),
140+
"display_name": _str_schema(
141+
"The display name of the sandbox environment. Format: any"
142+
" string."
143+
),
144+
},
145+
),
146+
),
147+
]
148+
149+
150+
def agent_tools_to_config_tools(
151+
agent_tools: Optional[list[Any]],
152+
has_environment: bool = False,
153+
) -> Optional[list[genai_types.Tool]]:
154+
"""Maps Gemini Agents API tools to ``genai_types.Tool`` for display.
155+
156+
Expands built-in agent tool types into their concrete function declarations
157+
using ``BUILTIN_TOOL_DECLARATIONS`` (a display-only duplicate of the
158+
server-side catalog in ``interaction_converter.py``).
159+
160+
Mapping rules:
161+
* ``code_execution`` is expanded to ``run_command`` with full parameter
162+
schema.
163+
* ``filesystem`` is expanded to ``view_file``, ``create_file``,
164+
``edit_file``, ``list_dir``, ``delete_file``, ``move_file``.
165+
* ``google_search`` and ``url_context`` are mapped to their typed
166+
``genai_types.Tool`` variant.
167+
* ``mcp_server`` is represented as a named declaration with a
168+
human-readable label.
169+
* Tools carrying explicit ``function_declarations`` are passed through.
170+
* When ``has_environment`` is True, sandbox orchestration tools
171+
(``provision_sandbox``, ``load_sandbox``) are appended.
172+
173+
Args:
174+
agent_tools: The ``tools`` list from a fetched Gemini agent dict.
175+
has_environment: Whether the agent has a sandbox environment configured.
176+
177+
Returns:
178+
A list of ``genai_types.Tool``, or ``None`` if there are no mappable
179+
tools.
180+
"""
181+
if not agent_tools and not has_environment:
182+
return None
183+
tools: list[genai_types.Tool] = []
184+
for tool in agent_tools or []:
185+
if not isinstance(tool, dict):
186+
continue
187+
tool_type = tool.get("type")
188+
remainder = {k: v for k, v in tool.items() if k != "type"}
189+
190+
# Check the built-in catalog first (code_execution, filesystem).
191+
catalog_decls = BUILTIN_TOOL_DECLARATIONS.get(tool_type or "")
192+
if catalog_decls:
193+
tools.append(genai_types.Tool(function_declarations=list(catalog_decls)))
194+
elif tool_type == "google_search":
195+
tools.append(genai_types.Tool(google_search=genai_types.GoogleSearch()))
196+
elif tool_type == "url_context":
197+
tools.append(genai_types.Tool(url_context=genai_types.UrlContext()))
198+
elif "function_declarations" in remainder:
199+
# Real function tool with explicit declarations.
200+
tools.append(genai_types.Tool.model_validate(remainder))
201+
elif tool_type == "mcp_server":
202+
label = remainder.get("name") or remainder.get("url")
203+
description = f"MCP server: {label}" if label else "MCP server."
204+
tools.append(
205+
genai_types.Tool(
206+
function_declarations=[
207+
genai_types.FunctionDeclaration(
208+
name="mcp_server", description=description
209+
)
210+
]
211+
)
212+
)
213+
elif tool_type:
214+
# Unknown built-in: show by name so it isn't silently dropped.
215+
tools.append(
216+
genai_types.Tool(
217+
function_declarations=[
218+
genai_types.FunctionDeclaration(name=tool_type)
219+
]
220+
)
221+
)
222+
elif remainder:
223+
tools.append(genai_types.Tool.model_validate(remainder))
224+
225+
if has_environment:
226+
tools.append(genai_types.Tool(function_declarations=list(SANDBOX_DECLARATIONS)))
227+
228+
return tools or None

agentplatform/_genai/_evals_common.py

Lines changed: 16 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from tqdm import tqdm
4343
from pydantic import ValidationError
4444

45+
from . import _evals_builtin_tools
4546
from . import _evals_constant
4647
from . import _evals_data_converters
4748
from . import _evals_metric_handlers
@@ -483,6 +484,7 @@ def _get_default_prompt_template(
483484
and eval_item.evaluation_request
484485
and eval_item.evaluation_request.prompt
485486
and eval_item.evaluation_request.prompt.prompt_template_data
487+
and eval_item.evaluation_request.prompt.prompt_template_data.values
486488
):
487489
if (
488490
"prompt"
@@ -837,48 +839,7 @@ def _merge_text_parts_in_agent_data(
837839
content.parts = merged_parts
838840

839841

840-
def _agent_tools_to_config_tools(
841-
agent_tools: Optional[list[Any]],
842-
) -> Optional[list[genai_types.Tool]]:
843-
"""Maps Gemini Agents API tools to ``genai_types.Tool`` for an AgentConfig.
844-
845-
The Gemini Agents API returns built-in tool variants (``google_search``,
846-
``code_execution``, ``url_context``) whose schema differs from
847-
``genai_types.Tool``. Each recognised built-in variant is mapped to the
848-
matching ``genai_types.Tool`` field. Tools with a non-empty body after
849-
stripping the ``type`` key (e.g. ``function_declarations``) are passed
850-
through ``model_validate``. Variants without a ``genai_types.Tool``
851-
equivalent (e.g. ``filesystem``, ``mcp_server``) are skipped.
852-
853-
Args:
854-
agent_tools: The ``tools`` list from a fetched Gemini agent dict.
855-
856-
Returns:
857-
A list of ``genai_types.Tool``, or ``None`` if there are no mappable
858-
tools.
859-
"""
860-
if not agent_tools:
861-
return None
862-
tools: list[genai_types.Tool] = []
863-
for tool in agent_tools:
864-
if not isinstance(tool, dict):
865-
continue
866-
tool_type = tool.get("type")
867-
if tool_type == "google_search":
868-
tools.append(genai_types.Tool(google_search=genai_types.GoogleSearch()))
869-
elif tool_type == "code_execution":
870-
tools.append(
871-
genai_types.Tool(code_execution=genai_types.ToolCodeExecution())
872-
)
873-
elif tool_type == "url_context":
874-
tools.append(genai_types.Tool(url_context=genai_types.UrlContext()))
875-
else:
876-
# For non-built-in tools (e.g. function_declarations), strip the
877-
# type key and validate through genai_types.Tool.
878-
remainder = {k: v for k, v in tool.items() if k != "type"}
879-
if remainder:
880-
tools.append(genai_types.Tool.model_validate(remainder))
881-
return tools or None
842+
_agent_tools_to_config_tools = _evals_builtin_tools.agent_tools_to_config_tools
882843

883844

884845
def _fetch_agent_config_dict(
@@ -916,7 +877,13 @@ def _fetch_agent_config_dict(
916877
instruction = agent_dict.get("system_instruction") or None
917878
description = agent_dict.get("description") or None
918879
agent_type = agent_dict.get("base_agent") or None
919-
tools = _agent_tools_to_config_tools(agent_dict.get("tools"))
880+
has_environment = bool(
881+
agent_dict.get("environment_config")
882+
or agent_dict.get("base_environment")
883+
)
884+
tools = _agent_tools_to_config_tools(
885+
agent_dict.get("tools"), has_environment=has_environment
886+
)
920887
except Exception as e: # pylint: disable=broad-exception-caught
921888
logger.warning(
922889
"Failed to fetch agent config for '%s' (continuing without it): %s",
@@ -988,33 +955,6 @@ def _agent_data_response_text(agent_data: types.evals.AgentData) -> Optional[str
988955
return "".join(text_parts) or None
989956

990957

991-
def _agent_resource_to_agent_info(
992-
agent: str, api_client: BaseApiClient
993-
) -> "types.evals.AgentInfo":
994-
"""Builds an `AgentInfo` from a Gemini Agents API agent resource name.
995-
996-
Fetches the agent through the SDK's `api_client` (so replay recording is
997-
preserved) via `_fetch_agent_config_dict` and derives a single-agent
998-
`AgentInfo`: the agent's short name is the agents-map key and
999-
`root_agent_id`.
1000-
1001-
Args:
1002-
agent: The Gemini Agents API agent resource name
1003-
(`projects/{p}/locations/{l}/agents/{name}`).
1004-
api_client: The API client used to fetch the agent.
1005-
1006-
Returns:
1007-
An `AgentInfo` describing the fetched agent.
1008-
"""
1009-
agent_config = _fetch_agent_config_dict(api_client, agent)
1010-
short_name = agent_config.agent_id
1011-
return types.evals.AgentInfo( # pytype: disable=missing-parameter
1012-
name=short_name,
1013-
agents={short_name: agent_config},
1014-
root_agent_id=short_name,
1015-
)
1016-
1017-
1018958
_INTERACTION_TERMINAL_STATES = frozenset(
1019959
["completed", "failed", "cancelled", "incomplete", "budget_exceeded"]
1020960
)
@@ -1108,6 +1048,11 @@ def _run_gemini_agent_inference(
11081048

11091049
interactions_client = _get_interactions_client(api_client)
11101050

1051+
# Best-effort: fetch the agent config (instruction, tools, description)
1052+
# once, so every row's agent_data carries the agents map and the display
1053+
# can render the System Topology section.
1054+
agent_config = _fetch_agent_config_dict(api_client, gemini_agent)
1055+
11111056
agent_short_id = gemini_agent.split("/")[-1]
11121057
prompts: list[str] = []
11131058
responses: list[Optional[str]] = []
@@ -1128,6 +1073,7 @@ def _run_gemini_agent_inference(
11281073
)
11291074
interaction = _await_interaction(interactions_client, interaction)
11301075
agent_data_obj = _interaction_dict_to_agent_data(interaction)
1076+
agent_data_obj.agents = {agent_config.agent_id: agent_config}
11311077
_merge_text_parts_in_agent_data(agent_data_obj)
11321078
responses.append(_agent_data_response_text(agent_data_obj))
11331079
interaction_ids.append(interaction.get("id"))

0 commit comments

Comments
 (0)