Skip to main content

Evaluation criteria

When to use this

Use this page when you need the class-level API: constructor arguments, the criterion name that appears in reports, and what the score means. It covers the criterion objects themselves.

Two related pages cover the layers above:

  • EvaluationAgentEvaluator, EvalSet, EvalCase, reports.
  • Criteria (concepts) — choosing thresholds and configuring criteria through CriterionConfig factory methods.

You only need the classes directly when you are writing a custom criterion, composing criteria by hand, or calling one outside AgentEvaluator.

Import paths

from agentflow.qa.evaluation import (
BaseCriterion,
SyncCriterion,
CompositeCriterion,
WeightedCriterion,
ExactMatchCriterion,
ContainsKeywordsCriterion,
ResponseMatchCriterion,
RougeMatchCriterion,
LLMJudgeCriterion,
RubricBasedCriterion,
FactualAccuracyCriterion,
HallucinationCriterion,
SafetyCriterion,
TrajectoryMatchCriterion,
NodeOrderMatchCriterion,
ToolNameMatchCriterion,
CriterionConfig,
MatchType,
Rubric,
)

The classes also live under agentflow.qa.evaluation.criteria, together with CRITERIA_REGISTRY (the name → class mapping AgentEvaluator uses):

from agentflow.qa.evaluation.criteria import CRITERIA_REGISTRY

Which criterion do I want?

If you want to check…UseCalls an LLM
The response is byte-for-byte the expected stringExactMatchCriterionno
Specific words appear in the responseContainsKeywordsCriterionno
Word overlap with the expected answerRougeMatchCriterionno
The answer means the same thing, however it is wordedResponseMatchCriterionyes
Overall answer quality against the expected answerLLMJudgeCriterionyes
Your own written grading rulesRubricBasedCriterionyes
Facts, numbers, dates and names are correctFactualAccuracyCriterionyes
Claims are grounded in tool results / provided contextHallucinationCriterionyes
The response is harmless and policy-safeSafetyCriterionyes
The right tools were called, in the right orderTrajectoryMatchCriterionno
The right tools were called, ignoring argumentsToolNameMatchCriterionno
The graph routed through the right nodesNodeOrderMatchCriterionno
Several criteria as one AND / OR gateCompositeCriteriondepends
Several criteria averaged by importanceWeightedCriteriondepends
A custom rule of your ownsubclass BaseCriterion / SyncCriterionyour choice

Common interface

Every criterion derives from BaseCriterion.

from agentflow.qa.evaluation import BaseCriterion, CriterionConfig

criterion = SomeCriterion(config=CriterionConfig(threshold=0.9))
result = await criterion.evaluate(execution_result, eval_case) # CriterionResult

BaseCriterion.__init__

ParameterTypeDefaultDescription
configCriterionConfig | NoneNoneThreshold, judge model, match type and friends. None builds a default CriterionConfig().

Members

MemberKindDescription
nameclass attr strCriterion key written into CriterionResult.criterion and EvalSummary.criterion_stats.
descriptionclass attr strHuman-readable description.
configattr CriterionConfigThe active configuration.
thresholdproperty floatShortcut for config.threshold.
is_enabledproperty boolShortcut for config.enabled.
evaluateasync (actual: ExecutionResult, expected: EvalCase) -> CriterionResultAbstract. Scores one case.
validate_config() -> list[str]Returns config error strings; empty list means valid.

evaluate() is async on every criterion, including the deterministic ones — SyncCriterion simply awaits its own synchronous implementation.

Configuration

All criteria are configured with the same CriterionConfig model. Its fields and factory methods are documented in Criteria (concepts). Each criterion only reads the fields relevant to it, listed per class below.


String and keyword criteria

ExactMatchCriterion

from agentflow.qa.evaluation import ExactMatchCriterion

name = "exact_match". Compares actual.actual_response.strip() against the last non-empty expected_final_response in the case. Score is 1.0 or 0.0; there is no partial credit.

ParameterTypeDefaultDescription
configCriterionConfig | NoneNoneOnly threshold and enabled are read.
criterion = ExactMatchCriterion()
result = await criterion.evaluate(execution, case)
print(result.score, result.details["match"])

CriteriaConfig has no exact_match field, so this criterion cannot be turned on through EvalConfig.criteria. Instantiate it directly, or look it up under the "exact_match" key in CRITERIA_REGISTRY.

ContainsKeywordsCriterion

from agentflow.qa.evaluation import ContainsKeywordsCriterion

name = "contains_keywords". Case-insensitive substring search over the final response. Score is len(found) / len(keywords).

ParameterTypeDefaultDescription
keywordslist[str] | NoneNoneKeywords to look for. Ignored when config.keywords is non-empty.
configCriterionConfig | NoneNoneconfig.keywords takes precedence over the keywords argument.

With no keywords from either source the criterion returns 1.0 and a {"note": "No keywords specified"} detail.

from agentflow.qa.evaluation import ContainsKeywordsCriterion, CriterionConfig

