Evaluation

In shortAgentEvaluator, EvalSet, EvalCase, EvalConfig, EvalReport and TrajectoryCollector — how a scored evaluation run is wired together.

  • 14 min read
  • 17 sections
  • Updated
  • v0.9.2
  • Markdown

When to use this

Use the evaluation framework when you need to:

  • Score agent accuracy on a labelled dataset (golden answers).
  • Assert that the agent follows the correct tool-call sequence (trajectory matching).
  • Run LLM-as-judge scoring against a rubric.
  • Run goal-driven user simulation against the agent.

The evaluation runner is separate from unit tests. It is designed to run as a CI step or a recurring offline report.

This page covers the data model and the runner. Two companion pages go deeper:

Import paths

Python
from tenxgraph.qa.evaluation import (
    AgentEvaluator,
    EvalConfig,
    CriteriaConfig,
    CriterionConfig,
    MatchType,
    Rubric,
    EvalSet,
    EvalSetBuilder,
    EvalCase,
    Invocation,
    MessageContent,
    SessionInput,
    ToolCall,
    TrajectoryStep,
    StepType,
    EvalReport,
    EvalCaseResult,
    CriterionResult,
    TrajectoryCollector,
    make_trajectory_callback,
)

Everything above is also re-exported from tenxgraph.qa.


Wiring a run

Evaluation needs two objects: a compiled graph and the TrajectoryCollector whose callback manager that graph was compiled with. Without that pairing the collector sees no events and every trajectory criterion scores zero.

The short way:

Python
from tenxgraph.qa.evaluation import AgentEvaluator, create_eval_app

app, collector = create_eval_app(build_my_graph())   # uncompiled StateGraph in
evaluator = AgentEvaluator(app, collector)

The explicit way, when you need to control compilation yourself:

Python
from tenxgraph.qa.evaluation import TrajectoryCollector, make_trajectory_callback

collector = TrajectoryCollector(capture_all_events=True)
_, callback_mgr = make_trajectory_callback(collector, config={"thread_id": "eval-1"})

app = my_state_graph.compile(callback_manager=callback_mgr)

make_trajectory_callback() returns a tuple (collector, callback_manager) — pass the second element to compile().


EvalCase

A single labelled test case. Fields:

Field Type Default Description
eval_id str random UUID Unique ID for this case.
name str "" Label shown in reports.
description str "" Longer description.
conversation list[Invocation] [] One entry per turn. Expected tools, node order and responses live here.
session_input SessionInput default instance app_name, user_id ("test_user"), state, config. config is merged into the run config.
tags list[str] [] Used by EvalSet.filter_by_tags().
metadata dict[str, Any] {} Free-form. HallucinationCriterion reads metadata["context"]; FactualAccuracyCriterion reads metadata["reference_facts"].

There is no expected_response or expected_tools attribute on EvalCase itself — those are constructor arguments of the factory methods, stored on the Invocation objects inside conversation.

EvalCase.single_turn

Python
from tenxgraph.qa.evaluation import EvalCase, ToolCall

case = EvalCase.single_turn(
    eval_id="weather-paris-001",
    user_query="What is the weather in Paris today?",
    expected_response="It is sunny in Paris.",
    expected_tools=[ToolCall(name="get_weather", args={"location": "Paris"})],
    expected_node_order=["MAIN", "TOOL", "MAIN"],
    name="Paris weather",
)
Parameter Type Default Description
eval_id str required Unique ID for this case.
user_query str required Input message from the user.
expected_response str | None None Expected final response. None means response criteria skip the case.
expected_tools list[ToolCall] | None None Expected tool calls. ToolCall objects, not plain strings.
expected_node_order list[str] | None None Expected node visit sequence.
name str "" Label shown in reports.
description str "" Longer description.

EvalCase.multi_turn

Python
case = EvalCase.multi_turn(
    eval_id="multi-turn-001",
    conversation=[
        ("Hello", "Hi"),
        ("What can you do?", "I can answer questions"),
        ("Tell me about Python", "Python is a programming language"),
    ],
    expected_tools=[ToolCall(name="search_docs")],
)
Parameter Type Default Description
eval_id str required Unique ID for this case.
conversation list[tuple[str, str]] required (user_query, expected_response) per turn.
expected_tools list[ToolCall] | None None Attached to the first invocation only.
name str "" Label shown in reports.
description str "" Longer description.

