File size: 2,446 Bytes
6733714
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import re
from typing import List, Dict, Any

class CitationValidator:
    """
    Performs post-synthesis validation of legal citations.
    Ensures that every Section/Rule cited by the LLM exists in the retrieved context.
    """

    @staticmethod
    def validate_citations(answer: str, retrieved_chunks: List[Dict[str, Any]]) -> str:
        """
        Scans the answer for citations and compares them against the chunks.
        Appends a verification report to the answer.
        """
        # Find all mentions of "Section X" or "Sec X" or "Rule X"
        citation_pattern = r"(Section|Sec|Rule)\s*(\d+[A-Z]?(\(\d+\))?)"
        found_in_answer = re.findall(citation_pattern, answer, re.IGNORECASE)
        
        # Unique normalized citations from answer
        citations_to_verify = set()
        for type_pref, num, _ in found_in_answer:
            citations_to_verify.add(f"{type_pref.capitalize()} {num}")

        # Extract all available provisions from retrieved chunks
        available_provisions = set()
        for chunk in retrieved_chunks:
            prov = chunk.get("provision", "")
            if prov:
                available_provisions.add(prov.strip())

            # Search the FULL chunk text for section/rule markers
            chunk_text = chunk.get("text", "")
            for text_match in re.finditer(citation_pattern, chunk_text, re.IGNORECASE):
                available_provisions.add(f"{text_match.group(1).capitalize()} {text_match.group(2)}")

        # Cross-reference
        verified = []
        hallucinated = []  
        
        for cit in citations_to_verify:
            # Loose matching: if "Section 17" is checked, "Section 17(5)" in available is a match
            matches = [avail for avail in available_provisions if cit.lower() in avail.lower() or avail.lower() in cit.lower()]
            if matches:
                verified.append(cit)
            else:
                hallucinated.append(cit)

        # Log internally — never append to user-facing output
        import logging
        _cv_logger = logging.getLogger("citation_validator")
        if hallucinated:
            _cv_logger.warning(
                f"Unverified citations: {hallucinated} | Verified: {verified}"
            )
        else:
            _cv_logger.debug(f"All citations verified: {verified}")

        # Return the answer unchanged — no report appended to user output
        return answer