Skip to content

evaluatorq

evaluatorq async

evaluatorq(
    name: str,
    params: EvaluatorParams | dict[str, Any] | None = None,
    *,
    data: DatasetIdInput
    | ExperimentInput
    | Sequence[Awaitable[DataPoint] | DataPointInput]
    | None = None,
    jobs: list[Job] | None = None,
    evaluators: list[Evaluator] | None = None,
    datapoint_parallelism: int | None = None,
    llm_parallelism: int | None = None,
    parallelism: int | None = None,
    print_results: bool = True,
    description: str | None = None,
    path: str | None = None,
    inference: bool = True,
    single_trace: bool = False,
) -> EvaluatorqResult

Run an evaluation with the given parameters.

Can be called with either a params dict/object or keyword arguments:

# Using keyword arguments (recommended):
await evaluatorq("name", data=[...], jobs=[...], datapoint_parallelism=5)

# Using a dict:
await evaluatorq("name", {"data": [...], "jobs": [...], "datapoint_parallelism": 5})

# Using EvaluatorParams:
await evaluatorq("name", EvaluatorParams(data=[...], jobs=[...]))

Parameters:

Name Type Description Default
name str

Name of the evaluation run

required
params EvaluatorParams | dict[str, Any] | None

Optional EvaluatorParams instance or dict with all parameters.

None
data DatasetIdInput | ExperimentInput | Sequence[Awaitable[DataPoint] | DataPointInput] | None

The data to evaluate. A DatasetIdInput to fetch from Orq platform, an ExperimentInput to replay an experiment's recorded responses (requires inference=False), or a list of DataPoint instances/awaitables.

None
jobs list[Job] | None

The jobs to run on the data.

None
evaluators list[Evaluator] | None

The evaluators to use. If not provided, only jobs will run.

None
datapoint_parallelism int | None

Task concurrency, applied at two levels: datapoints run at most datapoint_parallelism at a time, and within one datapoint a single shared budget of the same size bounds its jobs and then its evaluators (the budget is not split between them — a job releases its slot before its evaluators take theirs). Defaults to 10; set to 1 for sequential execution. Bounds tasks, not requests: see llm_parallelism.

None
parallelism int | None

Deprecated alias for datapoint_parallelism.

None
llm_parallelism int | None

Ceiling on in-flight LLM requests for the whole run, counted per request rather than per task, so it holds however the datapoint/job/evaluator/jury fan-out nests. Unbounded by default. This is the knob to set against a provider concurrency limit; datapoint_parallelism bounds tasks, and one task can issue many requests. Only requests routed through evaluatorq are counted — wrap a job's own provider calls in evaluatorq.common.llm_limit.llm_slot() to include them.

None
print_results bool

Whether to print results table to console. Defaults to True.

True
description str | None

Optional description for the evaluation run.

None
path str | None

Optional path (e.g. "MyProject/MyFolder") to place the experiment in a specific project and folder on the Orq platform.

None
inference bool

When True (default) jobs run to generate responses. When False, generation is skipped and evaluators score the pre-recorded response in each row's messages column; jobs is then optional and ignored.

True
single_trace bool

Group every row under one evaluatorq.run span so the whole evaluation is a single trace. Defaults to False, which leaves each row's orq.job as its own root — an N-row run is then N separate traces.

False

Returns:

Type Description
EvaluatorqResult

List of DataPointResult objects

Raises:

Type Description
ValidationError

If parameters fail validation.

ValueError

If neither params nor required kwargs are provided.

Example
from evaluatorq import DataPoint, EvaluationResult, evaluatorq, job

@job("uppercase")
async def uppercase_job(data: DataPoint, row: int):
    return data.inputs["text"].upper()

async def matches_expected(params):
    return EvaluationResult(value=1 if params["output"] == params["data"].expected_output else 0)

await evaluatorq(
    "uppercase-eval",
    data=[DataPoint(inputs={"text": "hi"}, expected_output="HI")],
    jobs=[uppercase_job],
    evaluators=[{"name": "matches-expected", "scorer": matches_expected}],
)

