Scoring — overview
scoring_app/ is the shared scoring service for the workspace. It is a
single FastAPI application (scoring_service/) that scores
already-generated predictions: it runs evaluator metrics, aggregates the
results, evaluates a pass/fail gate, and exposes an evaluator-discovery
endpoint so manifest authors can find out what metrics exist before they
write a run manifest.
Definition: release-facing scoring Release-facing scoring is the scoring used to support keep, change, or promote decisions for workflow changes. In this repository, it means calling this shared service over HTTP, not running ad hoc local scoring inside a pod. See release-facing scoring.
The problem this service solves
Every pod that evaluates a workflow needs the same handful of things: a place to run DeepEval and Azure AI Evaluation metrics, a fixed-option Microsoft Decision-1 evaluator for free-text judgements, a way to run deterministic checks without paying for an LLM call, a way to define a pod-specific LLM-judge rubric, and a gate decision at the end. Building that once per pod means duplicated evaluator wiring, duplicated provider credentials scattered across pod codebases, and no shared place to add a new metric.
scoring_app/ centralizes all of it behind one HTTP boundary. Pods send
already-generated evaluation items and predictions; the service decides how
to score them and returns metrics, aggregates, and a gate result. Evaluator
backend credentials (Azure OpenAI or Foundry Decision endpoints and keys)
live only inside this service's
connection profiles — callers never
see or send them.
Where evaluator definitions live
The service is pod-agnostic: it never stores which metrics a pod uses, at
what threshold, or with what parameters. Every metric definition —
MetricConfig's (framework, evaluator) pair, threshold,
evaluator_params, and so on — travels inline inside the evaluators field
of each ScoreRequest, built pod-side from whatever run manifest or
catalogue that pod maintains (see
Run configuration: RunManifest
for how a pod builds one). This is true for every framework, not only
custom_rubric — a deepeval/GEval metric's criteria and threshold are
just as request-local as a custom_rubric/inline rubric.
What this service registers instead is the
evaluator catalogue: a fixed
set of generic (framework, evaluator) types — for example
builtin_deterministic/ToolCallMatch — each with a factory and a
supported_params allow-list. The catalogue says which evaluator types
exist and what parameters they accept. It says nothing about which pod uses
them, under what metric name, or at what threshold.
The one thing that is both server-side and shared across pods is a named
custom_rubric/{name} entry: a rubric committed to
custom_evaluators/accepted/ after review becomes a catalogue entry any pod
can reference by name instead of repeating the full rubric definition inline
on every request. See
03-custom-rubric-and-registration.md.
Current responsibilities
| Area | Owned here |
|---|---|
| HTTP scoring runtime | FastAPI app, request logging, health route, scoring-job submission and status via POST /v1/score-jobs/GET /v1/score-jobs/{job_id} |
| Evaluator catalogue and discovery | Catalogue-driven (framework, evaluator) dispatch, GET /v1/evaluators |
| Evaluator dispatch and execution | One evaluator instance per logical metric, built through framework-specific factories; built-in routing and deterministic checks, DeepEval, Azure AI Evaluation, custom-rubric, and Decision-1 judge metrics, run concurrently up to OFFLINE_SCORING_MAX_CONCURRENT_JUDGE_CALLS per job |
| Score normalization | Linear mapping of every framework-native score onto the 1-to-5 scale |
| Evaluator parameter validation | evaluator_params keys are checked against the catalogue's supported_params before construction |
| Aggregation and gating | Mean/pass-rate aggregation per (scope, metric) pair and gate evaluation on the normalized scale |
| Connection selection | Service-owned named scoring connection profiles loaded from a local JSON file, resolved via Azure Key Vault when configured |
| Pod-driven rubric registration | Reviewing and accepting named custom_rubric evaluators submitted by pods |
Current non-responsibilities
| Area | Owned elsewhere |
|---|---|
Shared request/response contracts (ScoreRequest, MetricConfig, MetricResult, ...) |
sdk/semantic_evaluation/ |
| The HTTP client that calls this service | sdk/semantic_evaluation/ (ScoringServiceClient) |
| Predictor execution — running the workflow under test | reference_pod_implementations/{agent-framework-pod,langchain-pod}/ (or any other pod) |
| Dataset loading | Pod-side runners through semantic_evaluation |
| Review-sync execution | review_sync_app/ |
| Langfuse dataset-run publication | The shared offline-evaluation runner in sdk/semantic_evaluation/ |
The service does not run the system under test. It only evaluates the items and predictions a caller sends to it.
Architecture
graph TD
subgraph Caller["Pod side caller"]
A["Submit ScoreRequest"]
P["Poll for job status"]
end
subgraph Service["scoring_app scoring_service"]
B["FastAPI app"]
J["Job store shared state mount"]
C["Connection profile registry"]
D["Evaluator catalogue"]
E["Evaluator executor concurrent judge calls"]
F["Aggregation and gate"]
end
subgraph Shared["sdk semantic_evaluation"]
G["Shared scoring contracts"]
end
A --> G
G --> B
B -->|"pending record"| J
B --> D
D --> E
C --> E
E --> F
F -->|"completed record"| J
P --> B
B -->|"reads record"| J
style Caller fill:#e9ebec,stroke:#273943,stroke-width:2px
style Service fill:#e5f4fd,stroke:#0098eb,stroke-width:2px
style Shared fill:#e7eeec,stroke:#125a40,stroke-width:2px
Submitting a ScoreRequest (items, predictions, per-scope metric
configuration, and gate configuration) creates a pending
scoring job and returns immediately. In
the background, the app looks up each requested (framework, evaluator) pair
in the catalogue, which builds one evaluator instance per metric — resolving
a connection profile first if the metric needs one — and runs the resulting
judge calls concurrently. Every evaluator returns results on the normalized
1-to-5 scale; those results are aggregated per (scope, metric) pair and fed
into the gate, which decides pass/fail for the whole run. The finished record
is written to the job store, which the caller polls for status and, once
complete, the result.
Module map
| Path | Purpose |
|---|---|
scoring_service/app.py |
FastAPI app: lifespan, request-logging middleware (logs api_version and user_agent, adds retirement headers), /health, and the v1 router mounted under /v1 and unversioned |
scoring_service/api/v1.py |
Every route of API v1 and the API-key dependency |
scoring_service/api/versioning.py |
SUPPORTED_MAJORS, api_version_for_path(), and RETIREMENT_SCHEDULE with the Deprecation/Sunset/Link headers it produces |
scoring_service/openapi_snapshot.py |
Writes openapi/<major>.json; the root test gate fails when the committed file is stale, and the PR pipeline diffs it for breaking changes |
scoring_service/main.py |
Local entrypoint (uv run python scoring_service/main.py) |
scoring_service/service_config.py |
Environment-backed settings, .env loading, profile-registry construction |
scoring_service/scoring/catalogue.py |
The evaluator catalogue — every supported (framework, evaluator) pair and its factory |
scoring_service/scoring/profiles.py |
Azure OpenAI and Foundry Decision profile models plus ScoringProfileRegistry (registration, backend assignment, JSON persistence) |
scoring_service/scoring/evaluator_requests.py |
Pod-driven rubric submission model and the pending-submission file registry |
scoring_service/scoring/evaluators/ |
One module per evaluator family: DeepEval, Azure AI Evaluation, custom rubric, Decision-1, routing exact-match, and the nine builtin_deterministic pattern matchers |
scoring_service/scoring/gating/ |
Aggregation (aggregate_metric_results) and gate evaluation (build_gate_result) |
scoring_service/scoring/executor/ |
prepare_score_request() (fast validation and evaluator construction) and run_prepared_score_request() (concurrent judge-call execution, aggregation, and gating) — score_request() composes both for any caller that wants the all-in-one behavior |
scoring_service/scoring/jobs.py |
Scoring-job persistence and background execution: creates and updates job records under OFFLINE_SCORING_STATE_DIR/jobs/, and runs the background task POST /v1/score-jobs dispatches |
scoring_service/custom_evaluators/accepted/ |
Committed accepted rubric JSON files, loaded at startup as custom_rubric/{name} catalogue entries |
scoring_service/tests/ |
Service test suite — see 05-testing.md |
Design rules
- Every
MetricConfiga pod uses travels inline in itsScoreRequest, built pod-side; the catalogue only fixes generic evaluator types, never a pod's specific metrics. See Where evaluator definitions live. - Provider credentials never travel inside a scoring request. They live only in registered connection profiles on the service side.
- The catalogue is the single source of truth for evaluator dispatch. Adding a metric that belongs to an already-supported framework needs one catalogue entry — no new adapter code.
- Every evaluator normalizes its output onto the same 1-to-5 scale before aggregation or gating ever sees a value. Thresholds, gate decisions, and aggregates always operate on that normalized scale, never the framework-native one. See normalized score.
- Rubric definitions committed to
custom_evaluators/accepted/become namedcustom_rubric/{name}catalogue entries automatically at startup — no code change is needed to add one. - Deterministic checks (
builtin,builtin_deterministic) never call an LLM and never require a connection profile, so routing-only evaluation always works with an empty profile registry.
Reading order
| File | What it covers |
|---|---|
| getting-started.md | Install, run locally, first scoring-job submission, first connection-profile registration |
| 00-scoring-overview.md ← you are here | What this service is, why it exists, boundary, architecture, module map |
| 01-evaluator-catalogue.md | The catalogue concept, every framework family, every catalogue entry, score normalization, aggregation and gating |
| 02-connection-profiles.md | The connection-profile model, registration, backend keys, resolution and failure behavior |
| 03-custom-rubric-and-registration.md | Inline vs. named custom rubrics, the pod-driven registration lifecycle |
| 03-decision-evaluator.md | Fixed-option Microsoft Decision-1 questions, score mappings, and result semantics |
| 04-api-reference.md | The full HTTP API surface |
| 05-testing.md | Test-suite layers and the dependency-upgrade workflow |
| glossary.md | Definitions for terms local to this service |