Spaces:
Running
Running
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 |