auralynq-rag / tests /test_eval_trust.py
asdfasdfqrqwer's picture
sync: bring the Space up to the current GitHub tree
656439d
Raw
History Blame Contribute Delete
4.67 kB
"""Trust metrics β€” citation attribution, confidence calibration (ECE), and the
regression gate. Deterministic, no model/network."""
from __future__ import annotations
from auralynq.eval.calibration import answer_correct, calibration_scores
from auralynq.eval.citation_eval import citation_scores
from auralynq.eval.gate import eval_gate
# ── calibration ─────────────────────────────────────────────────────────────
def test_calibration_perfect_is_zero_ece():
# bin [0.9,1.0): confidence 0.9, 9/10 correct β†’ acc 0.9 == conf 0.9 β†’ ECE 0
pairs = [(0.9, True)] * 9 + [(0.9, False)]
s = calibration_scores(pairs)
assert s.accuracy == 0.9
assert s.ece == 0.0
def test_calibration_overconfident_has_max_ece():
# confidence 1.0 everywhere, but only half correct β†’ gap 0.5 in the top bin
pairs = [(1.0, True)] * 5 + [(1.0, False)] * 5
s = calibration_scores(pairs)
assert s.accuracy == 0.5
assert s.avg_confidence == 1.0
assert s.ece == 0.5
assert s.mce == 0.5
assert s.brier == 0.5 # mean((1-1)^2 x5, (1-0)^2 x5) = 0.5
def test_calibration_empty():
s = calibration_scores([])
assert s.n == 0 and s.ece == 0.0
def test_answer_correct():
assert answer_correct("The capital is Paris.", "Paris") is True
assert (
answer_correct("Ericsson filed FRAND patent licensing terms", "FRAND patent licensing")
is True
)
assert answer_correct("The weather is sunny", "Paris") is False
assert answer_correct("", "Paris") is False
# ── citation attribution ────────────────────────────────────────────────────
def test_citation_precision_penalizes_spurious():
good = {
"text": "Ericsson filed fair reasonable FRAND patent licensing terms with standards bodies",
"source": "ericsson.pdf",
}
spurious = {
"text": "The weather in Paris was sunny throughout the summer holidays",
"source": "weather.pdf",
}
answer = "Ericsson filed FRAND patent licensing terms."
both = citation_scores([{"answer": answer, "citations": [good, spurious]}])
only_good = citation_scores([{"answer": answer, "citations": [good]}])
assert only_good.citation_precision == 1.0
# the spurious citation drops precision (it doesn't back the answer)
assert both.citation_precision < only_good.citation_precision
assert both.avg_citations == 2.0
def test_attribution_and_unsupported_rate():
# one supported claim + one unsupported claim
cites = [
{"text": "Ericsson filed fair reasonable FRAND patent licensing terms", "source": "e.pdf"}
]
answer = "Ericsson filed FRAND patent licensing terms. The moon orbits earth every month."
s = citation_scores([{"answer": answer, "citations": cites}])
assert 0.0 < s.attribution_rate < 1.0 # first claim supported, second not
assert round(s.attribution_rate + s.unsupported_claim_rate, 4) == 1.0
def test_citation_empty():
s = citation_scores([])
assert s.n == 0 and s.citation_precision == 0.0
# ── gate ────────────────────────────────────────────────────────────────────
def _report(cit_prec, ece, faith=0.8, recall=0.8, attr=0.8, unsup=0.2):
return {
"agentic": {
"retrieval": {"recall_at_k": recall},
"ragas": {"faithfulness": faith},
"citation": {
"citation_precision": cit_prec,
"attribution_rate": attr,
"unsupported_claim_rate": unsup,
},
"calibration": {"ece": ece},
}
}
def test_gate_passes_when_healthy():
g = eval_gate(_report(cit_prec=0.9, ece=0.05))
assert g["passed"] is True and g["failures"] == []
def test_gate_fails_on_low_citation_precision_and_high_ece():
g = eval_gate(_report(cit_prec=0.3, ece=0.4))
assert g["passed"] is False
failed = {f["metric"] for f in g["failures"]}
assert "citation_precision" in failed
assert "ece" in failed
def test_gate_skips_absent_metrics():
# a report missing citation/calibration must not spuriously fail
g = eval_gate({"agentic": {"retrieval": {"recall_at_k": 0.9}, "ragas": {"faithfulness": 0.9}}})
assert g["passed"] is True
metrics = {c["metric"] for c in g["checks"]}
assert "citation_precision" not in metrics