Skip to content

Evaluators

Evaluators decide whether an agent's reply passes or fails a turn. All evaluators implement an async evaluate(ctx: TurnContext) -> EvalResult method.

ContainsEvaluator

Checks that the reply contains expected substrings or matches regex patterns (case-insensitive by default).

from pytest_agent_eval import ContainsEvaluator

# Pass if reply contains at least one of these
ContainsEvaluator(any_of=["confirmed", "booked"])

# Pass if reply contains ALL of these
ContainsEvaluator(all_of=["booking", "reference number"])

# Regex patterns (evaluated with re.search)
ContainsEvaluator(matches_any=[r"ref(erence)? number[:# ]*[A-Z]{2}-\d+"])
ContainsEvaluator(matches_all=[r"\d{1,2}(am|pm)", r"tomorrow"])

# Substring and regex checks compose freely
ContainsEvaluator(
    any_of=["confirmed", "booked"],
    matches_all=[r"BK-\d+"],
)

# Exact-case matching
ContainsEvaluator(all_of=["Booking"], case_sensitive=True)

Invalid regex patterns raise ValueError at construction time, so a typo fails the test suite immediately instead of silently failing every turn.

Parameters:

Parameter Type Description
any_of list[str] Reply must contain at least one of these substrings
all_of list[str] Reply must contain every one of these substrings
matches_any list[str] Reply must match at least one of these regex patterns (re.search)
matches_all list[str] Reply must match every one of these regex patterns (re.search)
case_sensitive bool When False (default), all checks ignore case

ToolCallEvaluator

Validates that specific tools were (or were not) called during a turn.

from pytest_agent_eval import ToolCallEvaluator

# Require a tool and forbid another
ToolCallEvaluator(
    must_include=["book_slot"],
    must_exclude=["cancel_slot"],
)

# Enforce call order
ToolCallEvaluator(
    must_include=["authenticate", "fetch_availability", "create_booking"],
    ordered=True,
)

Parameters:

Parameter Type Description
must_include list[str] Tool names that must appear in the turn's tool calls
must_exclude list[str] Tool names that must NOT appear in the turn's tool calls
ordered bool If True, must_include tools must appear in the specified order

ToolCallArgsEvaluator

Asserts the arguments a tool was called with. Requires an adapter (or custom agent) that captures arguments: all bundled adapters do; custom agents return ToolCall(name, args) instead of plain strings (see Adapters).

from pytest_agent_eval import ToolCallArgsEvaluator

# Subset (default): expected top-level keys/values must appear; extra observed keys are fine
ToolCallArgsEvaluator(tool="book_slot", args={"time": "10am"})

# Exact: observed args must equal the expected dict exactly
ToolCallArgsEvaluator(tool="book_slot", args={"time": "10am", "date": "tomorrow"}, mode="exact")

Subset matching compares top-level keys only; a nested dict value is compared exactly ({"opts": {"a": 1}} does not subset-match {"opts": {"a": 1, "b": 2}}). Use mode="exact" when you want full equality, or assert the nested keys with a separate entry.

If the tool is called several times in a turn, the check passes when any call matches. Failure messages distinguish three cases: the tool was never called, the tool was called but no dict arguments were captured, and a genuine argument mismatch (which shows expected vs observed).

Parameters:

Parameter Type Default Description
tool str required Name of the tool to check
args dict required Expected arguments
mode str "subset" "subset" or "exact"

ToolCallArgsJudgeEvaluator

Uses an LLM to judge a tool's arguments against a natural-language rubric, for constraints that are awkward to express as exact values ("the time must be within business hours", "the query must mention the user's city").

from pytest_agent_eval import ToolCallArgsJudgeEvaluator

ToolCallArgsJudgeEvaluator(
    tool="book_slot",
    rubric="The booking time must be within business hours (9am-5pm).",
)

The judge receives the tool name and the JSON arguments of every call to that tool in the turn, and passes if any call satisfies the rubric. Never-called and args-not-captured fail deterministically before any LLM call, so no judge tokens are spent on structural failures.

Parameters:

Parameter Type Default Description
tool str required Name of the tool whose arguments to judge
rubric str required Natural-language rubric for acceptable arguments
model str \| None None pydantic-ai model ID; falls back to [tool.agent_eval] model
retries int 2 Retries on API failure
timeout float 30.0 Per-call timeout in seconds

JudgeEvaluator

Uses an LLM (via pydantic-ai) to evaluate the reply against a natural-language rubric. Good for open-ended quality checks that are hard to express as string patterns.

from pytest_agent_eval import JudgeEvaluator

JudgeEvaluator(
    rubric=(
        "The reply must confirm the booking, include a reference number, "
        "mention the date and time, and have a friendly professional tone."
    ),
    model="openai:gpt-4o",      # optional; falls back to [tool.agent_eval] model
    retries=2,                   # retry API failures
    timeout=30.0,                # per-call timeout in seconds
)

Parameters:

Parameter Type Default Description
rubric str required Natural-language description of what a passing reply looks like
model str \| None None pydantic-ai model ID; falls back to [tool.agent_eval] model
retries int 2 Number of retries on API failure before returning a FAIL verdict
timeout float 30.0 Seconds before the judge call times out

Writing a custom evaluator

Implement the Evaluator protocol: an object with an async evaluate method.

from pytest_agent_eval.models import TurnContext, EvalResult

class SentimentEvaluator:
    """Fail if the reply has negative sentiment."""

    def __init__(self, threshold: float = 0.5):
        self.threshold = threshold

    async def evaluate(self, ctx: TurnContext) -> EvalResult:
        # ctx.reply:      the agent's string reply
        # ctx.user:       the user message for this turn
        # ctx.tool_calls: list of tool names called
        # ctx.history:    OpenAI-format message history

        score = await compute_sentiment(ctx.reply)   # your own logic
        passed = score >= self.threshold
        return EvalResult(
            passed=passed,
            reasoning=f"Sentiment score {score:.2f} vs threshold {self.threshold:.2f}",
        )

Then use it like any built-in evaluator:

from pytest_agent_eval import Turn, Expect

Turn(
    user="How was your experience?",
    expect=Expect(evaluators=[SentimentEvaluator(threshold=0.6)]),
)

The protocol requires only that evaluate is async and returns an EvalResult. There is no base class to inherit from.