The evaluator feeds the agent one user message per turn and accumulates tool calls, node visits and messages across all turns. Criteria then score the accumulated execution once, not per turn.

Invocation

One turn inside EvalCase.conversation.

Field Type Default Description
invocation_id str random UUID Turn identifier.
user_content MessageContent required The user message for this turn.
expected_tool_trajectory list[ToolCall] [] Tools expected during this turn.
expected_node_order list[str] [] Node sequence expected during this turn.
expected_intermediate_responses list[MessageContent] [] Reserved for intermediate output.
expected_final_response MessageContent | None None Expected reply for this turn.

Invocation.simple(user_query, expected_response=None, expected_tools=None, expected_node_order=None) builds one without touching MessageContent directly.


ToolCall

Represents an expected or actual tool invocation.

Python
from tenxgraph.qa.evaluation import ToolCall

tc = ToolCall(
    name="get_weather",
    args={"location": "Paris", "units": "celsius"},
    call_id=None,   # optional
    result=None,    # populated on actual calls
)

ToolCall.matches

Python
tc.matches(other, check_args=True, check_call_id=False)  # -> bool

Compares against another ToolCall. Names must always be equal. Arguments are only compared when check_args=True and the expected call defines non-empty args — an expected ToolCall with args={} accepts any arguments. Trajectory criteria call this with check_call_id=False.


TrajectoryStep

A recorded step in the execution trajectory.

Field Type Default
step_type StepType required
name str required
args dict[str, Any] {}
timestamp float | None None
metadata dict[str, Any] {}
Python
from tenxgraph.qa.evaluation import TrajectoryStep

node_step = TrajectoryStep.node("RESEARCH_NODE")
tool_step = TrajectoryStep.tool("search", args={"query": "AI trends"})

Both factories take the step name as a plain string and accept extra keyword arguments, which are stored in metadata.

StepType

Value Description
StepType.NODE A graph node was entered.
StepType.TOOL A tool was called.
StepType.MESSAGE A message step.
StepType.CONDITIONAL A conditional-edge step.

EvalSet

A collection of EvalCase objects.

Field Type Default
eval_set_id str random UUID
name str ""
description str ""
eval_cases list[EvalCase] []
metadata dict[str, Any] {}
Python
from tenxgraph.qa.evaluation import EvalSet

eval_set = EvalSet(eval_set_id="capitals", name="Capitals", eval_cases=[case1, case2])
Method Description
EvalSet.from_file(path) Classmethod. Load from a JSON file matching the EvalSet schema.
to_file(path) / save(path) Write to a JSON file.
add_case(case) Append a case.
get_case(eval_id) Look up a case, or None.
filter_by_tags(tags) Cases carrying all of the given tags.
len(eval_set) / iteration Case count / iterate over cases.

The on-disk format is a single JSON object, not JSONL. EvalSetBuilder offers a fluent way to construct one — see Building eval sets.


EvalConfig

Holds the criteria to apply during evaluation, plus run-level settings.

Field Type Default Description
criteria CriteriaConfig empty CriteriaConfig() Which criteria run.
user_simulator_config UserSimulatorConfig | None None Simulator settings.
parallel bool False Run cases concurrently.
max_concurrency int 4 Concurrency cap.
timeout float 300.0 Seconds per case.
verbose bool False Verbose logging.
mock_mode bool False Skip actual execution.
reporter ReporterConfig default instance Automatic report generation.

criteria is a typed model, not a free-form dict. CriteriaConfig forbids unknown fields, so each criterion goes in its own named slot:

Python
from tenxgraph.qa.evaluation import (
    CriteriaConfig,
    CriterionConfig,
    EvalConfig,
    MatchType,
)

config = EvalConfig(
    criteria=CriteriaConfig(
        tool_name_match=CriterionConfig.tool_name_match(threshold=1.0),
        trajectory=CriterionConfig.trajectory(match_type=MatchType.IN_ORDER),
        llm_judge=CriterionConfig.llm_judge(),
    ),
    parallel=True,
    max_concurrency=4,
)

