YAML API¶
YAML transcripts let you define evaluation tests without writing Python. They are loaded automatically from any directory listed in yaml_dirs.
Directory setup¶
Any *.yaml file inside tests/evals/ (searched recursively) becomes a test.
Editor and agent support¶
A published JSON Schema describes every transcript field. Add this first line to any transcript to get completion and validation in editors (and to help coding agents write valid files):
# yaml-language-server: $schema=https://datarootsio.github.io/pytest-agent-eval/schema/transcript.json
Full annotated transcript¶
# tests/evals/booking.yaml
# yaml-language-server: $schema=https://datarootsio.github.io/pytest-agent-eval/schema/transcript.json
# Unique ID, used as the pytest test name
id: booking_confirmation
# Fraction of runs that must pass (0.0 to 1.0)
threshold: 0.8
# Number of times to run the full transcript
runs: 3
# Optional tags for quality-gate filtering
tags:
- gate:booking
- smoke
turns:
- user: "Book me a table for 2 tomorrow at 10am."
expect:
# Reply must contain at least one of these strings (case-insensitive)
reply_contains_any:
- "confirmed"
- "booked"
# Reply must contain ALL of these strings (case-insensitive)
reply_contains_all:
- "tomorrow"
- "10"
# Tool names that must appear in this turn's tool calls
tool_calls_include:
- create_booking
# Tool names that must NOT appear in this turn's tool calls
tool_calls_exclude:
- cancel_booking
# LLM-as-judge rubric (requires a model in [tool.agent_eval])
judge:
rubric: >
The reply must confirm a booking with a date, time, and
reference number. The tone should be friendly and professional.
model: "openai:gpt-4o" # optional; overrides [tool.agent_eval] model
- user: "Can you email me the confirmation?"
expect:
reply_contains_any:
- "email"
- "sent"
Field reference¶
Top-level fields¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
str |
yes | n/a | Unique test identifier, used as the test name |
threshold |
float |
no | [tool.agent_eval] threshold |
Pass fraction required |
runs |
int |
no | [tool.agent_eval] runs |
Number of executions |
tags |
list[str] |
no | [] |
Quality-gate tags for filtering |
turns |
list[Turn] |
yes | n/a | Ordered list of turns |
turns[].user¶
The user message string for this turn. Required for every turn. Also acts as the transcript when an audio: fixture is generated for this turn.
turns[].audio¶
Optional path to a WAV file used by voice adapters (e.g. LiveKitAdapter). Resolved relative to the YAML file's directory unless absolute. Text adapters ignore this field; turns can mix audio and non-audio freely.
turns:
- user: "Book me a slot tomorrow at 10am."
audio: booking_t1.wav # → tests/evals/booking_t1.wav
expect:
tool_calls_include: [create_booking]
Generate the WAV from user: text via:
The CLI hashes turn.user into a <wav>.hash sidecar and only re-synthesises when the transcript changes. See the LiveKit adapter docs for the full pipeline.
turns[].expect¶
All expect fields are optional. Omit expect entirely for turns where you only care about the agent not crashing.
| Field | Type | Description |
|---|---|---|
reply_contains_any |
list[str] |
At least one string must appear in the reply |
reply_contains_all |
list[str] |
All strings must appear in the reply |
reply_matches_any |
list[str] |
At least one regex pattern must match the reply |
reply_matches_all |
list[str] |
All regex patterns must match the reply |
tool_calls_include |
list[str] |
These tool names must be present in the turn's calls |
tool_calls_exclude |
list[str] |
These tool names must be absent from the turn's calls |
tool_calls_ordered |
bool |
If true, tool_calls_include must appear in order |
tool_calls_args |
list |
Assertions on the arguments of specific tool calls |
judge |
JudgeConfig |
LLM-as-judge rubric evaluation |
String and regex checks are case-insensitive. Regex patterns use Python re.search semantics: quote them in YAML so \d and friends survive parsing:
turns[].expect.tool_calls_args¶
Each entry asserts the arguments of one tool. Provide args for a deterministic check, judge for an LLM-judged rubric, or both:
expect:
tool_calls_args:
# Deterministic: these keys/values must appear in the call's arguments
- tool: book_slot
args:
time: "10am"
mode: subset # subset (default) or exact
# LLM-judged: rubric evaluated against the JSON arguments
- tool: book_slot
judge:
rubric: "The booking time must be within business hours."
| Field | Type | Default | Description |
|---|---|---|---|
tool |
str |
required | Tool name to check |
args |
dict |
null |
Expected arguments (deterministic check) |
mode |
str |
"subset" |
subset: expected items must appear; exact: full dict equality |
judge |
JudgeConfig |
null |
Rubric + optional model, judged against the call's JSON arguments |
If the tool is called several times in the turn, the assertion passes when any call matches. Argument capture requires an adapter that records arguments: all bundled adapters do; custom agents must return ToolCall(name, args) (see Adapters).
turns[].expect.judge¶
| Field | Type | Description |
|---|---|---|
rubric |
str |
Natural-language rubric sent to the judge model |
model |
str \| null |
pydantic-ai model ID override; falls back to global config |
Agent fixture¶
YAML-loaded tests require a pytest fixture named llm_eval_agent that returns your agent callable:
# tests/conftest.py
import pytest
from pytest_agent_eval import AgentReply
@pytest.fixture
def llm_eval_agent():
async def my_agent(history):
# history is a list of Message records; history[-1].content is this turn's user text.
# Return AgentReply(reply, tool_calls): the reply string plus the tool names called.
return AgentReply("Booking confirmed! Reference BK-1234.", ["create_booking"])
return my_agent
The fixture is resolved at collection time, so you can parametrize it or switch agents per test directory.