criterion = ContainsKeywordsCriterion(
config=CriterionConfig.contains_keywords(
keywords=["refund", "7 days"],
threshold=1.0,
),
)

RougeMatchCriterion

from agentflow.qa.evaluation import RougeMatchCriterion

name = "rouge_match". ROUGE-1 F1 over lowercased, punctuation-stripped unigram sets. No LLM call. details carries precision, recall and f1, plus the first 500 characters of each response.

ParameterTypeDefaultDescription
configCriterionConfig | NoneNoneOnly threshold and enabled are read.

When the case defines no expected response, the score is 1.0 if the agent also produced nothing, otherwise 0.5.


LLM-judged criteria

These five share a base class, TemplatedLLMCriterion, which calls the judge model config.num_samples times (default 3), averages the score field of each JSON reply, and records the combined token usage on CriterionResult.token_usage. Samples that raise are logged and dropped; if every sample fails the criterion returns a failure result with error="All LLM samples failed".

Shared constructor:

ParameterTypeDefaultDescription
configCriterionConfig | NoneNoneReads threshold (0.8), judge_model ("gemini-2.5-flash"), num_samples (3), api_style ("responses"), and rubrics for RubricBasedCriterion.

The judge provider is picked from judge_model; set GOOGLE_GENAI_USE_VERTEXAI=true to route Google models through Vertex AI. If no provider can be resolved, each sample degrades to score=0.5 with reasoning "No LLM provider available".

ResponseMatchCriterion

from agentflow.qa.evaluation import ResponseMatchCriterion

name = "response_match_score". Subclass of LLMJudgeCriterion with the same prompt — semantic equivalence between actual and expected response. This is the recommended default over RougeMatchCriterion.

from agentflow.qa.evaluation import CriterionConfig, ResponseMatchCriterion

criterion = ResponseMatchCriterion(
config=CriterionConfig.response_match(threshold=0.8, num_samples=3),
)
result = await criterion.evaluate(execution, case)
print(result.score, result.details["reason"])

LLMJudgeCriterion

from agentflow.qa.evaluation import LLMJudgeCriterion

name = "final_response_match_v2". Asks the judge whether the actual response is semantically equivalent to the expected one. Skips with score 1.0 when the case defines no expected final response, or when the agent produced no response.

details: samples, individual_scores, reasonings, judge_model, reason.

RubricBasedCriterion

from agentflow.qa.evaluation import RubricBasedCriterion, Rubric

name = "rubric_based_final_response_quality_v1". Renders every config.rubrics entry into the judge prompt as "- {rubric_id}: {content}". Returns 1.0 immediately when config.rubrics is empty.

criterion = RubricBasedCriterion(
config=CriterionConfig.rubric_based(
rubrics=[
Rubric(rubric_id="concise", content="Under 100 words.", weight=1.0),
Rubric(rubric_id="empathetic", content="Acknowledges the complaint.", weight=0.5),
],
threshold=0.8,
),
)

details: rubrics (the ids), scores, reasonings.

FactualAccuracyCriterion

from agentflow.qa.evaluation import FactualAccuracyCriterion

name = "factual_accuracy_v1". Reference material is the last expected final response, falling back to expected.metadata["reference_facts"].

details: is_accurate (final_score >= threshold), samples, reasonings, errors (de-duplicated across samples).

HallucinationCriterion

from agentflow.qa.evaluation import HallucinationCriterion

name = "hallucinations_v1". Builds the grounding context from every tool call result in the execution plus expected.metadata["context"], then asks whether the response is supported by it. 1.0 is fully grounded.

details: is_grounded, samples, reasonings, hallucinations.

SafetyCriterion

from agentflow.qa.evaluation import SafetyCriterion

name = "safety_v1". The judge returns one overall score plus per-category sub-scores for harmful_content, hate_speech, privacy, misinformation and manipulation. The criterion score is the average overall score across samples; the category values are averaged and reported as category_scores for triage.

details: is_safe, samples, reasonings, issues, category_scores.


Trajectory criteria

All three are SyncCriterion subclasses — deterministic, no API calls. Expected values come from the Invocation entries of the case, concatenated across turns.

TrajectoryMatchCriterion

from agentflow.qa.evaluation import TrajectoryMatchCriterion

name = "tool_trajectory_avg_score". Compares actual.tool_calls against every invocation.expected_tool_trajectory, using config.match_type and config.check_args.

Config fieldDefaultEffect
match_typeMatchType.EXACTEXACT — same tools in the same positions. IN_ORDER — expected tools in order, extras allowed. ANY_ORDER — expected tools anywhere.
check_argsFalseWhen True, tool arguments must also match. An expected ToolCall with empty args accepts any arguments.
threshold0.8Score is the fraction of expected tools matched.
from agentflow.qa.evaluation import CriterionConfig, MatchType, TrajectoryMatchCriterion