deployment async

deployment(
    key: str,
    inputs: dict[str, object] | None = None,
    context: dict[str, object] | None = None,
    metadata: dict[str, object] | None = None,
    thread: ThreadConfig | None = None,
    messages: list[MessageDict] | None = None,
) -> DeploymentResponse

Invoke an Orq deployment and return the response content.

Parameters:

Name Type Description Default
key str

The deployment key (name)

required
inputs dict[str, object] | None

Input variables for the deployment template

None
context dict[str, object] | None

Context attributes for routing

None
metadata dict[str, object] | None

Metadata to attach to the request

None
thread ThreadConfig | None

Thread configuration for conversation tracking. Must include 'id' key.

None
messages list[MessageDict] | None

Chat messages for conversational deployments

None

Returns:

Type Description
DeploymentResponse

DeploymentResponse with content and raw response

Example
# Simple invocation
response = await deployment("my-deployment")
print(response.content)

# With inputs
response = await deployment("summarizer", inputs={"text": "Long text..."})

# With messages for chat-style deployments
response = await deployment("chatbot", messages=[
    {"role": "user", "content": "Hello!"}
])

# With thread tracking
response = await deployment("assistant",
    inputs={"query": "What is AI?"},
    thread={"id": "conversation-123"}
)

llm_jury

llm_jury(
    *,
    name: str,
    criteria: str | None = None,
    prompt: str | None = None,
    system_prompt: str | None = None,
    judges: list[str] | None = None,
    model: str | None = None,
    repetitions: int = 1,
    assignment: Literal['all', 'cyclic'] = 'all',
    replacement_judges: list[str] | None = None,
    min_successful_judges: int = 1,
    verdict_kind: Literal['categorical', 'numeric'] = 'categorical',
    labels: list[str] | None = None,
    passing_labels: list[str] | None = None,
    aggregator: AggregatorSpec | None = None,
    threshold: float = 0.5,
    score_range: tuple[float, float] = (0.0, 1.0),
    tie_break: TieBreak | None = None,
    structured_output: bool = True,
    temperature: float | None = None,
    max_tokens: int = 8000,
    timeout_ms: int = 90000,
    extra_kwargs: dict[str, Any] | None = None,
    client: Any = None,
) -> Evaluator

Build a jury (or single-judge) LLM evaluator for evaluators=[...].

Verdict modes

The judge's verdict type and the passed (pass/fail) field are decided by verdict_kind together with labels. verdict_kind is not inferred from labels — it defaults to "categorical" and you pick the mode explicitly. There are three modes:

how you configure it judge returns passed is
boolean modeverdict_kind="categorical" (default), labels=None a JSON boolean true/false the boolean itself
labeled modeverdict_kind="categorical" with labels=[...] one of labels (a string) verdict in passing_labels (None if no passing_labels given)
numeric modeverdict_kind="numeric" a float in score_range score >= threshold

