Skip to content

Evaluators

An evaluator scores one trace of one case. Write one as a function with @agentlens.evaluator; it may return a bool, a score in [0, 1] (passes at >= threshold, default 0.5), (score, reason) or a full EvalResult. Extra parameters come from the suite spec, and an evaluator that raises gives a failed result instead of crashing the run:

import agentlens
from agentlens.evals import evaluate_case, get_evaluator
from agentlens.schema import Case

@agentlens.evaluator(threshold=0.8)
def short_answer(trace, case, max_words: int = 50):
    words = len(str(trace.output).split())
    return min(1.0, max_words / max(words, 1)), f"{words} words"

with agentlens.run("support") as trace:
    trace.output = "Your refund for order 1234 has been issued."

case = Case(id="refund", input="refund 1234", evaluators=["short_answer"])
print(evaluate_case(trace, case)[0].passed)  # True: 8 words <= 50
check = get_evaluator({"short_answer": 4})  # YAML shorthand: scalar -> first parameter
print(check.evaluate(trace, case).reason)  # "8 words" (score 0.5 < 0.8: failed)

Suites refer to evaluators by name (no_error, {max_steps: 8}, {tool_sequence: {mode: ordered}}); agentlens.evals.register(name, factory) adds class-based ones and available_evaluators() lists them.

Built-in output checks

exact_match, contains, regex, json_schema and no_error are registered out of the box. Each returns a score in [0, 1], a readable reason and details; bad parameters (an invalid regex or schema) fail when the suite is loaded. Text checks see a dict/list output as canonical JSON, and path: answer (or items.0.text) picks one field:

import agentlens
from agentlens.evals import evaluate_case
from agentlens.schema import Case

with agentlens.run("support") as trace:
    trace.output = {"answer": "Refund issued for order 1234", "status": "refunded"}

case = Case(id="refund", input="refund 1234", expected="Refund issued for order 1234")
for result in evaluate_case(trace, case, [
    "no_error",                                  # no exception in the run or its spans
    {"exact_match": {"path": "answer"}},         # value defaults to case.expected
    {"contains": {"text": ["refund", "1234"], "case_sensitive": False}},
    {"regex": {"pattern": r"order \d{4}", "path": "answer"}},
    {"json_schema": {"schema": {"type": "object", "required": ["answer", "status"]}}},
]):
    print(result.evaluator, result.score, result.reason)

Built-in trajectory checks

tool_sequence, max_steps, cost_budget and latency_budget check how the agent worked. tool_sequence compares the tools called with tools (or the case's expected.tools) in strict (exact list), ordered (default: in this order, extra calls allowed) or subset (all called, any order) mode; repeats count, forbidden: [...] fails on any call to those tools. Budgets score 1 within the limit and limit / actual above it; an unknown cost fails cost_budget unless on_unknown: pass:

import agentlens
from agentlens.evals import evaluate_case
from agentlens.schema import Case

@agentlens.wrap_tool
def lookup_order(order_id: str) -> dict:
    return {"id": order_id, "amount": 42}

@agentlens.wrap_tool
def issue_refund(order_id: str) -> str:
    return "refunded"

with agentlens.run("support") as trace:
    lookup_order("1234")
    issue_refund("1234")
    trace.output = "Refund issued"

case = Case(id="refund", input="refund 1234",
            expected={"tools": ["lookup_order", "issue_refund"]})
for result in evaluate_case(trace, case, [
    {"tool_sequence": {"mode": "ordered"}},      # tools from case.expected
    {"tool_sequence": {"mode": "strict", "forbidden": ["delete_account"]}},
    {"max_steps": 8},                            # agent_step spans, else LLM calls
    {"latency_budget": 5000},                    # milliseconds
    {"cost_budget": {"max_usd": 0.05, "on_unknown": "pass"}},  # no LLM calls here: unknown
]):
    print(result.evaluator, result.passed, result.reason)

For judgement calls (tone, helpfulness, faithfulness) use the LLM judges. See also: design §5, API: agentlens.evals.