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,628 @@
|
|
|
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
|
+
"""Transformers module for Vertex addons."""
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from google.genai._common import get_value_by_path as getv
|
|
22
|
+
|
|
23
|
+
from . import _evals_constant
|
|
24
|
+
from . import _evals_data_converters
|
|
25
|
+
from . import types
|
|
26
|
+
|
|
27
|
+
_METRIC_RES_NAME_RE = r"^projects/[^/]+/locations/[^/]+/evaluationMetrics/[^/]+$"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def t_metrics(
|
|
31
|
+
metrics: "list[types.MetricSubclass]",
|
|
32
|
+
set_default_aggregation_metrics: bool = False,
|
|
33
|
+
) -> list[dict[str, Any]]:
|
|
34
|
+
"""Prepares the metric payload for the evaluation request.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
metrics: A list of metrics used for evaluation.
|
|
38
|
+
set_default_aggregation_metrics: Whether to set default aggregation metrics.
|
|
39
|
+
Returns:
|
|
40
|
+
A list of resolved metric payloads for the evaluation request.
|
|
41
|
+
"""
|
|
42
|
+
metrics_payload = []
|
|
43
|
+
|
|
44
|
+
for metric in metrics:
|
|
45
|
+
metric_payload_item: dict[str, Any] = {}
|
|
46
|
+
|
|
47
|
+
metric_id = getv(metric, ["metric"]) or getv(metric, ["name"])
|
|
48
|
+
metric_name = metric_id.lower() if metric_id else None
|
|
49
|
+
|
|
50
|
+
if set_default_aggregation_metrics:
|
|
51
|
+
metric_payload_item["aggregation_metrics"] = [
|
|
52
|
+
"AVERAGE",
|
|
53
|
+
"STANDARD_DEVIATION",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
if metric_name == "exact_match":
|
|
57
|
+
metric_payload_item["exact_match_spec"] = {}
|
|
58
|
+
elif metric_name == "bleu":
|
|
59
|
+
metric_payload_item["bleu_spec"] = {}
|
|
60
|
+
elif metric_name and metric_name.startswith("rouge"):
|
|
61
|
+
rouge_type = metric_name.replace("_", "")
|
|
62
|
+
metric_payload_item["rouge_spec"] = {"rouge_type": rouge_type}
|
|
63
|
+
# API Pre-defined metrics
|
|
64
|
+
elif (
|
|
65
|
+
metric_name and metric_name in _evals_constant.SUPPORTED_PREDEFINED_METRICS
|
|
66
|
+
):
|
|
67
|
+
metric_payload_item["predefined_metric_spec"] = {
|
|
68
|
+
"metric_spec_name": metric_name,
|
|
69
|
+
"metric_spec_parameters": metric.metric_spec_parameters,
|
|
70
|
+
}
|
|
71
|
+
# Custom Code Execution Metric
|
|
72
|
+
elif (
|
|
73
|
+
hasattr(metric, "remote_custom_function") and metric.remote_custom_function
|
|
74
|
+
):
|
|
75
|
+
metric_payload_item["custom_code_execution_spec"] = {
|
|
76
|
+
"evaluation_function": metric.remote_custom_function
|
|
77
|
+
}
|
|
78
|
+
elif (
|
|
79
|
+
isinstance(metric, types.CodeExecutionMetric)
|
|
80
|
+
or (
|
|
81
|
+
isinstance(metric, types.Metric)
|
|
82
|
+
and isinstance(getattr(metric, "custom_function", None), str)
|
|
83
|
+
)
|
|
84
|
+
) and getattr(metric, "custom_function", None):
|
|
85
|
+
metric_payload_item["custom_code_execution_spec"] = {
|
|
86
|
+
"evaluation_function": metric.custom_function
|
|
87
|
+
}
|
|
88
|
+
# LLM-based metrics
|
|
89
|
+
elif hasattr(metric, "prompt_template") and metric.prompt_template:
|
|
90
|
+
llm_based_spec: dict[str, Any] = {
|
|
91
|
+
"metric_prompt_template": metric.prompt_template
|
|
92
|
+
}
|
|
93
|
+
system_instruction = getv(metric, ["judge_model_system_instruction"])
|
|
94
|
+
if system_instruction:
|
|
95
|
+
llm_based_spec["system_instruction"] = system_instruction
|
|
96
|
+
rubric_group_name = getv(metric, ["rubric_group_name"])
|
|
97
|
+
if rubric_group_name:
|
|
98
|
+
llm_based_spec["rubric_group_key"] = rubric_group_name
|
|
99
|
+
return_raw_output = getv(metric, ["return_raw_output"])
|
|
100
|
+
if return_raw_output:
|
|
101
|
+
llm_based_spec["custom_output_format_config"] = {
|
|
102
|
+
"return_raw_output": return_raw_output
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
autorater_config: dict[str, Any] = {}
|
|
106
|
+
if hasattr(metric, "judge_model") and metric.judge_model:
|
|
107
|
+
autorater_config["autorater_model"] = metric.judge_model
|
|
108
|
+
if (
|
|
109
|
+
hasattr(metric, "judge_model_generation_config")
|
|
110
|
+
and metric.judge_model_generation_config
|
|
111
|
+
):
|
|
112
|
+
autorater_config["generation_config"] = (
|
|
113
|
+
metric.judge_model_generation_config
|
|
114
|
+
)
|
|
115
|
+
if (
|
|
116
|
+
hasattr(metric, "judge_model_sampling_count")
|
|
117
|
+
and metric.judge_model_sampling_count
|
|
118
|
+
):
|
|
119
|
+
autorater_config["sampling_count"] = metric.judge_model_sampling_count
|
|
120
|
+
|
|
121
|
+
if autorater_config:
|
|
122
|
+
llm_based_spec["judge_autorater_config"] = autorater_config
|
|
123
|
+
|
|
124
|
+
result_parsing_function = getv(metric, ["result_parsing_function"])
|
|
125
|
+
if result_parsing_function:
|
|
126
|
+
llm_based_spec["result_parser_config"] = {
|
|
127
|
+
"custom_code_parser_config": {
|
|
128
|
+
"parsing_function": result_parsing_function
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
metric_payload_item["llm_based_metric_spec"] = llm_based_spec
|
|
133
|
+
elif getattr(metric, "metric_resource_name", None) is not None:
|
|
134
|
+
# Safe pass
|
|
135
|
+
pass
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Unsupported metric type or invalid metric name: {metric_name}"
|
|
139
|
+
)
|
|
140
|
+
metrics_payload.append(metric_payload_item)
|
|
141
|
+
return metrics_payload
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def t_metric_sources(metrics: list[Any]) -> list[dict[str, Any]]:
|
|
145
|
+
"""Prepares the MetricSource payload."""
|
|
146
|
+
sources_payload = []
|
|
147
|
+
for metric in metrics:
|
|
148
|
+
resource_name = getattr(metric, "metric_resource_name", None)
|
|
149
|
+
if (
|
|
150
|
+
not resource_name
|
|
151
|
+
and isinstance(metric, str)
|
|
152
|
+
and re.match(_METRIC_RES_NAME_RE, metric)
|
|
153
|
+
):
|
|
154
|
+
resource_name = metric
|
|
155
|
+
|
|
156
|
+
if resource_name:
|
|
157
|
+
sources_payload.append({"metric_resource_name": resource_name})
|
|
158
|
+
else:
|
|
159
|
+
if hasattr(metric, "metric") and not isinstance(metric, str):
|
|
160
|
+
metric = metric.metric
|
|
161
|
+
|
|
162
|
+
if not hasattr(metric, "name"):
|
|
163
|
+
metric = types.Metric(name=str(metric))
|
|
164
|
+
|
|
165
|
+
metric_payload = t_metrics([metric])[0]
|
|
166
|
+
sources_payload.append({"metric": metric_payload})
|
|
167
|
+
return sources_payload
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def t_user_scenario_generation_config(
|
|
171
|
+
config: "types.evals.UserScenarioGenerationConfigOrDict",
|
|
172
|
+
) -> dict[str, Any]:
|
|
173
|
+
"""Transforms UserScenarioGenerationConfig to Vertex AI format."""
|
|
174
|
+
payload: dict[str, Any] = {}
|
|
175
|
+
config_dict = config if isinstance(config, dict) else config.model_dump()
|
|
176
|
+
|
|
177
|
+
if getv(config_dict, ["count"]) is not None:
|
|
178
|
+
payload["user_scenario_count"] = getv(config_dict, ["count"])
|
|
179
|
+
if getv(config_dict, ["generation_instruction"]) is not None:
|
|
180
|
+
payload["simulation_instruction"] = getv(
|
|
181
|
+
config_dict, ["generation_instruction"]
|
|
182
|
+
)
|
|
183
|
+
if getv(config_dict, ["environment_context"]) is not None:
|
|
184
|
+
payload["environment_data"] = getv(config_dict, ["environment_context"])
|
|
185
|
+
if getv(config_dict, ["model_name"]) is not None:
|
|
186
|
+
payload["model_name"] = getv(config_dict, ["model_name"])
|
|
187
|
+
|
|
188
|
+
return payload
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def t_metric_for_registry(
|
|
192
|
+
metric: "types.Metric",
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""Prepares the metric payload specifically for EvaluationMetric registration."""
|
|
195
|
+
metric_payload_item: dict[str, Any] = {}
|
|
196
|
+
metric_name = getattr(metric, "name", None)
|
|
197
|
+
if metric_name:
|
|
198
|
+
metric_name = metric_name.lower()
|
|
199
|
+
|
|
200
|
+
# Custom Code Execution Metric
|
|
201
|
+
if hasattr(metric, "remote_custom_function") and metric.remote_custom_function:
|
|
202
|
+
metric_payload_item["custom_code_execution_spec"] = {
|
|
203
|
+
"evaluation_function": metric.remote_custom_function
|
|
204
|
+
}
|
|
205
|
+
elif (
|
|
206
|
+
isinstance(metric, types.CodeExecutionMetric)
|
|
207
|
+
or (
|
|
208
|
+
isinstance(metric, types.Metric)
|
|
209
|
+
and isinstance(getattr(metric, "custom_function", None), str)
|
|
210
|
+
)
|
|
211
|
+
) and getattr(metric, "custom_function", None):
|
|
212
|
+
metric_payload_item["custom_code_execution_spec"] = {
|
|
213
|
+
"evaluation_function": metric.custom_function
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# LLM-based metric
|
|
217
|
+
elif (hasattr(metric, "prompt_template") and metric.prompt_template) or (
|
|
218
|
+
hasattr(metric, "rubric_group_name") and metric.rubric_group_name
|
|
219
|
+
):
|
|
220
|
+
llm_based_spec: dict[str, Any] = {}
|
|
221
|
+
|
|
222
|
+
if hasattr(metric, "prompt_template") and metric.prompt_template:
|
|
223
|
+
llm_based_spec["metric_prompt_template"] = metric.prompt_template
|
|
224
|
+
system_instruction = getv(metric, ["judge_model_system_instruction"])
|
|
225
|
+
if system_instruction:
|
|
226
|
+
llm_based_spec["system_instruction"] = system_instruction
|
|
227
|
+
rubric_group_name = getv(metric, ["rubric_group_name"])
|
|
228
|
+
if rubric_group_name:
|
|
229
|
+
llm_based_spec["rubric_group_key"] = rubric_group_name
|
|
230
|
+
|
|
231
|
+
autorater_config: dict[str, Any] = {}
|
|
232
|
+
if hasattr(metric, "judge_model") and metric.judge_model:
|
|
233
|
+
autorater_config["autorater_model"] = metric.judge_model
|
|
234
|
+
if (
|
|
235
|
+
hasattr(metric, "judge_model_generation_config")
|
|
236
|
+
and metric.judge_model_generation_config
|
|
237
|
+
):
|
|
238
|
+
autorater_config["generation_config"] = metric.judge_model_generation_config
|
|
239
|
+
if (
|
|
240
|
+
hasattr(metric, "judge_model_sampling_count")
|
|
241
|
+
and metric.judge_model_sampling_count
|
|
242
|
+
):
|
|
243
|
+
autorater_config["sampling_count"] = metric.judge_model_sampling_count
|
|
244
|
+
|
|
245
|
+
if autorater_config:
|
|
246
|
+
llm_based_spec["judge_autorater_config"] = autorater_config
|
|
247
|
+
|
|
248
|
+
result_parsing_function = getv(metric, ["result_parsing_function"])
|
|
249
|
+
if result_parsing_function:
|
|
250
|
+
llm_based_spec["result_parser_config"] = {
|
|
251
|
+
"custom_code_parser_config": {
|
|
252
|
+
"parsing_function": result_parsing_function
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
metric_payload_item["llm_based_metric_spec"] = llm_based_spec
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError(f"Unsupported metric type: {metric_name}")
|
|
260
|
+
|
|
261
|
+
return metric_payload_item
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
_ALLOWED_PART_FIELDS = frozenset(
|
|
265
|
+
{
|
|
266
|
+
"text",
|
|
267
|
+
"inline_data",
|
|
268
|
+
"file_data",
|
|
269
|
+
"function_call",
|
|
270
|
+
"function_response",
|
|
271
|
+
"video_metadata",
|
|
272
|
+
"thought",
|
|
273
|
+
"thought_signature",
|
|
274
|
+
"code_execution_result",
|
|
275
|
+
"executable_code",
|
|
276
|
+
"media_resolution",
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _sanitize_agent_data(agent_data: dict[str, Any]) -> dict[str, Any]:
|
|
282
|
+
"""Strips SDK-only fields from agent_data so the API accepts the payload.
|
|
283
|
+
|
|
284
|
+
The SDK's AgentData model may contain fields like 'tool_call',
|
|
285
|
+
'tool_response', 'part_metadata', and 'will_continue' that don't exist
|
|
286
|
+
in the API's AgentData / Content proto. This function recursively removes
|
|
287
|
+
them from content parts and keeps only API-recognized top-level fields.
|
|
288
|
+
"""
|
|
289
|
+
if not isinstance(agent_data, dict):
|
|
290
|
+
return agent_data
|
|
291
|
+
|
|
292
|
+
sanitized: dict[str, Any] = {}
|
|
293
|
+
for key, value in agent_data.items():
|
|
294
|
+
if key == "turns" and isinstance(value, list):
|
|
295
|
+
sanitized["turns"] = [
|
|
296
|
+
_sanitize_turn(t) for t in value if isinstance(t, dict)
|
|
297
|
+
]
|
|
298
|
+
elif key == "agents" and isinstance(value, dict):
|
|
299
|
+
sanitized["agents"] = {
|
|
300
|
+
k: _sanitize_agent_config(v) if isinstance(v, dict) else v
|
|
301
|
+
for k, v in value.items()
|
|
302
|
+
}
|
|
303
|
+
# Skip unknown top-level fields (e.g. "error" from failed agent runs).
|
|
304
|
+
return sanitized
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _sanitize_agent_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
308
|
+
"""Sanitizes an AgentConfig dict, keeping only API-known fields."""
|
|
309
|
+
allowed = {
|
|
310
|
+
"agent_id",
|
|
311
|
+
"agent_type",
|
|
312
|
+
"description",
|
|
313
|
+
"instruction",
|
|
314
|
+
"tools",
|
|
315
|
+
"sub_agents",
|
|
316
|
+
}
|
|
317
|
+
return {k: v for k, v in config.items() if k in allowed}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _sanitize_turn(turn: dict[str, Any]) -> dict[str, Any]:
|
|
321
|
+
"""Sanitizes a ConversationTurn dict."""
|
|
322
|
+
sanitized: dict[str, Any] = {}
|
|
323
|
+
for key, value in turn.items():
|
|
324
|
+
if key == "events" and isinstance(value, list):
|
|
325
|
+
sanitized["events"] = [
|
|
326
|
+
_sanitize_event(e) for e in value if isinstance(e, dict)
|
|
327
|
+
]
|
|
328
|
+
else:
|
|
329
|
+
sanitized[key] = value
|
|
330
|
+
return sanitized
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _sanitize_event(event: dict[str, Any]) -> dict[str, Any]:
|
|
334
|
+
"""Sanitizes an AgentEvent dict."""
|
|
335
|
+
sanitized: dict[str, Any] = {}
|
|
336
|
+
for key, value in event.items():
|
|
337
|
+
if key == "content" and isinstance(value, dict):
|
|
338
|
+
sanitized["content"] = _sanitize_content(value)
|
|
339
|
+
elif key in ("author", "event_time", "state_delta", "active_tools"):
|
|
340
|
+
sanitized[key] = value
|
|
341
|
+
# Skip unknown event-level fields.
|
|
342
|
+
return sanitized
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _sanitize_content(content: dict[str, Any]) -> dict[str, Any]:
|
|
346
|
+
"""Sanitizes a Content dict, stripping unknown fields from parts."""
|
|
347
|
+
sanitized: dict[str, Any] = {}
|
|
348
|
+
for key, value in content.items():
|
|
349
|
+
if key == "parts" and isinstance(value, list):
|
|
350
|
+
sanitized["parts"] = [
|
|
351
|
+
_sanitize_part(p) for p in value if isinstance(p, dict)
|
|
352
|
+
]
|
|
353
|
+
elif key == "role":
|
|
354
|
+
sanitized["role"] = value
|
|
355
|
+
return sanitized
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _sanitize_part(part: dict[str, Any]) -> dict[str, Any]:
|
|
359
|
+
"""Keeps only API-recognized fields in a Part dict."""
|
|
360
|
+
sanitized: dict[str, Any] = {}
|
|
361
|
+
for key, value in part.items():
|
|
362
|
+
if key in _ALLOWED_PART_FIELDS:
|
|
363
|
+
if key == "function_response" and isinstance(value, dict):
|
|
364
|
+
# Strip unknown sub-fields like 'will_continue'.
|
|
365
|
+
sanitized[key] = {
|
|
366
|
+
k: v for k, v in value.items() if k in ("name", "id", "response")
|
|
367
|
+
}
|
|
368
|
+
else:
|
|
369
|
+
sanitized[key] = value
|
|
370
|
+
return sanitized
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _extract_agent_data_from_df(
|
|
374
|
+
eval_dataset: Any,
|
|
375
|
+
case_idx: int,
|
|
376
|
+
) -> Any:
|
|
377
|
+
"""Extracts agent_data from a DataFrame-based EvaluationDataset by row index."""
|
|
378
|
+
if not eval_dataset:
|
|
379
|
+
return None
|
|
380
|
+
ds = eval_dataset[0] if isinstance(eval_dataset, list) else eval_dataset
|
|
381
|
+
df = getv(ds, ["eval_dataset_df"])
|
|
382
|
+
if df is None or not hasattr(df, "iloc"):
|
|
383
|
+
return None
|
|
384
|
+
if case_idx < 0 or case_idx >= len(df):
|
|
385
|
+
return None
|
|
386
|
+
row = df.iloc[case_idx]
|
|
387
|
+
if "agent_data" not in row or row["agent_data"] is None:
|
|
388
|
+
return None
|
|
389
|
+
return row["agent_data"]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def t_inline_results(
|
|
393
|
+
eval_results: list[Any],
|
|
394
|
+
) -> list[dict[str, Any]]:
|
|
395
|
+
"""Transforms a list of SDK EvaluationResults into API EvaluationResults."""
|
|
396
|
+
api_results: list[dict[str, Any]] = []
|
|
397
|
+
|
|
398
|
+
for eval_result in eval_results:
|
|
399
|
+
metadata = getv(eval_result, ["metadata"])
|
|
400
|
+
candidate_names = getv(metadata, ["candidate_names"]) if metadata else []
|
|
401
|
+
candidate_names = candidate_names or []
|
|
402
|
+
|
|
403
|
+
eval_dataset = getv(eval_result, ["evaluation_dataset"])
|
|
404
|
+
eval_cases: list[Any] = []
|
|
405
|
+
if isinstance(eval_dataset, list) and eval_dataset:
|
|
406
|
+
eval_cases = getv(eval_dataset[0], ["eval_cases"]) or []
|
|
407
|
+
|
|
408
|
+
eval_case_results = getv(eval_result, ["eval_case_results"]) or []
|
|
409
|
+
|
|
410
|
+
for case_result in eval_case_results:
|
|
411
|
+
case_idx = getv(case_result, ["eval_case_index"]) or 0
|
|
412
|
+
|
|
413
|
+
eval_case = None
|
|
414
|
+
if 0 <= case_idx < len(eval_cases):
|
|
415
|
+
eval_case = eval_cases[case_idx]
|
|
416
|
+
|
|
417
|
+
prompt_payload: dict[str, Any] = {}
|
|
418
|
+
if eval_case:
|
|
419
|
+
agent_data = getv(eval_case, ["agent_data"])
|
|
420
|
+
prompt = getv(eval_case, ["prompt"])
|
|
421
|
+
|
|
422
|
+
if agent_data:
|
|
423
|
+
if hasattr(agent_data, "model_dump"):
|
|
424
|
+
prompt_payload["agent_data"] = _sanitize_agent_data(
|
|
425
|
+
agent_data.model_dump(exclude_none=True)
|
|
426
|
+
)
|
|
427
|
+
elif isinstance(agent_data, dict):
|
|
428
|
+
prompt_payload["agent_data"] = _sanitize_agent_data(agent_data)
|
|
429
|
+
else:
|
|
430
|
+
prompt_payload["agent_data"] = agent_data
|
|
431
|
+
elif prompt:
|
|
432
|
+
text = _evals_data_converters._get_content_text(
|
|
433
|
+
prompt
|
|
434
|
+
) # pylint: disable=protected-access
|
|
435
|
+
if text:
|
|
436
|
+
prompt_payload["text"] = str(text)
|
|
437
|
+
|
|
438
|
+
# Fallback: extract agent_data from the DataFrame when eval_cases
|
|
439
|
+
# are not available (e.g., run_inference -> evaluate flow).
|
|
440
|
+
if not prompt_payload:
|
|
441
|
+
df_agent_data = _extract_agent_data_from_df(eval_dataset, case_idx)
|
|
442
|
+
if df_agent_data is not None:
|
|
443
|
+
if hasattr(df_agent_data, "model_dump"):
|
|
444
|
+
prompt_payload["agent_data"] = _sanitize_agent_data(
|
|
445
|
+
df_agent_data.model_dump(exclude_none=True)
|
|
446
|
+
)
|
|
447
|
+
elif isinstance(df_agent_data, str):
|
|
448
|
+
try:
|
|
449
|
+
parsed = json.loads(df_agent_data)
|
|
450
|
+
if isinstance(parsed, dict) and "error" in parsed:
|
|
451
|
+
pass # Skip error payloads from failed agent runs.
|
|
452
|
+
else:
|
|
453
|
+
prompt_payload["agent_data"] = _sanitize_agent_data(
|
|
454
|
+
parsed
|
|
455
|
+
)
|
|
456
|
+
except (json.JSONDecodeError, ValueError):
|
|
457
|
+
pass
|
|
458
|
+
elif isinstance(df_agent_data, dict):
|
|
459
|
+
if "error" not in df_agent_data:
|
|
460
|
+
prompt_payload["agent_data"] = _sanitize_agent_data(
|
|
461
|
+
df_agent_data
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
cand_results = getv(case_result, ["response_candidate_results"]) or []
|
|
465
|
+
for resp_cand_result in cand_results:
|
|
466
|
+
resp_idx = getv(resp_cand_result, ["response_index"]) or 0
|
|
467
|
+
cand_name = f"candidate-{resp_idx}"
|
|
468
|
+
if 0 <= resp_idx < len(candidate_names):
|
|
469
|
+
cand_name = candidate_names[resp_idx]
|
|
470
|
+
|
|
471
|
+
metric_results = getv(resp_cand_result, ["metric_results"]) or {}
|
|
472
|
+
|
|
473
|
+
for metric_name, metric_res in metric_results.items():
|
|
474
|
+
api_rubric_verdicts: list[dict[str, Any]] = []
|
|
475
|
+
rubric_verdicts = getv(metric_res, ["rubric_verdicts"]) or []
|
|
476
|
+
|
|
477
|
+
for verdict in rubric_verdicts:
|
|
478
|
+
verdict_dict: dict[str, Any] = {}
|
|
479
|
+
eval_rubric = getv(verdict, ["evaluated_rubric"])
|
|
480
|
+
|
|
481
|
+
if eval_rubric:
|
|
482
|
+
rubric_dict: dict[str, Any] = {}
|
|
483
|
+
rubric_id = getv(eval_rubric, ["rubric_id"])
|
|
484
|
+
if rubric_id:
|
|
485
|
+
rubric_dict["rubric_id"] = str(rubric_id)
|
|
486
|
+
|
|
487
|
+
rubric_content = getv(eval_rubric, ["content"])
|
|
488
|
+
if rubric_content:
|
|
489
|
+
text = getv(rubric_content, ["text"])
|
|
490
|
+
prop = getv(rubric_content, ["property"])
|
|
491
|
+
|
|
492
|
+
content_dict: dict[str, Any] = {}
|
|
493
|
+
if text:
|
|
494
|
+
content_dict["text"] = str(text)
|
|
495
|
+
if prop:
|
|
496
|
+
desc = getv(prop, ["description"])
|
|
497
|
+
if desc:
|
|
498
|
+
content_dict["property"] = {
|
|
499
|
+
"description": str(desc)
|
|
500
|
+
}
|
|
501
|
+
rubric_dict["content"] = content_dict
|
|
502
|
+
verdict_dict["evaluated_rubric"] = rubric_dict
|
|
503
|
+
|
|
504
|
+
verdict_bool = getv(verdict, ["verdict"])
|
|
505
|
+
if verdict_bool is not None:
|
|
506
|
+
verdict_dict["verdict"] = bool(verdict_bool)
|
|
507
|
+
|
|
508
|
+
reasoning = getv(verdict, ["reasoning"])
|
|
509
|
+
if reasoning:
|
|
510
|
+
verdict_dict["reasoning"] = str(reasoning)
|
|
511
|
+
|
|
512
|
+
if verdict_dict:
|
|
513
|
+
api_rubric_verdicts.append(verdict_dict)
|
|
514
|
+
|
|
515
|
+
score = getv(metric_res, ["score"])
|
|
516
|
+
explanation = getv(metric_res, ["explanation"])
|
|
517
|
+
|
|
518
|
+
candidate_result_payload: dict[str, Any] = {
|
|
519
|
+
"candidate": str(cand_name),
|
|
520
|
+
"metric": str(metric_name),
|
|
521
|
+
}
|
|
522
|
+
if score is not None:
|
|
523
|
+
candidate_result_payload["score"] = float(score)
|
|
524
|
+
if explanation:
|
|
525
|
+
candidate_result_payload["explanation"] = str(explanation)
|
|
526
|
+
if api_rubric_verdicts:
|
|
527
|
+
candidate_result_payload["rubric_verdicts"] = (
|
|
528
|
+
api_rubric_verdicts
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
api_eval_result = {
|
|
532
|
+
"request": {"prompt": prompt_payload},
|
|
533
|
+
"metric": str(metric_name),
|
|
534
|
+
"candidate_results": [candidate_result_payload],
|
|
535
|
+
}
|
|
536
|
+
api_results.append(api_eval_result)
|
|
537
|
+
|
|
538
|
+
return api_results
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
_ENDPOINT_RES_NAME_RES = (
|
|
542
|
+
re.compile(r"^projects/[^/]+/locations/[^/]+/endpoints/[^/]+$"),
|
|
543
|
+
re.compile(r"^endpoints/[^/]+$"),
|
|
544
|
+
)
|
|
545
|
+
_PUBLISHER_MODEL_RES_NAME_RES = (
|
|
546
|
+
re.compile(r"^projects/[^/]+/locations/[^/]+/publishers/[^/]+/models/[^/]+$"),
|
|
547
|
+
re.compile(r"^publishers/[^/]+/models/[^/]+$"),
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def is_endpoint_resource_name(name: str) -> bool:
|
|
552
|
+
"""Returns whether the name addresses an Endpoint resource."""
|
|
553
|
+
return any(pattern.match(name) for pattern in _ENDPOINT_RES_NAME_RES)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _is_publisher_model_resource_name(name: str) -> bool:
|
|
557
|
+
return any(pattern.match(name) for pattern in _PUBLISHER_MODEL_RES_NAME_RES)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def t_endpoint(endpoint: str) -> str:
|
|
561
|
+
"""Validates a name that may address an Endpoint or a publisher model."""
|
|
562
|
+
if not endpoint:
|
|
563
|
+
raise ValueError("endpoint is required.")
|
|
564
|
+
|
|
565
|
+
if is_endpoint_resource_name(endpoint) or _is_publisher_model_resource_name(
|
|
566
|
+
endpoint
|
|
567
|
+
):
|
|
568
|
+
return endpoint
|
|
569
|
+
|
|
570
|
+
raise ValueError(
|
|
571
|
+
f"Invalid endpoint format: {endpoint}. Must be in the format of"
|
|
572
|
+
" projects/.../locations/.../endpoints/... or"
|
|
573
|
+
" projects/.../locations/.../publishers/.../models/... or"
|
|
574
|
+
" endpoints/... or publishers/.../models/..."
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def t_strict_endpoint(endpoint: str) -> str:
|
|
579
|
+
"""Validates a name that must address an Endpoint resource.
|
|
580
|
+
|
|
581
|
+
A publisher model is served by the shared prediction endpoint and has no
|
|
582
|
+
Endpoint resource behind it, so it can be predicted against but never
|
|
583
|
+
fetched, undeployed or deleted. Methods that address the resource itself
|
|
584
|
+
must reject it here rather than issue a request against a publishers/... URL.
|
|
585
|
+
"""
|
|
586
|
+
if not endpoint:
|
|
587
|
+
raise ValueError("endpoint is required.")
|
|
588
|
+
|
|
589
|
+
if is_endpoint_resource_name(endpoint):
|
|
590
|
+
return endpoint
|
|
591
|
+
|
|
592
|
+
if _is_publisher_model_resource_name(endpoint):
|
|
593
|
+
raise ValueError(
|
|
594
|
+
f"{endpoint} is a publisher model, which does not support this"
|
|
595
|
+
" method. Only endpoints in the format of"
|
|
596
|
+
" projects/.../locations/.../endpoints/... or endpoints/... are"
|
|
597
|
+
" supported."
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
raise ValueError(
|
|
601
|
+
f"Invalid endpoint format: {endpoint}. Must be in the format of"
|
|
602
|
+
" projects/.../locations/.../endpoints/... or endpoints/..."
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
_EXAMPLE_STORE_RES_NAME_RES = (
|
|
607
|
+
re.compile(r"^projects/[^/]+/locations/[^/]+/exampleStores/[^/]+$"),
|
|
608
|
+
re.compile(r"^exampleStores/[^/]+$"),
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def is_example_store_resource_name(name: str) -> bool:
|
|
613
|
+
"""Returns whether the name addresses an ExampleStore resource."""
|
|
614
|
+
return any(pattern.match(name) for pattern in _EXAMPLE_STORE_RES_NAME_RES)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def t_example_store(example_store: str) -> str:
|
|
618
|
+
"""Validates a name that must address an ExampleStore resource."""
|
|
619
|
+
if not example_store:
|
|
620
|
+
raise ValueError("example_store is required.")
|
|
621
|
+
|
|
622
|
+
if is_example_store_resource_name(example_store):
|
|
623
|
+
return example_store
|
|
624
|
+
|
|
625
|
+
raise ValueError(
|
|
626
|
+
f"Invalid example store format: {example_store}. Must be in the format"
|
|
627
|
+
" of projects/.../locations/.../exampleStores/... or exampleStores/..."
|
|
628
|
+
)
|