CriteriaConfig fields

Every field is CriterionConfig | None, defaulting to None (criterion off).

Field Criterion class Reported as
tool_name_match ToolNameMatchCriterion tool_name_match_score
trajectory TrajectoryMatchCriterion tool_trajectory_avg_score
node_order NodeOrderMatchCriterion node_order_score
response_match ResponseMatchCriterion response_match_score
rouge_match RougeMatchCriterion rouge_match
contains_keywords ContainsKeywordsCriterion contains_keywords
llm_judge LLMJudgeCriterion final_response_match_v2
rubric_based RubricBasedCriterion rubric_based_final_response_quality_v1
factual_accuracy FactualAccuracyCriterion factual_accuracy_v1
hallucination HallucinationCriterion hallucinations_v1
safety SafetyCriterion safety_v1
simulation_goals SimulationGoalsCriterion simulation_goals

The “reported as” column is the criterion’s name — the key you will see in EvalSummary.criterion_stats and in CriterionResult.criterion.

Built-in configurations

Classmethod Criteria enabled
EvalConfig.default() trajectory (EXACT, threshold 1.0) + response_match (LLM, threshold 0.8)
EvalConfig.strict() trajectory (EXACT, check_args=True, 1.0) + response_match (0.9) + llm_judge (0.9, 5 samples)
EvalConfig.relaxed() trajectory (IN_ORDER, check_args=False, 0.8) + response_match (0.6)

Methods

Method Description
EvalConfig.from_file(path) Classmethod. Load config from JSON.
to_file(path) Write config to JSON.
get_criterion_config(name) Fetch a CriterionConfig by CriteriaConfig field name.
enable_criterion(name, config=None) Enable by field name. Raises ValueError for unknown names.
disable_criterion(name) Set enabled=False on that criterion.
with_rubrics(rubrics) Return a deep copy with rubric_based configured.

CriterionConfig fields and factory methods are documented in Criteria; the criterion classes themselves in Evaluation criteria.


MatchType

Value Description
MatchType.EXACT Same items, same positions.
MatchType.IN_ORDER Expected items must appear in order; extras allowed between them.
MatchType.ANY_ORDER All expected items must appear, in any order.

Rubric

Python
from tenxgraph.qa.evaluation import Rubric

rubric = Rubric(
    rubric_id="accuracy",
    content="The response must contain the correct capital city.",
    weight=1.0,
)
Field Type Default Description
rubric_id str required Unique ID; shown in details["rubrics"].
content str required Grading criterion text passed to the judge.
weight float 1.0 Relative importance. Weights do not have to sum to 1.

TrajectoryCollector

Records node visits, tool calls and per-node LLM output during a graph run. It is a BasePublisher, fed by PublisherCallback.

Python
from tenxgraph.qa.evaluation import TrajectoryCollector, make_trajectory_callback

collector = TrajectoryCollector(capture_all_events=True)
_, callback_mgr = make_trajectory_callback(collector)

app = graph.compile(callback_manager=callback_mgr)
await app.ainvoke({"messages": [...]})

for step in collector.trajectory:
    print(step.step_type, step.name)
Parameter Type Default Description
capture_all_events bool False Also retain every raw EventModel on collector.events.
Attribute Type Description
trajectory list[TrajectoryStep] Ordered node and tool steps.
tool_calls list[ToolCall] Tool calls with args and results.
node_visits list[str] Node names in visit order.
node_responses list[NodeResponse] Per-node input/output snapshots.
final_response str Text from the last non-tool-call node invocation.
events list[EventModel] Raw events, when capture_all_events=True.
start_time / end_time float | None First and last event timestamps.

AgentEvaluator calls collector.reset() before every case.

make_trajectory_callback

Python
def make_trajectory_callback(
    collector: TrajectoryCollector,
    config: dict | None = None,
) -> tuple[TrajectoryCollector, CallbackManager]

Builds a CallbackManager with a PublisherCallback registered for the TOOL, MCP and AI invocation types. config may carry thread_id and run_id, which are stamped onto every emitted event. Pass the returned callback manager to graph.compile().


AgentEvaluator

Main evaluation runner.

Python
from tenxgraph.qa.evaluation import AgentEvaluator, EvalConfig

