soev_evals.run
Execution, persistence and reporting of evaluation runs: run, archive, rescore and observe them.
A run freezes its inputs in a manifest, makes k attempts of each case with an arm, and evaluates the evidence of
each attempt with the criteria of the case. An archive keeps the manifest, the evidence, a journal of the attempts
and the report, so a run can resume after it stops, and a rescore can evaluate the same evidence again.
Runs
A runner makes the attempts of a manifest and returns the report.
Runner
dataclassRunner(arm, registry, ctx, archive, setup=None, tracker=None, observer=None)A runner that runs the attempts of a manifest, evaluates them and archives them.
Each attempt runs the case on a fresh conversation of the arm, and each variant of the case on another fresh
conversation. Then the criteria of the manifest evaluate the evidence. The archive saves the evidence first, and
then appends the attempt to the journal of the run. So a run that stops can resume: run with resume=True
runs only the attempts that the journal does not have.
Example
from pathlib import Path
from soev_evals.cli import load
from soev_evals.judges import load_prompts, prompt_shas
from soev_evals.run import FileRunArchive, Runner, build_manifest
loaded = load(Path("project.yaml"))
arm = ... # an Arm, for example a SoevThreadArm of soev_evals.arms
archive = FileRunArchive(Path("runs"))
manifest = build_manifest(
loaded.project, loaded.suites, run_id="baseline", revision="unknown", shas=prompt_shas(load_prompts())
)
report = await Runner(arm, loaded.registry, loaded.context, archive).run(manifest)paramarmArmThe system under test.
paramregistryCriterionRegistryThe criterion kinds that evaluate the criteria of the manifest.
paramctxCriterionContextWhat the criteria get: the judge, the language detector, the messages and the corpus lookup.
paramarchiveRunArchiveWhere the runner keeps the manifest, the evidence, the journal and the report of each run.
paramsetupCaseSetup | None= NoneThe fixtures of the project. If set, the runner prepares them before each conversation, collects
their artifacts after it and cleans them up. If None, conversations run without fixtures.
paramtrackerTracker | None= NoneWhere the runner also publishes the run, each attempt with its evidence, and the report. A failure
of the tracker is logged as a warning and does not stop the run. If None, nothing is published.
paramobserverEvaluationObserver | None= NoneWhat records the run and its attempts, for example as spans. If None, the runner uses an
OtelObserver of soev_evals.track when the [otel] extra is installed, and records nothing when it
is not.
methodrunasync (manifest, *, resume=False) -> ReportExecute unfinished attempts and persist the aggregate report.
The run makes manifest.k attempts of each active case of the manifest, at most
manifest.project.concurrency at the same time. An ArmError of the arm or a SetupError of the setup does
not stop the run. It becomes the failure of the evidence, and the criteria evaluate that evidence. Any
other exception of the arm or the setup stops the run (see ExceptionGroup).
parammanifestManifestThe manifest of the run, for example from build_manifest.
paramresumebool= FalseIf false, the run is new, and the archive creates it. If true, the run continues the archived
run manifest.run_id: the runner checks the archived manifest and journal, and then runs only the
attempts that the journal does not have.
Returns
ReportThe report over all attempts of the journal. The archive saves it too, and replaces an earlier report.
Raises:
ArchiveError: The archive cannot create or read the run before the attempts start. Withresume, also: the archived manifest differs frommanifest, or the journal has a duplicate or unexpected attempt.ExceptionGroup: An attempt raised an error, for example anArchiveErrorwhen the archive cannot save the attempt. The other attempts stop too. The attempts that the journal has stay archived.
build_manifest
functionbuild_manifest(project, suites, *, run_id, revision, shas, k=None, split=None, case_ids=None) -> ManifestFreeze selected cases, suite digests and all effective criteria.
The manifest holds everything that a run needs to repeat or resume: the project, the selected cases, a digest of each suite, and the criteria of each case. The criteria of a case are the project invariants, then the suite defaults, then the criteria of the case. The manifest names the credentials of the system by their environment variables only, so it holds no secret.
paramprojectProjectThe project of the run.
paramsuitesSequence[Suite]The suites to select cases from. The manifest has a digest of each of them.
paramrun_idstrThe id of the run in the archive.
paramrevisionstrThe revision of the code under test, for example a git commit.
paramshasMapping[str, str]A hash of each judge prompt and rubric, for example from prompt_shas of soev_evals.judges.
paramkint | None= NoneThe number of attempts of each case. If None, the project setting runs.k.
paramsplitstr | None= NoneIf set, only the cases with this split, "dev" or "test". If None, the cases of each split, and
the cases without a split.
paramcase_idsfrozenset[str] | None= NoneIf set, only the cases with these ids, for example the cases of an archived run.
Returns
ManifestThe manifest. Only active cases are selected.
Raises:
ProjectError: No case is selected, or two selected cases have the same id.
Manifest
modelManifestThe inputs of one run, frozen when the run starts.
It holds the reproducibility inputs without resolved environment values. build_manifest makes it. The archive
keeps it as manifest.json, and a resume must give an equal manifest.
attributerun_idstrThe id of the run in the archive.
attributegit_shastrThe revision of build_manifest: the revision of the code under test, or "unknown" when the
CLI cannot read it from git.
attributeprojectProjectThe project of the run.
attributesuite_digestsdict[str, str]A digest of the content of each suite, by suite name.
attributetest_digestsdict[str, str]= dict()A digest of the test cases and the defaults of each suite that has test cases, by suite name.
attributekintThe number of attempts of each case.
attributejudge_endpointstrThe judge endpoint of the project.
attributeprompt_shasdict[str, str]A hash of each judge prompt and rubric, by name.
attributecaseslist[Case]The selected cases.
attributecriteriadict[str, list[CriterionSpec]]The criteria of each case, by case id: the project invariants, then the suite defaults, then the criteria of the case.
Report
modelReportThe metrics of a run, over all its attempts.
The report holds run metrics with private per-case detail and an adjudication queue. The archive keeps it as
report.json. A set of metrics has the keys cases, attempts, pass_k (the share of all cases that have
k attempts and passed each of them, so a case with fewer attempts counts as not passed), pass_any (the
share of cases with a passed attempt), mean_score (the mean score of the attempts), flaky (the number of
cases that passed and failed) and undersampled (the number of cases with fewer than k attempts).
attributeoveralldict[str, JsonValue]The metrics over all requested cases, also the cases without attempts.
attributegroupsdict[str, dict[str, dict[str, JsonValue]]]The metrics of each group, by grouping and group name. The groupings are suite, category,
rule and criterion. The criterion grouping scores each criterion kind on its own verdicts.
attributecaseslist[dict[str, JsonValue]]One row for each case: its id, k, its attempts, its passed attempts, its mean score, and the
flags pass_k, pass_any, flaky and undersampled.
attributeadjudicationlist[dict[str, JsonValue]]= list()One row for each source id in the unseen of an attempt: the case id, the attempt number
and the source id. A person decides whether the source is correct.
attributerefusalsdict[str, JsonValue]= dict()The results of the refusal.code criteria: a confusion matrix of the expected and the detected
refusal code, the miss rate and the false refusal rate of each rule, and the number of excluded error
verdicts.
attributeconsistencydict[str, JsonValue]= dict()How consistent the attempts of a case are, over the cases with at least two attempts that
all record cited articles. mean_article_agreement is the mean share of attempts that cite the most
common set of articles. score_spread gives the min, max and mean over cases of the min, max and
population standard deviation of the scores of each case. It names no case.
attributesealedboolWhether a requested case is a test case. The CLI then prints the aggregates only, without
cases and adjudication.
Attempt
modelAttemptOne evaluated attempt of a case.
It is one durable completion in the run journal: a line of attempts.jsonl.
attributenumberintThe number of the attempt, from 1.
attributescoredScoredAttemptThe case and the verdicts of its criteria, in the order of criteria.
attributecriterialist[CriterionSpec]The criteria that the attempt was evaluated with.
attributejudgedlist[int]= list()The positions in criteria of the criteria that called the judge, and of the built-in judge
criteria. An offline rescore reuses the verdicts at these positions.
attributecited_articleslist[str] | None= NoneThe source ids that the evidence cites, each without its paragraph part, sorted. None in
a record without them.
attributeunseenlist[str]= list()The cited source ids that match no source role of the case, for each citations.unseen criterion.
The report lists them for adjudication.
methodas_json() -> dict[str, JsonValue]Return a JSON-compatible archive record.
Archives
An archive keeps runs for a resume, a report and a rescore.
FileRunArchive
classFileRunArchive(root)A RunArchive that keeps each run as files in a directory below one root directory.
It persists complete runs beneath one injected root directory. A run <run> has these files:
<run>/manifest.json: the manifest, written once.<run>/attempts.jsonl: the journal, one JSON object for each completed attempt.<run>/evidence/<case>/<n>.json: the evidence of attemptnof a case.<run>/report.json: the latest report.
The run id and the case id are percent-encoded, so each is one path component. Each JSON file is replaced as a whole: the archive writes a temporary file, flushes it to disk and renames it. The journal survives an interrupted write: a read ignores an incomplete last line, and the next append removes it.
Make an archive below root. The archive creates the directory with the first run.
paramrootPathThe directory that holds the runs.
methodcreate(run_id, manifest) -> NoneCreate a new run and write its manifest.
paramrun_idstrThe id of the new run.
parammanifestMapping[str, JsonValue]The manifest as JSON.
Raises:
ArchiveError: The run exists already, the id is empty,.or.., or the archive cannot write the manifest.
methodappend(run_id, attempt) -> NoneAppend one attempt to the journal.
The archive flushes the line to disk before it returns.
paramrun_idstrThe id of the run.
paramattemptMapping[str, JsonValue]The attempt as JSON.
Raises:
ArchiveError: The archive cannot write the journal.
methodattempts(run_id) -> list[dict[str, JsonValue]]Read the attempts of the journal.
paramrun_idstrThe id of the run.
Returns
list[dict[str, JsonValue]]The attempts of the journal as JSON, in the order of their completion. Empty when the run has no journal yet.
Raises:
ArchiveError: The archive cannot read the journal, or a line is not a JSON object.
methodsave_evidence(run_id, case_id, *, attempt, evidence) -> NoneWrite the evidence of one attempt.
paramrun_idstrThe id of the run.
paramcase_idstrThe id of the case.
paramattemptintThe number of the attempt, from 1.
paramevidenceEvidenceThe evidence of the attempt, with the evidence of its variants.
Raises:
ArchiveError:attemptis below 1, or the archive cannot write the file.
methodload_evidence(run_id, case_id, *, attempt) -> EvidenceRead evidence for one archived attempt.
paramrun_idstrThe id of the run.
paramcase_idstrThe id of the case.
paramattemptintThe number of the attempt, from 1.
Returns
EvidenceThe evidence of the attempt.
Raises:
ArchiveError:attemptis below 1, or the archive cannot read the file.
methodmanifest(run_id) -> dict[str, JsonValue]Read the manifest of a run.
paramrun_idstrThe id of the run.
Returns
dict[str, JsonValue]The manifest as JSON.
Raises:
ArchiveError: The archive cannot read the manifest, for example because the run does not exist.
methodsave_report(run_id, report) -> NoneReplace the derived report atomically.
paramrun_idstrThe id of the run.
paramreportMapping[str, JsonValue]The report as JSON.
Raises:
ArchiveError: The archive cannot write the report.
Rescores
A rescore evaluates the archived evidence of a run with the current criteria, without the arm.
rescore
functionrescoreasync (archive, run_id, *, cases, criteria, registry, ctx, offline=False, observer=None) -> ReportEvaluate the archived evidence of a run again with the current criteria, and replace its report.
A rescore does not run the arm. It evaluates the archived evidence of each attempt of the journal again, with the criteria that you give. Then the archive replaces the report of the run. The manifest, the journal and the evidence stay as they are.
paramarchiveRunArchiveThe archive that holds the run.
paramrun_idstrThe id of the archived run.
paramcasesSequence[Case]The current definitions of the cases. Each attempt of the journal needs its case here. The report includes each case, also a case without attempts. A case that was a test case in the archived run stays sealed in the report.
paramcriteriaMapping[str, Sequence[CriterionSpec]]The criteria of each case, by case id. Each case of the journal needs an entry.
paramregistryCriterionRegistryThe criterion kinds that evaluate the criteria.
paramctxCriterionContextWhat the criteria get: the judge, the language detector, the messages and the corpus lookup.
paramofflinebool= FalseIf true, the rescore calls no judge. A criterion takes the archived judge verdict of the same kind
and parameters, when the archived attempt has one. A built-in judge criterion without such a verdict
gets the status error.
paramobserverEvaluationObserver | None= NoneWhat records the rescore and its attempts. If None, the rescore uses an OtelObserver of
soev_evals.track when the [otel] extra is installed, and records nothing when it is not.
Returns
ReportThe new report, with the k of the archived manifest.
Raises:
ArchiveError: The archive cannot read the run or write the report, or a case of the journal is not incases.KeyError:criteriahas no entry for a case of the journal.
Observation
An observer records a run and each of its attempts, for example as OpenTelemetry spans.
EvaluationObserver
protocolEvaluationObserverAn observer of runs and rescores, for example to record traces.
Runner and rescore call it around the whole run, and around each attempt. Without an observer, they use an
OtelObserver of soev_evals.track when the [otel] extra is installed, and NoObservation when it is not.
methodrun(manifest, *, operation, resume) -> AbstractContextManager[None]Return the context manager that wraps a whole run or rescore.
parammanifestManifestThe manifest of the run.
paramoperationOperationWhether this is a run or a rescore.
paramresumeboolWhether the run continues an archived run. Always false for a rescore.
methodattempt(manifest, case, number) -> AbstractContextManager[AttemptObservation]Return the context manager that wraps one attempt, and gives its observation.
In a run, the block covers the conversation, the criteria and the archive writes of the attempt. In a rescore, it covers the evidence read and the criteria.
parammanifestManifestThe manifest of the run.
paramcaseCaseThe case of the attempt. In a rescore, a case that was a test case in the archived run has the
split test.
paramnumberintThe number of the attempt, from 1.
AttemptObservation
classAttemptObservationWhat an observer records about one attempt while the attempt runs.
EvaluationObserver.attempt gives one for each attempt. The methods of this class record nothing: subclass it
and override the methods that you need.
methodcriterion(spec, index, verdict, *, cached, judged) -> NoneRecord the verdict of one criterion of the attempt.
paramspecCriterionSpecThe criterion.
paramindexintThe position of the criterion in the criteria of the attempt, from 0. A manifest lists the project invariants first, then the suite defaults, then the criteria of the case.
paramverdictVerdictThe verdict of the criterion.
paramcachedboolWhether an offline rescore took the verdict from the archived attempt, without a judge call.
paramjudgedboolWhether the criterion called the judge in this evaluation.
methodcomplete(attempt, evidence) -> NoneRecord the result of the attempt, after all its criteria.
In a run, this happens after the archive saved the attempt. In a rescore, it happens after the attempt got its new verdicts.
paramattemptAttemptThe scored attempt.
paramevidenceEvidenceThe evidence of the attempt, with the evidence of its variants.
Operation
type aliasOperation = Literal['run', 'rescore']What an observed run does: "run" runs the cases, "rescore" evaluates the evidence of an archived run.
NoObservation
classNoObservationAn EvaluationObserver that records nothing.
Give it to Runner or rescore to turn off the default OpenTelemetry observer.
methodrun(manifest, *, operation, resume) -> AbstractContextManager[None]Return a context manager that does nothing.
parammanifestManifestparamoperationOperationparamresumeboolmethodattempt(manifest, case, number) -> AbstractContextManager[AttemptObservation]Return a context manager that gives an AttemptObservation that records nothing.
parammanifestManifestparamcaseCaseparamnumberint