Notes

  • For a yes/no judge, use boolean mode (the default — just omit labels). passed is populated automatically; you do not need passing_labels.
  • labels and passing_labels are valid only for verdict_kind="categorical"; passing them with "numeric" raises ValueError.
  • In labeled mode, passing_labels must be a subset of labels. If you omit it, the verdict is still recorded but passed is None (no pass/fail, so no pass-rate to aggregate). passing_labels requires labels — it is rejected in boolean mode (which derives pass/fail from the verdict directly).
  • labels must be strings. Native True/False are not labels — use boolean mode for that.
  • aggregator picks the panel consensus rule and must match the verdict kind (a mismatch raises ValueError):

  • categorical: "mode" (default — most common; plurality ties go to tie_break) or "majority" (strict >50%, else inconclusive).

  • numeric: "mean_std" (default — mean verdict; std reported in stats on a conclusive verdict), "median", "min", or "max".
  • a custom Callable[[list[JuryVote]], bool | float | str | None] for either kind (return None for "no consensus" / inconclusive). The same numeric keyword also collapses a single judge's repetitions.
  • assignment picks how judges are allocated across datapoints:

  • "all" (default): every judge scores every datapoint — per-item consensus at K times single-judge cost.

  • "cyclic": round-robin (CyclicJudge, arXiv:2603.01865) — each datapoint is scored by exactly one judge, cycling through the panel so every judge covers an equal share. Panel-relative judge bias cancels in expectation over the run at single-judge cost; per-item verdicts are single-judge opinions, so use it for benchmark/run-level scores, not when each individual verdict must be trustworthy. Per-item stats and raw_agreement are None: one vote has no cross-judge agreement to report. The rotation runs over the deduplicated panel, so listing a judge twice to up-weight it only works under "all". repetitions still applies to the single assigned judge — N calls to one judge, never N judges. Inside evaluatorq() the assignment is keyed on the dataset row (datapoint i goes to judge i % len(panel)), deterministic at any parallelism and across evaluator reuse. A direct scorer call carries no row and rotates in arrival order on a cursor that lives on the evaluator — only the equal share balance is guaranteed there. Shuffle the dataset first if its order is meaningful. Requires min_successful_judges=1; a failed item degrades to inconclusive unless replacement_judges is set.

Examples

Boolean — "is the answer correct?" (verdict_kind="categorical", the default):

llm_jury(name="correct", criteria="Is the answer factually correct?")

Labeled categorical (verdict_kind="categorical"):

llm_jury(
    name="grade",
    criteria="Grade the answer.",
    labels=["correct", "partially_correct", "incorrect"],
    passing_labels=["correct", "partially_correct"],
)

Numeric (verdict_kind="numeric"):

llm_jury(
    name="helpfulness",
    criteria="Rate helpfulness from 0 to 1.",
    verdict_kind="numeric",
    score_range=(0.0, 1.0),
    threshold=0.7,
)

EvaluatorQ Python - An evaluation framework for LLM applications.

job

job(
    name: str,
) -> Callable[[Callable[[DataPoint, int], Awaitable[Output] | Output]], Job]
job(name: str, fn: Callable[[DataPoint, int], Awaitable[Output] | Output]) -> Job
job(
    name: str, fn: Callable[[DataPoint, int], Awaitable[Output] | Output] | None = None
) -> Job | Callable[[Callable[[DataPoint, int], Awaitable[Output] | Output]], Job]

Helper function/decorator to create a named job that ensures the job name is preserved even when errors occur during execution.

This wrapper:

  • Automatically formats the return value as {"name": ..., "output": ...}
  • Attaches the job name to errors for better error tracking
  • Can be used as a decorator (@job("name")) or function (job("name", fn))

Parameters:

Name Type Description Default
name str

The name of the job

required
fn Callable[[DataPoint, int], Awaitable[Output] | Output] | None

The job function that returns the output (optional when used as decorator)

None

Returns:

Type Description
Job | Callable[[Callable[[DataPoint, int], Awaitable[Output] | Output]], Job]

A Job function that always includes the job name

Example
# As a decorator:
@job("text-analyzer")
async def analyze_text(data: DataPoint, row: int):
    return {"length": len(data.inputs["text"])}

# As a function wrapper:
my_job = job("my-job", async_function)

# With lambda for simple cases:
uppercase_job = job("uppercase", lambda data, row: data.inputs["text"].upper())

DataPoint

Bases: BaseModel

A data point for evaluation.

Parameters:

Name Type Description Default
inputs

The inputs to pass to the job.

required
expected_output

The expected output of the data point. Used for evaluation and comparing the output of the job.

required
Example
from evaluatorq import DataPoint

DataPoint(inputs={"text": "Hello world"}, expected_output="HELLO WORLD")

Evaluator

Bases: TypedDict

A named scorer passed to evaluatorq's evaluators list.

Example
from evaluatorq import EvaluationResult

