| name | pydantic-evals |
| description | Test and evaluate AI agents and LLM outputs using code-first evaluation framework with strong typing. Use when the user wants to: (1) Create evaluation datasets with test cases for AI agents, (2) Define evaluators (deterministic, LLM-as-Judge, custom, or span-based), (3) Run evaluations and generate reports, (4) Compare model performance across experiments, (5) Integrate evaluations with Pydantic AI agents, (6) Set up observability with Logfire, (7) Generate test datasets using LLMs, (8) Implement regression testing for AI systems. |
Pydantic Evals
Overview
Pydantic Evals provides rigorous testing and evaluation for AI agents and LLM outputs using a code-first approach with Pydantic models. It enables "Evaluation-Driven Development" (EDD) where evaluation suites live alongside application code, subject to version control and CI/CD.
Core Concepts
Understand these key primitives:
Case
A single test scenario with inputs, optional expected output, and metadata.
from pydantic_evals import Case
case = Case(
name="refund_request",
inputs="What is your refund policy?",
expected_output="30 days full refund",
metadata={"category": "policy"}
)
Dataset
Collection of Cases with default evaluators. Generic over input/output types.
from pydantic_evals import Dataset
dataset = Dataset(
name="my_eval_suite",
cases=[case1, case2, case3],
evaluators=[evaluator1, evaluator2]
)
Evaluator
Logic engine that assesses outputs. Returns bool (Pass/Fail), float/int (score), or str (label).
Experiment
Point-in-time performance capture when Dataset runs against a Task.
For detailed explanations, see references/core-concepts.md
Quick Start
Create and run a simple evaluation:
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import Contains, LLMJudge
cases = [
Case(
name="greeting",
inputs="Hello, who are you?",
expected_output="I am an AI assistant."
)
]
evaluators = [
Contains(value="AI assistant"),
LLMJudge(rubric="Is this response polite? Answer PASS or FAIL.")
]
dataset = Dataset(name="quickstart", cases=cases, evaluators=evaluators)
async def my_agent(query: str) -> str:
return "I am an AI assistant."
report = dataset.evaluate_sync(my_agent)
report.print()
Evaluator Types
Pydantic Evals supports a "Pyramid of Evaluation" from fast/cheap to slow/expensive:
1. Deterministic Evaluators
Fast, free, code-based checks. Use as first line of defense.
- Equals: Exact equality check
- EqualsExpected: Compare to Case.expected_output
- Contains: Substring/item presence
- IsInstance: Type validation
- MaxDuration: Latency SLA enforcement
Strategy: Always run deterministic checks before expensive LLM judges.
2. LLM-as-a-Judge
Use secondary LLM to score outputs based on natural language rubrics.
from pydantic_evals.evaluators import LLMJudge
judge = LLMJudge(
rubric="Response must: 1) Answer the question, 2) Cite context, 3) Be professional",
include_input=True,
include_expected_output=True,
model='openai:gpt-5.2'
)
Using OpenRouter for LLMJudge (v2 first-class support):
from pydantic_evals.evaluators.llm_as_a_judge import set_default_judge_model
from pydantic_ai.models.openrouter import OpenRouterModel
from pydantic_ai.providers.openrouter import OpenRouterProvider
model = OpenRouterModel(
'openai/gpt-5.2',
provider=OpenRouterProvider(api_key=os.getenv('OPENROUTER_API_KEY', '')),
)
set_default_judge_model(model)
judge = LLMJudge(rubric="Is this polite?", model=model)
Rubric best practices: Be specific and actionable, not vague.
3. Custom Evaluators
Implement arbitrary logic by inheriting from Evaluator.
from dataclasses import dataclass
from pydantic_evals.evaluators import Evaluator, EvaluatorContext
@dataclass
class ValidSQL(Evaluator):
def evaluate(self, ctx: EvaluatorContext) -> bool:
import sqlparse
try:
parsed = sqlparse.parse(ctx.output)
return len(parsed) > 0
except:
return False
Custom Evaluators for Structured Output (Pydantic Models)
Important: Built-in evaluators like Contains, Equals work with strings/lists/dicts. They do NOT work with Pydantic model outputs. For agents with output_type=MyModel, create custom evaluators:
from dataclasses import dataclass
from pydantic_evals.evaluators import Evaluator, EvaluatorContext
from pydantic import BaseModel
class MyAgentResponse(BaseModel):
message: str
status: str
complete: bool
@dataclass
class HasNonEmptyMessage(Evaluator[MyAgentResponse, None]):
"""Check that response has a non-empty message field."""
min_length: int = 1
def evaluate(self, ctx: EvaluatorContext[MyAgentResponse, None]) -> bool:
if not isinstance(ctx.output, MyAgentResponse):
return False
return len(ctx.output.message) >= self.min_length
@dataclass
class StatusIsValid(Evaluator[MyAgentResponse, None]):
"""Check that status is one of allowed values."""
allowed_values: tuple = ("pending", "complete", "error")
def evaluate(self, ctx: EvaluatorContext[MyAgentResponse, None]) -> bool:
return ctx.output.status in self.allowed_values
evaluators = [
IsInstance(type_name="MyAgentResponse"),
HasNonEmptyMessage(min_length=10),
StatusIsValid(),
]
4. Span-Based Evaluation
Inspect execution traces to verify internal agent behavior (tool calls, retrieval steps).
from pydantic_evals.evaluators import HasMatchingSpan
from pydantic_evals.otel import SpanQuery
tool_check = HasMatchingSpan(
query=SpanQuery(
name_equals='running tool',
has_attributes={'gen_ai.tool.name': 'calculator'}
)
)
For detailed guide, see references/evaluator-types.md
Integration with Pydantic AI
Define Agent as Task
Wrap agent execution in a task function:
from pydantic_ai import Agent
agent = Agent('openai:gpt-5.2', instructions="You are helpful.")
async def run_agent(query: str) -> str:
result = await agent.run(query)
return result.output
Handle Dependencies
Use dependency injection for deterministic testing:
from dataclasses import dataclass
@dataclass
class Deps:
api_key: str
test_deps = Deps(api_key="test_key")
For integration guide, see references/integration.md
Logfire Observability
Enable automatic tracing for debugging:
import logfire
logfire.configure(send_to_logfire='if-token-present')
logfire.instrument_pydantic_ai()
Benefits:
- Trace every evaluation run
- Visualize agent internal execution
- Compare experiments side-by-side
- Debug failures with full context
Dataset Management
Save/Load Datasets
dataset.to_file('evals.yaml', fmt='yaml')
dataset = Dataset.from_file('evals.yaml')
Important: Use typed Dataset for proper serialization:
dataset: Dataset[str, str, None] = Dataset(...)
from types import NoneType
dataset = Dataset[MyInputType, MyOutputType, NoneType].from_file(
'evals.yaml',
custom_evaluator_types=(MyCustomEvaluator,)
)
Generate Datasets with LLM
from pydantic_evals.generation import generate_dataset
dataset = await generate_dataset(
dataset_type=Dataset[str, str, None],
model='openai:gpt-5.2',
n_examples=10,
extra_instructions="Generate diverse test cases for customer support agent"
)
Best Practices
- Fail-fast: Run deterministic evaluators before LLM judges
- Cost-latency trade-off:
- Commit hooks: Deterministic only
- PR merges: Small LLM judges on critical cases
- Nightly builds: Full LLM judge suite
- Concurrency: Use
max_concurrency parameter to avoid rate limits
- Versioning: Store datasets in Git alongside code
- Regression testing: Compare experiments to detect degradation
Common Workflows
Workflow 1: Create Evaluation Suite
- Define Cases with inputs and expected outputs
- Choose evaluators based on requirements
- Create Dataset with cases and evaluators
- Save to YAML for version control
Workflow 2: Run Evaluations
- Load Dataset from file
- Define task function (agent wrapper)
- Run
dataset.evaluate_sync(task) or dataset.evaluate(task)
(v2: extras like name, max_concurrency, progress are keyword-only)
- Analyze report with
report.print() or Logfire
Accessing Results:
report = dataset.evaluate_sync(my_task)
report.print()
for case in report.cases:
print(f"Case: {case.name}")
print(f"Output: {case.output}")
print(f"Passed: {case.passed}")
Workflow 3: Compare Models
- Run same dataset against different models
- Generate Experiments for each run
- Compare metrics (pass rates, latency, scores)
- Use Logfire comparison view
Examples
Complete example files demonstrating patterns (self-contained: sibling files
import each other, e.g. from models import ...):
v2 API Changes (Pydantic Evals)
This skill targets pydantic-evals v2. Key changes from v1:
Dataset(name=...) is required (keyword-only)
Dataset.evaluate() / evaluate_sync(): name, max_concurrency,
progress, retry_task, retry_evaluators are keyword-only
EvaluationResult and EvaluatorFailure are keyword-only
- Custom evaluator default names: override
get_default_evaluation_name() /
get_evaluator_version() - the evaluation_name / evaluator_version
class-attribute fallback was removed
Evaluator.name classmethod → Evaluator.get_serialization_name()
- Agent results:
result.usage is a property; use result.output
Resources
references/