Run configuration
A project configures its offline evaluation in two kinds of YAML file, both under
evaluation/:
evaluation/evaluation_manifest.yaml, one per project: every metric with the scopes it scores (the metric catalogue), the dataset of each scope, how datasets are synced, and the baselineseval-dataset promote-baselinewrites.evaluation/runs/<run>.yaml, one per run: the regression gate, each experiment. A run file names its scopes and whatever it changes, and inherits everything else from the project manifest.
evaluation/
evaluation_manifest.yaml the project manifest
evaluation_manifest.schema.json its JSON Schema, for the editor
runs/
regression.yaml one run file per run
run_file.schema.json their JSON Schema
datasets/
archive/ the snapshot of each released dataset, committed
incoming/ working copies, gitignored
Both kinds of file are read as YAML 1.2: only true and false (in any of true, True,
TRUE, false, False, FALSE) are booleans. An unquoted yes, no, on or off, in any
casing, loads as a string, and the loader logs a warning naming the file and the line. Write
true or false for a boolean, and quote a string that could be read as one, for example an
enumeration value such as "YES" or "NO".
The runner runs a RunManifest: a run file resolved
against the project manifest, with every scope's dataset and metrics spelled out.
See 01-getting-started.md for the quick-start walkthrough.
See 04-offline-evaluation.md for execute_offline_evaluation(),
PredictionRunner, and the local tuning workflow.
The project manifest: evaluation_manifest.yaml
EvaluationManifestFile (semantic_evaluation.contracts) is the shape of the
file. Every block is optional, and an unknown key is refused, so a misspelled block fails
when the file is loaded rather than being ignored.
| Field | Type | Description |
|---|---|---|
evaluators |
dict[str, CatalogueMetricConfig] |
The metric catalogue, keyed by metric name |
dataset_sync |
DatasetSyncConfig |
The dataset of each scope, and where push-all, pull-all and archive read and write |
baselines |
dict[str, dict[str, float]] |
Per run name, the aggregate scores eval-dataset promote-baseline promoted from one of its dataset runs |
# yaml-language-server: $schema=evaluation_manifest.schema.json
evaluators:
groundedness:
framework: azure_ai_evaluation
evaluator: GroundednessEvaluator
threshold: 4.0
scopes: [grounding, playbook_adherence]
tool_call_match:
framework: builtin_deterministic
evaluator: ToolCallMatch
threshold: 5.0
evaluator_params:
tool_name: search_documents
arg_name: query
scopes: [routing]
dataset_sync:
scope_to_dataset:
grounding: product_001_grounding_v1.0
playbook_adherence: product_001_playbook_v1.0
routing: product_001_routing_v1.0
scoped_dir: datasets/incoming
archive_dir: datasets/archive
baselines: {}
load_evaluation_manifest_file(path) returns the whole file;
load_evaluation_manifest(path) returns the part a run resolves against (the catalogue and
scope_to_dataset), as an EvaluationManifest.
CatalogueMetricConfig
CatalogueMetricConfig extends MetricConfig with one field:
| Field | Type | Description |
|---|---|---|
scopes |
list[str] |
Every scope that uses this metric |
The catalogue is keyed by metric name, not by scope, so a metric several scopes use
(a groundedness check shared by grounding and playbook_adherence, for example) is
declared once with both scopes listed.
DatasetSyncConfig
| Field | Type | Default | Description |
|---|---|---|---|
scope_to_dataset |
dict[scope, str] |
{} |
The Langfuse dataset of each scope. A run inherits it; push-all and pull-all need it |
scoped_dir |
str \| None |
None |
One working copy per scope, {scoped_dir}/{scope}_dev.json (Pattern B) |
merged_path |
str \| None |
None |
One working copy holding every scope (Pattern A), with canonical_name |
canonical_name |
str |
"" |
The dataset_name written into the merged working copy |
archive_dir |
str |
datasets/archive |
Where eval-dataset archive files the snapshot of each release |
create_on_missing |
bool |
false |
Let push-all create a Langfuse dataset that does not exist yet |
input_schema, expected_output_schema |
str \| None |
None |
module.path:ClassName of a Pydantic model, or a JSON Schema file |
Relative paths resolve against the manifest's own directory, not the directory a
command runs from. scoped_dir: datasets/incoming in evaluation/evaluation_manifest.yaml
is evaluation/datasets/incoming whether eval-dataset runs from the project root or from
evaluation/. 03-scope-sync.md covers the two working-copy patterns.
Run files: evaluation/runs/<run>.yaml
RunFile (semantic_evaluation.contracts) is the shape of a run file. Like the
project manifest, it refuses an unknown key.
| Field | Type | Required | Description |
|---|---|---|---|
name |
str |
yes | Run name, used in artifact filenames and Langfuse labels |
description |
str \| None |
no | Free-form description |
predictor_adapter |
str |
yes | The predictor class, as module.path:ClassName; see The predictor a run names |
scopes |
list[str] |
yes | The scopes this run evaluates |
datasets |
dict[scope, DatasetSourceConfig] |
no | Replaces a scope's dataset from scope_to_dataset, for example when two runs evaluate one scope on different datasets |
evaluators |
dict[scope, overrides] |
no | Narrows or adjusts a scope's metrics; see How a run file resolves |
gate |
GateConfig |
no (defaults to GateConfig()) |
Run-level quality gate |
max_error_fraction |
float (0 to 1) |
no (default 0) |
The largest share of a scope's predictions that may carry an error before the gate fails. Errored predictions are never scored; the runner applies this limit, not the scoring service; see Errored predictions |
run_label |
str \| None |
no | Default experiment label in the Langfuse execution name, {name}_{run_label}_{timestamp}; a pod's CLI overrides it per invocation with --run-label |
baseline |
bool |
no (default false) |
When true, sets run_type = "baseline" in Langfuse and adds is_baseline: true to run metadata; defaults run_label to "baseline" when none is set |
run_config |
dict[str, str \| int \| float \| bool] |
no (default {}) |
The configuration under test, written onto the dataset runs and onto every prediction trace as run_config.<key>; merged with the run_config argument of execute_offline_evaluation() |
repeats |
int (at least 1) |
no (default 1) |
How many times each item is predicted; each repeat is its own Langfuse dataset run |
aggregate_by |
list[str] |
no (default []) |
Item metadata attributes (keys of the dataset item's metadata) to aggregate every metric by, as well as over the whole run; see Aggregates per group of items |
agreement |
{field, run_shift} |
no | How the repeats agree on a dotted field of the prediction, for example verdict or decision.label; needs repeats of 2 or more. run_shift (per, min_share, default 0.25) also checks whether one repeat moved a whole unit's items; see Agreement across repeats |
trace_metrics |
list[str] |
no (default []) |
Measurements taken per prediction, from its trace or from the predictor's measurements: wall_time_s, input_tokens, output_tokens, cached_tokens, reasoning_tokens, cost_usd, tool_calls, tool_calls_by_name |
prediction_traces |
"required" \| "create_when_missing" |
no (default required) |
Whether a prediction without a trace_id stops the run, or the runner writes a thin trace for it on publication. For agents on platforms that cannot be traced; the predictor then supplies measurements |
score_publication |
ScorePublicationConfig |
no (defaults write every score) | Which scores are written to Langfuse: aggregates_on (every_repeat or last_repeat), spread_statistics (any of median, p90, stdev), informative_spread_only, trace_metric_item_scores |
max_error_fraction, run_config, repeats, aggregate_by, agreement, trace_metrics and score_publication are described in
04-offline-evaluation.md: Comparing configurations;
prediction_traces in
04-offline-evaluation.md: Predictions from a runtime without a Langfuse trace.
# evaluation/runs/regression.yaml
# yaml-language-server: $schema=run_file.schema.json
name: product_001_regression
predictor_adapter: evaluation.predictor:MyPredictor
scopes: [grounding, routing]
gate:
mode: all_metrics
minimum_item_count: 10
Langfuse run naming
run_label |
baseline |
Langfuse run name |
|---|---|---|
null |
false |
{name}_{timestamp} |
"experiment" |
false |
{name}_experiment_{timestamp} |
null |
true |
{name}_baseline_{timestamp} |
"v2" |
true |
{name}_v2_{timestamp} |
With repeats above 1, each repeat's dataset run appends _r1, _r2, … to the name
above, and the execution name without the suffix is in each run's metadata.
How a run file resolves
For each scope in scopes:
- Dataset: the run's
datasets.{scope}if given, used as it is; otherwisescope_to_dataset[scope]with the defaultfetch_items_page_size(50). A scope with neither raisesRunManifestResolutionError. - Metrics: every catalogue metric whose
scopeslist contains the scope, then the run'sevaluators.{scope}applied by the rules below. A scope that resolves to no metric raisesRunManifestResolutionError.
Under evaluators.{scope} |
Effect |
|---|---|
only: [name, ...] |
Starts the scope from exactly these catalogue metrics, dropping the other candidates. Naming a metric not tagged for this scope is an error. |
{name}: {field: value, ...}, name in the catalogue |
Merges the given fields onto the catalogue's definition and adds the metric to the scope, even if only: left it out. evaluator_params merges key by key, one level deep; every other field is replaced. The metric must be tagged with this scope in the catalogue. |
{name}: {...}, name not in the catalogue |
A complete standalone MetricConfig; every required field must be present. Always included. Use it to try an evaluator before adding it to the catalogue. |
Because a named override always resolves against the whole catalogue, read only: as
"start from just these metrics" rather than "never include anything else".
A run narrowing a scope, tightening one metric's threshold, and trying a metric the catalogue does not have yet:
# evaluation/runs/fast_grounding_check.yaml
# yaml-language-server: $schema=run_file.schema.json
name: product_001_fast_grounding_check
predictor_adapter: evaluation.predictor:MyPredictor
scopes: [grounding]
evaluators:
grounding:
only: [groundedness]
groundedness:
threshold: 4.5 # tighten just this run's threshold
citation_precision: # not in the catalogue yet
framework: builtin_deterministic
evaluator: CitationPrecision
threshold: 5.0
This resolves to two metrics for grounding: groundedness from the catalogue with its
threshold at 4.5, and citation_precision as a standalone definition.
RunManifest: the resolved run
RunManifest (semantic_evaluation.contracts) is a run fully resolved: the run
file's fields (including max_error_fraction, aggregate_by and agreement), with datasets naming every scope's dataset and evaluators.{scope}.metrics
spelling out every metric as a MetricConfig. It is what
execute_offline_evaluation() runs and what a run's artifacts record.
from pathlib import Path
from semantic_evaluation.orchestration.config import load_resolved_run_manifest
manifest, run_path = load_resolved_run_manifest(
run_manifest_path=Path("evaluation/runs/regression.yaml"),
evaluation_manifest_path=Path("evaluation/evaluation_manifest.yaml"),
)
resolve_run_manifest(run_source, pod) does the same for a run file already loaded as a
dict and a project manifest already loaded with load_evaluation_manifest(). To see what a
run will execute:
eval-dataset resolve-run \
--run-file evaluation/runs/regression.yaml \
--manifest evaluation/evaluation_manifest.yaml
It prints the RunManifest as YAML:
name: product_001_regression
predictor_adapter: evaluation.predictor:MyPredictor
scopes: [grounding, routing]
datasets:
grounding:
dataset_name: product_001_grounding_v1.0
fetch_items_page_size: 50
# ...
evaluators:
grounding:
metrics:
groundedness:
framework: azure_ai_evaluation
evaluator: GroundednessEvaluator
threshold: 4.0
# ...
The predictor a run names
predictor_adapter names the project's PredictionRunner
class as module.path:ClassName, importable from where the run is started, for example
evaluation.offline_eval.predictors:SupportWorkflowPredictor. A bare label such as
support_workflow is refused when the file is loaded.
execute_offline_evaluation() imports that class. Called without a predictor, it builds
one, passing the run's run_config and repeats as keyword arguments to a constructor
that takes them, and none to one that takes neither. It then calls the predictor's
describe_candidate(), if it has one, and merges the result over run_config; see
Configurations reach the agent through the predictor.
A project whose predictor needs other arguments (a service URL, a client) builds it from
resolve_predictor_class(manifest) and passes it; the runner then checks it is an instance
of the class the run names, and refuses a run given any other predictor.
semantic_evaluation.platforms:PlatformPredictor is a predictor the SDK ships for agents on
Open WebUI and Copilot Studio; see Platform predictors.
Editor validation
Both files start with a yaml-language-server line naming a JSON Schema in the same
directory, which the YAML extension in VS Code uses to check the file as you type:
# yaml-language-server: $schema=evaluation_manifest.schema.json
# yaml-language-server: $schema=run_file.schema.json
eval-dataset schemas writes both schemas from the installed SDK, so they always match the
version that will run the files: evaluation/evaluation_manifest.schema.json and
evaluation/runs/run_file.schema.json (--dir names another evaluation directory). Commit
them, and run it again after upgrading semantic-evaluation. The schemas this repository
commits under sdk/semantic_evaluation/schemas/ are the same files, plus
run_manifest.schema.json for the resolved form.
DatasetSourceConfig
Controls how dataset items are loaded from Langfuse for one scope.
| Field | Type | Default | Description |
|---|---|---|---|
dataset_name |
str |
required | Name of the Langfuse dataset to load items from |
fetch_items_page_size |
int |
50 |
Items per page when fetching from Langfuse |
input_schema |
str \| None |
None |
Dotted module.path:ClassName reference to a Pydantic model. Resolved by LangfuseDatasetProvider.load_items() to drive schema-aware unwrap of a single-key-wrapped input; see Normalizing single-key wrapped input |
MetricConfig
Declares one evaluator and its gating parameters: a catalogue entry (with scopes), or one metric of a resolved run's ScopeEvaluatorConfig.
| Field | Type | Default | Description |
|---|---|---|---|
framework |
str |
required | Source library: "builtin_deterministic", "azure_ai_evaluation", "deepeval" |
evaluator |
str |
required | Concrete evaluator name within the framework |
enabled |
bool |
True |
Set false to skip the metric without removing it from the catalogue |
threshold |
float \| None |
None |
Pass threshold on the normalized 1-to-5 scale |
direction |
"higher_is_better" \| "lower_is_better" |
"higher_is_better" |
Score comparison direction |
model |
str \| None |
None |
Optional evaluator-side model label override |
evaluator_params |
dict[str, Any] |
{} |
Framework-specific constructor arguments forwarded to the evaluator |
The metrics field on ScopeEvaluatorConfig is a dict[str, MetricConfig]
keyed by metric name. That name appears in MetricResult.metric_name and in
Langfuse score labels.
See 05-scoring.md for MetricResult, AggregateMetricResult,
and the scoring service's normalized scale.
GateConfig
Run-level gating policy applied after all scores are collected.
| Field | Type | Default | Description |
|---|---|---|---|
mode |
"all_metrics" |
"all_metrics" |
Only all_metrics is supported: gate passes if every enabled metric passes its threshold |
minimum_item_count |
int |
1 |
Gate fails if fewer items were evaluated than this value |
A metric passes the gate when its mean normalized score over all items, and all repeats,
reaches its threshold. A gate means something only on a released dataset
({name}_v{M.m}), because a dev dataset can change between two runs and a score change
then says nothing about the agent; 03-datasets-lifecycle.md
explains the two states. A run on a dev dataset checks that the pieces work, and is not a
regression gate. How to set a threshold for a deterministic check and for an LLM judge is
in the scoring service's
evaluator catalogue.