async def length_check_scorer(params):
    return EvaluationResult(value=1 if len(params["output"]) > 10 else 0)

evaluator = {"name": "length-check", "scorer": length_check_scorer}

EvaluationResult

Bases: BaseModel

The score a scorer function returns for one job output.

Example
from evaluatorq import EvaluationResult

async def length_check_scorer(params):
    output = params["output"]
    return EvaluationResult(value=1 if len(output) > 10 else 0)

EvaluationResultCell

Bases: BaseModel

string_contains_evaluator

string_contains_evaluator(
    *, case_insensitive: bool = True, name: str = 'string-contains'
) -> Evaluator

Creates an evaluator that checks if the output contains the expected output. Uses the data.expected_output from the dataset to compare against.

Parameters:

Name Type Description Default
case_insensitive bool

Whether the comparison should be case-insensitive

True
name str

Optional name for the evaluator

'string-contains'

Returns:

Type Description
Evaluator

An Evaluator that checks if output contains expected output

Example
# Basic usage
evaluator = string_contains_evaluator()

# With case-sensitive matching
strict_evaluator = string_contains_evaluator(case_insensitive=False)

# With custom name
my_evaluator = string_contains_evaluator(name="my-contains-check")

DatasetIdInput

Bases: BaseModel

Input for fetching a dataset from Orq platform.

invoke async

invoke(
    key: str,
    inputs: dict[str, object] | None = None,
    context: dict[str, object] | None = None,
    metadata: dict[str, object] | None = None,
    thread: ThreadConfig | None = None,
    messages: list[MessageDict] | None = None,
) -> str

Invoke an Orq deployment and return just the text content. This is a convenience wrapper around deployment() for simple use cases.

Parameters:

Name Type Description Default
key str

The deployment key (name)

required
inputs dict[str, object] | None

Input variables for the deployment template

None
context dict[str, object] | None

Context attributes for routing

None
metadata dict[str, object] | None

Metadata to attach to the request

None
thread ThreadConfig | None

Thread configuration for conversation tracking. Must include 'id' key.

None
messages list[MessageDict] | None

Chat messages for conversational deployments

None

Returns:

Type Description
str

The text content of the response

Example
# In a job
@job("my-job")
async def my_job(data, row):
    return await invoke("summarizer", inputs=data.inputs)

llm_jury_pairwise

llm_jury_pairwise(
    *,
    judges: list[str] | None = None,
    model: str | None = None,
    criteria: str | None = None,
    prompt: str | None = None,
    system_prompt: str | None = None,
    swap: bool = True,
    repetitions: int = 1,
    assignment: Literal['all', 'cyclic'] = 'all',
    replacement_judges: list[str] | None = None,
    min_successful_judges: int = 1,
    max_tokens: int = 8000,
    timeout_ms: int = 90000,
    temperature: float | None = None,
    structured_output: bool = True,
    extra_kwargs: dict[str, Any] | None = None,
    client: Any = None,
    max_concurrency: int | None = None,
) -> PairwiseComparator

Build a pairwise (A-vs-B) LLM jury that reuses the shared panel machinery.

Judges compare two responses and pick a winner ('A'/'B'/'tie'). With swap on (default) each judge is run in both orderings to correct for position bias (see ADR-24). Panel/orchestration params mirror llm_jury. prompt overrides the built-in Mustache-style template (which exposes the response_a.*/response_b.* namespace via _side_to_namespace); leave it None to use the default. Returns a PairwiseComparator; call compare per A/B pair, and roll many comparisons up with evaluatorq.pairwise.build_report.

max_concurrency caps TOTAL in-flight judge LLM calls across all concurrently running compare calls on the returned comparator (each pair fans out judges x orderings x repetitions). None (default) keeps the fan-out unbounded.

