AgentEvalTool/backend/agenteval/evaluation/case_verdict.py
sinohqb 983a58d013 refactor(verdict): unify read path on authoritative case_outcomes
Read paths recomputed per-case pass/connectivity independently — report
generation, the logs endpoint, and the frontend each derived it, and the
frontend's every(passed) recompute ignored the engine's authoritative
verdict. Extract resolve_case_verdicts: a single pure seam that prefers
stored case_outcomes verbatim and approximates only for legacy runs. The
logs endpoint now surfaces case_verdicts so the frontend reads instead of
recomputing.
2026-07-31 14:11:58 +08:00

68 lines
2.5 KiB
Python

"""Case-verdict read seam — the single place the read path derives pass/connectivity.
The engine is the authority: it runs ``judgement.combine_case_outcome`` once and
writes each case's verdict into ``summary.case_outcomes``. Every read surface
(report generation, the run-logs endpoint) must present *that* verdict, never
recompute it — otherwise WEIGHTED/ANY logic and connectivity cases diverge from
what was judged.
This module is that single seam. It reads the authoritative ``case_outcomes``
when present, and only for older runs that predate it falls back to a documented
approximation from persisted turns/results. Pure — no I/O; callers build the
per-case ``CaseEvidence`` from whatever they already have in hand.
"""
from dataclasses import dataclass
from agenteval.models import CaseOutcomeSummary
@dataclass(frozen=True)
class CaseEvidence:
"""What the legacy approximation needs about one case's persisted record.
``result_passes`` is the per-rule pass flags (empty means no judged rule
result exists for the case — the connectivity-vs-fault fork).
"""
has_turns: bool
all_replied: bool
result_passes: tuple[bool, ...] = ()
def resolve_case_verdicts(
*,
case_outcomes: dict[str, CaseOutcomeSummary],
evidence: dict[str, CaseEvidence],
errored_case_ids: set[str],
) -> dict[str, CaseOutcomeSummary]:
"""Resolve every case in ``evidence`` to its authoritative-or-approximated verdict.
Authoritative ``case_outcomes`` win verbatim. For a case missing from it (an
older run), approximate per CONTEXT.md / ADR-0002: a case with no judged
results but turns that all replied and no case-level error is a *connectivity*
case (counts as passed); a case with results passes iff every rule passed;
anything else (a fault) fails.
"""
verdicts: dict[str, CaseOutcomeSummary] = {}
for case_id, ev in evidence.items():
authoritative = case_outcomes.get(case_id)
if authoritative is not None:
verdicts[case_id] = authoritative
continue
connectivity = (
not ev.result_passes
and ev.has_turns
and ev.all_replied
and case_id not in errored_case_ids
)
if connectivity:
passed = True
elif not ev.result_passes:
passed = False
else:
passed = all(ev.result_passes)
verdicts[case_id] = CaseOutcomeSummary(passed=passed, connectivity=connectivity)
return verdicts