Jaycee766's picture
Deploy FrontierLab v0.16 source 6162226f5abd
f0b4759 verified
Raw History Blame Contribute Delete
4.1 kB
"""Deterministic evidence gates; model responses are treated as untrusted inputs."""
from __future__ import annotations
from collections import defaultdict
from .claim_verification import normalize_excerpt
from .models import Audit, Claim, Paper
VALID_STANCES = {"supports", "contradicts", "mixed", "inconclusive"}
def validate_claims(papers: list[Paper], candidate_claims: list[dict], *, require_quote: bool = False) -> Audit:
known = {p.paper_id: p for p in papers}
audit = Audit()
for item in candidate_claims:
pid = str(item.get("paper_id", ""))
stance = str(item.get("stance", "")).lower()
key = str(item.get("claim_key", "")).strip().lower()
statement = str(item.get("statement", "")).strip()
reason = str(item.get("limitation", "")).strip()
if pid not in known or stance not in VALID_STANCES or not key or not statement:
audit.rejected_claims.append({"claim": item, "reason": "invalid / unknown citation or claim format"})
continue
raw_quote = item.get("evidence_quote", item.get("quote", ""))
quote = raw_quote.strip() if isinstance(raw_quote, str) else ""
# This only proves the quote exists in THIS SOURCE'S abstract.
# Never treat matching text as scientific support or independent entailment.
matches = bool(
quote and 24 <= len(quote) <= 500 and
normalize_excerpt(quote) in normalize_excerpt(known[pid].abstract)
)
if require_quote and not matches:
audit.rejected_claims.append({
"claim": item,
"reason": "missing / short / unmatched verbatim quote in cited source abstract"
})
continue
audit.verified_claims.append(Claim(
paper_id=pid, stance=stance, claim_key=key,
statement=statement, limitation=reason or "Not specified; manual review required",
evidence_quote=quote if matches else "",
passage_matched=matches
))
grouped = defaultdict(list)
for claim in audit.verified_claims:
grouped[claim.claim_key].append(claim)
for key, group in grouped.items():
supports = sorted({c.paper_id for c in group if c.stance == "supports"})
contradicts = sorted({c.paper_id for c in group if c.stance == "contradicts"})
if supports and contradicts:
audit.conflicts.append({"claim_key": key, "supports": supports, "contradicts": contradicts})
if any(p.synthetic for p in papers):
audit.warnings.append("SYNTHETIC FIXTURE: All example sources are invented for testing and are NOT literature citations.")
if require_quote:
audit.warnings.append(
"Passage grounding checked against cited abstracts; semantic entailment is NOT "
"verified. Researcher review of methods and full text is required."
)
if not audit.verified_claims:
audit.warnings.append("No verified, machine-readable evidence claims available. Do not infer a conclusion.")
if audit.rejected_claims:
audit.warnings.append(f"Rejected {len(audit.rejected_claims)} claims with invalid IDs, formats or missing/unaligned source passages.")
return audit
def enforce_citations(items: list[dict], papers: list[Paper], key: str = "evidence_ids") -> tuple[list[dict], list[dict]]:
known = {p.paper_id for p in papers}
accepted, rejected = [], []
for item in items:
ids = item.get(key, [])
if not isinstance(ids, list) or not ids or any(not isinstance(x, str) or x not in known for x in ids):
rejected.append({"item": item, "reason": "missing or unrecognized evidence IDs"})
else:
accepted.append(item)
return accepted, rejected
def risk_decision(audit: Audit, claims_needed: int = 2) -> str:
distinct_sources = {c.paper_id for c in audit.verified_claims}
if audit.conflicts:
return "REVIEW_CONFLICT"
if len(distinct_sources) < claims_needed:
return "INSUFFICIENT_EVIDENCE"
return "HUMAN_REVIEW_REQUIRED"