feat: wave 5b evidence verification - vision fact-check of cited sheet text

This commit is contained in:
2026-08-10 11:30:28 -05:00
parent 82952df307
commit 3df359500c
4 changed files with 202 additions and 2 deletions
+95
View File
@@ -0,0 +1,95 @@
"""Wave 5b: vision fact-check of extracted evidence against cited sheet images."""
from backend import config
from backend.agents.base import AgentResult, AgentScope, AgentUsage, failure
from backend.llm import call_json
from backend.pipeline._serialize import dumps
from backend.pipeline._stage import collect_list, render
from backend.prompts import VERIFY_SYSTEM_PROMPT, VERIFY_USER_INSTRUCTION
_SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3}
_VERDICTS = ("confirmed", "corrected", "not_found")
def select_findings(findings, clusters, max_checks, severities):
"""Severity-gated selection plus any finding tied to a disputed cluster."""
disputed_keys = {c.get("key") for c in clusters if c.get("disputed_attributes")}
selected = [f for f in findings
if str(f.get("severity") or "").lower() in severities
or f.get("cluster_key") in disputed_keys]
selected.sort(key=lambda f: _SEVERITY_RANK.get(
str(f.get("severity") or "").lower(), 9))
return selected[:max_checks]
def _valid_verdict(item):
if not isinstance(item, dict):
return None
verdict = str(item.get("verdict") or "").lower()
if verdict not in _VERDICTS:
return None
return {"sheet": item.get("sheet") or "",
"source_text": item.get("source_text") or "",
"verdict": verdict,
"actual_text": item.get("actual_text"),
"notes": item.get("notes")}
def _status(verdicts):
if not verdicts:
return "unverified"
confirmed = sum(1 for v in verdicts if v["verdict"] == "confirmed")
if confirmed == len(verdicts):
return "confirmed"
if confirmed == 0:
return "refuted"
return "mixed"
class EvidenceVerifierAgent:
name = "verify"
def __init__(self, usage: AgentUsage) -> None:
self.usage = usage
def run(self, scope: AgentScope) -> AgentResult:
try:
finding = scope.payload["finding"]
instruction = render(VERIFY_USER_INSTRUCTION, {"finding": dumps(finding)})
parsed = call_json(
system_prompt=VERIFY_SYSTEM_PROMPT,
user_text=instruction,
images_b64=scope.payload.get("images_b64") or [],
max_tokens=config.VERIFY_MAX_TOKENS,
model=config.AGENT_VERIFY_MODEL,
reasoning_effort=config.AGENT_VERIFY_REASONING_EFFORT or None,
usage_tracker=self.usage,
usage_stage="agent.verify",
)
verdicts = collect_list(parsed, "verdicts", _valid_verdict)
return AgentResult(scope_id=scope.scope_id, artifacts=[{
"finding_index": scope.payload["finding_index"],
"status": _status(verdicts),
"verdicts": verdicts,
}])
except Exception as exc:
return failure(scope, exc)
def apply_verdicts(findings, verify_results):
"""Annotate findings with verification; return refuted ones to suppress."""
by_index = {}
for result in verify_results:
for artifact in result.artifacts:
by_index[artifact["finding_index"]] = artifact
suppressed = []
for index, finding in enumerate(findings):
artifact = by_index.get(index)
if not artifact:
continue
finding["verification"] = {"status": artifact["status"],
"verdicts": artifact["verdicts"]}
if artifact["status"] == "refuted":
finding["confidence"] = "low"
suppressed.append(finding)
return suppressed