google-cloud-agentplatform 1.165.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. agentplatform/__init__.py +72 -0
  2. agentplatform/_genai/__init__.py +43 -0
  3. agentplatform/_genai/_agent_engines_utils.py +2341 -0
  4. agentplatform/_genai/_bigquery_utils.py +49 -0
  5. agentplatform/_genai/_datasets_utils.py +344 -0
  6. agentplatform/_genai/_evals_builtin_tools.py +209 -0
  7. agentplatform/_genai/_evals_common.py +4268 -0
  8. agentplatform/_genai/_evals_constant.py +122 -0
  9. agentplatform/_genai/_evals_data_converters.py +926 -0
  10. agentplatform/_genai/_evals_metric_handlers.py +1783 -0
  11. agentplatform/_genai/_evals_metric_loaders.py +401 -0
  12. agentplatform/_genai/_evals_utils.py +1043 -0
  13. agentplatform/_genai/_evals_visualization.py +2070 -0
  14. agentplatform/_genai/_gcs_utils.py +262 -0
  15. agentplatform/_genai/_logging_utils.py +47 -0
  16. agentplatform/_genai/_memory_bank_utils.py +206 -0
  17. agentplatform/_genai/_observability_data_converter.py +186 -0
  18. agentplatform/_genai/_operations_utils.py +94 -0
  19. agentplatform/_genai/_prompt_management_utils.py +147 -0
  20. agentplatform/_genai/_prompt_optimizer_utils.py +215 -0
  21. agentplatform/_genai/_skills_utils.py +69 -0
  22. agentplatform/_genai/_transformers.py +628 -0
  23. agentplatform/_genai/a2a_task_events.py +509 -0
  24. agentplatform/_genai/a2a_tasks.py +861 -0
  25. agentplatform/_genai/agent_engines.py +3931 -0
  26. agentplatform/_genai/client.py +519 -0
  27. agentplatform/_genai/datasets.py +3045 -0
  28. agentplatform/_genai/endpoints.py +1149 -0
  29. agentplatform/_genai/evals.py +6883 -0
  30. agentplatform/_genai/example_stores.py +1445 -0
  31. agentplatform/_genai/feedback_contexts.py +700 -0
  32. agentplatform/_genai/feedback_entries.py +1644 -0
  33. agentplatform/_genai/live.py +64 -0
  34. agentplatform/_genai/live_agent_engines.py +179 -0
  35. agentplatform/_genai/memories.py +2962 -0
  36. agentplatform/_genai/memory_banks.py +1927 -0
  37. agentplatform/_genai/memory_revisions.py +465 -0
  38. agentplatform/_genai/model_garden.py +2638 -0
  39. agentplatform/_genai/prompt_optimizer.py +995 -0
  40. agentplatform/_genai/prompts.py +4515 -0
  41. agentplatform/_genai/rag.py +4961 -0
  42. agentplatform/_genai/runtime_revisions.py +1257 -0
  43. agentplatform/_genai/runtimes.py +78 -0
  44. agentplatform/_genai/sandbox_snapshots.py +1015 -0
  45. agentplatform/_genai/sandbox_templates.py +1088 -0
  46. agentplatform/_genai/sandboxes.py +1604 -0
  47. agentplatform/_genai/session_events.py +543 -0
  48. agentplatform/_genai/sessions.py +1449 -0
  49. agentplatform/_genai/skill_revisions.py +377 -0
  50. agentplatform/_genai/skills.py +1708 -0
  51. agentplatform/_genai/types/__init__.py +4695 -0
  52. agentplatform/_genai/types/agent_engines.py +16 -0
  53. agentplatform/_genai/types/common.py +32784 -0
  54. agentplatform/_genai/types/evals.py +1031 -0
  55. agentplatform/_genai/types/prompt_optimizer.py +107 -0
  56. agentplatform/_genai/types/prompts.py +107 -0
  57. agentplatform/version.py +17 -0
  58. google_cloud_agentplatform-1.165.1.dev0.dist-info/METADATA +79 -0
  59. google_cloud_agentplatform-1.165.1.dev0.dist-info/RECORD +62 -0
  60. google_cloud_agentplatform-1.165.1.dev0.dist-info/WHEEL +5 -0
  61. google_cloud_agentplatform-1.165.1.dev0.dist-info/licenses/LICENSE +202 -0
  62. google_cloud_agentplatform-1.165.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,49 @@
