"""Post-generation output validator & abstention layer. Deterministic, pure-Python gate that runs AFTER extraction/generation and REJECTS any output that smuggles in non-evidence-grounded language: * forbidden hedging / synthesis phrases (``appears to``, ``likely``, ``overall``…), * fabricated metrics (percentages, confidence/authenticity/consistency scores), * unsupported severity escalation. A forbidden token only survives if it is present verbatim in the cited evidence (STEP: ABSOLUTE GROUNDING POLICY). The layer never rewrites text — it reports violations and the caller abstains (drops the record). No network, fully unit-testable. """ from __future__ import annotations import re from app.extraction.citation_validator import ( BANNED_SEVERITY_TERMS, normalize_for_match, ) # Hedging / synthesis / evaluation vocabulary that must never be introduced. FORBIDDEN_PHRASES: frozenset[str] = frozenset({ "appears to", "seems to", "likely", "suggests", "indicates", "approximately reliable", "overall", "authenticity", "consistency score", "confidence score", "reliability score", "assessment", "evaluation", "in summary", "in conclusion", "executive summary", "we recommend", "it is recommended", "concerning", "potentially hazardous", }) # Fabricated-metric patterns: percentages and explicit scoring statements. _PERCENT_RE = re.compile(r"\b\d{1,3}(?:\.\d+)?\s?%") _SCORE_RE = re.compile( r"\b(?:authenticity|reliability|consistency|confidence|quality)\s+" r"(?:score|rating|level|index)\b", re.IGNORECASE, ) def _present_in_evidence(term: str, evidence_norm: str) -> bool: return term in evidence_norm def find_violations(text: str, evidence: str = "") -> list[str]: """Return a list of violation descriptions for ``text``. A forbidden phrase / severity term is only a violation when it is NOT present verbatim in ``evidence``. Percentages and scoring statements are violations unless the identical token appears in evidence. """ if not text: return [] text_norm = normalize_for_match(text) evidence_norm = normalize_for_match(evidence or "") violations: list[str] = [] for phrase in FORBIDDEN_PHRASES: if phrase in text_norm and not _present_in_evidence(phrase, evidence_norm): violations.append(f"forbidden phrase {phrase!r}") for term in BANNED_SEVERITY_TERMS: if term in text_norm and not _present_in_evidence(term, evidence_norm): violations.append(f"unsupported severity term {term!r}") for m in _PERCENT_RE.findall(text): token = normalize_for_match(m) if token not in evidence_norm: violations.append(f"fabricated percentage {m.strip()!r}") for m in _SCORE_RE.findall(text): if normalize_for_match(m) not in evidence_norm: violations.append(f"fabricated metric {m.strip()!r}") return violations def is_clean(text: str, evidence: str = "") -> bool: """True when ``text`` introduces no forbidden/fabricated content.""" return not find_violations(text, evidence) def should_abstain( text: str, evidence: str = "", *, min_confidence: float = 1.0, confidence: float = 1.0, evidence_aligned: bool = True, source_attributed: bool = True, ) -> bool: """Abstention logic (STEP: ABSTENTION LOGIC). Returns True (drop the record / RETURN NOTHING) when any of: * confidence is below ``min_confidence``, * evidence alignment failed, * source attribution failed, * the text contains forbidden/fabricated content not grounded in evidence. """ if confidence < min_confidence: return True if not evidence_aligned: return True if not source_attributed: return True return not is_clean(text, evidence)