assignment="cyclic" (CyclicJudge, arXiv:2603.01865) gives each comparison exactly one judge, cycling through the panel so every judge covers an equal share of the run. Judge bias cancels in expectation over many comparisons at single-judge cost, and the assigned judge still runs both orderings when swap is on. Per-pair winners are single-judge opinions — roll them up with build_report and read run-level rates. The rotation runs over the deduplicated panel (duplicate judge entries add weight only under "all"), repetitions still applies to the single assigned judge, and the cursor lives on the comparator: a reused comparator continues where the previous run stopped, so exact balance holds per freshly built comparator, not per run. Shuffle your pairs first if their order is meaningful. Requires min_successful_judges=1.

Usage:

from evaluatorq import llm_jury_pairwise

comparator = llm_jury_pairwise(
    criteria="The answer is accurate, complete, and directly addresses the question.",
    judges=["anthropic/claude-sonnet-4-6", "openai/gpt-5.4-mini"],
)
comparison = await comparator.compare(
    question="What is the capital of France?",
    response_a="The capital of France is Paris.",
    response_b="The capital of France is Berlin.",
)
print(comparison.winner)

AgentResponse

Bases: BaseModel

Structured response from a target agent as an ordered list of output messages.

Each item in output is a TextOutputItem, ToolCallOutputItem, or ReasoningOutputItem, preserving the order in which they were produced. Item type discriminators align with the OpenResponses intermediate data format (output_text, function_call, reasoning).

Accessors

.text — all TextOutputItem contents concatenated, or "" if none .tool_calls — list of ToolCallOutputItem filtered from output in order

text property

text: str

Concatenate all text output items into a single string.

tool_calls property

tool_calls: list[FunctionCall]

Return the tool call items from .output in order.

from_openresponses classmethod

from_openresponses(response: Any) -> AgentResponse

Build a full AgentResponse from a Responses API response object.

Single parse path for the OpenResponses wire format. Populates output + usage + model + finish_reason + response_id.

usage is None when the response carries no usage block — callers must distinguish "no usage reported" from "zero tokens used" so cost reports stay honest. usage.calls is intentionally left at 0: this is a pure parse; per-call-site accounting (calls=1 / +1) is applied by callers to the returned object's usage.

BTFit

Bases: BaseModel

Result of a Bradley-Terry family fit.

reliability

reliability(judge: str) -> float

1 / sigma for a judge - the paper's unsupervised reliability signal.

BTSigmaAggregation

Bases: BaseModel

Reliability-weighted rollup of a pairwise run (BT-sigma, arXiv:2602.16610).

Fitted unsupervised on the run's own reconciled votes: hard BT-sigma jointly infers the A-vs-B skill gap and a discriminator per judge, then re-derives each comparison's winner as a reliability-weighted vote instead of uniform plurality. Down-weights noisy judges; needs no labels. The weight depends on the path: on the pooled two-item fit it is 1/sigma; on a repetition run (repetition_consistency non-empty and at quorum) it is instead max(shrunk_consistency, 0.05), since within-datapoint consistency is the better reliability signal there.

Two caveats of the two-item collapse, both handled here:

  • With only two items a judge's sigma is pinned by its own vote distribution, so a judge whose decisive votes are unanimous gets an arbitrarily small sigma that measures one-sidedness (e.g. position or verbosity bias), not reliability. Such judges are excluded from the weighting — they vote with a neutral weight, their sigma is dropped from judge_sigmas, and fit_warnings names them.
  • p_a_beats_b/skill_gap come from the pooled global fit, while winners come from the per-comparison weighted vote. These are two different estimators and need not agree; read p_a_beats_b as the run-level headline and winners as the per-row calls.

DataPointDict

Bases: _DataPointDictRequired

Dict representation of a DataPoint for type checking.

DataPointInput module-attribute

DataPointInput = DataPoint | DataPointDict

Type alias for DataPoint that accepts both model instances and dicts.

DataPointResult

Bases: BaseModel

DeploymentResponse dataclass

Response from a deployment invocation.

content instance-attribute

content: str

The text content of the response

raw instance-attribute

raw: object

The raw response from the API

usage class-attribute instance-attribute

usage: TokenUsage | None = None