1
+ # Copyright 2025 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
+
16
+ import logging
17
+
18
+ from google.cloud import bigquery
19
+ from google.genai._api_client import BaseApiClient
20
+ import pandas as pd
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class BigQueryUtils:
27
+ """Handles BigQuery operations."""
28
+
29
+ def __init__(self, api_client: BaseApiClient):
30
+ self.api_client = api_client
31
+ self.bigquery_client = bigquery.Client(
32
+ project=self.api_client.project,
33
+ credentials=self.api_client._credentials,
34
+ )
35
+
36
+ def load_bigquery_to_dataframe(self, table_uri: str) -> "pd.DataFrame":
37
+ """Loads data from a BigQuery table into a DataFrame."""
38
+ table = self.bigquery_client.get_table(table_uri)
39
+ return self.bigquery_client.list_rows(table).to_dataframe()
40
+
41
+ def upload_dataframe_to_bigquery(
42
+ self, df: "pd.DataFrame", bq_table_uri: str
43
+ ) -> None:
44
+ """Uploads a Pandas DataFrame to a BigQuery table."""
45
+ job = self.bigquery_client.load_table_from_dataframe(df, bq_table_uri)
46
+ job.result()
47
+ logger.info(
48
+ f"DataFrame successfully uploaded to BigQuery table: {bq_table_uri}"
49
+ )
@@ -0,0 +1,344 @@
1
+ # Copyright 2025 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
+ """Utility functions for multimodal dataset."""
16
+
17
+ import asyncio
18
+ import datetime
19
+ from typing import Any, Type, TypeVar
20
+ import uuid
21
+
22
+ import google.auth.credentials
23
+ from agentplatform._genai.types import common
24
+ from google.genai import _common
25
+
26
+
27
+ METADATA_SCHEMA_URI = (
28
+ "gs://google-cloud-aiplatform/schema/dataset/metadata/multimodal_1.0.0.yaml"
29
+ )
30
+ _BQ_MULTIREGIONS = {"us", "eu"}
31
+ _DEFAULT_BQ_DATASET_PREFIX = "vertex_datasets"
32
+ _DEFAULT_BQ_TABLE_PREFIX = "multimodal_dataset"
33
+
34
+ T = TypeVar("T", bound=_common.BaseModel)
35
+
36
+
37
+ def create_from_response(
38
+ model_type: Type[T],
39
+ response: dict[str, Any],
40
+ config: Any | None = None,
41
+ ) -> T:
42
+ """Creates a model from a response."""
43
+ kwargs = (
44
+ {
45
+ "config": {
46
+ "response_schema": getattr(config, "response_schema", None),
47
+ "response_json_schema": getattr(config, "response_json_schema", None),
48
+ "include_all_fields": getattr(config, "include_all_fields", None),
49
+ }
50
+ }
51
+ if config
52
+ else {}
53
+ )
54
+ return model_type._from_response(response=response, kwargs=kwargs)
55
+
56
+
57
+ def validate_multimodal_dataset_bigquery_uri(
58
+ multimodal_dataset: common.MultimodalDataset,
59
+ ) -> None:
60
+ """Validates that a multimodal dataset has a bigquery uri or raises ValueError."""
61
+ if (
62
+ not hasattr(multimodal_dataset, "metadata")
63
+ or multimodal_dataset.metadata is None
64
+ ):
65
+ raise ValueError("Multimodal dataset metadata is required.")
66
+ if (
67
+ not hasattr(multimodal_dataset.metadata, "input_config")
68
+ or multimodal_dataset.metadata.input_config is None
69
+ ):
70
+ raise ValueError("Multimodal dataset input config is required.")
71
+ if (
72
+ not hasattr(multimodal_dataset.metadata.input_config, "bigquery_source")
73
+ or multimodal_dataset.metadata.input_config.bigquery_source is None
74
+ ):
75
+ raise ValueError("Multimodal dataset input config bigquery source is required.")
76
+ if (
77
+ not hasattr(multimodal_dataset.metadata.input_config.bigquery_source, "uri")
78
+ or multimodal_dataset.metadata.input_config.bigquery_source.uri is None
79
+ ):
80
+ raise ValueError(
81
+ "Multimodal dataset input config bigquery source uri is required."
82
+ )
83
+ if not str(multimodal_dataset.metadata.input_config.bigquery_source.uri).startswith(
84
+ "bq://"
85
+ ):
86
+ raise ValueError(
87
+ "Multimodal dataset bigquery source uri must start with 'bq://'."
88
+ )
89
+
90
+
91
+ def _try_import_bigframes() -> Any:
92
+ """Tries to import `bigframes`."""
93
+ try:
94
+ import bigframes
95
+ import bigframes.pandas
96
+ import bigframes.bigquery
97
+
98
+ return bigframes
99
+ except ImportError as exc:
100
+ raise ImportError(
101
+ "`bigframes` is not installed. Please call 'pip install bigframes'."
102
+ ) from exc
103
+
104
+
105
+ def _try_import_bigquery() -> Any:
106
+ """Tries to import `bigquery`."""
107
+ try:
108
+ from google.cloud import bigquery
109
+
110
+ return bigquery
111
+ except ImportError as exc:
112
+ raise ImportError(
113
+ "`bigquery` is not installed. Please call 'pip install"
114
+ " google-cloud-bigquery'."
115
+ ) from exc
116
+
117
+
118
+ def _bq_dataset_location_allowed(
119
+ vertex_location: str, bq_dataset_location: str
120
+ ) -> bool:
121
+ if bq_dataset_location == vertex_location:
122
+ return True
123
+ if bq_dataset_location in _BQ_MULTIREGIONS:
124
+ return vertex_location.startswith(bq_dataset_location)
125
+ return False
126
+
127
+
128
+ def _normalize_and_validate_table_id(
129
+ *,
130
+ table_id: str,
131
+ project: str,
132
+ location: str,
133
+ credentials: google.auth.credentials.Credentials,
134
+ ) -> str:
135
+ bigquery = _try_import_bigquery()
136
+
137
+ table_ref = bigquery.TableReference.from_string(table_id, default_project=project)
138
+ if table_ref.project != project:
139
+ raise ValueError(
140
+ "The BigQuery table "
141
+ f"`{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}`"
142
+ " must be in the same project as the multimodal dataset."
143
+ f" The multimodal dataset is in `{project}`, but the BigQuery table"
144
+ f" is in `{table_ref.project}`."
145
+ )
146
+
147
+ dataset_ref = bigquery.DatasetReference(
148
+ project=table_ref.project, dataset_id=table_ref.dataset_id
149
+ )
150
+ client = bigquery.Client(project=project, credentials=credentials)
151
+ bq_dataset = client.get_dataset(dataset_ref=dataset_ref)
152
+ if not _bq_dataset_location_allowed(location, bq_dataset.location):
153
+ raise ValueError(
154
+ "The BigQuery dataset"
155
+ f" `{dataset_ref.project}.{dataset_ref.dataset_id}` must be in the"
156
+ " same location as the multimodal dataset. The multimodal dataset"
157
+ f" is in `{location}`, but the BigQuery dataset is in"
158
+ f" `{bq_dataset.location}`."
159
+ )
160
+ return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}"
161
+
162
+
163
+ async def _normalize_and_validate_table_id_async(
164
+ *,
165
+ table_id: str,
166
+ project: str,
167
+ location: str,
168
+ credentials: google.auth.credentials.Credentials,
169
+ ) -> str:
170
+ bigquery = _try_import_bigquery()
171
+
172
+ table_ref = bigquery.TableReference.from_string(table_id, default_project=project)
173
+ if table_ref.project != project:
174
+ raise ValueError(
175
+ "The BigQuery table "
176
+ f"`{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}`"
177
+ " must be in the same project as the multimodal dataset."
178
+ f" The multimodal dataset is in `{project}`, but the BigQuery table"
179
+ f" is in `{table_ref.project}`."
180
+ )
181
+
182
+ dataset_ref = bigquery.DatasetReference(
183
+ project=table_ref.project, dataset_id=table_ref.dataset_id
184
+ )
185
+ client = bigquery.Client(project=project, credentials=credentials)
186
+ bq_dataset = await asyncio.to_thread(client.get_dataset, dataset_ref=dataset_ref)
187
+ if not _bq_dataset_location_allowed(location, bq_dataset.location):
188
+ raise ValueError(
189
+ "The BigQuery dataset"
190
+ f" `{dataset_ref.project}.{dataset_ref.dataset_id}` must be in the"
191
+ " same location as the multimodal dataset. The multimodal dataset"
192
+ f" is in `{location}`, but the BigQuery dataset is in"
193
+ f" `{bq_dataset.location}`."
194
+ )
195
+ return f"{table_ref.project}.{table_ref.dataset_id}.{table_ref.table_id}"
196
+
197
+
198
+ def _create_default_bigquery_dataset_if_not_exists(
199
+ *,
200
+ project: str,
201
+ location: str,
202
+ credentials: google.auth.credentials.Credentials,
203
+ ) -> str:
204
+ bigquery = _try_import_bigquery()
205
+
206
+ bigquery_client = bigquery.Client(project=project, credentials=credentials)
207
+ location_str = location.lower().replace("-", "_")
208
+ dataset_id = bigquery.DatasetReference(
209
+ project, f"{_DEFAULT_BQ_DATASET_PREFIX}_{location_str}"
210
+ )
211
+ dataset = bigquery.Dataset(dataset_ref=dataset_id)
212
+ dataset.location = location
213
+ bigquery_client.create_dataset(dataset, exists_ok=True)
214
+ return f"{dataset_id.project}.{dataset_id.dataset_id}"
215
+
216
+
217
+ async def _create_default_bigquery_dataset_if_not_exists_async(
218
+ *,
219
+ project: str,
220
+ location: str,
221
+ credentials: google.auth.credentials.Credentials,
222
+ ) -> str:
223
+ bigquery = _try_import_bigquery()
224
+
225
+ bigquery_client = bigquery.Client(project=project, credentials=credentials)
226
+ location_str = location.lower().replace("-", "_")
227
+ dataset_id = bigquery.DatasetReference(
228
+ project, f"{_DEFAULT_BQ_DATASET_PREFIX}_{location_str}"
229
+ )
230
+ dataset = bigquery.Dataset(dataset_ref=dataset_id)
231
+ dataset.location = location
232
+ await asyncio.to_thread(bigquery_client.create_dataset, dataset, exists_ok=True)
233
+ return f"{dataset_id.project}.{dataset_id.dataset_id}"
234
+
235
+
236
+ def _generate_target_table_id(dataset_id: str) -> str:
237
+ return f"{dataset_id}.{_DEFAULT_BQ_TABLE_PREFIX}_{str(uuid.uuid4())}"
238
+
239
+
240
+ def generate_multimodal_dataset_display_name() -> str:
241
+ """Generates a display name with a timestamp."""
242
+ return f"MultimodalDataset {datetime.datetime.now().isoformat(sep=' ')}"
243
+
244
+
245
+ def get_batch_job_unique_name() -> str:
246
+ """Generates a unique name suffix for a batch job destination."""
247
+ timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
248
+ unique_id = uuid.uuid4().hex[0:5]
249
+ return f"{timestamp}_{unique_id}"
250
+
251
+
252
+ def save_dataframe_to_bigquery(
253
+ dataframe: "bigframes.pandas.DataFrame", # type: ignore # noqa: F821
254
+ target_table_id: str,
255
+ bq_client: "bigquery.Client", # type: ignore # noqa: F821
256
+ ) -> None:
257
+ # `to_gbq` does not support cross-region use cases. We use `copy_table` as a workaround.
258
+ temp_table_id = dataframe.to_gbq()
259
+ copy_job = bq_client.copy_table(
260
+ sources=temp_table_id,
261
+ destination=target_table_id,
262
+ )
263
+ copy_job.result()
264
+ bq_client.delete_table(temp_table_id)
265
+
266
+
267
+ async def save_dataframe_to_bigquery_async(
268
+ dataframe: "bigframes.pandas.DataFrame", # type: ignore # noqa: F821
269
+ target_table_id: str,
270
+ bq_client: "bigquery.Client", # type: ignore # noqa: F821
271
+ ) -> None:
272
+ # `to_gbq` does not support cross-region use cases. We use `copy_table` as a workaround.
273
+ temp_table_id = await asyncio.to_thread(dataframe.to_gbq)
274
+ copy_job = await asyncio.to_thread(
275
+ bq_client.copy_table,
276
+ sources=temp_table_id,
277
+ destination=target_table_id,
278
+ )
279
+ await asyncio.to_thread(copy_job.result)
280
+ await asyncio.to_thread(bq_client.delete_table, temp_table_id)
281
+
282
+
283
+ def load_dataframe_from_bigquery(
284
+ *,
285
+ bigquery_uri: str,
286
+ project: str,
287
+ location: str,
288
+ credentials: google.auth.credentials.Credentials,
289
+ ) -> "bigframes.pandas.DataFrame": # type: ignore # noqa: F821
290
+ """Loads a BigQuery table into a BigFrames DataFrame.
291
+
292
+ Args:
293
+ bigquery_uri: The URI of the BigQuery table, with or without the `bq://`
294
+ prefix.
295
+ project: The project to use for the BigFrames session.
296
+ location: The location to use for the BigFrames session.
297
+ credentials: The credentials to use for the BigFrames session.
298
+
299
+ Returns:
300
+ A BigFrames DataFrame backed by the BigQuery table.
301
+ """
302
+ bigframes = _try_import_bigframes()
303
+ session_options = bigframes.BigQueryOptions(
304
+ credentials=credentials,
305
+ project=project,
306
+ location=location,
307
+ )
308
+ with bigframes.connect(session_options) as session:
309
+ return session.read_gbq(bigquery_uri.removeprefix("bq://"))
310
+
311
+
312
+ async def load_dataframe_from_bigquery_async(
313
+ *,
314
+ bigquery_uri: str,
315
+ project: str,
316
+ location: str,
317
+ credentials: google.auth.credentials.Credentials,
318
+ ) -> "bigframes.pandas.DataFrame": # type: ignore # noqa: F821
319
+ """Loads a BigQuery table into a BigFrames DataFrame.
320
+
321
+ Args:
322
+ bigquery_uri: The URI of the BigQuery table, with or without the `bq://`
323
+ prefix.
324
+ project: The project to use for the BigFrames session.
325
+ location: The location to use for the BigFrames session.
326
+ credentials: The credentials to use for the BigFrames session.
327
+
328
+ Returns:
329
+ A BigFrames DataFrame backed by the BigQuery table.
330
+ """
331
+ return await asyncio.to_thread(
332
+ load_dataframe_from_bigquery,
333
+ bigquery_uri=bigquery_uri,
334
+ project=project,
335
+ location=location,
336
+ credentials=credentials,
337
+ )
338
+
339
+
340
+ def resolve_dataset_name(resource_name_or_id: str, project: str, location: str) -> str:
341
+ """Resolves a dataset name or ID to a full resource name."""
342
+ if "/" not in resource_name_or_id:
343
+ return f"projects/{project}/locations/{location}/datasets/{resource_name_or_id}"
344
+ return resource_name_or_id
@@ -0,0 +1,209 @@
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 and descriptions without a
24
+ server round-trip. Parameter schemas are intentionally omitted to avoid
25
+ publishing internal tool contract details.
26
+
27
+ **If the server catalog changes, this SDK-side copy must be updated to match.**
28
+
29
+ Sandbox orchestration tools (``provision_sandbox``, ``load_sandbox``) are
30
+ intentionally excluded from the tool catalog. They are infrastructure
31
+ initialization, not user-facing agent capabilities.
32
+ """
33
+
34
+ from typing import Any, Optional
35
+
36
+ from google.genai import types as genai_types
37
+
38
+
39
+ # Maps a built-in Gemini Agent tool type to the concrete FunctionDeclarations
40
+ # the agent actually exposes for that type.
41
+ #
42
+ # Source of truth: interaction_converter.py, _BUILTIN_TOOL_FUNCTION_DECLARATIONS
43
+ BUILTIN_TOOL_DECLARATIONS: dict[str, list[genai_types.FunctionDeclaration]] = {
44
+ "code_execution": [
45
+ genai_types.FunctionDeclaration(
46
+ name="run_command",
47
+ description="Runs a shell command on the sandbox VM.",
48
+ ),
49
+ ],
50
+ "filesystem": [
51
+ genai_types.FunctionDeclaration(
52
+ name="view_file",
53
+ description="Reads the content of a workspace file.",
54
+ ),
55
+ genai_types.FunctionDeclaration(
56
+ name="create_file",
57
+ description="Writes content to a new or existing file.",
58
+ ),
59
+ genai_types.FunctionDeclaration(
60
+ name="edit_file",
61
+ description="Replaces a specific block of text in a file.",
62
+ ),
63
+ genai_types.FunctionDeclaration(
64
+ name="list_dir",
65
+ description="Lists the files in a directory.",
66
+ ),
67
+ genai_types.FunctionDeclaration(
68
+ name="delete_file",
69
+ description="Removes a file from the workspace.",
70
+ ),
71
+ genai_types.FunctionDeclaration(
72
+ name="move_file",
73
+ description="Renames or moves a file.",
74
+ ),
75
+ ],
76
+ }
77
+
78
+
79
+ # Sandbox-environment orchestration tools.
80
+ # Source of truth: interaction_converter.py, _SANDBOX_TOOL_NAMES
81
+ SANDBOX_TOOL_NAMES: frozenset[str] = frozenset(
82
+ {
83
+ "provision_sandbox",
84
+ "load_sandbox",
85
+ }
86
+ )
87
+
88
+
89
+ def is_sandbox_only_turn(
90
+ events: list[Any],
91
+ ) -> bool:
92
+ """Returns True if a turn contains only sandbox initialization events.
93
+
94
+ Sandbox provisioning events (``provision_sandbox``, ``load_sandbox``)
95
+ are infrastructure setup steps that happen before the user's first
96
+ real prompt.
97
+
98
+ A turn is sandbox-only when every event is either a
99
+ ``function_call`` or ``function_response`` referencing a sandbox
100
+ tool name. Events with plain text content (model output, user
101
+ input) disqualify the turn.
102
+
103
+ Args:
104
+ events: The list of AgentEvents in the turn.
105
+
106
+ Returns:
107
+ True if the turn is sandbox-only and should be merged into the
108
+ next real turn for display.
109
+ """
110
+ if not events:
111
+ return True
112
+
113
+ for event in events:
114
+ content = getattr(event, "content", None)
115
+ if not content:
116
+ continue
117
+ parts = getattr(content, "parts", None)
118
+ if not parts:
119
+ continue
120
+ for part in parts:
121
+ if getattr(part, "function_call", None):
122
+ if part.function_call.name not in SANDBOX_TOOL_NAMES:
123
+ return False
124
+ elif getattr(part, "function_response", None):
125
+ if part.function_response.name not in SANDBOX_TOOL_NAMES:
126
+ return False
127
+ else:
128
+ # Any other part type (text, inline_data, executable_code,
129
+ # code_execution_result, etc.) means this is a real
130
+ # conversational event, not sandbox infrastructure.
131
+ return False
132
+ return True
133
+
134
+
135
+ def agent_tools_to_config_tools(
136
+ agent_tools: Optional[list[Any]],
137
+ ) -> Optional[list[genai_types.Tool]]:
138
+ """Maps Gemini Agents API tools to ``genai_types.Tool`` for display.
139
+
140
+ Expands built-in agent tool types into their concrete function declarations
141
+ using ``BUILTIN_TOOL_DECLARATIONS`` (a display-only duplicate of the
142
+ server-side catalog in ``interaction_converter.py``).
143
+
144
+ Mapping rules:
145
+ * ``code_execution`` is expanded to ``run_command``.
146
+ * ``filesystem`` is expanded to ``view_file``, ``create_file``,
147
+ ``edit_file``, ``list_dir``, ``delete_file``, ``move_file``.
148
+ * ``google_search`` and ``url_context`` are mapped to their typed
149
+ ``genai_types.Tool`` variant.
150
+ * ``mcp_server`` is represented as a named declaration with a
151
+ human-readable label.
152
+ * Tools carrying explicit ``function_declarations`` are passed through.
153
+
154
+ Sandbox orchestration tools (``provision_sandbox``, ``load_sandbox``)
155
+ are intentionally excluded. They are infrastructure initialization,
156
+ not user-facing capabilities.
157
+
158
+ Args:
159
+ agent_tools: The ``tools`` list from a fetched Gemini agent dict.
160
+
161
+ Returns:
162
+ A list of ``genai_types.Tool``, or ``None`` if there are no mappable
163
+ tools.
164
+ """
165
+ if not agent_tools:
166
+ return None
167
+ tools: list[genai_types.Tool] = []
168
+ for tool in agent_tools or []:
169
+ if not isinstance(tool, dict):
170
+ continue
171
+ tool_type = tool.get("type")
172
+ remainder = {k: v for k, v in tool.items() if k != "type"}
173
+
174
+ # Check the built-in catalog first (code_execution, filesystem).
175
+ catalog_decls = BUILTIN_TOOL_DECLARATIONS.get(tool_type or "")
176
+ if catalog_decls:
177
+ tools.append(genai_types.Tool(function_declarations=list(catalog_decls)))
178
+ elif tool_type == "google_search":
179
+ tools.append(genai_types.Tool(google_search=genai_types.GoogleSearch()))
180
+ elif tool_type == "url_context":
181
+ tools.append(genai_types.Tool(url_context=genai_types.UrlContext()))
182
+ elif "function_declarations" in remainder:
183
+ # Real function tool with explicit declarations.
184
+ tools.append(genai_types.Tool.model_validate(remainder))
185
+ elif tool_type == "mcp_server":
186
+ label = remainder.get("name") or remainder.get("url")
187
+ description = f"MCP server: {label}" if label else "MCP server."
188
+ tools.append(
189
+ genai_types.Tool(
190
+ function_declarations=[
191
+ genai_types.FunctionDeclaration(
192
+ name="mcp_server", description=description
193
+ )
194
+ ]
195
+ )
196
+ )
197
+ elif tool_type:
198
+ # Unknown built-in: show by name so it isn't silently dropped.
199
+ tools.append(
200
+ genai_types.Tool(
201
+ function_declarations=[
202
+ genai_types.FunctionDeclaration(name=tool_type)
203
+ ]
204
+ )
205
+ )
206
+ elif remainder:
207
+ tools.append(genai_types.Tool.model_validate(remainder))
208
+
209
+ return tools or None