File size: 12,821 Bytes
6d11371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import logging
import re
import ast
from utils.models import get_llm_model

logger = logging.getLogger("misinformation_detector")

def extract_most_relevant_evidence(evidence_results):
    """
    Intelligently extract the most relevant piece of evidence
    
    Args:
        evidence_results (list): List of evidence items
    
    Returns:
        str: Most relevant evidence piece
    """
    if not evidence_results:
        return None

    # If evidence is a dictionary with 'evidence' key
    if isinstance(evidence_results[0], dict):
        # Sort by confidence if available
        sorted_evidence = sorted(
            evidence_results, 
            key=lambda x: x.get('confidence', 0), 
            reverse=True
        )
        
        # Return the evidence from the highest confidence item
        for item in sorted_evidence:
            evidence = item.get('evidence')
            if evidence:
                return evidence

    # If plain list of evidence
    return next((ev for ev in evidence_results if ev and isinstance(ev, str)), None)

def generate_explanation(claim, evidence_results, truth_label, confidence=None):
    """
    Generate an explanation for the claim's classification based on evidence.
    
    This function creates a human-readable explanation of why a claim was classified
    as true, false, or uncertain. It handles different truth label formats through
    normalization and provides robust fallback mechanisms for error cases.
    
    Args:
        claim (str): The original factual claim being verified
        evidence_results (list/str): Evidence supporting the classification, can be
                                    a list of evidence items or structured results
        truth_label (str): Classification of the claim (True/False/Uncertain),
                           which may come in various formats
        confidence (float, optional): Confidence level between 0 and 1
                                     
    Returns:
        str: Natural language explanation of the verdict with appropriate
             confidence framing and evidence citations
    """
    logger.info(f"Generating explanation for claim with verdict: {truth_label}")
    
    try:
        # Normalize truth_label to handle different formats consistently
        normalized_label = normalize_truth_label(truth_label)
        
        # Normalize evidence_results to a list
        if not isinstance(evidence_results, list):
            try:
                evidence_results = ast.literal_eval(str(evidence_results)) if evidence_results else []
            except:
                evidence_results = [evidence_results] if evidence_results else []

        # Get the LLM model
        explanation_model = get_llm_model()

        # Extract most relevant evidence
        most_relevant_evidence = extract_most_relevant_evidence(evidence_results)

        # Prepare evidence text for prompt
        evidence_text = "\n".join([
            f"Evidence {i+1}: {str(ev)[:200] + '...' if len(str(ev)) > 200 else str(ev)}" 
            for i, ev in enumerate(evidence_results[:5])
        ])

        # Filter only supporting and contradicting evidence for clarity
        support_items = [item for item in evidence_results if isinstance(item, dict) and item.get("label") == "support"]
        contradict_items = [item for item in evidence_results if isinstance(item, dict) and item.get("label") == "contradict"]
        
        # Convert confidence to percentage and description
        confidence_desc = ""
        very_low_confidence = False
        
        # For Uncertain verdicts, always use 0% confidence regardless of evidence confidence values
        if "uncertain" in normalized_label.lower():
            confidence = 0.0
            confidence_desc = "no confidence (0%)"
        elif confidence is not None:
            confidence_pct = int(confidence * 100)
            
            if confidence == 0.0:
                confidence_desc = "no confidence (0%)"
            elif confidence < 0.1:
                confidence_desc = f"very low confidence ({confidence_pct}%)"
                very_low_confidence = True
            elif confidence < 0.3:
                confidence_desc = f"low confidence ({confidence_pct}%)"
            elif confidence < 0.7:
                confidence_desc = f"moderate confidence ({confidence_pct}%)"
            elif confidence < 0.9:
                confidence_desc = f"high confidence ({confidence_pct}%)"
            else:
                confidence_desc = f"very high confidence ({confidence_pct}%)"
        else:
            # Default if no confidence provided
            confidence_desc = "uncertain confidence"

        # Create prompt with specific instructions based on the type of claim
        has_negation = any(neg in claim.lower() for neg in ["not", "no longer", "isn't", "doesn't", "won't", "cannot"])
        
        # For claims with "True" verdict
        if "true" in normalized_label.lower():
            # Special case for very low confidence (but not zero)
            if very_low_confidence:
                prompt = f"""
                Claim: "{claim}"
                
                Verdict: {normalized_label} (with {confidence_desc})

                Available Evidence:
                {evidence_text}

                Task: Generate a clear explanation that:
                1. States that the claim appears to be true based on the available evidence
                2. EMPHASIZES that the confidence level is VERY LOW ({confidence_pct}%)
                3. Explains that this means the evidence slightly favors the claim but is not strong enough to be certain
                4. STRONGLY recommends that the user verify this with other authoritative sources
                5. Is factual and precise
                """
            else:
                prompt = f"""
                Claim: "{claim}"
                
                Verdict: {normalized_label} (with {confidence_desc})

                Available Evidence:
                {evidence_text}

                Task: Generate a clear explanation that:
                1. Clearly states that the claim IS TRUE based on the evidence
                2. {"Pay special attention to the logical relationship since the claim contains negation" if has_negation else "Explains why the evidence supports the claim"}
                3. Uses confidence level of {confidence_desc}
                4. Highlights the most relevant supporting evidence
                5. Is factual and precise
                """

        # For claims with "False" verdict
        elif "false" in normalized_label.lower():
            # Special case for very low confidence (but not zero)
            if very_low_confidence:
                prompt = f"""
                Claim: "{claim}"
                
                Verdict: {normalized_label} (with {confidence_desc})

                Available Evidence:
                {evidence_text}

                Task: Generate a clear explanation that:
                1. States that the claim appears to be false based on the available evidence
                2. EMPHASIZES that the confidence level is VERY LOW ({confidence_pct}%)
                3. Explains that this means the evidence slightly contradicts the claim but is not strong enough to be certain
                4. STRONGLY recommends that the user verify this with other authoritative sources
                5. Is factual and precise
                """
            else:
                prompt = f"""
                Claim: "{claim}"
                
                Verdict: {normalized_label} (with {confidence_desc})

                Available Evidence:
                {evidence_text}

                Task: Generate a clear explanation that:
                1. Clearly states that the claim IS FALSE based on the evidence
                2. {"Pay special attention to the logical relationship since the claim contains negation" if has_negation else "Explains why the evidence contradicts the claim"}
                3. Uses confidence level of {confidence_desc}
                4. Highlights the contradicting evidence
                5. Is factual and precise
                """

        # For uncertain claims
        else:
            prompt = f"""
            Claim: "{claim}"
            
            Verdict: {normalized_label} (with {confidence_desc})

            Available Evidence:
            {evidence_text}

            Task: Generate a clear explanation that:
            1. Clearly states that there is insufficient evidence to determine if the claim is true or false
            2. Explains what information is missing or why the available evidence is insufficient
            3. Uses confidence level of {confidence_desc}
            4. Makes NO speculation about whether the claim might be true or false
            5. Explicitly mentions that the user should seek information from other reliable sources
            """

        # Generate explanation with multiple attempts for reliability
        max_attempts = 3
        for attempt in range(max_attempts):
            try:
                # Invoke the model
                response = explanation_model.invoke(prompt)
                explanation = response.content.strip()

                # Validate explanation length
                if explanation and len(explanation.split()) >= 5:
                    return explanation

            except Exception as attempt_error:
                logger.error(f"Explanation generation attempt {attempt+1} failed: {str(attempt_error)}")

        # Ultimate fallback explanations if all attempts fail
        if "uncertain" in normalized_label.lower():
            return f"The claim '{claim}' cannot be verified due to insufficient evidence. The available information does not provide clear support for or against this claim. Consider consulting reliable sources for verification."
        elif very_low_confidence:
            return f"The claim '{claim}' appears to be {'supported' if 'true' in normalized_label.lower() else 'contradicted'} by the evidence, but with very low confidence ({confidence_pct}%). The evidence is not strong enough to make a definitive determination. It is strongly recommended to verify this information with other authoritative sources."
        elif "true" in normalized_label.lower():
            return f"The claim '{claim}' is supported by the evidence with {confidence_desc}. {most_relevant_evidence or 'The evidence indicates this claim is accurate.'}"
        else:
            return f"The claim '{claim}' is contradicted by the evidence with {confidence_desc}. {most_relevant_evidence or 'The evidence indicates this claim is not accurate.'}"

    except Exception as e:
        logger.error(f"Comprehensive error in explanation generation: {str(e)}")
        # Final fallback with minimal but useful information
        normalized_label = normalize_truth_label(truth_label)
        return f"The claim is classified as {normalized_label} based on the available evidence."