evaluator = AgentEvaluator(app, collector, config=EvalConfig.default())
report = await evaluator.evaluate(eval_set)
print(report.format_summary())

Constructor parameters

Parameter Type Default Description
graph CompiledGraph required Graph compiled with the collector’s callback manager.
collector TrajectoryCollector required Collector wired into that graph.
config EvalConfig | None None Falls back to EvalConfig.default().

Concurrency is not a constructor argument — pass parallel and max_concurrency to evaluate(), or set them on EvalConfig.

evaluate

Python
async def evaluate(
    eval_set: EvalSet | str,
    parallel: bool = False,
    max_concurrency: int = 4,
    verbose: bool = False,
    output_dir: str | None = None,
) -> EvalReport

eval_set accepts an EvalSet or a path to an eval set JSON file (a missing file raises FileNotFoundError). After building the report, evaluate() runs the configured reporters unless config.reporter.enabled is False; output_dir overrides ReporterConfig.output_dir for that run. Parallel mode is only used when parallel=True and the set has more than one case.

Other methods

Method Signature Description
evaluate_case async (case: EvalCase) -> EvalCaseResult Run one case. Useful for per-case pytest assertions.
evaluate_sync classmethod (graph, collector, eval_set, config=None, verbose=False) -> EvalReport Blocking wrapper around evaluate() via asyncio.run.
evaluate_file classmethod async (agent_module, eval_file, config_file=None) -> EvalReport Import a module exposing graph / compiled_graph / agent_graph / app and evaluate it.

Result types

EvalReport

Returned by evaluate().

Attribute Type Description
eval_set_id str ID of the evaluated set.
eval_set_name str Human-readable name.
results list[EvalCaseResult] Individual case results.
summary EvalSummary Aggregate statistics.
config_used dict[str, Any] Snapshot of the config for provenance.
timestamp float When the run finished.
metadata dict[str, Any] Free-form.
passed property bool True when summary.pass_rate == 1.0.
failed_cases property list[EvalCaseResult] Cases where passed is False, errors included.
passed_cases property list[EvalCaseResult] Cases that passed.
Method Description
get_case_result(eval_id) Fetch one case result, or None.
format_summary() Human-readable multi-line summary string.
to_file(path) / EvalReport.from_file(path) JSON round-trip.
EvalReport.create(eval_set_id, results, eval_set_name="", config_used=None) Build a report and compute its summary.

The report has no overall_score or pass_rate attribute of its own — the rate lives on report.summary.pass_rate. Full EvalSummary fields are documented in Evaluation harness.

EvalCaseResult

Attribute Type Description
eval_id str The case ID.
name str The case name.
passed bool True when every criterion passed.
criterion_results list[CriterionResult] One entry per criterion, in evaluation order.
actual_trajectory list[TrajectoryStep] Recorded trajectory.
actual_tool_calls list[ToolCall] Recorded tool calls.
actual_response str The agent’s final response.
messages list[dict[str, Any]] Flattened message history.
node_responses / node_visits list[...] Per-node snapshots and visit order.
duration_seconds float Time for this case.
error str | None Set when the run itself failed.
turn_results list[dict[str, Any]] Per-turn data for multi-turn cases.
token_usage / agent_token_usage TokenUsage Total (agent + judges) and agent-only.
node_details list[NodeDetail] Per-node LLM input/output and tokens.
Member Description
get_criterion_result(name) Fetch one CriterionResult by criterion name.
failed_criteria / passed_criteria Properties filtering criterion_results.
is_error Property; True when error is set.

criterion_results is a list, not a dict. Use get_criterion_result(name) for lookups by name.

CriterionResult

Attribute Type Description
criterion str The criterion’s name.
score float Score 0.0–1.0.
passed bool score >= threshold.
threshold float Threshold used.
details dict[str, Any] Criterion-specific detail.
error str | None Set when the criterion itself failed.
token_usage TokenUsage Judge tokens for this criterion.
reason property str | None Shortcut for details.get("reason").
is_error property bool True when error is set.

Reporters

Reporters run automatically after evaluate() unless disabled. To drive them yourself, use ReporterManager — see Evaluation harness. The individual reporters are also usable directly:

