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