File size: 3,829 Bytes
865bc90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""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)