criterion = TrajectoryMatchCriterion(
config=CriterionConfig.trajectory(
threshold=1.0,
match_type=MatchType.IN_ORDER,
check_args=True,
),
)

With no expected tools, EXACT scores 1.0 only if the agent called nothing; IN_ORDER and ANY_ORDER score 1.0 unconditionally.

NodeOrderMatchCriterion

from agentflow.qa.evaluation import NodeOrderMatchCriterion

name = "node_order_score". Same three match modes, applied to actual.node_visits versus invocation.expected_node_order. Reads match_type and threshold; check_args is not used.

criterion = NodeOrderMatchCriterion(
config=CriterionConfig.node_order(threshold=1.0, match_type=MatchType.IN_ORDER),
)

ToolNameMatchCriterion

from agentflow.qa.evaluation import ToolNameMatchCriterion

name = "tool_name_match_score". Multiset comparison of tool names only, order and arguments ignored. Score is matched names over expected names. When nothing is expected, the score is 1.0 if the agent called no tools and 0.5 if it did.


Composing criteria

CompositeCriterion

from agentflow.qa.evaluation import CompositeCriterion

name = "composite_criterion". Runs every sub-criterion and combines the scores.

ParameterTypeDefaultDescription
criterialist[BaseCriterion]requiredSub-criteria to run, in order.
require_allboolTrueTrue (AND) takes the minimum score; False (OR) takes the maximum.
configCriterionConfig | NoneNoneSupplies the threshold applied to the combined score.
from agentflow.qa.evaluation import (
CompositeCriterion,
ContainsKeywordsCriterion,
CriterionConfig,
ToolNameMatchCriterion,
)

criterion = CompositeCriterion(
criteria=[
ToolNameMatchCriterion(),
ContainsKeywordsCriterion(keywords=["refund"]),
],
require_all=True,
config=CriterionConfig(threshold=1.0),
)

details contains sub_results (each sub-result serialised) and require_all.

WeightedCriterion

from agentflow.qa.evaluation import WeightedCriterion

name = "weighted_criterion". Weighted average of the sub-scores; sum(score * weight) / sum(weight). Weights need not sum to 1.

ParameterTypeDefaultDescription
criteria_weightslist[tuple[BaseCriterion, float]]required(criterion, weight) pairs.
configCriterionConfig | NoneNoneSupplies the threshold applied to the weighted score.
criterion = WeightedCriterion(
criteria_weights=[
(ToolNameMatchCriterion(), 0.3),
(ResponseMatchCriterion(), 0.7),
],
config=CriterionConfig(threshold=0.8),
)

An empty criteria_weights (or total weight 0) scores 0.0.


Writing your own criterion

SyncCriterion

Subclass SyncCriterion when your rule is pure computation. Implement evaluate_sync(); the inherited async evaluate() wraps it, so the criterion drops straight into AgentEvaluator.

from agentflow.qa.evaluation import SyncCriterion


class MaxLengthCriterion(SyncCriterion):
name = "max_length"
description = "Response must stay under 500 characters"

def evaluate_sync(self, actual, expected):
length = len(actual.actual_response)
return self._result(
1.0 if length <= 500 else 0.0,
{"length": length},
)

Subclass BaseCriterion instead when you need to await something inside the rule, and implement the async evaluate() directly.

Two protected helpers from BaseCriterion build results for you:

HelperReturns
self._result(score, details=None, token_usage=None)Passing/failing result judged against self.threshold.
self._failure(error)Error result with score=0.0, passed=False.

To use a custom criterion through EvalConfig, register it before building the evaluator:

from agentflow.qa.evaluation.criteria import CRITERIA_REGISTRY

CRITERIA_REGISTRY["max_length"] = MaxLengthCriterion

CriteriaConfig forbids unknown fields, so a registry-only criterion still has to be attached to the evaluator by hand or mapped onto an existing field name.


Common errors

Error / symptomCauseFix
TypeError: Can't instantiate abstract class ... evaluateSubclassed BaseCriterion without an async evaluate(), or SyncCriterion without evaluate_sync().Implement the abstract method.
RuntimeWarning: coroutine 'evaluate' was never awaitedevaluate() is async on every criterion, including the deterministic ones.await criterion.evaluate(...).
Score is exactly 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; set GOOGLE_GENAI_USE_VERTEXAI=true for Vertex-only projects.
CriterionResult.error == "All LLM samples failed"Every judge call raised.Check the agentflow.evaluation logger for the per-sample warnings.
contains_keywords always scores 1.0Neither keywords= nor config.keywords was set.Pass CriterionConfig.contains_keywords(keywords=[...]).
ValueError: Unknown criterion field: 'exact_match'CriteriaConfig has no field for it.Instantiate ExactMatchCriterion directly.
TrajectoryMatchCriterion fails on correct-looking runsMatchType.EXACT rejects extra tool calls.Switch to MatchType.IN_ORDER or ANY_ORDER.
check_args=True never matchesExpected ToolCall.args must equal the actual args exactly.Leave args={} to accept any arguments for that call.