thetruthbureau / decision_engine.py
nahArnav's picture
Upload 13 files
39bbca0 verified
"""
Decision Engine for VeriLens AI
Combines ML prediction, verification similarity, source credibility,
and NLP analysis into a final verdict.
"""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class Decision:
prediction: str # "REAL", "FAKE", or "UNCERTAIN"
confidence: int # 0 – 100
explanation: str
factors: dict = field(default_factory=dict)
def make_decision(
ml_label: str,
ml_confidence: float,
similarity_score: float,
sources_verified: bool,
suspicious_info: dict,
high_trust_count: int = 0,
low_trust_count: int = 0,
) -> Decision:
"""Weighted decision combining multiple signals."""
# ── ML score contribution (0-45) ────────────────────────────────────────
if ml_label == "FAKE":
ml_score = (1 - ml_confidence) * 45
elif ml_label == "REAL":
ml_score = ml_confidence * 45
else:
ml_score = 22.5
# ── Verification score contribution (0-25) ──────────────────────────────
if sources_verified:
verify_score = similarity_score * 25
else:
verify_score = 12.5
# ── Source credibility contribution (0-15) ──────────────────────────────
if high_trust_count + low_trust_count > 0:
cred_ratio = high_trust_count / (high_trust_count + low_trust_count)
cred_score = cred_ratio * 15
elif sources_verified:
cred_score = 7.5
else:
cred_score = 7.5
# ── Suspicious language penalty (0-15) ──────────────────────────────────
sus_count = suspicious_info.get("total_suspicious_count", 0)
if sus_count == 0:
sus_score = 15
elif sus_count <= 2:
sus_score = 10
elif sus_count <= 5:
sus_score = 5
else:
sus_score = 0
# ── Aggregate ───────────────────────────────────────────────────────────
total = ml_score + verify_score + cred_score + sus_score
total = max(0, min(100, total))
# ── Guard: prevent FAKE ML prediction from flipping to Real ─────────
ml_fake_overridden = False
if ml_label == "FAKE" and ml_confidence >= 0.6 and total >= 65:
total = 55
ml_fake_overridden = True
# ── Decide verdict (STANDARDIZED TO UPPERCASE) ──────────────────────
if total >= 65:
prediction = "REAL"
elif total <= 40:
prediction = "FAKE"
else:
prediction = "UNCERTAIN"
# ── Confidence relative to the prediction ───────────────────────────
if prediction == "REAL":
confidence = int(round(total))
elif prediction == "FAKE":
confidence = 100 - int(round(total))
else:
distance = abs(total - 52.5)
confidence = max(30, min(50, int(round(50 - distance))))
# ── Build explanation ───────────────────────────────────────────────────
explanations: list[str] = []
if ml_label == "FAKE":
explanations.append(f"The AI model classified this as FAKE with {ml_confidence:.0%} confidence.")
if ml_fake_overridden:
explanations.append("Although related articles exist online, they may be debunking the claim rather than confirming it.")
elif ml_label == "REAL":
explanations.append(f"The AI model classified this as REAL with {ml_confidence:.0%} confidence.")
else:
explanations.append("The AI model could not reach a strong conclusion.")
if sources_verified:
if similarity_score > 0.6:
explanations.append("The claim is well-corroborated by multiple online sources.")
elif similarity_score > 0.3:
explanations.append("Some related articles were found, but corroboration is partial.")
else:
explanations.append("Very few matching sources were found online.")
else:
explanations.append("Internet verification was not available; the verdict relies on AI analysis.")
if sus_count > 3:
explanations.append("High levels of suspicious, sensationalist, or emotional language detected.")
elif sus_count > 0:
explanations.append("Minor suspicious language patterns were noted.")
explanation = " ".join(explanations)
factors = {
"ml_score": round(ml_score, 2),
"verification_score": round(verify_score, 2),
"credibility_score": round(cred_score, 2),
"language_score": round(sus_score, 2),
}
return Decision(
prediction=prediction,
confidence=confidence,
explanation=explanation,
factors=factors,
)