Token usage extracted from the response, when available

EvaluationResultCellValue module-attribute

EvaluationResultCellValue = (
    str | int | float | dict[str, str | float | dict[str, str | float]]
)

EvaluatorParams

Bases: BaseModel

Parameters for running an evaluation.

Parameters:

Name Type Description Default
data

The data to evaluate. A DatasetIdInput to fetch from Orq platform, an ExperimentInput to replay an experiment's recorded responses (requires inference=False), or a list of DataPoint instances/awaitables.

required
jobs

The jobs to run on the data.

required
evaluators

The evaluators to use. If not provided, only jobs will run.

required
datapoint_parallelism

Number of datapoints to process in parallel. Defaults to 10; set to 1 for sequential execution. Accepts the former name parallelism, which is deprecated.

required
llm_parallelism

Ceiling on in-flight LLM requests for the whole run, counted per request rather than per task. Unbounded by default. Use this against a provider concurrency limit — one datapoint can issue many requests, so the datapoint count cannot be sized against one.

required
print_results

Whether to print results table to console. Defaults to True. Also accepts "print" as an alias.

required
description

Optional description for the evaluation run.

required
path

Optional path (e.g. "MyProject/MyFolder") to place the experiment in a specific project and folder on the Orq platform.

required
single_trace

Group every row of the run under one evaluatorq.run span, so the whole evaluation is a single trace. Off by default: each row's orq.job is its own root, i.e. one trace per row.

required

inference class-attribute instance-attribute

inference: bool = True

When False, skip generation and evaluate the pre-recorded response in each row's messages column instead of running jobs.

single_trace class-attribute instance-attribute

single_trace: bool = False

When True, open one evaluatorq.run span for the whole run so every row shares a trace. Default False keeps each row's orq.job as its own root.

EvaluatorScore

Bases: BaseModel

EvaluatorqResult module-attribute

EvaluatorqResult = list[DataPointResult]

Type alias for evaluation results

ExperimentInput

Bases: BaseModel

Input for sourcing pre-recorded responses from an Orq experiment.

Used with inference=False to re-run evaluators against the responses an earlier experiment already produced, without regenerating them.

experiment_id instance-attribute

experiment_id: str

The experiment ID to load responses from. Read it off the experiment URL in the Orq UI (/experiments/<experiment_id>). The API refers to experiments as "spreadsheets", so you will also see this ID in /v2/spreadsheets/<id> routes.

run_id class-attribute instance-attribute

run_id: str | None = None

A specific run ID (a "manifest" in the API). When omitted, the latest run is used. Every execution of an experiment creates a new run; open it from the experiment's run history to read its ID from the URL.

Job module-attribute

Job = Callable[[DataPoint, int], Awaitable[dict[str, Any]]]

Job function type - returns a dict with 'name' and 'output' keys

JobResult

Bases: BaseModel

JobReturn

Bases: TypedDict

Job return structure

JudgeStats

Bases: BaseModel

Per-judge behaviour rolled up across a set of comparisons.

JudgedComparison

Bases: BaseModel

One judge's preference on one item pair, in a fixed (a, b) frame.

p_a is the judge's preference probability that item_a beats item_b. Callers that only have categorical votes map them as A -> 1.0, B -> 0.0, tie -> 0.5. Position-bias symmetrisation (paper Eq. 4) is the caller's job: build p_a from both orderings, e.g. via the pairwise module's swap/reconcile machinery, before fitting.

MessageDict

Bases: TypedDict

Chat message structure compatible with Orq SDK.

Output module-attribute

Output = str | int | float | bool | dict[str, Any] | AgentResponse | None

Output type alias

PairwiseComparator

A configured pairwise LLM jury. Call compare on an A/B pair.

compare async

compare(*, question: str, response_a: Output, response_b: Output) -> PairwiseComparison

Run the panel over one A-vs-B comparison and return the reconciled verdict.

A side that carries a target-level error is never judged — comparing against a failed generation would score noise as a preference.

