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.
- agentplatform/__init__.py +72 -0
- agentplatform/_genai/__init__.py +43 -0
- agentplatform/_genai/_agent_engines_utils.py +2341 -0
- agentplatform/_genai/_bigquery_utils.py +49 -0
- agentplatform/_genai/_datasets_utils.py +344 -0
- agentplatform/_genai/_evals_builtin_tools.py +209 -0
- agentplatform/_genai/_evals_common.py +4268 -0
- agentplatform/_genai/_evals_constant.py +122 -0
- agentplatform/_genai/_evals_data_converters.py +926 -0
- agentplatform/_genai/_evals_metric_handlers.py +1783 -0
- agentplatform/_genai/_evals_metric_loaders.py +401 -0
- agentplatform/_genai/_evals_utils.py +1043 -0
- agentplatform/_genai/_evals_visualization.py +2070 -0
- agentplatform/_genai/_gcs_utils.py +262 -0
- agentplatform/_genai/_logging_utils.py +47 -0
- agentplatform/_genai/_memory_bank_utils.py +206 -0
- agentplatform/_genai/_observability_data_converter.py +186 -0
- agentplatform/_genai/_operations_utils.py +94 -0
- agentplatform/_genai/_prompt_management_utils.py +147 -0
- agentplatform/_genai/_prompt_optimizer_utils.py +215 -0
- agentplatform/_genai/_skills_utils.py +69 -0
- agentplatform/_genai/_transformers.py +628 -0
- agentplatform/_genai/a2a_task_events.py +509 -0
- agentplatform/_genai/a2a_tasks.py +861 -0
- agentplatform/_genai/agent_engines.py +3931 -0
- agentplatform/_genai/client.py +519 -0
- agentplatform/_genai/datasets.py +3045 -0
- agentplatform/_genai/endpoints.py +1149 -0
- agentplatform/_genai/evals.py +6883 -0
- agentplatform/_genai/example_stores.py +1445 -0
- agentplatform/_genai/feedback_contexts.py +700 -0
- agentplatform/_genai/feedback_entries.py +1644 -0
- agentplatform/_genai/live.py +64 -0
- agentplatform/_genai/live_agent_engines.py +179 -0
- agentplatform/_genai/memories.py +2962 -0
- agentplatform/_genai/memory_banks.py +1927 -0
- agentplatform/_genai/memory_revisions.py +465 -0
- agentplatform/_genai/model_garden.py +2638 -0
- agentplatform/_genai/prompt_optimizer.py +995 -0
- agentplatform/_genai/prompts.py +4515 -0
- agentplatform/_genai/rag.py +4961 -0
- agentplatform/_genai/runtime_revisions.py +1257 -0
- agentplatform/_genai/runtimes.py +78 -0
- agentplatform/_genai/sandbox_snapshots.py +1015 -0
- agentplatform/_genai/sandbox_templates.py +1088 -0
- agentplatform/_genai/sandboxes.py +1604 -0
- agentplatform/_genai/session_events.py +543 -0
- agentplatform/_genai/sessions.py +1449 -0
- agentplatform/_genai/skill_revisions.py +377 -0
- agentplatform/_genai/skills.py +1708 -0
- agentplatform/_genai/types/__init__.py +4695 -0
- agentplatform/_genai/types/agent_engines.py +16 -0
- agentplatform/_genai/types/common.py +32784 -0
- agentplatform/_genai/types/evals.py +1031 -0
- agentplatform/_genai/types/prompt_optimizer.py +107 -0
- agentplatform/_genai/types/prompts.py +107 -0
- agentplatform/version.py +17 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/METADATA +79 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/RECORD +62 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/WHEEL +5 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/licenses/LICENSE +202 -0
- 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)
|