Skip to content

Commit a5bc69e

Browse files
committed
fix(evals): report citation support as unmeasured rather than zero when verification is off
1 parent 5816ba1 commit a5bc69e

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

app/evals/graders/deterministic.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def grade_refusal(question: GoldenQuestion, response: dict) -> RefusalGrade:
6565
@dataclass
6666
class CitationGrade:
6767
validity: float
68-
support: float
68+
support: float | None
6969
citation_count: int
7070
invalid_count: int
7171
unsupported_count: int
@@ -99,7 +99,14 @@ def grade_citations(response: dict) -> CitationGrade:
9999
# where refusals are excluded from this metric.
100100
validity = 1.0 if verification.get("is_refusal") else 0.0
101101

102-
support = float(verification.get("citation_coverage", 0.0))
102+
# `citation_coverage` counts an unverified claim as unsupported, which is
103+
# correct at runtime but wrong as a *measurement* when verification was
104+
# switched off for the config: every claim comes back UNVERIFIED and the
105+
# metric reads 0.000, indistinguishable from "nothing was supported". None
106+
# means not measured, and the aggregate skips it.
107+
support = (
108+
float(verification.get("citation_coverage", 0.0)) if verification.get("verified") else None
109+
)
103110
unsupported = len(verification.get("unsupported_claims", []))
104111

105112
return CitationGrade(

tests/test_retrieval_logic.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,60 @@ def test_meter_breaks_cost_down_by_stage():
204204
by_stage = meter.cost_by_label()
205205
assert set(by_stage) == {"generation", "embed"}
206206
assert meter.total_cost_usd == pytest.approx(0.40 + 0.02)
207+
208+
209+
# --- unmeasured vs zero -----------------------------------------------------
210+
211+
212+
def test_citation_support_is_none_when_verification_was_disabled():
213+
"""Configs A-C run with verification off. `citation_coverage` counts every
214+
unverified claim as unsupported, so the metric came out 0.000 — visually
215+
identical to "none of the citations were supported", when the truth is that
216+
nothing was measured. A published table cannot carry that ambiguity.
217+
"""
218+
from app.evals.graders.deterministic import grade_citations
219+
220+
unverified = grade_citations(
221+
{
222+
"verification": {
223+
"verified": False,
224+
"citations": [{"label": "C1", "resolved": True}],
225+
"invalid_labels": [],
226+
"claims": [{"claim": "x", "verdict": "unverified"}],
227+
"citation_coverage": 0.0,
228+
}
229+
}
230+
)
231+
assert unverified.support is None
232+
assert unverified.validity == 1.0 # resolution still measured; it is free
233+
234+
verified = grade_citations(
235+
{
236+
"verification": {
237+
"verified": True,
238+
"citations": [{"label": "C1", "resolved": True}],
239+
"invalid_labels": [],
240+
"claims": [{"claim": "x", "verdict": "yes"}],
241+
"citation_coverage": 1.0,
242+
}
243+
}
244+
)
245+
assert verified.support == 1.0
246+
247+
248+
def test_a_genuinely_unsupported_answer_still_scores_zero():
249+
"""The None must mean 'not measured', never 'measured as bad'."""
250+
from app.evals.graders.deterministic import grade_citations
251+
252+
graded = grade_citations(
253+
{
254+
"verification": {
255+
"verified": True,
256+
"citations": [{"label": "C1", "resolved": True}],
257+
"invalid_labels": [],
258+
"claims": [{"claim": "x", "verdict": "no"}],
259+
"citation_coverage": 0.0,
260+
}
261+
}
262+
)
263+
assert graded.support == 0.0

0 commit comments

Comments
 (0)