Skip to content

Scoring

The scoring service evaluates predictions against golden dataset items and returns per-item metric results, aggregates, and a gate verdict. The SDK provides the request/response contracts and an HTTP client.


Normalized 1-to-5 scale

All scoring-service evaluators map their framework-native output onto a common 1-to-5 normalized scale before returning results:

Scale anchor Value
Minimum (worst) 1.0
Maximum (best) 5.0

threshold in MetricConfig is always expressed on this scale, regardless of the evaluator framework's native range. Example mappings from native to normalized:

raw_scale Native range Normalized result
"1-5" 1.0 – 5.0 Passed through unchanged
"0-1" 0.0 – 1.0 Linearly scaled: 0.0 → 1.0, 1.0 → 5.0
"binary" 0 or 1 0 → 1.0, 1 → 5.0

MetricResult.normalized_score is comparable across evaluator frameworks. MetricResult.raw_score and MetricResult.raw_scale carry the framework-native value and scale tag for debugging and traceability.


MetricConfig

Declares one evaluator within a ScopeEvaluatorConfig.metrics dict. See 04-run-config.md — MetricConfig for the full field reference.


MetricResult

Per-item metric result returned by the scoring service. One MetricResult is produced per (item_id, repeat, metric_name): once per item, or once per repeat of it when the run sets repeats.

A run's trace metrics are MetricResults too, computed by the runner rather than the scoring service. They are measurements, not scores: normalized_score, threshold and passed are None, and the value is raw_score in the unit raw_scale names (seconds, tokens, usd, count).

Field Type Description
item_id str Links the result back to its source EvaluationItem
scope str Scope the item belongs to
metric_name str The metric key from ScopeEvaluatorConfig.metrics
repeat int \| None Which repeat of the item the scored prediction was, from 1; the scoring service copies it from PredictionRecord.repeat
normalized_score float \| None Score on the 1-to-5 scale; None if scoring failed
justification str \| None Reviewer-facing explanation from the evaluator
raw_score float \| None Framework-native score before normalization
raw_scale str \| None Tag describing the native scale (e.g. "binary", "0-1", "1-5")
threshold float \| None Pass threshold (copied from MetricConfig, on 1-to-5 scale)
direction "higher_is_better" \| "lower_is_better" Inherited from MetricConfig
passed bool \| None True if normalized_score meets threshold; None if no threshold set
details dict[str, Any] Evaluator-specific supplementary data

AggregateMetricResult

Aggregated result across all items for one (scope, metric_name) pair, counting every repeat of every item. The scoring service computes these from the per-item MetricResult list with aggregate_metric_results, which lives in the SDK (semantic_evaluation.scoring.aggregation) and applies the gate on top. The runner computes the ones for trace metrics, with the same statistics, and sets raw_scale on them. It also computes the per-group aggregates a run file's aggregate_by asks for, with the same function, over the results of the items in one group; those carry group_by and group_value and never reach the gate.

Field Type Description
scope str Scope
metric_name str Metric key
threshold float \| None Pass threshold (from MetricConfig)
direction MetricDirection From MetricConfig
item_count int Results aggregated: items, times repeats
scored_count int Results that returned a non-None normalized_score
mean_score float \| None Mean of normalized_score across scored results
min_score float \| None Minimum normalized_score
max_score float \| None Maximum normalized_score
median_score float \| None Median normalized_score
p90_score float \| None 90th percentile, interpolated between the closest ranks, so within the observed values
stdev_score float \| None Sample standard deviation; None for fewer than two results
pass_rate float \| None Fraction of scored results where passed=True
passed bool True if mean_score meets threshold; used for the gate decision. Always True for a trace metric
raw_scale str \| None Set only on a trace metric's aggregate: its statistics are then in this unit, not on the normalized scale
details dict[str, Any] Supplementary data
group_by str \| None The item metadata attribute this aggregate is grouped by; None on a whole-run aggregate
group_value str \| None The attribute's value for the group; None on a whole-run aggregate

Agreement contracts

The runner builds these from a run file's agreement block with compute_agreement (semantic_evaluation.scoring.agreement); they are not part of the scoring service's request or response. See 04-offline-evaluation.md: Agreement across repeats for what a run file asks for and how the terms are defined.

Contract Holds
ItemAgreement One item: values (the field in each repeat, None where missing), modal_value (None when values tie for most frequent), modal_share and unanimous
AgreementSummary One scope, or one group of it (group_by, group_value): item_count, unanimous_count, unanimous_share, mean_modal_share
RunShift One repeat of one unit (per, group_value, repeat): item_count items with a value, differing_count of them off the strict majority of the other repeats, and shifted
AgreementResult The field, the repeats, and the items, summaries and run_shifts lists

GateResult

The gate verdict returned with every ScoreResponse. See also 04-run-config.md — GateConfig.

Field Type Description
passed bool Overall gate pass/fail
mode str Gate mode used (currently always "all_metrics")
minimum_item_count int Minimum item count required to pass
total_items int Total items across all scopes that were evaluated
failing_metrics list[GateFailure] Details of each failing metric
evaluated_at str ISO 8601 UTC timestamp

Each GateFailure carries scope, metric_name, actual (mean score), threshold, and reason.


ScoringServiceClient

