Create Eval Quality Flywheel Skill for preview (#4505)

This commit is contained in:
Jason Dai
2026-04-23 16:37:08 +00:00
committed by GitHub
parent 563f423b93
commit daf56bcd0b
10 changed files with 2264 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
suite_name: quality_flywheel
timeout_seconds: 300
max_tool_calls: 30
cases:
- name: cold_start_dataset_creation
prompt: >
I'm building a RAG agent that answers questions about internal
docs but I have no evaluation data yet. Help me get started
with evaluations.
skills: [quality-flywheel]
difficulty: medium
outcome_assertions:
- "The agent identifies this as a cold-start scenario and recommends synthetic data generation"
- "The agent explains the evaluation dataset format (EvalCase, EvaluationDataset)"
- "The agent suggests relevant metrics for RAG evaluation (hallucination, grounding, or general quality)"
- "The agent asks for or attempts to discover the GCP Project ID and Location"
expect_keywords_any:
- "synthetic"
- "generate"
- "dataset"
- "EvalCase"
- name: metric_selection_for_tool_agent
prompt: >
My agent uses 5 different tools (search, calculator, calendar,
email, file manager). It's working okay but I need to set up
evals to measure quality. What metrics should I track?
skills: [quality-flywheel]
difficulty: easy
outcome_assertions:
- "The agent recommends tool-call-specific metrics like tool_use_quality or tool_call_valid"
- "The agent recommends task completion metrics like multi_turn_task_success"
- "The agent explains how to assess correct tool selection vs wrong tool usage"
- "The agent does not recommend only generic text quality metrics"
expect_keywords_any:
- "tool_use"
- "tool_call"
- "task_success"
- "trajectory"
- name: trace_ingestion_adk
prompt: >
I have ADK session logs from my local development in the default
ADK format. How do I turn these into an evaluation dataset I can
run through Vertex Eval?
skills: [quality-flywheel]
difficulty: medium
outcome_assertions:
- "The agent describes how to convert traces into AgentData format"
- "The agent references the canonical types: AgentData, ConversationTurn, AgentEvent"
- "The agent provides concrete code or a step-by-step conversion plan"
expect_keywords_any:
- "AgentData"
- "ConversationTurn"
- "AgentEvent"
- "EvalCase"
- name: analyze_poor_results
prompt: >
I ran evals on my RAG agent and got 0.45 on the hallucination
metric and 0.62 on grounding. The agent seems to make up answers
instead of using the retrieved context. What should I fix?
skills: [quality-flywheel]
difficulty: easy
outcome_assertions:
- "The agent identifies low hallucination score as a critical problem"
- "The agent connects the symptom (making up answers) to a grounding/prompting root cause"
- "The agent suggests specific prompt or system instruction changes to improve grounding"
- "The agent recommends re-running evals after applying the fix"
expect_keywords_any:
- "grounding"
- "prompt"
- "hallucin"
- "context"
- name: custom_llm_metric_creation
prompt: >
I need to evaluate whether my customer support agent is always
polite and empathetic, even when delivering bad news. The
predefined metrics don't cover this. Can you help me create a
custom metric for this?
skills: [quality-flywheel]
difficulty: medium
outcome_assertions:
- "The agent creates a custom LLMMetric with a prompt_template for politeness/empathy"
- "The metric template evaluates tone specifically, not just general quality"
- "The agent provides runnable Python code for the custom metric"
- "The agent explains how to integrate it with client.evals.evaluate()"
expect_keywords_any:
- "LLMMetric"
- "prompt_template"
- "polite"
- "empathy"
+192
View File
@@ -0,0 +1,192 @@
---
name: quality-flywheel
description: >-
Evaluate and improve GenAI models and agents using the Google GenAI
Evaluation SDK. Creates eval datasets (from session traces or synthetic
generation), selects and configures metrics (RubricMetric, LLMMetric,
CodeExecutionMetric), executes evals via client.evals.evaluate(), and
analyzes results to suggest concrete fixes. Supports both single-turn
model evaluation and multi-turn agent trajectory evaluation. Use when
asked to "evaluate my agent", "evaluate my model", "create eval dataset",
"run evals", "analyze eval results", "which metrics should I use",
"generate test data", or "improve quality".
---
# Quality Flywheel Skill
You are the **Quality Flywheel** — an expert in GenAI evaluation. Your
mission is to help users evaluate and iteratively improve their GenAI
models and agents using the Google GenAI Evaluation SDK
(`google.genai` / `vertexai`).
## When to use this skill
- Evaluating GenAI agents or models using `client.evals.evaluate()`
- Creating synthetic datasets or ingesting session traces
- Selecting, configuring, or writing custom evaluation metrics
- Analyzing rubric verdicts and loss patterns
- Suggesting concrete code/prompt improvements based on eval results
## Workflow
Follow this workflow sequentially when assisting users:
### Step 0. Setup & Project Initialization
* **CRITICAL:** Before generating or executing any scripts, obtain the
**GCP Project ID** and **Location** (e.g., `global`, `us-central1`).
Check environment variables first (`GOOGLE_CLOUD_PROJECT`,
`GOOGLE_CLOUD_LOCATION`). If not found, ask the user.
* Newer Gemini models may only be available in the `global` region — use
`location="global"` if the user wants to use them.
### Step 1. Dataset Creation & Formatting
* **Parse Inputs:** Convert user-provided descriptions into the SDK
formats (`EvalCase`, `AgentData`, `ConversationTurn`,
`EvaluationDataset`). See
[references/dataset_schema.md](references/dataset_schema.md) for the
full type hierarchy and examples.
* **Single-Turn (Model Eval):** Create `EvalCase` objects with `prompt`
strings. Use `client.evals.run_inference(model=..., src=dataset)` to
populate model responses if needed.
* **Multi-Turn (Agent Eval):** If the user wants to test a multi-turn
agent but lacks data:
1. **Generate Scenarios:** Use `client.evals.generate_user_scenarios`
with a `UserScenarioGenerationConfig` specifying
`user_scenario_count`, `simulation_instruction`, and
`environment_data`.
2. **Run Inference:** Use `client.evals.run_inference` with a
`user_simulator_config` to simulate interactions up to `max_turn`.
### Step 2. Metric Selection & Customization
Use the quick-reference table to pick metrics. For the full catalog, see
[references/metric_registry.md](references/metric_registry.md).
| Use Case | Recommended Metrics |
|---|---|
| RAG / QA | `hallucination_v1`, `grounding_v1`, `general_quality_v1` |
| Tool-use agent | `tool_use_quality_v1`, `multi_turn_task_success_v1`, `tool_call_valid`, `tool_name_match` |
| Multi-turn conversation | `multi_turn_general_quality_v1`, `multi_turn_text_quality_v1`, `safety_v1` |
| Code generation | `CodeExecutionMetric` (custom), `exact_match`, `instruction_following_v1` |
| Summarization | `RubricMetric.SUMMARIZATION_QUALITY`, `rouge_l_sum` |
| Single-turn model eval | `general_quality_v1`, `text_quality_v1`, `instruction_following_v1` |
* **Predefined:** Access via `types.RubricMetric.<NAME>`. Server-side
AutoRater — no judge model needed.
* **Custom LLM-as-a-judge:** `types.LLMMetric` with `prompt_template` or
`types.MetricPromptBuilder` for structured rubrics.
* **Custom Code:** `types.CodeExecutionMetric` with a `custom_function`
string containing `def evaluate(instance: dict)` for remote sandboxed
execution. Or `types.Metric` with `custom_function=<callable>` for
local execution.
### Step 3. Automated Execution
* Generate a complete Python evaluation script using
`client.evals.evaluate(dataset=..., metrics=...)`.
* Save the script to a file and execute it to get real results.
* Ensure the script prints results in a parseable format (JSON).
### Step 4. Result Analysis & Auto-Optimization
* Read the stdout/stderr from the evaluation run.
* **CRITICAL — DO NOT HALLUCINATE:** Only analyze the exact
`summary_metrics` and `eval_case_results` returned by the executed
script. Never fabricate scores or results.
* Perform loss pattern analysis: Identify *why* a model or agent failed
based on the returned explanations and rubric verdicts. See
[references/failure_patterns.md](references/failure_patterns.md) for
common failure modes and their fixes.
* Suggest concrete improvements to the user's prompt, system instruction,
or agent code based on the failed examples.
### Step 5. Iterate (The Flywheel)
After applying fixes, re-run evaluation (Step 3) and compare results.
Repeat until quality targets are met. Track progress across iterations:
| Iteration | Metric A | Metric B | Change Made |
|---|---|---|---|
| Baseline | 0.62 | 0.55 | — |
| v2 | 0.78 | 0.68 | Added grounding prompt |
| v3 | 0.81 | 0.72 | Fixed tool selection |
### Rules of Engagement
1. **Always Plan First:** Before writing a script, output a `<plan>`
block detailing the steps you are about to take.
2. **Step-by-Step Execution:** Write the script, execute it, wait for
output, then analyze. Don't do everything in one response.
3. **Standard Python:** Use standard Python imports (`import vertexai`,
`from google.genai import types`). Don't use internal import paths.
4. **Verify Before Guessing:** When unsure about SDK types or metrics,
check the SDK source code rather than guessing or hallucinating.
### Error Handling
If execution returns a traceback:
1. Analyze the error immediately.
2. Fix the script.
3. Run again.
4. Keep iterating until success or user input is needed.
### SDK Quick Reference
```python
import vertexai
from vertexai import Client, types
from google.genai import types as genai_types
# Initialize client
client = vertexai.Client(project="PROJECT_ID", location="LOCATION")
# --- SINGLE-TURN EVAL ---
dataset = types.EvaluationDataset(eval_cases=[
types.EvalCase(prompt="Query here", response="Model response here"),
])
# --- MULTI-TURN AGENT EVAL ---
agent_data = types.evals.AgentData(
agents={"my_agent": types.evals.AgentConfig(
agent_id="my_agent", instruction="You are helpful.")},
turns=[types.evals.ConversationTurn(turn_index=0, events=[
types.evals.AgentEvent(author="user",
content=genai_types.Content(role="user",
parts=[genai_types.Part(text="Hello")])),
types.evals.AgentEvent(author="my_agent",
content=genai_types.Content(role="model",
parts=[genai_types.Part(text="Hi! How can I help?")])),
])],
)
dataset = types.EvaluationDataset(
eval_cases=[types.EvalCase(agent_data=agent_data)])
# --- METRICS ---
predefined = types.RubricMetric.MULTI_TURN_TRAJECTORY_QUALITY
custom_llm = types.LLMMetric(name="tone",
prompt_template="Is this polite? Response: {response}")
custom_code = types.CodeExecutionMetric(name="check",
custom_function='def evaluate(instance): return 1.0')
# --- EVALUATE ---
result = client.evals.evaluate(dataset=dataset, metrics=[predefined])
# --- RESULTS ---
for s in result.summary_metrics:
print(f"{s.metric_name}: mean={s.mean_score}, pass_rate={s.pass_rate}")
for case in result.eval_case_results:
for cand in case.response_candidate_results:
for name, r in cand.metric_results.items():
print(f" {name}: score={r.score}, explanation={r.explanation}")
```
See [references/sdk_patterns.md](references/sdk_patterns.md) for
advanced patterns: synthetic data generation, pairwise comparison,
MetricPromptBuilder, multi-agent evaluation.
+83
View File
@@ -0,0 +1,83 @@
# Quality Flywheel Skill - Manual Test Plan
## Prerequisites
- Read `SKILL.md` to understand the workflow
- Have a GCP project with the GenAI Evaluation API enabled
- Access to an environment with the skill installed
---
## Test 1: Cold Start — No Data
**Prompt:** "I want to evaluate my customer support agent but I don't
have any test data. Can you help me set up evals from scratch?"
**Verify:**
- Agent asks for GCP Project ID and Location (or attempts env vars)
- Agent recommends synthetic data generation via `generate_user_scenarios`
- Agent explains the EvalCase / EvaluationDataset format
- Agent suggests appropriate metrics for customer support
---
## Test 2: Metric Selection
**Prompt:** "My agent is a multi-turn travel booking assistant that
uses tools like search_flights and book_hotel. Which metrics should
I use to evaluate it?"
**Verify:**
- Agent recommends multi-turn agent metrics (trajectory quality, task success)
- Agent recommends tool-specific computation metrics (tool_call_valid, tool_name_match)
- Agent uses `types.RubricMetric.<NAME>` syntax
- Agent does NOT recommend only single-turn text quality metrics
---
## Test 3: Custom Metric
**Prompt:** "I need a metric that checks if my agent always includes
a disclaimer when giving financial advice. Can you write one?"
**Verify:**
- Agent creates a `CodeExecutionMetric` or `LLMMetric`
- The metric logic specifically checks for disclaimer presence
- Code is syntactically valid Python
- Agent shows how to use it with `client.evals.evaluate()`
---
## Test 4: End-to-End Execution
**Prompt:** "Here's my agent — it's a simple Q&A bot. I have 3 test
questions. Can you write an eval script and run it?"
Provide sample questions when asked.
**Verify:**
- Agent writes a complete Python script with proper imports
- Script uses `vertexai.Client()` initialization
- Agent saves the script to a file and executes it
- Agent reads and analyzes the actual output (no hallucinated results)
- Agent uses standard Python imports (not internal paths)
---
## Test 5: Result Analysis
**Prompt:** "My eval results show 0.3 on tool_use_quality and 0.8
on general_quality. The agent keeps calling the wrong tool when
users ask about order status. What should I fix?"
**Verify:**
- Agent identifies tool_use_quality as the critical failure
- Agent suggests specific tool description or system prompt improvements
- Agent recommends checking `tool_name_match` for granular diagnosis
- Agent references failure patterns (from references/failure_patterns.md)
---
## Cleanup
No persistent state created. Scripts written to /tmp/ can be deleted.
@@ -0,0 +1,273 @@
# Evaluation Dataset Schema
Canonical formats for evaluation datasets in the Google GenAI Evaluation SDK.
Source of truth: `vertexai/_genai/types/evals.py` and
`vertexai/_genai/types/common.py`.
## Core Types
```
EvaluationDataset
├── eval_cases: list[EvalCase] # Primary: list of cases
└── eval_dataset_df: pd.DataFrame # Alternative: pandas DataFrame
EvalCase
├── prompt: str # Single-turn: the user query
├── response: str # Single-turn: the model response
├── reference: str # Ground truth (for reference-based metrics)
├── agent_data: AgentData # Multi-turn: full conversation trajectory
└── (extra fields allowed) # Custom fields for custom metrics
AgentData
├── agents: dict[str, AgentConfig] # Agent definitions
└── turns: list[ConversationTurn] # Ordered conversation turns
ConversationTurn
├── turn_index: int # 0-based turn number
└── events: list[AgentEvent] # Events within this turn
AgentEvent
├── author: str # "user", agent_id, or "tool"
└── content: genai_types.Content # Content with role and parts
```
## Single-Turn Dataset
For simple prompt-response evaluation (e.g., QA, summarization).
```python
from vertexai import types
dataset = types.EvaluationDataset(eval_cases=[
types.EvalCase(
prompt="What is the capital of France?",
response="The capital of France is Paris.",
reference="Paris",
),
types.EvalCase(
prompt="Summarize this article: ...",
response="The article discusses...",
),
])
```
### From pandas DataFrame
```python
import pandas as pd
from vertexai import types
df = pd.DataFrame({
"prompt": ["What is 2+2?", "Name the planets"],
"response": ["4", "Mercury, Venus, Earth, ..."],
"reference": ["4", "Mercury, Venus, Earth, Mars, ..."],
})
dataset = types.EvaluationDataset(eval_dataset_df=df)
```
### Required fields by metric type
| Metric category | Required fields |
|---|---|
| Predefined (single-turn) | `prompt`, `response` |
| Computation-based | `response`, `reference` |
| Translation | `prompt` (source), `response`, `reference` |
| Custom LLM/code | Fields referenced in your template/function |
## Multi-Turn Dataset (AgentData)
For evaluating multi-turn agent conversations with tool calls.
```python
from vertexai import types
from google.genai import types as genai_types
agent_data = types.evals.AgentData(
agents={
"support_agent": types.evals.AgentConfig(
agent_id="support_agent",
instruction="You are a helpful support agent.",
tools=[genai_types.Tool(function_declarations=[
genai_types.FunctionDeclaration(
name="lookup_order",
description="Look up order status by ID",
parameters=genai_types.Schema(
type="OBJECT",
properties={"order_id": genai_types.Schema(type="STRING")},
),
)
])],
)
},
turns=[
types.evals.ConversationTurn(
turn_index=0,
events=[
# User message
types.evals.AgentEvent(
author="user",
content=genai_types.Content(
role="user",
parts=[genai_types.Part(text="Where is my order #12345?")]
),
),
# Agent calls tool
types.evals.AgentEvent(
author="support_agent",
content=genai_types.Content(
role="model",
parts=[genai_types.Part(
function_call=genai_types.FunctionCall(
name="lookup_order",
args={"order_id": "12345"},
)
)]
),
),
# Tool response
types.evals.AgentEvent(
author="support_agent",
content=genai_types.Content(
role="tool",
parts=[genai_types.Part(
function_response=genai_types.FunctionResponse(
name="lookup_order",
response={"status": "shipped", "eta": "tomorrow"},
)
)]
),
),
# Agent final response
types.evals.AgentEvent(
author="support_agent",
content=genai_types.Content(
role="model",
parts=[genai_types.Part(
text="Your order #12345 has been shipped and should arrive tomorrow!"
)]
),
),
],
),
],
)
eval_case = types.EvalCase(agent_data=agent_data)
dataset = types.EvaluationDataset(eval_cases=[eval_case])
```
## Multi-Agent Dataset
For evaluating systems with multiple collaborating agents.
```python
agent_data = types.evals.AgentData(
agents={
"router": types.evals.AgentConfig(
agent_id="router",
agent_type="RouterAgent",
instruction="Route requests to the appropriate specialist.",
),
"flight_bot": types.evals.AgentConfig(
agent_id="flight_bot",
agent_type="SpecialistAgent",
instruction="Search and book flights.",
tools=[genai_types.Tool(function_declarations=[
genai_types.FunctionDeclaration(name="search_flights")
])],
),
},
turns=[
types.evals.ConversationTurn(
turn_index=0,
events=[
types.evals.AgentEvent(
author="user",
content=genai_types.Content(
role="user",
parts=[genai_types.Part(text="Book a flight to NYC")]
),
),
# Router delegates
types.evals.AgentEvent(
author="router",
content=genai_types.Content(
role="model",
parts=[genai_types.Part(
function_call=genai_types.FunctionCall(
name="delegate_to_agent",
args={"agent_name": "flight_bot"},
)
)]
),
),
],
),
types.evals.ConversationTurn(
turn_index=1,
events=[
# Specialist works
types.evals.AgentEvent(
author="flight_bot",
content=genai_types.Content(
role="model",
parts=[genai_types.Part(
function_call=genai_types.FunctionCall(
name="search_flights",
args={"destination": "NYC"},
)
)]
),
),
],
),
],
)
```
## Synthetic Data Generation
### Generate User Scenarios (Cold Start)
```python
scenarios = client.evals.generate_user_scenarios(
agents={
"my_agent": types.evals.AgentConfig(
agent_id="my_agent",
instruction="You are a helpful customer support agent.",
)
},
root_agent_id="my_agent",
user_scenario_generation_config=types.evals.UserScenarioGenerationConfig(
user_scenario_count=10,
simulation_instruction="Simulate a customer asking about order status.",
environment_data="Orders can be: pending, shipped, delivered, cancelled.",
model_name="gemini-2.5-flash",
),
)
```
### Run Inference (Populate Responses)
```python
dataset_with_responses = client.evals.run_inference(
agent=my_agent_callable,
src=scenarios,
config={
"user_simulator_config": {
"model_name": "gemini-2.5-flash",
"max_turn": 5,
}
},
)
```
## Common Mistakes
| Mistake | Fix |
|---|---|
| Using `role="assistant"` | Use `role="model"` (Vertex convention) |
| Missing `turn_index` | Always set sequential 0-based indices |
| Tool response without `function_response` | Wrap in `genai_types.FunctionResponse` |
| Using `response` field for multi-turn | Use `agent_data` with full trajectory |
| Mixing `prompt` and `agent_data` | Use one or the other per EvalCase |
@@ -0,0 +1,153 @@
# Evaluation Failure Patterns & Fixes
Common failure modes observed in GenAI agent evaluations, mapped to their
root causes and concrete fixes.
## Metric-Specific Failures
### Low `hallucination_v1` or `grounding_v1` Score
**Symptom:** Agent generates plausible-sounding but factually incorrect
information, or doesn't use the provided context.
**Root causes:**
- System prompt lacks explicit grounding instructions
- Retrieved context not passed into the prompt
- Agent ignores context in favor of parametric knowledge
**Fixes:**
1. Add to system prompt: "Base ALL answers strictly on the provided context.
If the context doesn't contain the answer, say 'I don't have that
information.'"
2. Verify context is actually injected into the prompt (check tool responses)
3. Add `temperature=0` or lower temperature to reduce creative generation
### Low `general_quality_v1` or `text_quality_v1`
**Symptom:** Agent responses are poorly structured, unclear, or unhelpful.
**Root causes:**
- System prompt too vague
- Agent over-explains or under-explains
- Missing output format instructions
**Fixes:**
1. Add explicit format instructions: "Respond concisely in 2-3 sentences."
2. Add few-shot examples in the system prompt
3. Review rubric verdicts for specific quality dimensions that scored low
### Low `tool_use_quality_v1` or `tool_call_valid`
**Symptom:** Agent calls the wrong tool, uses wrong parameters, or doesn't
call tools when it should.
**Root causes:**
- Tool descriptions are ambiguous
- Multiple tools have overlapping functionality
- Function declaration parameter schemas are incomplete
**Fixes:**
1. Make tool `description` fields precise and mutually exclusive
2. Add parameter descriptions and constraints to `FunctionDeclaration`
3. Add to system prompt: "Always use {tool_name} when the user asks about
{specific_topic}."
4. Check `tool_name_match` and `tool_parameter_kv_match` for granular diagnosis
### Low `multi_turn_trajectory_quality_v1`
**Symptom:** Agent takes suboptimal paths through a conversation — unnecessary
tool calls, redundant questions, or wrong delegation order.
**Root causes:**
- Router agent lacks clear delegation rules
- Agent retries failed operations without adaptation
- Missing escalation logic
**Fixes:**
1. Add explicit routing rules: "Route to {agent} when {condition}."
2. Add retry limits: "If {tool} fails twice, inform the user and suggest
alternatives."
3. Review the trajectory events in `agent_data` to identify the specific
turn where the agent deviated
### Low `multi_turn_task_success_v1`
**Symptom:** Agent engages in conversation but doesn't complete the user's
actual goal.
**Root causes:**
- Agent gets sidetracked by follow-up questions
- Missing confirmation/completion step
- Agent doesn't track task state across turns
**Fixes:**
1. Add to system prompt: "Always confirm task completion with the user before
ending the conversation."
2. Implement explicit task tracking in agent logic
3. Verify `max_turn` in user simulator is sufficient for the task complexity
### Low `safety_v1`
**Symptom:** Agent generates unsafe content or complies with harmful requests.
**Root causes:**
- System prompt lacks safety constraints
- Agent follows user instructions too literally
- Missing refusal logic for out-of-scope requests
**Fixes:**
1. Add safety guardrails: "Never provide medical/legal/financial advice.
Redirect to appropriate professionals."
2. Add refusal patterns: "If the user asks for {harmful_category}, politely
decline."
3. Use `safety_v1` alongside domain-specific `LLMMetric` safety checks
## Structural Failures
### `is_infra_error: true`
**Symptom:** Eval case fails with infrastructure error, not a quality issue.
**Root causes:**
- API quota exceeded
- Network timeout
- Model endpoint temporarily unavailable
**Fix:** Re-run the evaluation. If persistent, check quota and endpoint health.
### Timeout
**Symptom:** Evaluation times out before completing.
**Root causes:**
- Dataset too large for a single API call
- Complex custom metric code takes too long
- Judge model sampling count too high
**Fixes:**
1. Reduce dataset size or batch into smaller chunks
2. Optimize custom metric code (avoid network calls in `evaluate()`)
3. Reduce `judge_model_sampling_count` (default 1, max 32)
### `KeyError` in Custom Metric
**Symptom:** Custom function crashes with missing field.
**Root cause:** Metric function expects a field not present in the eval case.
**Fix:** Check available fields in the `instance` dict. Common fields:
`prompt`, `response`, `reference`, `agent_data`. Always use `.get()` with
defaults.
## Analysis Workflow
When eval results show failures:
1. **Start with `summary_metrics`** — identify which metrics scored lowest
2. **Drill into `eval_case_results`** — find specific failing cases
3. **Read `rubric_verdicts`** — understand why the judge scored low
4. **Cross-reference with `agent_data`** — find the exact turn/event
that caused the failure
5. **Identify the pattern** — is it a prompt issue, tool issue, or data issue?
6. **Apply the targeted fix** — from the table above
7. **Re-run and compare** — verify the fix improved the target metric
@@ -0,0 +1,264 @@
# GenAI Evaluation Metric Registry
Complete catalog of evaluation metrics available in the `vertexai._genai` SDK.
Source of truth: `vertexai/_genai/_evals_metric_loaders.py` and
`vertexai/_genai/_evals_metric_handlers.py`.
## Metric Type Hierarchy
```
Metric (base)
├── LLMMetric — LLM-as-a-judge with prompt_template
├── CodeExecutionMetric — Sandboxed remote Python function
└── (base Metric) — Local callable or predefined name
```
Access predefined metrics via `types.RubricMetric.<NAME>` (preferred).
`types.PrebuiltMetric` is an alias with identical behavior.
## Predefined API Metrics (AutoRater)
Server-side evaluation via Vertex AI AutoRater. No judge model needed.
### Single-Turn
| Metric name | What it measures | Required fields |
|---|---|---|
| `general_quality_v1` | Overall response quality | `prompt`, `response` |
| `text_quality_v1` | Text quality (grammar, clarity) | `prompt`, `response` |
| `instruction_following_v1` | How well response follows instructions | `prompt`, `response` |
| `grounding_v1` | Factual grounding in provided context | `prompt`, `response`, context |
| `safety_v1` | Safety assessment | `prompt`, `response` |
| `hallucination_v1` | Hallucination detection | `prompt`, `response` |
| `tool_use_quality_v1` | Quality of tool/function calling | `agent_data` with tool calls |
### Multi-Turn / Agent
| Metric name | What it measures | Required fields |
|---|---|---|
| `multi_turn_general_quality_v1` | Overall multi-turn quality | `agent_data` (1+ turns) |
| `multi_turn_text_quality_v1` | Text quality across turns | `agent_data` (1+ turns) |
| `multi_turn_tool_use_quality_v1` | Tool call quality across trajectory | `agent_data` with function calls |
| `multi_turn_trajectory_quality_v1` | Quality of agent's action sequence | `agent_data` with full trajectory |
| `multi_turn_task_success_v1` | Whether agent completed the task | `agent_data` with task context |
### Agent Final Response
| Metric name | What it measures | Required fields |
|---|---|---|
| `final_response_match_v2` | Reference-based final response matching | `agent_data`, `reference` |
| `final_response_reference_free_v1` | Final response quality (no reference) | `agent_data` |
| `final_response_quality_v1` | Final response quality | `agent_data` |
### Multimodal
| Metric name | What it measures | Required fields |
|---|---|---|
| `gecko_text2image_v1` | Text-to-image quality | image content |
| `gecko_text2video_v1` | Text-to-video quality | video content |
### Accessing predefined metrics
```python
from vertexai import types
# Via RubricMetric (preferred)
metric = types.RubricMetric.MULTI_TURN_TRAJECTORY_QUALITY
# Via PrebuiltMetric (alias — identical behavior)
metric = types.PrebuiltMetric.MULTI_TURN_TRAJECTORY_QUALITY
# With version override
metric = types.RubricMetric.GENERAL_QUALITY(version="v2")
```
## Computation-Based Metrics
No LLM judge. Deterministic comparison of `response` vs `reference`.
| Metric name | What it measures | Notes |
|---|---|---|
| `exact_match` | Exact string match | Case-sensitive |
| `bleu` | BLEU score (translation/generation) | Standard BLEU |
| `rouge_1` | ROUGE-1 (unigram overlap) | Summarization |
| `rouge_l_sum` | ROUGE-L (longest common subsequence) | Summary-level |
| `tool_call_valid` | Whether tool calls are syntactically valid | Agent evals |
| `tool_name_match` | Whether tool names match reference | Agent evals |
| `tool_parameter_key_match` | Whether tool parameter keys match | Agent evals |
| `tool_parameter_kv_match` | Whether tool parameter key-value pairs match | Agent evals |
```python
# Usage
metric = types.Metric(name="exact_match")
metric = types.Metric(name="tool_call_valid")
```
## Translation Metrics
| Metric name | Default version | Notes |
|---|---|---|
| `comet` | `COMET_22_SRC_REF` | Requires `prompt` (source), `response`, `reference` |
| `metricx` | `METRICX_24_SRC_REF` | Requires `prompt` (source), `response`, `reference` |
## RubricMetric / PrebuiltMetric (GCS-Loaded LLM Recipes)
These resolve first against the API predefined list, then fall back to
GCS-hosted LLM metric YAML definitions.
| Property | Resolution |
|---|---|
| `GENERAL_QUALITY` | API predefined |
| `TEXT_QUALITY` | API predefined |
| `INSTRUCTION_FOLLOWING` | API predefined |
| `SAFETY` | API predefined |
| `HALLUCINATION` | API predefined |
| `TOOL_USE_QUALITY` | API predefined |
| `MULTI_TURN_GENERAL_QUALITY` | API predefined |
| `MULTI_TURN_TEXT_QUALITY` | API predefined |
| `MULTI_TURN_TOOL_USE_QUALITY` | API predefined |
| `MULTI_TURN_TRAJECTORY_QUALITY` | API predefined |
| `MULTI_TURN_TASK_SUCCESS` | API predefined |
| `FINAL_RESPONSE_MATCH` | API predefined (v2) |
| `FINAL_RESPONSE_REFERENCE_FREE` | API predefined |
| `FINAL_RESPONSE_QUALITY` | API predefined |
| `COHERENCE` | GCS-loaded LLM recipe |
| `FLUENCY` | GCS-loaded LLM recipe |
| `VERBOSITY` | GCS-loaded LLM recipe |
| `SUMMARIZATION_QUALITY` | GCS-loaded LLM recipe |
| `QUESTION_ANSWERING_QUALITY` | GCS-loaded LLM recipe |
| `MULTI_TURN_CHAT_QUALITY` | GCS-loaded LLM recipe |
| `MULTI_TURN_SAFETY` | GCS-loaded LLM recipe |
Any arbitrary name can be tried via `RubricMetric.<NAME>` — it will
attempt resolution against the API list and then GCS.
## Custom Metrics
### Custom Local Function
Runs client-side. Fastest iteration, no API call.
```python
def my_evaluator(instance: dict) -> float:
response_text = instance.get("response", "")
return 1.0 if "thank you" in response_text.lower() else 0.0
metric = types.Metric(
name="politeness_check",
custom_function=my_evaluator,
)
```
### CodeExecutionMetric (Remote Sandboxed)
Runs server-side in a secure sandbox. Must contain `def evaluate(instance)`.
```python
metric = types.CodeExecutionMetric(
name="link_validator",
custom_function='''
import re
def evaluate(instance: dict) -> dict:
text = instance.get("response", "")
links = re.findall(r"https?://\\S+", text)
valid = all(link.startswith("https://") for link in links)
return {"score": 1.0 if valid else 0.0, "explanation": f"Found {len(links)} links"}
''',
)
```
### LLMMetric (LLM-as-a-Judge)
Uses a judge model to evaluate with a custom prompt template.
```python
metric = types.LLMMetric(
name="helpfulness",
prompt_template="""
Evaluate whether the response is helpful for the given query.
Query: {prompt}
Response: {response}
Score 1 if helpful, 0 if not. Explain your reasoning.
""",
judge_model="gemini-2.5-flash",
judge_model_sampling_count=3,
)
# Load from YAML/JSON file
metric = types.LLMMetric.load("path/to/metric_config.yaml")
```
### MetricPromptBuilder (Structured Judge Prompt)
Builds structured LLM judge prompts from criteria, rating scores, and
evaluation steps. Preferred over raw `prompt_template` strings for complex
rubrics.
```python
metric = types.LLMMetric(
name="structured_quality",
prompt_template=types.MetricPromptBuilder(
criteria={
"Accuracy": "Response contains factually correct information",
"Completeness": "Response addresses all aspects of the query",
},
rating_scores={
"1": "Poor — fails on both criteria",
"3": "Acceptable — meets one criterion",
"5": "Excellent — meets both criteria",
},
),
judge_model="gemini-2.5-flash",
)
```
### Registered Metric (Server-Side Resource)
For reusable metrics shared across teams.
```python
# Create once
resource = client.evals.create_evaluation_metric(metric_config)
# Use by resource name
metric = types.Metric(
name="team_quality",
metric_resource_name="projects/.../evaluationMetrics/...",
)
```
## Metric Selection Guide
| Agent Type | Recommended Metrics |
|---|---|
| **RAG agent** | `hallucination_v1`, `grounding_v1`, `general_quality_v1` |
| **Tool-use agent** | `tool_use_quality_v1`, `multi_turn_task_success_v1`, `tool_call_valid`, `tool_name_match` |
| **Multi-turn conversational** | `multi_turn_general_quality_v1`, `multi_turn_text_quality_v1`, `safety_v1` |
| **Code generation** | `CodeExecutionMetric` (custom), `exact_match`, `instruction_following_v1` |
| **Summarization** | `RubricMetric.SUMMARIZATION_QUALITY`, `rouge_l_sum` |
| **Translation** | `comet`, `metricx` |
## Pairwise Comparison
There is no `PairwiseMetric` class. For model comparison, provide multiple
`EvaluationDataset` instances and use `calculate_win_rates()`:
```python
result_a = client.evals.evaluate(dataset=dataset_a, metrics=[...])
result_b = client.evals.evaluate(dataset=dataset_b, metrics=[...])
win_rates = calculate_win_rates(result_a, result_b)
```
## Handler Dispatch Order
When the SDK receives a metric, it checks in this order:
1. `CodeExecutionMetric` with `custom_function` (str) or `remote_custom_function`
2. `Metric` with `custom_function` (local `Callable`)
3. `Metric` with `metric_resource_name` (registered)
4. Name in computation metrics (`exact_match`, `bleu`, etc.)
5. Name in translation metrics (`comet`, `metricx`)
6. Name in predefined API metrics (`general_quality_v1`, etc.)
7. `LLMMetric` with `prompt_template` (custom LLM judge)
@@ -0,0 +1,251 @@
# Vertex Evaluation SDK Patterns
Code patterns for common evaluation scenarios using `vertexai._genai.evals`.
## Initialization
```python
import vertexai
from vertexai import types
from google.genai import types as genai_types
client = vertexai.Client(project="{PROJECT_ID}", location="{LOCATION}")
```
For Gemini 3+ models, use `location="global"`.
## Pattern 1: Single-Turn Evaluation
Simplest case — evaluate prompt/response pairs against predefined metrics.
```python
dataset = types.EvaluationDataset(eval_cases=[
types.EvalCase(
prompt="What causes rain?",
response="Rain is caused by water evaporating...",
reference="Rain forms when water vapor condenses...",
),
])
result = client.evals.evaluate(
dataset=dataset,
metrics=[
types.RubricMetric.GENERAL_QUALITY,
types.Metric(name="rouge_l_sum"),
],
)
```
## Pattern 2: Multi-Turn Agent Evaluation
Evaluate a full agent conversation trajectory with tool calls.
```python
agent_data = types.evals.AgentData(
agents={
"my_agent": types.evals.AgentConfig(
agent_id="my_agent",
instruction="You are a helpful assistant.",
tools=[genai_types.Tool(function_declarations=[
genai_types.FunctionDeclaration(
name="search",
description="Search the web",
parameters=genai_types.Schema(
type="OBJECT",
properties={"query": genai_types.Schema(type="STRING")},
),
),
])],
),
},
turns=[
types.evals.ConversationTurn(turn_index=0, events=[
types.evals.AgentEvent(
author="user",
content=genai_types.Content(role="user",
parts=[genai_types.Part(text="Find me the weather in NYC")]),
),
types.evals.AgentEvent(
author="my_agent",
content=genai_types.Content(role="model",
parts=[genai_types.Part(function_call=genai_types.FunctionCall(
name="search", args={"query": "NYC weather"}))]),
),
types.evals.AgentEvent(
author="my_agent",
content=genai_types.Content(role="tool",
parts=[genai_types.Part(function_response=genai_types.FunctionResponse(
name="search", response={"result": "72F, sunny"}))]),
),
types.evals.AgentEvent(
author="my_agent",
content=genai_types.Content(role="model",
parts=[genai_types.Part(text="It's 72F and sunny in NYC.")]),
),
]),
],
)
result = client.evals.evaluate(
dataset=types.EvaluationDataset(eval_cases=[
types.EvalCase(agent_data=agent_data),
]),
metrics=[
types.RubricMetric.MULTI_TURN_TRAJECTORY_QUALITY,
types.RubricMetric.MULTI_TURN_TASK_SUCCESS,
],
)
```
## Pattern 3: Synthetic Data Generation (Cold Start)
Generate user scenarios when no eval data exists.
```python
# Step 1: Generate scenarios
scenarios = client.evals.generate_user_scenarios(
agents={
"agent": types.evals.AgentConfig(
agent_id="agent",
instruction="You are a customer support agent for an airline.",
),
},
root_agent_id="agent",
user_scenario_generation_config=types.evals.UserScenarioGenerationConfig(
user_scenario_count=10,
simulation_instruction="Simulate customers with flight booking issues.",
environment_data="Flights available: NYC-LAX, NYC-SFO. Cancellation policy: free within 24h.",
model_name="gemini-2.5-flash",
),
)
# Step 2: Run inference with user simulation
dataset_with_responses = client.evals.run_inference(
agent=my_agent, # Your callable agent
src=scenarios,
config={
"user_simulator_config": {
"model_name": "gemini-2.5-flash",
"max_turn": 5,
},
},
)
# Step 3: Evaluate
result = client.evals.evaluate(
dataset=dataset_with_responses,
metrics=[types.RubricMetric.MULTI_TURN_GENERAL_QUALITY, types.RubricMetric.SAFETY],
)
```
## Pattern 4: Custom LLM-as-a-Judge with MetricPromptBuilder
For domain-specific evaluation with structured rubrics.
```python
metric = types.LLMMetric(
name="domain_expertise",
prompt_template=types.MetricPromptBuilder(
metric_definition="Evaluates domain expertise in the response.",
criteria={
"Accuracy": "Claims are factually correct for the domain",
"Depth": "Response shows understanding beyond surface level",
"Actionability": "Advice is specific and actionable",
},
rating_scores={
"1": "Incorrect or misleading information",
"2": "Partially correct but superficial",
"3": "Correct and shows reasonable understanding",
"4": "Accurate with good depth",
"5": "Expert-level accuracy, depth, and actionability",
},
),
judge_model="gemini-2.5-flash",
judge_model_sampling_count=3,
)
```
## Pattern 5: CodeExecutionMetric for Structured Validation
For programmatic checks that go beyond text comparison.
```python
# Validate JSON output structure
json_validator = types.CodeExecutionMetric(
name="json_structure_check",
custom_function='''
import json
def evaluate(instance: dict) -> dict:
try:
data = json.loads(instance.get("response", ""))
required_keys = {"name", "status", "result"}
missing = required_keys - set(data.keys())
if missing:
return {"score": 0.0, "explanation": f"Missing keys: {missing}"}
return {"score": 1.0, "explanation": "All required keys present"}
except json.JSONDecodeError as e:
return {"score": 0.0, "explanation": f"Invalid JSON: {e}"}
''',
)
```
## Pattern 6: Pairwise Model Comparison
Compare two models using `calculate_win_rates()`.
```python
# Same dataset, two different model responses
dataset_a = types.EvaluationDataset(eval_cases=[
types.EvalCase(prompt="Explain quantum computing", response="Model A response..."),
])
dataset_b = types.EvaluationDataset(eval_cases=[
types.EvalCase(prompt="Explain quantum computing", response="Model B response..."),
])
result_a = client.evals.evaluate(dataset=dataset_a, metrics=[types.RubricMetric.GENERAL_QUALITY])
result_b = client.evals.evaluate(dataset=dataset_b, metrics=[types.RubricMetric.GENERAL_QUALITY])
# Compare
from vertexai._genai._evals_metric_handlers import calculate_win_rates
win_rates = calculate_win_rates(result_a, result_b)
```
## Pattern 7: Parsing Results
```python
result = client.evals.evaluate(dataset=dataset, metrics=metrics)
# Summary level
for summary in result.summary_metrics:
print(f"{summary.metric_name}: mean={summary.mean_score}, pass_rate={summary.pass_rate}")
# Per-case level
for case in result.eval_case_results:
for candidate in case.response_candidate_results:
for metric_name, metric_result in candidate.metric_results.items():
print(f" {metric_name}: score={metric_result.score}")
print(f" explanation: {metric_result.explanation}")
# Rubric verdicts (for rubric-based metrics)
if metric_result.rubric_verdicts:
for v in metric_result.rubric_verdicts:
print(f" rubric {v.evaluated_rubric.rubric_id}: "
f"{'PASS' if v.verdict else 'FAIL'} - {v.reasoning}")
```
## Error Handling
```python
try:
result = client.evals.evaluate(dataset=dataset, metrics=metrics)
except Exception as e:
error_type = type(e).__name__
if "PermissionDenied" in error_type:
print("Check: GCP project permissions, API enabled, billing active")
elif "InvalidArgument" in error_type:
print("Check: dataset format, metric compatibility with data type")
elif "ResourceExhausted" in error_type:
print("Check: API quota, reduce dataset size or add delay")
else:
raise
```
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""Generate a runnable Vertex Evaluation SDK script from a dataset and metrics.
Reads an evaluation dataset JSON and generates a complete Python script
that executes the evaluation and prints results.
Usage:
python generate_eval_code.py --dataset dataset.json --metrics
hallucination_v1,safety_v1
python generate_eval_code.py --dataset dataset.json --metrics hallucination_v1
--output eval_script.py
python generate_eval_code.py --dataset dataset.json # defaults to
general_quality_v1
The generated script can be run directly:
python eval_script.py --project my-project --location us-central1
"""
import argparse
import json
import os
import sys
from typing import Any
# Predefined metrics accessed via types.RubricMetric.<NAME>
_RUBRIC_METRICS = frozenset({
"general_quality_v1",
"text_quality_v1",
"instruction_following_v1",
"grounding_v1",
"safety_v1",
"hallucination_v1",
"tool_use_quality_v1",
"multi_turn_general_quality_v1",
"multi_turn_text_quality_v1",
"multi_turn_tool_use_quality_v1",
"multi_turn_trajectory_quality_v1",
"multi_turn_task_success_v1",
"final_response_match_v2",
"final_response_reference_free_v1",
"final_response_quality_v1",
})
# Computation metrics accessed via types.Metric(name="...")
_COMPUTATION_METRICS = frozenset({
"exact_match",
"bleu",
"rouge_1",
"rouge_l_sum",
"tool_call_valid",
"tool_name_match",
"tool_parameter_key_match",
"tool_parameter_kv_match",
})
# Map from metric name to RubricMetric constant name
_RUBRIC_CONSTANT_MAP = {
"general_quality_v1": "GENERAL_QUALITY",
"text_quality_v1": "TEXT_QUALITY",
"instruction_following_v1": "INSTRUCTION_FOLLOWING",
"grounding_v1": "GROUNDING",
"safety_v1": "SAFETY",
"hallucination_v1": "HALLUCINATION",
"tool_use_quality_v1": "TOOL_USE_QUALITY",
"multi_turn_general_quality_v1": "MULTI_TURN_GENERAL_QUALITY",
"multi_turn_text_quality_v1": "MULTI_TURN_TEXT_QUALITY",
"multi_turn_tool_use_quality_v1": "MULTI_TURN_TOOL_USE_QUALITY",
"multi_turn_trajectory_quality_v1": "MULTI_TURN_TRAJECTORY_QUALITY",
"multi_turn_task_success_v1": "MULTI_TURN_TASK_SUCCESS",
"final_response_match_v2": "FINAL_RESPONSE_MATCH",
"final_response_reference_free_v1": "FINAL_RESPONSE_REFERENCE_FREE",
"final_response_quality_v1": "FINAL_RESPONSE_QUALITY",
}
def _metric_to_code(metric_name: str) -> str:
"""Convert a metric name to its Python SDK expression."""
if metric_name in _RUBRIC_CONSTANT_MAP:
return f"types.RubricMetric.{_RUBRIC_CONSTANT_MAP[metric_name]}"
if metric_name in _COMPUTATION_METRICS:
return f'types.Metric(name="{metric_name}")'
# Unknown metric — try as RubricMetric constant
return f'types.Metric(name="{metric_name}")'
def _detect_dataset_type(dataset: dict[str, Any]) -> str:
"""Detect whether dataset is single-turn or multi-turn."""
cases = dataset.get("eval_cases", [])
if not cases:
return "unknown"
first = cases[0]
if "agent_data" in first or "agentData" in first:
return "multi_turn"
if "prompt" in first:
return "single_turn"
return "unknown"
def generate_script(
dataset_path: str,
metrics: list[str],
dataset: dict[str, Any],
) -> str:
"""Generate a complete evaluation Python script."""
dataset_type = _detect_dataset_type(dataset)
n_cases = len(dataset.get("eval_cases", []))
metrics_code = ",\n ".join(_metric_to_code(m) for m in metrics)
script = f'''#!/usr/bin/env python3
"""Auto-generated Vertex Evaluation script.
Dataset: {dataset_path} ({n_cases} case(s), {dataset_type})
Metrics: {", ".join(metrics)}
Run: python <this_script>.py --project <PROJECT_ID> --location <LOCATION>
"""
import argparse
import json
import sys
import vertexai
from vertexai import types
from google.genai import types as genai_types
def load_dataset(path: str) -> types.EvaluationDataset:
"""Load evaluation dataset from JSON file."""
with open(path) as f:
data = json.load(f)
return types.EvaluationDataset.model_validate(data)
def run_evaluation(project: str, location: str, dataset_path: str):
"""Run evaluation and print results."""
print(f"Initializing Vertex AI client (project={{project}}, location={{location}})...")
client = vertexai.Client(project=project, location=location)
print(f"Loading dataset from {{dataset_path}}...")
dataset = load_dataset(dataset_path)
metrics = [
{metrics_code}
]
print(f"Running evaluation with {{len(metrics)}} metric(s)...")
result = client.evals.evaluate(dataset=dataset, metrics=metrics)
# Print summary metrics
print("\\n" + "=" * 60)
print("SUMMARY METRICS")
print("=" * 60)
if result.summary_metrics:
for summary in result.summary_metrics:
print(f" {{summary.metric_name}}:")
print(f" Mean Score: {{summary.mean_score}}")
print(f" Pass Rate: {{summary.pass_rate}}")
print(f" Std Dev: {{summary.stdev_score}}")
print(f" Valid/Total: {{summary.num_cases_valid}}/{{summary.num_cases_total}}")
print()
# Print per-case results
print("=" * 60)
print("PER-CASE RESULTS")
print("=" * 60)
if result.eval_case_results:
for case_result in result.eval_case_results:
print(f"\\n Case {{case_result.eval_case_index}}:")
if case_result.response_candidate_results:
for candidate in case_result.response_candidate_results:
if candidate.metric_results:
for metric_name, metric_result in candidate.metric_results.items():
print(f" {{metric_name}}:")
print(f" Score: {{metric_result.score}}")
if metric_result.explanation:
explanation = metric_result.explanation[:200]
print(f" Explanation: {{explanation}}...")
if metric_result.rubric_verdicts:
for v in metric_result.rubric_verdicts:
status = "PASS" if v.verdict else "FAIL"
rubric_id = v.evaluated_rubric.rubric_id if v.evaluated_rubric else "?"
print(f" Rubric {{rubric_id}}: {{status}}")
if v.reasoning:
print(f" Reasoning: {{v.reasoning[:150]}}...")
# Output as JSON for programmatic consumption
print("\\n" + "=" * 60)
print("JSON OUTPUT")
print("=" * 60)
json_output = {{
"summary": [
{{
"metric": s.metric_name,
"mean_score": s.mean_score,
"pass_rate": s.pass_rate,
"stdev": s.stdev_score,
}}
for s in (result.summary_metrics or [])
],
}}
print(json.dumps(json_output, indent=2, default=str))
def main():
parser = argparse.ArgumentParser(description="Run Vertex Evaluation")
parser.add_argument("--project", "-p", required=True, help="GCP Project ID")
parser.add_argument("--location", "-l", default="us-central1", help="GCP Location")
parser.add_argument("--dataset", "-d", default="{dataset_path}", help="Dataset JSON path")
args = parser.parse_args()
run_evaluation(args.project, args.location, args.dataset)
if __name__ == "__main__":
main()
'''
return script
def main():
parser = argparse.ArgumentParser(
description="Generate a runnable Vertex Eval SDK script."
)
parser.add_argument(
"--dataset",
"-d",
required=True,
help="Path to the evaluation dataset JSON file.",
)
parser.add_argument(
"--metrics",
"-m",
default="general_quality_v1",
help="Comma-separated metric names (default: general_quality_v1).",
)
parser.add_argument(
"--output",
"-o",
help="Output script path. Prints to stdout if not specified.",
)
args = parser.parse_args()
if not os.path.exists(args.dataset):
print(f"ERROR: File not found: {args.dataset}", file=sys.stderr)
sys.exit(1)
try:
with open(args.dataset) as f:
dataset = json.load(f)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON in {args.dataset}: {e}", file=sys.stderr)
sys.exit(1)
metrics = [m.strip() for m in args.metrics.split(",")]
# Validate metric names
all_known = _RUBRIC_METRICS | _COMPUTATION_METRICS
unknown = [m for m in metrics if m not in all_known]
if unknown:
print(
f"WARNING: Unknown metric(s): {', '.join(unknown)}. "
"They will be passed as types.Metric(name=...).",
file=sys.stderr,
)
script = generate_script(args.dataset, metrics, dataset)
if args.output:
with open(args.output, "w") as f:
f.write(script)
os.chmod(args.output, 0o755)
print(f"Generated eval script: {args.output}", file=sys.stderr)
print(
f"Run: python {args.output} --project <PROJECT> --location <LOCATION>",
file=sys.stderr,
)
else:
print(script)
if __name__ == "__main__":
main()
@@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""Parse ADK session traces into Vertex Evaluation SDK dataset format.
Reads serialized ADK session JSON (from Session.model_dump_json() or
DatabaseSessionService exports) and converts to the canonical
EvaluationDataset format for use with client.evals.evaluate().
Usage:
python parse_adk_traces.py --input session.json --output dataset.json
python parse_adk_traces.py --input_dir ./sessions/ --output dataset.json
python parse_adk_traces.py --input session.json # prints to stdout
Input format: JSON file(s) with ADK Session structure:
{
"id": "...", "app_name": "...", "user_id": "...",
"events": [{"author": "user"|"agent_name", "content": {...}}, ...]
}
Output format: JSON with EvaluationDataset structure:
{
"eval_cases": [{"agent_data": {"agents": {...}, "turns": [...]}}]
}
"""
import argparse
import json
import os
import sys
from typing import Any
def _is_user_event(event: dict[str, Any]) -> bool:
"""Check if an event is from the user."""
if event.get("author") == "user":
return True
content = event.get("content")
if isinstance(content, dict) and content.get("role") == "user":
return True
if event.get("role") == "user":
return True
return False
def _extract_content(event: dict[str, Any]) -> dict[str, Any] | None:
"""Extract genai Content from an ADK event dict."""
if "content" in event:
raw = event["content"]
if isinstance(raw, dict) and "parts" in raw:
return raw
if isinstance(raw, str):
return {
"role": "user" if _is_user_event(event) else "model",
"parts": [{"text": raw}],
}
if "parts" in event:
return {"role": event.get("role", "model"), "parts": event["parts"]}
return None
def _extract_author(event: dict[str, Any], default_agent_id: str) -> str:
"""Extract the author from an event, preserving sub-agent attribution."""
author = event.get("author")
if author:
return author
content = event.get("content")
if isinstance(content, dict) and content.get("role") == "user":
return "user"
if event.get("role") == "user":
return "user"
return default_agent_id
def _extract_agent_configs(
session: dict[str, Any],
) -> dict[str, dict[str, Any]]:
"""Extract agent configs from session metadata if available."""
configs = {}
# Check for agent_config in session metadata
agent_config = session.get("agent_config") or session.get("agentConfig")
if agent_config:
agent_id = agent_config.get("agent_id") or agent_config.get(
"agentId", "agent"
)
configs[agent_id] = {
"agent_id": agent_id,
"agent_type": agent_config.get(
"agent_type", agent_config.get("agentType")
),
"instruction": agent_config.get("instruction"),
"description": agent_config.get("description"),
}
return configs
# Infer from events — collect unique non-user authors
events = session.get("events", [])
authors = set()
for event in events:
author = event.get("author", "")
if author and author != "user":
authors.add(author)
if not authors:
authors.add(session.get("app_name", session.get("appName", "agent")))
for author in authors:
configs[author] = {"agent_id": author}
return configs
def _segment_into_turns(
events: list[dict[str, Any]], default_agent_id: str
) -> list[dict[str, Any]]:
"""Segment a flat event list into ConversationTurns.
A new turn starts with each user message (matching AgentData.from_session()
behavior).
"""
turns = []
current_events = []
for event in events:
is_user = _is_user_event(event)
# Start new turn on user message (if we have accumulated events)
if is_user and current_events:
turns.append({
"turn_index": len(turns),
"turn_id": f"turn_{len(turns)}",
"events": current_events,
})
current_events = []
content = _extract_content(event)
if content is None:
continue
author = _extract_author(event, default_agent_id)
agent_event = {"author": author, "content": content}
# Preserve state_delta if present (from EventActions)
actions = event.get("actions", {})
state_delta = actions.get("state_delta") or actions.get("stateDelta")
if state_delta:
agent_event["state_delta"] = state_delta
current_events.append(agent_event)
# Don't forget the last turn
if current_events:
turns.append({
"turn_index": len(turns),
"turn_id": f"turn_{len(turns)}",
"events": current_events,
})
return turns
def parse_session(session: dict[str, Any]) -> dict[str, Any]:
"""Convert a single ADK session dict to an EvalCase dict."""
events = session.get("events", [])
if not events:
raise ValueError(f"Session {session.get('id', 'unknown')} has no events.")
agent_configs = _extract_agent_configs(session)
default_agent_id = next(iter(agent_configs))
turns = _segment_into_turns(events, default_agent_id)
if not turns:
raise ValueError(
f"Session {session.get('id', 'unknown')} produced no turns."
)
agent_data = {"agents": agent_configs, "turns": turns}
return {"agent_data": agent_data}
def parse_file(filepath: str) -> list[dict[str, Any]]:
"""Parse a JSON file containing one or more ADK sessions."""
with open(filepath) as f:
data = json.load(f)
# Handle single session or list of sessions
if isinstance(data, list):
sessions = data
elif isinstance(data, dict):
if "events" in data:
sessions = [data]
elif "sessions" in data:
sessions = data["sessions"]
else:
raise ValueError(
f"Unrecognized format in {filepath}. Expected a session object "
"with 'events' field, a list of sessions, or an object with "
"'sessions' field."
)
else:
raise ValueError(f"Unexpected JSON type in {filepath}: {type(data)}")
eval_cases = []
for i, session in enumerate(sessions):
try:
eval_cases.append(parse_session(session))
except ValueError as e:
print(f"WARNING: Skipping session {i}: {e}", file=sys.stderr)
return eval_cases
def main():
parser = argparse.ArgumentParser(
description="Parse ADK session traces into Vertex Eval dataset format."
)
parser.add_argument(
"--input",
"-i",
help="Path to a single ADK session JSON file.",
)
parser.add_argument(
"--input_dir",
"-d",
help="Path to a directory of ADK session JSON files.",
)
parser.add_argument(
"--output",
"-o",
help="Output file path. Prints to stdout if not specified.",
)
args = parser.parse_args()
if not args.input and not args.input_dir:
parser.error("Specify --input or --input_dir")
eval_cases = []
if args.input:
if not os.path.exists(args.input):
print(f"ERROR: File not found: {args.input}", file=sys.stderr)
sys.exit(1)
eval_cases.extend(parse_file(args.input))
if args.input_dir:
if not os.path.isdir(args.input_dir):
print(f"ERROR: Directory not found: {args.input_dir}", file=sys.stderr)
sys.exit(1)
json_files = sorted(
f for f in os.listdir(args.input_dir) if f.endswith(".json")
)
if not json_files:
print(f"ERROR: No .json files in {args.input_dir}", file=sys.stderr)
sys.exit(1)
for filename in json_files:
filepath = os.path.join(args.input_dir, filename)
eval_cases.extend(parse_file(filepath))
if not eval_cases:
print("ERROR: No eval cases produced from input.", file=sys.stderr)
sys.exit(1)
dataset = {"eval_cases": eval_cases}
output_json = json.dumps(dataset, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output_json)
print(
f"Wrote {len(eval_cases)} eval case(s) to {args.output}",
file=sys.stderr,
)
else:
print(output_json)
if __name__ == "__main__":
main()
@@ -0,0 +1,395 @@
#!/usr/bin/env python3
"""Validate an evaluation dataset for Vertex Eval SDK compatibility.
Checks structural compliance with the EvaluationDataset schema,
required fields per metric type, and common formatting mistakes.
Usage:
python validate_dataset.py --dataset dataset.json
python validate_dataset.py --dataset dataset.json --metrics
hallucination_v1,tool_call_valid
Exit codes: 0 = valid, 1 = invalid (with specific errors).
"""
import argparse
import json
import sys
from typing import Any
# Metrics that require specific fields
_SINGLE_TURN_METRICS = frozenset({
"general_quality_v1",
"text_quality_v1",
"instruction_following_v1",
"grounding_v1",
"safety_v1",
"hallucination_v1",
})
_MULTI_TURN_METRICS = frozenset({
"multi_turn_general_quality_v1",
"multi_turn_text_quality_v1",
"multi_turn_tool_use_quality_v1",
"multi_turn_trajectory_quality_v1",
"multi_turn_task_success_v1",
})
_FINAL_RESPONSE_METRICS = frozenset({
"final_response_match_v2",
"final_response_reference_free_v1",
"final_response_quality_v1",
})
_COMPUTATION_METRICS = frozenset({
"exact_match",
"bleu",
"rouge_1",
"rouge_l_sum",
"tool_call_valid",
"tool_name_match",
"tool_parameter_key_match",
"tool_parameter_kv_match",
})
_VALID_ROLES = frozenset({"user", "model", "tool"})
class ValidationError:
"""A single validation error with location context."""
def __init__(self, path: str, message: str, severity: str = "ERROR"):
self.path = path
self.message = message
self.severity = severity
def __str__(self):
return f"[{self.severity}] {self.path}: {self.message}"
def _validate_content(content: Any, path: str) -> list[ValidationError]:
"""Validate a genai Content object."""
errors = []
if not isinstance(content, dict):
errors.append(
ValidationError(path, f"Expected dict, got {type(content).__name__}")
)
return errors
role = content.get("role")
if role and role not in _VALID_ROLES:
errors.append(
ValidationError(
f"{path}.role",
f"Invalid role '{role}'. Must be one of:"
f" {', '.join(sorted(_VALID_ROLES))}",
)
)
if role == "assistant":
errors.append(
ValidationError(
f"{path}.role",
"Use 'model' instead of 'assistant' (Vertex convention).",
)
)
parts = content.get("parts")
if parts is None:
errors.append(ValidationError(f"{path}.parts", "Missing 'parts' field."))
elif not isinstance(parts, list):
errors.append(ValidationError(f"{path}.parts", "Must be a list."))
elif not parts:
errors.append(
ValidationError(
f"{path}.parts",
"Empty parts list.",
severity="WARNING",
)
)
return errors
def _validate_agent_event(event: Any, path: str) -> list[ValidationError]:
"""Validate an AgentEvent object."""
errors = []
if not isinstance(event, dict):
errors.append(
ValidationError(path, f"Expected dict, got {type(event).__name__}")
)
return errors
if "author" not in event:
errors.append(ValidationError(f"{path}", "Missing 'author' field."))
content = event.get("content")
if content is None:
errors.append(ValidationError(f"{path}", "Missing 'content' field."))
else:
errors.extend(_validate_content(content, f"{path}.content"))
return errors
def _validate_turn(turn: Any, path: str) -> list[ValidationError]:
"""Validate a ConversationTurn object."""
errors = []
if not isinstance(turn, dict):
errors.append(
ValidationError(path, f"Expected dict, got {type(turn).__name__}")
)
return errors
if "turn_index" not in turn and "turnIndex" not in turn:
errors.append(ValidationError(f"{path}", "Missing 'turn_index' field."))
events = turn.get("events")
if events is None:
errors.append(ValidationError(f"{path}", "Missing 'events' field."))
elif not isinstance(events, list):
errors.append(ValidationError(f"{path}.events", "Must be a list."))
elif not events:
errors.append(ValidationError(f"{path}.events", "Empty events list."))
else:
for i, event in enumerate(events):
errors.extend(_validate_agent_event(event, f"{path}.events[{i}]"))
return errors
def _validate_agent_data(agent_data: Any, path: str) -> list[ValidationError]:
"""Validate an AgentData object."""
errors = []
if not isinstance(agent_data, dict):
errors.append(
ValidationError(path, f"Expected dict, got {type(agent_data).__name__}")
)
return errors
agents = agent_data.get("agents")
if agents is None:
errors.append(ValidationError(f"{path}", "Missing 'agents' field."))
elif not isinstance(agents, dict):
errors.append(ValidationError(f"{path}.agents", "Must be a dict."))
elif not agents:
errors.append(ValidationError(f"{path}.agents", "Empty agents map."))
else:
for agent_id, config in agents.items():
if not isinstance(config, dict):
errors.append(
ValidationError(
f"{path}.agents.{agent_id}",
f"Expected dict, got {type(config).__name__}",
)
)
elif "agent_id" not in config and "agentId" not in config:
errors.append(
ValidationError(
f"{path}.agents.{agent_id}",
"Missing 'agent_id' field.",
severity="WARNING",
)
)
turns = agent_data.get("turns")
if turns is None:
errors.append(ValidationError(f"{path}", "Missing 'turns' field."))
elif not isinstance(turns, list):
errors.append(ValidationError(f"{path}.turns", "Must be a list."))
elif not turns:
errors.append(ValidationError(f"{path}.turns", "Empty turns list."))
else:
for i, turn in enumerate(turns):
errors.extend(_validate_turn(turn, f"{path}.turns[{i}]"))
# Check turn_index ordering
indices = []
for turn in turns:
idx = turn.get("turn_index", turn.get("turnIndex"))
if idx is not None:
indices.append(idx)
if indices and indices != list(range(len(indices))):
errors.append(
ValidationError(
f"{path}.turns",
f"turn_index values are not sequential 0-based: {indices}",
severity="WARNING",
)
)
return errors
def _validate_eval_case(
case: Any, index: int, metrics: list[str] | None
) -> list[ValidationError]:
"""Validate a single EvalCase."""
path = f"eval_cases[{index}]"
errors = []
if not isinstance(case, dict):
errors.append(
ValidationError(path, f"Expected dict, got {type(case).__name__}")
)
return errors
has_prompt = "prompt" in case
has_agent_data = "agent_data" in case or "agentData" in case
has_response = "response" in case
has_reference = "reference" in case
if not has_prompt and not has_agent_data:
errors.append(
ValidationError(
path,
"Must have either 'prompt' (single-turn) or 'agent_data'"
" (multi-turn).",
)
)
if has_prompt and has_agent_data:
errors.append(
ValidationError(
path,
"Has both 'prompt' and 'agent_data'. Use one or the other.",
severity="WARNING",
)
)
# Validate agent_data structure
if has_agent_data:
ad = case.get("agent_data") or case.get("agentData")
errors.extend(_validate_agent_data(ad, f"{path}.agent_data"))
# Check metric-specific requirements
if metrics:
for metric in metrics:
if (
metric in _SINGLE_TURN_METRICS
and not has_prompt
and not has_agent_data
):
errors.append(
ValidationError(
path,
f"Metric '{metric}' requires 'prompt' and 'response' fields.",
)
)
if metric in _MULTI_TURN_METRICS and not has_agent_data:
errors.append(
ValidationError(
path,
f"Metric '{metric}' requires 'agent_data' with conversation"
" turns.",
)
)
if metric in _FINAL_RESPONSE_METRICS and not has_agent_data:
errors.append(
ValidationError(
path,
f"Metric '{metric}' requires 'agent_data'.",
)
)
if metric in _COMPUTATION_METRICS and not has_reference:
errors.append(
ValidationError(
path,
f"Metric '{metric}' requires a 'reference' field.",
severity="WARNING",
)
)
return errors
def validate_dataset(
dataset: dict[str, Any], metrics: list[str] | None = None
) -> list[ValidationError]:
"""Validate an entire EvaluationDataset."""
errors = []
if not isinstance(dataset, dict):
errors.append(
ValidationError("root", f"Expected dict, got {type(dataset).__name__}")
)
return errors
eval_cases = dataset.get("eval_cases")
if eval_cases is None:
errors.append(ValidationError("root", "Missing 'eval_cases' field."))
return errors
if not isinstance(eval_cases, list):
errors.append(ValidationError("eval_cases", "Must be a list."))
return errors
if not eval_cases:
errors.append(ValidationError("eval_cases", "Empty eval_cases list."))
return errors
for i, case in enumerate(eval_cases):
errors.extend(_validate_eval_case(case, i, metrics))
return errors
def main():
parser = argparse.ArgumentParser(
description="Validate an evaluation dataset for Vertex Eval SDK."
)
parser.add_argument(
"--dataset",
"-d",
required=True,
help="Path to the evaluation dataset JSON file.",
)
parser.add_argument(
"--metrics",
"-m",
help="Comma-separated list of metrics to validate against.",
)
args = parser.parse_args()
try:
with open(args.dataset) as f:
dataset = json.load(f)
except FileNotFoundError:
print(f"ERROR: File not found: {args.dataset}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"ERROR: Invalid JSON in {args.dataset}: {e}", file=sys.stderr)
sys.exit(1)
metrics = args.metrics.split(",") if args.metrics else None
errors = validate_dataset(dataset, metrics)
# Report
n_cases = len(dataset.get("eval_cases", []))
real_errors = [e for e in errors if e.severity == "ERROR"]
warnings = [e for e in errors if e.severity == "WARNING"]
print(f"Dataset: {args.dataset}")
print(f"Eval cases: {n_cases}")
if metrics:
print(f"Validating against metrics: {', '.join(metrics)}")
print()
if real_errors:
print(f"ERRORS ({len(real_errors)}):")
for e in real_errors:
print(f" {e}")
print()
if warnings:
print(f"WARNINGS ({len(warnings)}):")
for w in warnings:
print(f" {w}")
print()
if not real_errors and not warnings:
print("VALID: No issues found.")
if real_errors:
sys.exit(1)
if __name__ == "__main__":
main()