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,262 @@
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
+ from importlib.metadata import version as get_version
17
+ import io
18
+ import json
19
+ import logging
20
+ from typing import Any, Union
21
+ import uuid
22
+
23
+ from google.cloud import storage # type: ignore[attr-defined]
24
+ from google.genai._api_client import BaseApiClient
25
+ from packaging.version import Version
26
+ import pandas as pd
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ GCS_PREFIX = "gs://"
33
+
34
+
35
+ # Detect google-cloud-storage version once at module load
36
+ try:
37
+ _GCS_VERSION = Version(get_version("google-cloud-storage"))
38
+ except Exception:
39
+ # Fallback if version detection fails (should not happen in normal use)
40
+ _GCS_VERSION = Version("3.0.0")
41
+
42
+ _USE_FROM_URI = _GCS_VERSION >= Version("3.0.0")
43
+
44
+
45
+ def blob_from_uri(uri: str, client: storage.Client) -> storage.Blob:
46
+ """Create a Blob from a GCS URI, compatible with v2 and v3.
47
+
48
+ This function provides compatibility across google-cloud-storage versions:
49
+ - v3.x: Uses Blob.from_uri()
50
+ - v2.x: Uses Blob.from_string() (deprecated in v3)
51
+
52
+ Args:
53
+ uri: GCS URI (e.g., 'gs://bucket/path/to/blob')
54
+ client: Storage client instance
55
+
56
+ Returns:
57
+ storage.Blob: Blob instance
58
+ """
59
+ if _USE_FROM_URI:
60
+ return storage.Blob.from_uri(uri, client=client)
61
+ else:
62
+ return storage.Blob.from_string(uri, client=client)
63
+
64
+
65
+ class GcsUtils:
66
+ """Handles File I/O operations with Google Cloud Storage (GCS)"""
67
+
68
+ def __init__(self, api_client: BaseApiClient):
69
+ self.api_client = api_client
70
+ self.storage_client = storage.Client(
71
+ project=self.api_client.project,
72
+ credentials=self.api_client._credentials,
73
+ )
74
+
75
+ def parse_gcs_path(self, gcs_path: str) -> tuple[str, str]:
76
+ """Helper to parse gs://bucket/path into (bucket_name, blob_path)."""
77
+ if not gcs_path.startswith(GCS_PREFIX):
78
+ raise ValueError(
79
+ f"Invalid GCS path: '{gcs_path}'. It must start with '{GCS_PREFIX}'."
80
+ )
81
+ path_without_prefix = gcs_path[len(GCS_PREFIX) :]
82
+ if "/" not in path_without_prefix:
83
+ return path_without_prefix, ""
84
+ bucket_name, blob_path = path_without_prefix.split("/", 1)
85
+ return bucket_name, blob_path
86
+
87
+ def upload_file_to_gcs(self, upload_gcs_path: str, filename: str) -> None:
88
+ """Uploads the provided file to a Google Cloud Storage location."""
89
+
90
+ blob_from_uri(
91
+ uri=upload_gcs_path, client=self.storage_client
92
+ ).upload_from_filename(filename)
93
+
94
+ def upload_dataframe(
95
+ self,
96
+ df: "pd.DataFrame",
97
+ gcs_destination_blob_path: str,
98
+ file_type: str = "jsonl",
99
+ ) -> None:
100
+ """Uploads a Pandas DataFrame to a Google Cloud Storage location.
101
+
102
+ Args:
103
+ df: The Pandas DataFrame to upload.
104
+ gcs_destination_blob_path: The full GCS path for the destination blob
105
+ (e.g., 'gs://bucket/data/my_dataframe.jsonl').
106
+ file_type: The format to save the DataFrame ('jsonl' or 'csv'). Defaults
107
+ to 'jsonl'.
108
+ """
109
+ bucket_name, blob_name = self.parse_gcs_path(gcs_destination_blob_path)
110
+ if not blob_name:
111
+ raise ValueError(
112
+ f"Invalid GCS path for blob: '{gcs_destination_blob_path}'. "
113
+ "It must include the object name (e.g., gs://bucket/file.csv)."
114
+ )
115
+ bucket = self.storage_client.bucket(bucket_name)
116
+ blob = bucket.blob(blob_name)
117
+
118
+ buffer = io.StringIO()
119
+ if file_type == "csv":
120
+ df.to_csv(buffer, index=False)
121
+ content_type = "text/csv"
122
+ elif file_type == "jsonl":
123
+ df.to_json(buffer, orient="records", lines=True)
124
+ content_type = "application/jsonl"
125
+ else:
126
+ raise ValueError(
127
+ f"Unsupported file type: '{file_type}'. "
128
+ "Please provide 'jsonl' or 'csv'."
129
+ )
130
+ blob.upload_from_string(buffer.getvalue(), content_type=content_type)
131
+
132
+ logger.info(
133
+ f"DataFrame successfully uploaded to: gs://{bucket.name}/{blob.name}"
134
+ )
135
+
136
+ def upload_json(self, data: dict[str, Any], gcs_destination_blob_path: str) -> None:
137
+ """Uploads a dictionary as a JSON file to Google Cloud Storage."""
138
+ bucket_name, blob_name = self.parse_gcs_path(gcs_destination_blob_path)
139
+ if not blob_name:
140
+ raise ValueError(
141
+ f"Invalid GCS path for blob: '{gcs_destination_blob_path}'. "
142
+ "It must include the object name (e.g., gs://bucket/file.json)."
143
+ )
144
+ bucket = self.storage_client.bucket(bucket_name)
145
+ blob = bucket.blob(blob_name)
146
+
147
+ json_data = json.dumps(data, indent=2)
148
+ blob.upload_from_string(json_data, content_type="application/json")
149
+
150
+ logger.info(
151
+ f"JSON data successfully uploaded to: gs://{bucket_name}/{blob_name}"
152
+ )
153
+
154
+ def upload_json_to_prefix(
155
+ self,
156
+ data: dict[str, Any],
157
+ gcs_dest_prefix: str,
158
+ filename_prefix: str = "data",
159
+ ) -> str:
160
+ """Uploads a dictionary to a GCS prefix with a UUID JSON filename.
161
+
162
+ Args:
163
+ data: The dictionary to upload.
164
+ gcs_dest_prefix: The GCS prefix (e.g., 'gs://bucket/path/prefix/').
165
+ filename_prefix: Prefix for the generated filename. Defaults to 'data'.
166
+
167
+ Returns:
168
+ The full GCS path where the file was uploaded.
169
+
170
+ Raises:
171
+ ValueError: If the gcs_dest_prefix is not a valid GCS path.
172
+ """
173
+ if not gcs_dest_prefix.startswith(GCS_PREFIX):
174
+ raise ValueError(
175
+ f"Invalid GCS destination prefix: '{gcs_dest_prefix}'. Must start"
176
+ f" with '{GCS_PREFIX}'."
177
+ )
178
+
179
+ gcs_path_without_scheme = gcs_dest_prefix[len(GCS_PREFIX) :]
180
+ bucket_name, *path_parts = gcs_path_without_scheme.split("/")
181
+
182
+ user_prefix_path = "/".join(path_parts)
183
+ if user_prefix_path and not user_prefix_path.endswith("/"):
184
+ user_prefix_path += "/"
185
+
186
+ filename = f"{filename_prefix}_{uuid.uuid4()}.json"
187
+
188
+ blob_name = f"{user_prefix_path}{filename}"
189
+
190
+ full_gcs_path = f"{GCS_PREFIX}{bucket_name}/{blob_name}"
191
+
192
+ self.upload_json(data, full_gcs_path)
193
+ return full_gcs_path
194
+
195
+ def read_file_contents(self, gcs_filepath: str) -> Union[str, Any]:
196
+ """Reads the contents of a file from Google Cloud Storage."""
197
+
198
+ bucket_name, blob_path = self.parse_gcs_path(gcs_filepath)
199
+ if not blob_path:
200
+ raise ValueError(
201
+ f"Invalid GCS file path: '{gcs_filepath}'. Path must point to a file,"
202
+ " not just a bucket."
203
+ )
204
+ bucket = self.storage_client.bucket(bucket_name)
205
+ blob = bucket.blob(blob_path)
206
+ content = blob.download_as_bytes().decode("utf-8")
207
+ logger.info(f"Successfully read content from '{gcs_filepath}'")
208
+ return content
209
+
210
+ def read_gcs_file_to_dataframe(
211
+ self, gcs_filepath: str, file_type: str
212
+ ) -> "pd.DataFrame":
213
+ """Reads a file from Google Cloud Storage into a Pandas DataFrame."""
214
+ file_contents = self.read_file_contents(gcs_filepath)
215
+ if file_type == "csv":
216
+ return pd.read_csv(io.StringIO(file_contents), encoding="utf-8")
217
+ elif file_type == "jsonl":
218
+ return pd.read_json(io.StringIO(file_contents), lines=True)
219
+ else:
220
+ raise ValueError(
221
+ f"Unsupported file type: '{file_type}'. Please provide 'jsonl' or"
222
+ " 'csv'."
223
+ )
224
+
225
+ def _verify_bucket_ownership(
226
+ self,
227
+ bucket_name: str,
228
+ expected_project: str,
229
+ ) -> bool:
230
+ """Verifies that a GCS bucket belongs to the expected project.
231
+
232
+ This check mitigates bucket squatting attacks.
233
+
234
+ Args:
235
+ bucket_name: The GCS bucket to verify.
236
+ expected_project: The project ID or number that should own the bucket.
237
+
238
+ Returns:
239
+ True if the bucket belongs to the expected project, False otherwise.
240
+ """
241
+ try:
242
+ bucket = self.storage_client.bucket(bucket_name=bucket_name)
243
+ bucket.reload(client=self.storage_client)
244
+ bucket_project_number = str(bucket.project_number)
245
+
246
+ if expected_project.isdigit():
247
+ expected_project_number = expected_project
248
+ else:
249
+ from google.cloud import resourcemanager_v3
250
+
251
+ projects_client = resourcemanager_v3.ProjectsClient(
252
+ credentials=self.storage_client._credentials
253
+ )
254
+ project = projects_client.get_project(
255
+ name=f"projects/{expected_project}"
256
+ )
257
+
258
+ expected_project_number = project.name.split("/")[-1]
259
+
260
+ return bucket_project_number == expected_project_number
261
+ except Exception:
262
+ return False
@@ -0,0 +1,47 @@
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 functools
17
+ from typing import Any, Callable
18
+ from google.genai import _common
19
+ import warnings
20
+
21
+
22
+ def show_deprecation_warning_once(
23
+ message: str,
24
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
25
+ """Decorator to show a deprecation warning once for a function."""
26
+
27
+ def decorator(func: Any) -> Any:
28
+ warning_done = False
29
+
30
+ @functools.wraps(func)
31
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
32
+ nonlocal warning_done
33
+ if not warning_done:
34
+ warning_done = True
35
+ warnings.warn(message, DeprecationWarning, stacklevel=2)
36
+
37
+ # Suppress ExperimentalWarning while executing the deprecated wrapper
38
+ with warnings.catch_warnings():
39
+ # We ignore ExperimentalWarning because the user will see it
40
+ # when they migrate to the new prompts module
41
+ warnings.simplefilter("ignore", category=_common.ExperimentalWarning)
42
+ return func(*args, **kwargs)
43
+ return func(*args, **kwargs)
44
+
45
+ return wrapper
46
+
47
+ return decorator
@@ -0,0 +1,206 @@
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 memory banks."""
16
+
17
+ import asyncio
18
+ import json
19
+ import re
20
+ import time
21
+ from typing import (
22
+ Any,
23
+ Protocol,
24
+ Union,
25
+ )
26
+
27
+ from . import types as genai_types
28
+
29
+
30
+ MemoryBankOperation = Union[
31
+ genai_types.MemoryBankOperation,
32
+ genai_types.MemoryOperation,
33
+ genai_types.GenerateMemoriesOperation,
34
+ ]
35
+
36
+
37
+ class GetOperationFunction(Protocol):
38
+ def __call__(self, *, operation_name: str, **kwargs: Any) -> MemoryBankOperation:
39
+ pass
40
+
41
+
42
+ class GetAsyncOperationFunction(Protocol):
43
+ async def __call__(
44
+ self, *, operation_name: str, **kwargs: Any
45
+ ) -> MemoryBankOperation:
46
+ pass
47
+
48
+
49
+ def _get_memory_bank_id(operation_name: str = "", resource_name: str = "") -> str:
50
+ """Returns Memory Bank ID from operation name or resource name."""
51
+ if not resource_name and not operation_name:
52
+ raise ValueError("Resource name or operation name cannot be empty.")
53
+
54
+ if resource_name:
55
+ match = re.match(
56
+ r"^projects/[^/]+/locations/[^/]+/reasoningEngines/([^/]+)$",
57
+ resource_name,
58
+ )
59
+ if match:
60
+ return match.group(1)
61
+ match = re.match(
62
+ r"^projects/[^/]+/locations/[^/]+/memoryBanks/([^/]+)$",
63
+ resource_name,
64
+ )
65
+ if match:
66
+ return match.group(1)
67
+ raise ValueError(
68
+ "Failed to parse Memory Bank ID from resource name: " f"`{resource_name}`"
69
+ )
70
+
71
+ if not operation_name:
72
+ raise ValueError("Operation name cannot be empty.")
73
+
74
+ match = re.match(
75
+ r"^projects/[^/]+/locations/[^/]+/reasoningEngines/([^/]+)/operations/[^/]+$",
76
+ operation_name,
77
+ )
78
+ if match:
79
+ return match.group(1)
80
+
81
+ match = re.match(
82
+ r"^projects/[^/]+/locations/[^/]+/memoryBanks/([^/]+)/operations/[^/]+$",
83
+ operation_name,
84
+ )
85
+ raise ValueError(
86
+ "Failed to parse Memory Bank ID from operation name: " f"`{operation_name}`"
87
+ )
88
+
89
+
90
+ def _await_operation(
91
+ *,
92
+ operation_name: str,
93
+ get_operation_fn: GetOperationFunction,
94
+ poll_interval_seconds: float = 1,
95
+ ) -> Any:
96
+ """Waits for the operation to complete.
97
+
98
+ Args:
99
+ operation_name (str):
100
+ Required. The name of the operation.
101
+ poll_interval_seconds (float):
102
+ The number of seconds to wait between each poll.
103
+ get_operation_fn (Callable[[str], Any]):
104
+ Optional. The function to use for getting the operation. If not
105
+ provided, `self._get_memory_bank_operation` will be used.
106
+
107
+ Returns:
108
+ The operation that has completed (i.e. `operation.done==True`).
109
+ """
110
+ operation = get_operation_fn(operation_name=operation_name)
111
+ while not operation.done:
112
+ time.sleep(poll_interval_seconds)
113
+ operation = get_operation_fn(operation_name=operation.name)
114
+
115
+ return operation
116
+
117
+
118
+ async def _await_async_operation(
119
+ *,
120
+ operation_name: str,
121
+ get_operation_fn: GetAsyncOperationFunction,
122
+ poll_interval_seconds: float = 1,
123
+ ) -> Any:
124
+ """Waits for the operation to complete.
125
+
126
+ Args:
127
+ operation_name (str):
128
+ Required. The name of the operation.
129
+ poll_interval_seconds (float):
130
+ The number of seconds to wait between each poll.
131
+ get_operation_fn (Callable[[str], Awaitable[Any]]):
132
+ Optional. The async function to use for getting the operation. If not
133
+ provided, `self._get_memory_bank_operation` will be used.
134
+
135
+ Returns:
136
+ The operation that has completed (i.e. `operation.done==True`).
137
+ """
138
+ operation = await get_operation_fn(operation_name=operation_name)
139
+ while not operation.done:
140
+ await asyncio.sleep(poll_interval_seconds)
141
+ operation = await get_operation_fn(operation_name=operation.name)
142
+
143
+ return operation
144
+
145
+
146
+ def _managed_semantic_memory_config_to_memory_bank_config(
147
+ semantic_memory_config: genai_types.ManagedSemanticMemoryConfigOrDict,
148
+ ) -> genai_types.ReasoningEngineContextSpecMemoryBankConfigDict:
149
+ """Converts ManagedSemanticMemoryConfig to MemoryBankConfig."""
150
+ if semantic_memory_config is None:
151
+ semantic_memory_config = {}
152
+ if isinstance(semantic_memory_config, dict):
153
+ semantic_memory_config = genai_types.ManagedSemanticMemoryConfig.model_validate(
154
+ semantic_memory_config
155
+ )
156
+ elif not isinstance(
157
+ semantic_memory_config, genai_types.ManagedSemanticMemoryConfig
158
+ ):
159
+ raise TypeError(
160
+ "managed_semantic_memory_config must be a dict or "
161
+ "ManagedSemanticMemoryConfig, "
162
+ f"but got {type(semantic_memory_config)}."
163
+ )
164
+
165
+ memory_bank_config = json.loads(semantic_memory_config.model_dump_json())
166
+ if "unstructured_memory_configs" in memory_bank_config:
167
+ memory_bank_config["customization_configs"] = memory_bank_config.pop(
168
+ "unstructured_memory_configs"
169
+ )
170
+ return memory_bank_config
171
+
172
+
173
+ def _memory_bank_config_to_managed_semantic_memories_config(
174
+ memory_bank_config: genai_types.ReasoningEngineContextSpecMemoryBankConfig,
175
+ ) -> genai_types.ManagedSemanticMemoryConfigDict:
176
+ """Converts MemoryBankConfig to ManagedSemanticMemoriesConfig."""
177
+ memory_bank_config = json.loads(memory_bank_config.model_dump_json())
178
+ if "customization_configs" in memory_bank_config:
179
+ memory_bank_config["unstructured_memory_configs"] = memory_bank_config.pop(
180
+ "customization_configs"
181
+ )
182
+ return memory_bank_config
183
+
184
+
185
+ def _reasoning_engine_to_memory_bank(
186
+ reasoning_engine: genai_types.ReasoningEngine,
187
+ ) -> genai_types.MemoryBank:
188
+ """Converts ReasoningEngine to MemoryBank."""
189
+ if reasoning_engine.context_spec is not None:
190
+ semantic_memory_config = (
191
+ _memory_bank_config_to_managed_semantic_memories_config(
192
+ reasoning_engine.context_spec.memory_bank_config
193
+ )
194
+ )
195
+ else:
196
+ semantic_memory_config = {}
197
+ memory_bank = genai_types.MemoryBank(
198
+ name=reasoning_engine.name,
199
+ create_time=reasoning_engine.create_time,
200
+ update_time=reasoning_engine.update_time,
201
+ display_name=reasoning_engine.display_name,
202
+ description=reasoning_engine.description,
203
+ encryption_spec=reasoning_engine.encryption_spec,
204
+ managed_semantic_memory_config=semantic_memory_config,
205
+ )
206
+ return memory_bank
@@ -0,0 +1,186 @@
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
+ """Dataset converter for Google Observability GenAI data."""
16
+
17
+ import json
18
+ import logging
19
+ from typing import Any, Optional
20
+
21
+ from google.genai import types as genai_types
22
+ from typing_extensions import override
23
+
24
+ from . import _evals_utils
25
+ from . import types
26
+
27
+
28
+ logger = logging.getLogger("agentplatform_genai._observability_data_converters")
29
+
30
+
31
+ def _load_jsonl(data: Any, case_id: str) -> list[dict[Any, Any]]:
32
+ """Parses the raw JSONL data into a list of dict possible."""
33
+ if isinstance(data, str):
34
+ json_list = []
35
+ for line in data.splitlines():
36
+ loaded_json = json.loads(line)
37
+ if not isinstance(loaded_json, dict):
38
+ raise TypeError(
39
+ f"Decoded JSON payload is not a dict for case "
40
+ f"{case_id}. Type found: {type(loaded_json).__name__}"
41
+ )
42
+ json_list.append(loaded_json)
43
+ return json_list
44
+ else:
45
+ raise TypeError(
46
+ f"Payload is not a JSONL string for case {case_id}. Type "
47
+ f"found: {type(data).__name__}"
48
+ )
49
+
50
+
51
+ class ObservabilityDataConverter(_evals_utils.EvalDataConverter):
52
+ """Converter for dataset in GCP Observability GenAI format."""
53
+
54
+ def _message_to_content(self, message: dict[str, Any]) -> genai_types.Content:
55
+ """Converts Observability GenAI Message format to Content."""
56
+ parts = []
57
+ message_parts = message.get("parts", [])
58
+ if isinstance(message_parts, list):
59
+ for message_part in message_parts:
60
+ part = None
61
+ part_type = message_part.get("type", "")
62
+ if part_type == "text":
63
+ part = genai_types.Part(text=message_part.get("content", ""))
64
+ elif part_type == "blob":
65
+ part = genai_types.Part(
66
+ inline_data=genai_types.Blob(
67
+ data=message_part.get("data", ""),
68
+ mime_type=message_part.get("mime_type", ""),
69
+ )
70
+ )
71
+ elif part_type == "file_data":
72
+ part = genai_types.Part(
73
+ file_data=genai_types.FileData(
74
+ file_uri=message_part.get("file_uri", ""),
75
+ mime_type=message_part.get("mime_type", ""),
76
+ )
77
+ )
78
+ elif part_type == "tool_call":
79
+ # O11y format requires use of id in place of name
80
+ part = genai_types.Part(
81
+ function_call=genai_types.FunctionCall(
82
+ id=message_part.get("id", ""),
83
+ name=message_part.get("id", ""),
84
+ args=message_part.get("arguments", {}),
85
+ )
86
+ )
87
+ elif part_type == "tool_call_response":
88
+ # O11y format requires use of id in place of name
89
+ part = genai_types.Part(
90
+ function_response=genai_types.FunctionResponse(
91
+ id=message_part.get("id", ""),
92
+ name=message_part.get("id", ""),
93
+ response=message_part.get("result", {}),
94
+ )
95
+ )
96
+ else:
97
+ logger.warning(
98
+ "Skipping message part due to unrecognized message "
99
+ "part type of '%s'",
100
+ part_type,
101
+ )
102
+
103
+ if part is not None:
104
+ parts.append(part)
105
+
106
+ return genai_types.Content(parts=parts, role=message.get("role", ""))
107
+
108
+ def _parse_messages(
109
+ self,
110
+ eval_case_id: str,
111
+ request_msgs: list[Any],
112
+ response_msgs: list[Any],
113
+ system_instruction_msg: Optional[dict[str, Any]] = None,
114
+ ) -> types.EvalCase:
115
+ """Parses a set of Observability messages into an EvalCase."""
116
+ # System instruction message
117
+ system_instruction = None
118
+ if system_instruction_msg is not None:
119
+ system_instruction = self._message_to_content(system_instruction_msg)
120
+
121
+ # Request messages
122
+ prompt = None
123
+ conversation_history = []
124
+ if request_msgs:
125
+ # Extract latest message as prompt
126
+ prompt = self._message_to_content(request_msgs[-1])
127
+
128
+ # All previous messages are conversation history
129
+ if len(request_msgs) > 1:
130
+ for i, msg in enumerate(request_msgs[:-1]):
131
+ conversation_history.append(
132
+ types.evals.Message(
133
+ turn_id=str(i),
134
+ content=self._message_to_content(msg),
135
+ author=msg.get("role", ""),
136
+ )
137
+ )
138
+
139
+ # Output messages
140
+ responses = []
141
+ for msg in response_msgs:
142
+ response = types.ResponseCandidate(response=self._message_to_content(msg))
143
+ responses.append(response)
144
+
145
+ return types.EvalCase(
146
+ eval_case_id=eval_case_id,
147
+ prompt=prompt,
148
+ responses=responses,
149
+ system_instruction=system_instruction,
150
+ conversation_history=conversation_history,
151
+ reference=None,
152
+ )
153
+
154
+ @override
155
+ def convert(self, raw_data: list[dict[str, Any]]) -> types.EvaluationDataset:
156
+ """Converts a list of GCP Observability GenAI cases into an EvaluationDataset."""
157
+ eval_cases = []
158
+
159
+ for i, case in enumerate(raw_data):
160
+ eval_case_id = f"observability_eval_case_{i}"
161
+
162
+ if "request" not in case or "response" not in case:
163
+ logger.warning(
164
+ "Skipping case %s due to missing 'request' or 'response' key.",
165
+ eval_case_id,
166
+ )
167
+ continue
168
+
169
+ request_data = case.get("request", [])
170
+ request_list = _load_jsonl(request_data, eval_case_id)
171
+
172
+ response_data = case.get("response", [])
173
+ response_list = _load_jsonl(response_data, eval_case_id)
174
+
175
+ system_dict = None
176
+ if "system_instruction" in case:
177
+ system_data = case.get("system_instruction", {})
178
+ system_list = _load_jsonl(system_data, eval_case_id)
179
+ system_dict = system_list[0] if system_list else {}
180
+
181
+ eval_case = self._parse_messages(
182
+ eval_case_id, request_list, response_list, system_dict
183
+ )
184
+ eval_cases.append(eval_case)
185
+
186
+ return types.EvaluationDataset(eval_cases=eval_cases)