Python
from tenxgraph.qa.evaluation import (
    ConsoleReporter,
    HTMLReporter,
    JSONReporter,
    JUnitXMLReporter,
    print_report,
)

print_report(report, verbose=False, use_color=True)   # console shortcut
ConsoleReporter(verbose=True).report(report)          # console, full control

JSONReporter(indent=2).save(report, "eval-report.json")
HTMLReporter().save(report, "eval-report.html")
JUnitXMLReporter().save(report, "eval-report_junit.xml")

Every reporter implements BaseReporter.generate(report, output_dir=None). The file reporters additionally offer save(report, path) and a string form (JSONReporter.to_json, HTMLReporter.to_html, JUnitXMLReporter.to_xml).


DEFAULT_JUDGE_MODEL

Python
from tenxgraph.qa.evaluation.config.types import DEFAULT_JUDGE_MODEL
# "gemini-2.5-flash"

The default LLM for every judge-based criterion. Override per criterion with judge_model= on the relevant CriterionConfig factory method.


Full end-to-end example

Python
import asyncio

from tenxgraph.qa.evaluation import (
    AgentEvaluator,
    CriteriaConfig,
    CriterionConfig,
    EvalCase,
    EvalConfig,
    EvalSet,
    MatchType,
    ToolCall,
    create_eval_app,
)

from my_project.graph import build_graph   # returns an uncompiled StateGraph

async def main():
    # 1. Compile with a collector attached
    app, collector = create_eval_app(build_graph())

    # 2. Build the eval set
    eval_set = EvalSet(
        eval_set_id="capitals",
        name="Capital cities",
        eval_cases=[
            EvalCase.single_turn(
                eval_id="capitals-001",
                user_query="What is the capital of France?",
                expected_response="The capital of France is Paris.",
                expected_tools=[ToolCall(name="lookup_capital")],
            ),
            EvalCase.single_turn(
                eval_id="capitals-002",
                user_query="What is the capital of Germany?",
                expected_response="The capital of Germany is Berlin.",
                expected_tools=[ToolCall(name="lookup_capital")],
            ),
        ],
    )

    # 3. Configure criteria
    config = EvalConfig(
        criteria=CriteriaConfig(
            trajectory=CriterionConfig.trajectory(
                threshold=1.0,
                match_type=MatchType.IN_ORDER,
            ),
            response_match=CriterionConfig.response_match(threshold=0.8),
        ),
    )

    # 4. Run
    evaluator = AgentEvaluator(app, collector, config=config)
    report = await evaluator.evaluate(eval_set, verbose=True)

    # 5. Inspect
    print(report.format_summary())
    print(f"Pass rate: {report.summary.pass_rate:.0%}")
    for case in report.failed_cases:
        for cr in case.failed_criteria:
            print(f"{case.eval_id} · {cr.criterion}: {cr.score:.2f} < {cr.threshold}")

asyncio.run(main())

Common errors

Error Cause Fix
ValidationError: Extra inputs are not permitted on EvalConfig criteria was given a free-form dict. CriteriaConfig has fixed field names and forbids extras. Use CriteriaConfig(trajectory=..., response_match=...).
AttributeError on the object returned by make_trajectory_callback() It returns (collector, manager), not a manager. _, mgr = make_trajectory_callback(collector).
Trajectory and node-order criteria always score 0.0 The graph was compiled without the collector’s callback manager. Use create_eval_app(), or pass the manager to compile().
FileNotFoundError: Eval set file not found Path passed to evaluate() does not exist. Check the path relative to the working directory.
AttributeError: 'EvalReport' object has no attribute 'overall_score' No such attribute. Use report.summary.pass_rate and summary.criterion_stats.
AttributeError: 'EvalCaseResult' object has no attribute 'case_id' The field is eval_id. Use result.eval_id.
Criterion score is 0.5 with reasoning "No LLM provider available" The judge model could not be resolved to a configured provider. Install the provider extra and set its API key.
MatchType.EXACT failures when the run looks correct The actual trajectory has extra tool calls. Switch to MatchType.IN_ORDER.
report.summary.error_cases equals the case count The graph raises on every case. Run one evaluate_case() and read result.error.
Last updated for v0.9.2Edit this page on GitHubReport an issue