PairwiseComparison

Bases: BaseModel

The panel's result for a single A-vs-B comparison.

PairwiseReport

Bases: BaseModel

Cross-comparison rollup of a pairwise run.

PairwiseVote

Bases: BaseModel

One judge's reconciled verdict for a single A-vs-B comparison.

RepetitionObservation

Bases: BaseModel

One raw repetition pass of one judge in one ordering, canonicalized.

verdict is in the ORIGINAL A/B orientation regardless of ordering: a 'B' returned under the swapped ordering is recorded as 'A' here, so the observations are directly comparable across orderings. None is a pass that produced no usable verdict: a genuine abstention, an error, or an off-contract value. The per-vote repetition_failures count says how many of those Nones were errors or off-contract (as opposed to clean abstentions); those two lower reliability while an abstention does not (RES-1251). Nothing is silently collapsed.

Scorer module-attribute

Scorer = Callable[[ScorerParameter], Awaitable[EvaluationResult | dict[str, Any]]]

ScorerParameter

Bases: TypedDict

Parameters passed to a scorer function.

Parameters:

Name Type Description Default
data

The data point being evaluated.

required
output

The output produced by the job for the data point.

required
row

Zero-based dataset index of the data point. Present when the scorer runs inside evaluatorq(); absent on direct invocation. Lets evaluators key per-item decisions (e.g. cyclic judge assignment) on the dataset position rather than call-arrival order.

required

ThreadConfig

Bases: TypedDict

Thread configuration for conversation tracking.

bt_sigma_aggregation

bt_sigma_aggregation(comparisons: Sequence[PairwiseComparison]) -> BTSigmaAggregation

Fit hard BT-sigma over a run's reconciled votes and re-derive winners.

The two "items" are the run's A and B sides; every decisive reconciled vote is one comparison between them. Reconciliation has already symmetrised position bias (both orderings per judge), satisfying the model's commutativity requirement. Categorical votes make hard BT-sigma the natural variant, which is also the paper's most robust one under inconsistency.

Identical votes are collapsed into weighted records before the fit (a judge has at most three distinct judgements here: A, B, tie), so the fit cost stays flat no matter how many comparisons the run holds.

Judges whose decisive votes are unanimous are excluded from the reliability weighting: in the two-item collapse their sigma measures one-sidedness, not reliability (see BTSigmaAggregation). They vote with the median weight of the remaining judges (or uniformly when no judge remains), and fit_warnings names them.

build_report

build_report(
    comparisons: Sequence[PairwiseComparison], *, aggregation: str = 'plurality'
) -> PairwiseReport

Roll a set of pairwise comparisons up into headline and per-judge metrics.

aggregation='plurality' (default) keeps the existing uniform plurality consensus. aggregation='bt-sigma' additionally fits hard BT-sigma over the run and attaches the reliability-weighted rollup (report.bt_sigma) plus each judge's discriminator (JudgeStats.sigma); the headline plurality rates are unchanged so the two aggregations stay comparable.

exact_match_evaluator

exact_match_evaluator(
    *, case_insensitive: bool = False, name: str = 'exact-match'
) -> Evaluator

Creates an evaluator that checks if the output exactly matches the expected output. Uses the data.expected_output from the dataset to compare against.

Parameters:

Name Type Description Default
case_insensitive bool

Whether the comparison should be case-insensitive

False
name str

Optional name for the evaluator

'exact-match'

Returns:

Type Description
Evaluator

An Evaluator that checks if output exactly matches expected output

Example
# Basic usage (case-sensitive)
evaluator = exact_match_evaluator()

# With case-insensitive matching
loose_evaluator = exact_match_evaluator(case_insensitive=True)

fit_bt

fit_bt(
    comparisons: Sequence[JudgedComparison],
    *,
    judge_sigma: bool = True,
    hard: bool = False,
) -> BTFit

Fit the Bradley-Terry family by MLE on the given comparisons.