def normalize_truth_label(truth_label):
    """
    Normalize truth label to handle different formats consistently.
    
    This function extracts the core truth classification (True/False/Uncertain) from
    potentially complex or inconsistently formatted truth labels. It preserves
    contextual information like "(Based on Evidence)" when present.
    
    Args:
        truth_label (str): The truth label to normalize, which may contain 
                           additional descriptive text or formatting
        
    Returns:
        str: Normalized truth label that preserves the core classification and
             important context while eliminating inconsistencies
             
    Examples:
        >>> normalize_truth_label("True (Based on Evidence)")
        "True (Based on Evidence)"
        >>> normalize_truth_label("false (Based on Evidence)")
        "False (Based on Evidence)"
        >>> normalize_truth_label("The evidence shows this claim is False")
        "False"
    """
    if not truth_label:
        return "Uncertain"
    
    # Convert to string if not already
    label_str = str(truth_label)
    
    # Extract the core label if it contains additional text like "(Based on Evidence)"
    base_label_match = re.search(r'(True|False|Uncertain|Error)', label_str, re.IGNORECASE)
    if base_label_match:
        # Get the core label and capitalize it for consistency
        base_label = base_label_match.group(1).capitalize()
        
        # Add back the context if it was present
        if "(Based on Evidence)" in label_str:
            return f"{base_label} (Based on Evidence)"
        return base_label
    
    # Return the original if we couldn't normalize it
    return label_str