semantica.evals measures the quality of decision intelligence outputs. It takes the decisions, audit trails, and reasoning text your pipeline produces and scores them against expectations you define, returning a structured summary you can log, assert on in tests, or track across runs.
  • A registry of named evaluators, from exact string matching to ROUGE overlap and LLM-as-judge
  • decision_scores, a composite evaluator for Decision objects that checks outcome, confidence bounds, required fields, provenance, and (optionally) policy compliance
  • A evaluate() runner that applies several evaluators to a list of cases and aggregates pass / fail / error counts
  • Per-evaluator objectives that let you override an evaluator’s built-in verdict at the run level
The module is versioned separately from the package: semantica.evals.__version__ is "0.1.0". The public surface described here is stable, but expect additive changes (new evaluators, new objective options) before it reaches 1.0.

Public API

Built-in evaluators

Every evaluator is a plain function fn(actual, expected, config=None) -> EvalMetric registered under a stable name. list_evaluators() returns the current set:
An evaluator that cannot run (bad regex, unparseable datetime, no judge_fn) returns an EvalMetric with an "error" key in meta rather than raising. Evaluators that require numeric bounds (numeric_range, length_range) instead return a failing metric with a "reason" key when the bound is missing — they do not raise and do not set "error".

decision_scores

decision_scores accepts a Decision (from semantica.context.decision_models) or its dict form and runs a set of field-level and governance checks. The score is the fraction of checks that passed; passed is True only when all of them did. Passing causal_chain_exists in config raises NotImplementedError. That key is a reserved slot for a future release.

Running an evaluation

evaluate() takes a list of cases and a list of evaluator names. A case is either a (expected, actual) tuple or a dict:
If actual is missing, the runner calls the case’s target_fn (or the target_fn passed to evaluate()) to produce it. Per-case config is deep-merged over the top-level config, so a case can override one evaluator’s settings without discarding the rest.
Evaluators run independently per case. If one raises, that case’s status becomes "error" and the exception text is captured in the metric’s meta; the rest of the run continues.

Objectives

By default each evaluator decides its own pass / fail. An objective overrides that verdict at the run level, keyed by evaluator name under config:
Rules:
  • maximize with threshold: pass iff score >= threshold. maximize with no threshold is a no-op and the evaluator’s own verdict stands.
  • minimize with threshold: pass iff score <= threshold. minimize requires a threshold; omitting it raises ValueError.
  • expect (True / False): pass iff bool(score) equals it. Cannot be combined with direction or threshold, and must be a real boolean.
  • A metric that already carries an "error" in its meta is unaffected by any objective.
  • Invalid objective config is validated for every case before any evaluator runs, so a bad objective fails the whole run up front rather than partway through.

Reading the summary

EvalMetric, SampleStats, and RepeatedCaseResult are frozen dataclasses (score: float, passed: bool, meta: dict, …). CaseResult is a namedtuple and the *Summary classes are plain dataclasses, so all are straightforward to serialize for logging or regression tracking.

Repeated sampling

For nondeterministic targets (LLM-backed extraction, agent pipelines, sampling-based judges) a single verdict says little. evaluate_repeated reruns target_fn per case n times and aggregates per-evaluator statistics:
Per-evaluator SampleStats carries n, passes, errors, pass_rate (passes / n), mean_score, stddev, and the derived booleans any_passed (observed pass@n) and all_passed (observed pass^n). Verdicts pool all samples: stable_pass when every run passed every evaluator, stable_fail when none passed, flaky when the pool mixes passes and fails, and error when a run errored. Requirements and edge cases:
  • Each case can be a dict or a (expected, actual) tuple, matching evaluate(). A case that carries a non-null static actual has nothing to sample; using it with runs > 1 raises ValueError. An actual of None is treated as absent, so it falls back to the resolver exactly as in evaluate().
  • target_fn exceptions and evaluator failures become per-run error samples and mark the case error; they never crash the run.
  • The objective layer applies per run as in evaluate(), and the same objective gates the aggregate pass_rate through SampleStats.objective_passed (e.g. {direction: maximize, threshold: 0.8} requires an 80% pass rate across runs).
  • At least one evaluator is required, and evaluator names must be unique.
  • runs must be >= 1.

Notes

  • llm_as_judge needs config["judge_fn"], a callable judge_fn(actual, expected) -> bool you supply. No LLM backend is imported unless you pass one in.
  • decision_scores governance checks are opt-in: policy compliance is only evaluated when both policy_engine and policy_id are present.

See also