judge_sigma adds the per-judge discriminator (BT-sigma). hard binarizes preferences first (hard BT / hard BT-sigma) - the paper's most robust variant when judges are highly inconsistent.

Degradations (recorded in warnings, never raised):

  • a single judge with judge_sigma=True falls back to plain BT - a lone sigma is absorbed into the skill scale and carries no information (paper section 3.3);
  • a tiny ridge keeps disconnected graphs / perfect separation finite.

repetition_consistency

repetition_consistency(comparisons: Sequence[PairwiseComparison]) -> dict[str, float]

Per-judge within-datapoint repetition consistency, in [0, 1] (RES-1251).

The unit of analysis is one (judge, comparison, ordering) group: repeated passes of the SAME prompt. A group needs >= 2 decisive repetitions; its consistency is the mean pairwise agreement among them, then discounted by the share of that vote's passes that errored or came back off-contract (repetition_failures) so a flaky judge scores below a clean one; a genuine abstention is not counted as a failure and does not penalise. A comparison contributes at most ONE observation per judge (its groups averaged), so repetition count never multiplies a judge's evidence, and different datapoints are never compared to each other - which is exactly why this is interpretable as judge reliability where the global two-item fit was not.

Empty dict when no judge has any qualifying group (e.g. repetitions=1 or a legacy run without observations).

This is the SHRUNK reliability weight, not raw self-agreement: at the recommended R=2 a group's agreement is only 0.0 or 1.0, so a judge with one lucky group would otherwise carry the same weight as a judge with many. Each judge's mean is shrunk toward the panel mean by its evidence count (empirical-Bayes, _SHRINKAGE_PSEUDO_OBS pseudo-observations), so thin evidence is pulled to neutral and cannot dominate a run. A perfectly self-consistent judge therefore reads below 1.0 unless the whole panel is; repetition_consistency_raw publishes the un-shrunk mean beside it (RES-1251).

repetition_consistency_raw

repetition_consistency_raw(
    comparisons: Sequence[PairwiseComparison],
) -> dict[str, float]

Per-judge RAW within-datapoint self-agreement mean (RES-1251).

1.0 = a judge that always agreed with itself on repeated passes of the same prompt. This is PURE agreement, not failure-adjusted and not shrunk: a judge that agreed with itself on every completed pass reads 1.0 even if some passes failed (the failure discount and the shrinkage both live in the repetition_consistency weight, not here). Published beside that weight so the raw signal stays visible. Empty dict when no judge has a qualifying group.

run_pairwise async

run_pairwise(
    *,
    judge_fn: PairwiseJudgeFn,
    panel: Sequence[str],
    response_a: Any,
    response_b: Any,
    swap: bool = True,
    repetitions: int = 1,
    replacement_judges: Sequence[str] | None = None,
    min_successful_judges: int = 1,
    propagate_errors: bool = False,
    max_concurrency: int | Semaphore | None = None,
) -> PairwiseComparison

Run a panel over one A-vs-B comparison and reconcile it into a verdict.

Each judge runs through the shared run_jury. When swap is on (default) every judge is also run with A and B exchanged; the two verdicts are un-swapped and passed through reconcile_pair, so a judge that just follows slot order abstains and is recorded as a flip.

Both orderings run concurrently. Replacement judges are promoted at the pair level — a stand-in is run in both orderings so it can cast a real reconciled vote, and a primary that fails in either ordering is what it stands in for. The winner is the plurality of the reconciled votes, or 'inconclusive' when fewer than min_successful_judges cast a decisive reconciled vote.

max_concurrency caps how many judge calls run at once across the whole comparison (judges x orderings x repetitions, replacements included). Pass an existing asyncio.Semaphore to share one budget across several comparisons. None (default) keeps the fan-out unbounded.

Tracing: the whole comparison is ONE orq.pairwise_jury span (RES-985). Each ordering drives _run_jury_core rather than run_jury so it doesn't mint a second jury span whose aggregates describe half a comparison; every judge span hangs off this one, tagged judge.label_swapped.