HTTP client for the centralized scoring service. The client is stateless — create an instance per request or reuse across requests. Scoring is a scoring job: submitting a batch returns a job ID immediately, and the batch is scored concurrently in the background rather than inline in the request. This decouples the wait for a batch's judge calls from any single HTTP connection's lifetime — a reasoning-tier LLM judge scoring a large batch can legitimately take longer than any client should hold a connection open for.

from semantic_evaluation.scoring import ScoringServiceClient

client = ScoringServiceClient(
    base_url="http://localhost:8001",
    timeout_seconds=120.0,       # HTTP timeout for each individual submit/poll call
    poll_interval_seconds=3.0,   # how often score() polls job status
    max_wait_seconds=None,       # overall wait bound; None waits indefinitely
    api_key=None,                # X-API-Key header value; omit or None to send no header
    retry_attempts=6,            # tries per HTTP call, the first included
    retry_initial_backoff_seconds=2.0,  # delay before the first retry; doubles each time
)

Each HTTP call retries transient failures, waiting 2, 4, 8, 16 and 32 seconds between tries by default, so a DNS failure or a restarting service does not end a run. Reads (GET) retry on any transport error and on HTTP 502, 503 and 504. The job submission retries only when the request cannot have reached the service: a connection error, a connect timeout or HTTP 503. A read timeout on submission is raised at once, because the job may already exist. Each retry logs a warning.

base_url is the service host only. The client adds the API major (/v1) to every route, so a product never configures a version; see service API versions.

api_key is your pod's own caller key for the scoring service, which the client sends as X-API-Key on every request. The deployed service is in a warning phase for POST /v1/score-jobs and GET /v1/score-jobs/{job_id}: a call with a known key is identified in the service logs, and a call without one is still accepted. A key becomes required from an enforcement date announced in the scoring service's changelog, so pass yours now. The connection-profile, evaluator-backend and evaluator-request routes, which this client does not call, need a known key already.

score(request: ScoreRequest, *, on_progress=None) -> ScoreResponse

Submit a scoring request and poll until the job completes, returning the parsed result. This is the method most callers use — it is built from submit_score()/get_score_job() below, so it never blocks on one long HTTP call regardless of batch size.

from semantic_evaluation.contracts import ScoreRequest

response = await client.score(
    ScoreRequest(
        items=items,
        predictions=predictions,
        evaluators=manifest.evaluators,
        gate=manifest.gate,
        scopes=manifest.scopes,
    )
)

# per-item results
for r in response.metric_results:
    print(r.metric_name, r.normalized_score, r.passed)

# aggregate + gate
for agg in response.aggregate_results:
    print(agg.metric_name, agg.mean_score, agg.passed)

print("gate passed:", response.gate_result.passed)

Pass on_progress to observe the job while it runs — called once per poll with the current ScoreJobResult:

def log_progress(job):
    print(f"{job.status.value}: {job.completed_evaluations}/{job.total_evaluations}")

response = await client.score(request, on_progress=log_progress)

Raises RuntimeError if the job ends in failed status, or if any HTTP call returns an error status. Raises TimeoutError if max_wait_seconds is set and exceeded while the job is still pending/running.

ScoreRequest fields:

Field Type Description
items list[EvaluationItem] Items to score
predictions list[PredictionRecord] Corresponding predictions
evaluators dict[scope, ScopeEvaluatorConfig] Metric configuration per scope
gate GateConfig Gating policy
scopes list[str] \| None Subset of scopes to score; None means all

ScoreResponse fields:

Field Type Description
metric_results list[MetricResult] Per-item metric results
aggregate_results list[AggregateMetricResult] Per-scope, per-metric aggregates
gate_result GateResult Overall gate verdict
item_count_by_scope dict[str, int] Item count per scope

submit_score(request: ScoreRequest) -> ScoreJobSubmitted

Submit a scoring request as a background job and return immediately, without waiting for it to finish. Use this directly (with get_score_job()) for manual control over polling; most callers use score() instead.

submitted = await client.submit_score(request)
print(submitted.job_id, submitted.status.value)  # "<uuid>", "pending"

get_score_job(job_id: str) -> ScoreJobResult

Return one job's current status, and its result once it has completed.

job = await client.get_score_job(submitted.job_id)
if job.status.value == "completed":
    response = job.result

ScoreJobResult fields:

Field Type Description
job_id str The job's identifier
status ScoreJobStatus pending, running, completed, or failed
submitted_at str ISO 8601 UTC timestamp
started_at str \| None Set once the job leaves pending
finished_at str \| None Set once the job reaches completed/failed
completed_evaluations int \| None Judge calls finished so far
total_evaluations int \| None Total judge calls this job will run
result ScoreResponse \| None Set once status is completed
error str \| None Set once status is failed

list_evaluators() -> EvaluatorCatalogueResponse

Fetch the catalogue of supported (framework, evaluator) pairs from the service.

catalogue = await client.list_evaluators()
for entry in catalogue.entries:
    print(entry.framework, entry.evaluator, entry.supported_params)

EvaluatorCatalogueEntry fields:

Field Type Description
framework str Framework identifier
evaluator str Evaluator class name
scope str Default scope this evaluator targets
raw_scale str Native scoring scale (e.g. "1-5", "binary", "0-1")
supported_params list[str] Allowed keys in MetricConfig.evaluator_params

Passing an unknown evaluator_params key causes the scoring service to reject the request before evaluator construction.