Evaluator catalogue
The evaluator catalogue is a
frozen tuple of CatalogueEntry objects in
scoring_service/scoring/catalogue.py. It fixes the generic evaluator
types this service supports — every (framework, evaluator) pair, its
dataset scope, its native score
scale, and the parameter names it accepts under evaluator_params — not any
pod-specific metric. A pod's actual metric definitions (metric name,
threshold, evaluator_params values, ...) never live in the catalogue; they
travel inline inside every ScoreRequest, built pod-side from whatever run
manifest that pod maintains. See
00-scoring-overview.md — Where evaluator definitions live
for the full picture.
A run manifest names a metric by its (framework, evaluator) pair; the
catalogue is what turns that pair into a running evaluator instance for one
logical metric. Naming a pair the catalogue does not know is a hard failure
that lists every supported pair.
The same catalogue backs the discovery endpoint, GET /v1/evaluators — see
04-api-reference.md. Manifest authors
and the SDK's list_evaluators() helper use it to find out what is available
without reading this service's source.
Each entry declares:
| Field | Purpose |
|---|---|
framework |
Which evaluator framework the evaluator belongs to |
evaluator |
The concrete evaluator class or function name within that framework |
scope |
Which dataset scope the evaluator targets (routing or end_to_end) |
raw_scale |
The framework-native scale (binary, 0-1, likert_1_5, or categorical) — see Score normalization |
supported_params |
Parameter names accepted under MetricConfig.evaluator_params for this evaluator; anything else is rejected before construction |
factory |
The callable that builds one evaluator instance for one logical metric |
Scoring pipeline
A submitted scoring job flows through four
stages before its result is available via GET /v1/score-jobs/{job_id}:
graph TD
A["ScoreRequest items, predictions, evaluators, gate config"]
B["build_evaluators per metric per scope"]
C["connection profile resolved for LLM-judge frameworks"]
D["evaluator instances one per logical metric"]
E["run each evaluator against every item concurrently"]
F["MetricResult raw score and normalized score per item"]
G["aggregate_metric_results per scope and metric pair"]
H["AggregateMetricResult mean, pass rate, passed flag"]
I["build_gate_result minimum item count and threshold checks"]
J["GateResult passed or failed with failure list"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> J
style A fill:#e5f4fd,stroke:#0098eb,stroke-width:2px
style J fill:#e7eeec,stroke:#125a40,stroke-width:2px
Stages B–C run synchronously inside POST /v1/score-jobs, before any job is
created. If any evaluator cannot be built — for example, because a backend
has no connection profile assigned — the submission fails with HTTP 400 and
no job exists to poll. Stages D–J run as the job's background task.
Framework families
| Framework | Backend key | What it is |
|---|---|---|
builtin |
none | Deterministic exact-match for the routing scope. No LLM call. |
builtin_deterministic |
none | Generic deterministic checks for the end_to_end scope — tool-call shape, order and output, prediction fields against a bound or an expected value, URL allow-lists, message-content patterns, router-mode comparison. No LLM call. |
deepeval |
deepeval_llm_judge |
Reflective adapter that instantiates any allow-listed DeepEval metric class by name |
azure_ai_evaluation |
azure_ai_evaluation_llm_judge |
Reflective adapter that instantiates Azure AI Evaluation SDK evaluator classes by name |
custom_rubric |
custom_rubric_llm_judge |
Pod-defined LLM-judge, in two forms: inline (rubric embedded in the manifest) and {name} (rubric committed to custom_evaluators/accepted/ and reviewed by the eval core team) — see 03-custom-rubric-and-registration.md |
decision_judge |
decision_llm_judge |
One fixed-option question about free text, answered by Microsoft Decision-1 without generative reasoning — see 03-decision-evaluator.md |
Adding a metric that belongs to an already-supported framework needs one
catalogue entry — the deepeval and azure_ai_evaluation wrappers
instantiate metric classes reflectively by name, so no new adapter code is
required.
deepeval, azure_ai_evaluation, custom_rubric, and decision_judge
entries all resolve a connection profile for
their backend key before building the evaluator — including statistical
azure_ai_evaluation evaluators such as F1ScoreEvaluator that do not
themselves use an LLM. The catalogue's factory resolves the backend
unconditionally for every entry in that framework, so
azure_ai_evaluation_llm_judge must have an assigned profile before any
azure_ai_evaluation metric — statistical or LLM-judge — can run. See
02-connection-profiles.md — Resolution behavior.
Choosing a deterministic check or a judge
An offline evaluation measures the agent, not the code around it. That decides which kind of metric a behaviour gets:
| What is being checked | Metric |
|---|---|
| A decision the agent made about a tool: called or not called, with which arguments, in which order, what it returned, which URLs and policies it used | builtin_deterministic. It is exact, costs no model call, and gives the same score every time |
| What the agent said, and the answer is one fixed option such as yes/no or a named outcome | decision_judge/inline, with a Decision profile. It agrees with human labels as often as a generative judge, gives the same verdict every run, and is about five times faster and a hundred times cheaper; it returns no reasoning text (comparison) |
| What the agent said, and assessing it needs a graded scale, open-ended reasoning or a written reason | A generative LLM judge (custom_rubric, deepeval, azure_ai_evaluation), with a connection profile |
| Something only deterministic code decides: parsing, arithmetic, formatting, a tool's own implementation | Neither. It is a unit test in the project's test suite, which runs in milliseconds and fails on the line that broke |
A deterministic evaluator scores 1 or 5, so its threshold is 5.0 and the gate passes only when every item passes. A judge's score varies a little from one run to the next on the same answer, so its threshold leaves margin below the score a good answer gets (4.0 on a rubric whose good answers score 5), and a change is read from the metric's aggregate over many items, not from one item's score.
The full catalogue
| Framework | Evaluator | Scope | Raw scale | Notes |
|---|---|---|---|---|
builtin |
ExactMatch |
routing |
binary |
Deterministic string match between prediction["handoff_to"] and expected_output["handoff_to"] |
No connection profile required. Works with an empty profile registry.
| Framework | Evaluator | Scope | Raw scale | Notes |
|---|---|---|---|---|
builtin_deterministic |
ToolCallMatch |
end_to_end |
binary |
tool_name, arg_name, expected_field, match_mode_field, empty_means_not_called |
builtin_deterministic |
ToolCallAbsent |
end_to_end |
binary |
tool_name, arg_filters |
builtin_deterministic |
ToolCallOrdering |
end_to_end |
binary |
before, after, match_args |
builtin_deterministic |
ToolCallOutputMatch |
end_to_end |
binary |
tool_name, expected_field, fields, success_field, non_empty_fields, prefix_fields |
builtin_deterministic |
ToolCallPayloadMatch |
end_to_end |
binary |
tool_names, expected_field, scalar_args, dict_args |
builtin_deterministic |
FieldWithinThreshold |
end_to_end |
binary |
field, and either max/min or expected_field with tolerance |
builtin_deterministic |
FieldEquals |
end_to_end |
binary |
field, and one of value, one_of, expected_field, expected_one_of_field |
builtin_deterministic |
StructuredFieldMatch |
end_to_end |
binary |
field, expected_field |
builtin_deterministic |
URLAllowList |
end_to_end |
binary |
mode, extra_allowed_field, static_allowed, include_citation_sources |
builtin_deterministic |
PatternPresent |
end_to_end |
binary |
source, kind, must_include_field, must_not_include_field, static_patterns, static_not_patterns, case_sensitive, skip_when_field_equals |
builtin_deterministic |
RouterModeMatch |
end_to_end |
binary |
check_mode, check_playbook_id, check_step_id, check_divergence |
builtin_deterministic |
PolicyIdAllowList |
end_to_end |
binary |
allowed_policy_ids, tool_name, arg_name |
No connection profile required. Works with an empty profile registry.
Evaluator detail — what each check does
No evaluator here names a tool: the manifest's evaluator_params name
the tools and the prediction fields. The tool-call evaluators read
prediction["tool_calls"], a list of {"name", "args", "output"}
entries. Where an evaluator does not apply to an item, it skips it and
leaves no result, as noted per evaluator.
| Evaluator | What it checks |
|---|---|
ToolCallMatch |
The set of values a named tool received for a named argument matches an expected set from the item, in either "exact" or "superset" mode. When empty_means_not_called is true and the expected set is empty, the check inverts to "the tool was not called at all." |
ToolCallAbsent |
A named tool was not invoked with a specific argument shape (arg_filters). |
ToolCallOrdering |
Every call to the after tool is preceded by a call to the before tool whose match_args arguments have the same values. Without match_args it checks order alone. Items that never call after are skipped. |
ToolCallOutputMatch |
The last call to tool_name returned what the item's expected_output[expected_field] object expects; a string output is parsed as JSON. fields are compared for equality (default: every key of the expected object); non_empty_fields must be present and non-empty; prefix_fields ({output_field: expected_key}) must start with the prefix the expected object holds. When success_field is set and the expected object sets it false, only fields are compared. Items without the expected object are skipped; a tool never called fails. |
ToolCallPayloadMatch |
The arguments of the last call to each tool in tool_names match the item's expected_output[expected_field] object, one result per tool called, named {metric}_{tool}. scalar_args must be equal; dict_args must contain the expected keys with the expected values. Without either list, keys holding an object are dict arguments and the rest are scalars. Items without the expected object are skipped; if no listed tool was called, the item fails. |
FieldWithinThreshold |
A number at the dotted path field of the prediction (drawing.max_invert_error_m, pipes.0.slope) is within the inclusive max/min bounds, or within tolerance of the number at expected_field in the item's expected output. Items without that expected value are skipped. A missing or non-numeric field fails, with the reason. Every result's reason carries the measured value and the bound. |
FieldEquals |
The value at the dotted path field equals value, is in one_of, equals the value at expected_field in the item's expected output, or is in the list at expected_one_of_field in the item's expected output. Meant for booleans and enumerations; true does not equal 1. Quote string values in a YAML file ("YES"), because unquoted yes, no, on and off load as strings and true, false as booleans; a one_of that mixes booleans and strings is refused. Items without that expected value are skipped; a value at expected_one_of_field that is not a non-empty list fails, with the reason; a missing field fails, with the reason. |
StructuredFieldMatch |
A top-level prediction field equals a top-level expected-output field (a missing one compares as null). |
URLAllowList |
Two modes: every URL in the reply must come from a configured allow-list (all_urls_allowed), or at least one expected URL must appear (expected_present). |
PatternPresent |
Required substrings/regex patterns appear and forbidden ones do not, in either message or closing_line. Supports per-item skip via skip_when_field_equals. |
RouterModeMatch |
The router-classified mode/playbook/step/divergence matches the item's expectations, with independent per-field toggles. |
PolicyIdAllowList |
Every value passed as arg_name to tool_name exists in a snapshot allow-list (allowed_policy_ids) of legitimate documents. Regression canary — should never fire if prompt rules hold. |
The table above plus each evaluator's supported_params (listed in the
entry table) is the complete reference for configuring any of these
evaluators.
Pathfinder's tool names are filled in when a manifest names no tool
The manifests of Pathfinder's proof of concept, the HR assistant these
evaluators were written for, predate the parameters that name its
tools, so the catalogue fills them in for a metric that names none, and any
parameter the manifest does set wins. Setting the marker parameter
(after, tool_name or tool_names) turns the preset off for that
metric entirely; for PolicyIdAllowList each of its two parameters is
filled on its own. The preset is _PATHFINDER_PRESETS in
scoring/catalogue.py, and it goes once those manifests carry these
parameters or the proof of concept is retired, which reproduce the old behaviour exactly:
| Evaluator | Parameters filled in |
|---|---|
ToolCallOrdering |
before: preview_ticket, after: create_ticket, match_args: [service_key, opened_for_email, subject_email, fields] |
ToolCallOutputMatch |
tool_name: create_ticket, expected_field: expected_ticket_outcome, fields: [ok], success_field: ok, non_empty_fields: [ticket_number], prefix_fields: {ticket_url: expected_ticket_url_prefix} |
ToolCallPayloadMatch |
tool_names: [preview_ticket, create_ticket], expected_field: expected_ticket_call, scalar_args: [service_key, opened_for_email, subject_email, opened_for_name, subject_name], dict_args: [fields] |
PolicyIdAllowList |
tool_name: read_policy, arg_name: policy_id |
Example manifest excerpt: a figure measured on the drawing after the run, against a fixed bound and against the item's value; the order of two tools; and whether a write happened:
{
"max_invert_error": {
"framework": "builtin_deterministic",
"evaluator": "FieldWithinThreshold",
"threshold": 5.0,
"evaluator_params": {"field": "drawing.max_invert_error_m", "max": 0.005}
},
"outlet_invert": {
"framework": "builtin_deterministic",
"evaluator": "FieldWithinThreshold",
"threshold": 5.0,
"evaluator_params": {
"field": "drawing.outlet_invert_m",
"expected_field": "outlet_invert_m",
"tolerance": 0.01
}
},
"plan_before_apply": {
"framework": "builtin_deterministic",
"evaluator": "ToolCallOrdering",
"threshold": 5.0,
"evaluator_params": {
"before": "plan_minimum_slope",
"after": "apply_elevation_plan",
"match_args": ["network"]
}
},
"wrote_drawing": {
"framework": "builtin_deterministic",
"evaluator": "FieldEquals",
"threshold": 5.0,
"evaluator_params": {"field": "outcome.wrote", "expected_field": "should_write"}
}
}
| Framework | Evaluator | Scope | Raw scale | Notes |
|---|---|---|---|---|
deepeval |
GEval |
end_to_end |
0-1 |
Generic evaluator; accepts name, criteria, evaluation_steps, evaluation_params, rubric, context_key, skip_when_field_equals |
deepeval |
AnswerRelevancyMetric |
end_to_end |
0-1 |
Common DeepEval params only |
deepeval |
FaithfulnessMetric |
end_to_end |
0-1 |
Common DeepEval params only |
deepeval |
ContextualRecallMetric |
end_to_end |
0-1 |
Common DeepEval params only |
deepeval |
HallucinationMetric |
end_to_end |
0-1 |
Common DeepEval params only |
deepeval |
BiasMetric |
end_to_end |
0-1 |
Common DeepEval params only |
Requires backend key deepeval_llm_judge to be assigned before any
deepeval metric can run. See
02-connection-profiles.md.
All DeepEval evaluators additionally accept the common parameters
threshold, include_reason, strict_mode, async_mode, verbose_mode,
input_key, actual_output_key, and expected_output_key (folded into
"Common DeepEval params only" above). skip_when_field_equals is
implemented generically in the DeepEval wrapper, but only GEval's
catalogue entry currently lists it as a supported parameter — passing it
to any other DeepEval metric is rejected as an unknown parameter.
| Framework | Evaluator | Scope | Raw scale | Notes |
|---|---|---|---|---|
azure_ai_evaluation |
GroundednessEvaluator |
end_to_end |
likert_1_5 |
LLM-judge via Azure AI Evaluation SDK |
azure_ai_evaluation |
RelevanceEvaluator |
end_to_end |
likert_1_5 |
LLM-judge |
azure_ai_evaluation |
CoherenceEvaluator |
end_to_end |
likert_1_5 |
LLM-judge |
azure_ai_evaluation |
F1ScoreEvaluator |
end_to_end |
0-1 |
Statistical — no LLM call, but still resolves a connection profile, see Framework families above |
Requires backend key azure_ai_evaluation_llm_judge to be assigned before
any azure_ai_evaluation metric can run — including the statistical
F1ScoreEvaluator. See 02-connection-profiles.md.
| Framework | Evaluator | Scope | Raw scale | Notes |
|---|---|---|---|---|
custom_rubric |
inline |
end_to_end |
likert_1_5 |
name, criteria, scale, input_fields, output_fields, expected_output_fields, model — see 03-custom-rubric-and-registration.md |
custom_rubric |
{name} |
end_to_end |
likert_1_5 |
One entry per file in custom_evaluators/accepted/, loaded at startup; only model may be overridden |
Requires backend key custom_rubric_llm_judge to be assigned. See
03-custom-rubric-and-registration.md
for the pod-driven registration lifecycle.
| Framework | Evaluator | Scope | Raw scale | Notes |
|---|---|---|---|---|
decision_judge |
inline |
end_to_end |
categorical |
One noul, choice, or score question. type, instructions, criteria, scores, abstain_option, input_fields, output_fields, expected_output_fields, min_confidence, and model are accepted. |
Requires backend key decision_llm_judge to be assigned to a
foundry_decision profile. See 03-decision-evaluator.md
for the complete question contract.
Evaluator parameters
Definition: evaluator parameters Evaluator parameters (
evaluator_params) are the per-metric keyword arguments a manifest author declares for one metric. They flow through the matching evaluator wrapper and, fordeepevalandazure_ai_evaluationmetrics, into the underlying framework's constructor or call signature. Only names listed in the catalogue entry'ssupported_paramsare accepted. See evaluator parameters.
Example manifest excerpt (GEval with custom evaluation steps):
{
"correctness": {
"framework": "deepeval",
"evaluator": "GEval",
"enabled": true,
"threshold": 3.8,
"evaluator_params": {
"name": "Correctness",
"evaluation_steps": [
"Compare the factual claims in the response against the expected output.",
"Check that no critical facts are missing.",
"Check that no facts are invented."
],
"evaluation_params": ["ACTUAL_OUTPUT", "EXPECTED_OUTPUT"]
}
}
}
DeepEval field-name routing
By default, the DeepEval wrapper reads input["query"],
prediction["response"], and expected_output["response"] to build the
LLMTestCase. Manifests that use different field names (for example,
email_body / intent / reply_text) override this with:
evaluator_params key |
Default | Purpose |
|---|---|---|
input_key |
"query" |
Key in item.input passed as LLMTestCase(input=...) |
actual_output_key |
"response" |
Key in prediction.prediction passed as LLMTestCase(actual_output=...) |
expected_output_key |
"response" |
Key in item.expected_output passed as LLMTestCase(expected_output=...) |
context_key |
(none) | Key in prediction.prediction extracted and passed as LLMTestCase(retrieval_context=[...]). Populated as a single-element list when the field is non-empty; None otherwise. Use with "RETRIEVAL_CONTEXT" in evaluation_params to expose the retrieved context to the GEval judge. |
These keys configure the wrapper only. They are never forwarded to the underlying DeepEval metric constructor.
azure_ai_evaluation input pool
The azure_ai_evaluation wrapper builds an input pool from multiple sources
and passes the evaluator only the subset of keys it declares in its own
__call__ signature:
| Key | Primary source | Fallback |
|---|---|---|
query |
item.input["query"] |
prediction.prediction["query"] |
response |
prediction.prediction["response"] |
(none) |
context |
item.input["context"] or item.expected_output["context"] |
prediction.prediction["context"] |
ground_truth |
item.expected_output["response"] |
(none) |
The fallback for query and context to prediction.prediction
accommodates pods whose datasets do not flatten these into item.input but
carry them in the prediction payload instead. Pods that do provide
item.input["query"] take precedence. response is always read from
prediction.prediction — pods must include it there under that key.
Score normalization
Every evaluator emits its result on a single 1-to-5 normalized score.
Field on MetricResult |
Meaning |
|---|---|
normalized_score |
Float in [1.0, 5.0]. The only scale used for thresholds, gate decisions, and aggregates. |
raw_score |
The raw score — the framework-native value, preserved for inspection. |
raw_scale |
Tag describing the raw scale ("binary", "0-1", "likert_1_5", or "categorical"). |
justification |
Optional reviewer-facing explanation. |
Mapping rules by raw scale:
| Raw scale | Normalization | Examples |
|---|---|---|
binary |
match to 5.0, mismatch to 1.0 |
builtin/ExactMatch, every builtin_deterministic evaluator |
0-1 |
normalized = 1 + clamp(raw, 0, 1) * 4 |
DeepEval metrics, F1ScoreEvaluator |
likert_1_5 |
identity (clamped to [1, 5]) |
Azure AI Evaluation LLM-judge evaluators, custom_rubric |
categorical |
score assigned to the most likely configured option; the option probabilities remain in details |
decision_judge/inline |
Manifest thresholds (MetricConfig.threshold) are always expressed on the
normalized scale, never the raw one.
An abstaining Decision result has normalized_score: null and passed: null.
It remains in the item count but not the scored count, so it cannot make an
aggregate look better or worse by inventing a numeric value.
Aggregation and gating
After every item is scored, the service aggregates results per
(scope, metric_name) pair into one AggregateMetricResult: item count,
scored count, mean/min/max/median/p90/standard deviation of
normalized_score, pass rate, and an aggregate passed flag (mean score
compared against the metric's threshold and direction). The aggregation is
semantic_evaluation.scoring.aggregation.aggregate_metric_results, which the
SDK's runner also calls per group of items for a run that sets aggregate_by;
the gate itself is scoring/gating/policies.py. The statistics come from
semantic_evaluation.scoring.stats.spread, which the SDK's runner also uses
for trace metrics.
A request may carry several predictions for one item, one per repeat of a run
that sets repeats. Each is scored on its own, every result is stamped with
its prediction's repeat, and the aggregates and the gate count all of them;
item_count_by_scope and the gate's minimum item count still count items.
The gate (build_gate_result) then combines two kinds of failure into one
GateResult:
- Minimum item count — any requested scope that loaded fewer items than
GateConfig.minimum_item_countfails the gate, independent of scores. - Aggregate threshold — any
AggregateMetricResultwhosepassedflag isFalsefails the gate.
GateConfig.mode is currently always "all_metrics": every enabled,
thresholded metric must pass for the run to pass. GateResult.passed is
True only when there are zero failures.