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,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
+ )