""" ui.py — Information Integrity Analyzer One input. Automatic pipeline. Rich verdict output. """ # Load .env first so all API keys are available before any module reads os.getenv() try: from dotenv import load_dotenv load_dotenv() except ImportError: pass import re import time from typing import Generator import gradio as gr from data_collector import CollectionRequest, DataCollector from environment import InformationIntegrityEnv from explainability import ExplainabilityEngine, build_reasoning_from_episode from models import Action from tasks.graders import GraderFactory try: from reasoning import DeterministicReasoningEngine, reason_over_claims from retrieval import RetrievalPipeline from decision_engine import make_moderation_decisions _REASONING_AVAILABLE = True except Exception: _REASONING_AVAILABLE = False collector = DataCollector() explainer = ExplainabilityEngine() # ───────────────────────────────────────────────────────────────────────────── # Claim extractor # ───────────────────────────────────────────────────────────────────────────── _SIGNALS = re.compile( r"\b(confirm|prove|show|reveal|say|claim|report|find|cause|lead to|linked to|" r"associated with|increase|decrease|never|always|all|every|none|proven|fact|" r"truth|confirmed|vaccine|virus|cancer|disease|drug|radiation|5g|covid|" r"lockdown|percent|%|study|research|scientist|doctor|expert|government|" r"permanently|alter|spread|hide|secret|conspiracy|banned|cure|prevent)\b", re.IGNORECASE, ) _SKIP = re.compile( r"^(share|follow|click|subscribe|watch|read|download|buy|sign up|please|" r"thank you|breaking:|update:|alert:|warning:)", re.IGNORECASE, ) def extract_claims(text: str) -> list[str]: sentences = re.split(r"(?<=[.!?])\s+|\s*[|•–—]\s*", text.strip()) claims, seen = [], set() for s in sentences: s = s.strip().strip("\"'.,!?") if len(s) < 12 or len(s) > 280: continue if _SKIP.match(s): continue if _SIGNALS.search(s) and s.lower() not in seen: seen.add(s.lower()) claims.append(s) if not claims: claims = [s.strip() for s in sentences if len(s.strip()) >= 15][:3] return claims[:6] # ───────────────────────────────────────────────────────────────────────────── # HTML card builders # ───────────────────────────────────────────────────────────────────────────── def _progress_html(stage: str, pct: int, msg: str) -> str: stages = ["Extracting claims", "Searching sources", "Running analysis", "Complete"] dots = "".join( f'' for i, s in enumerate(stages) if (i <= stages.index(stage)) or True ) return f"""
No sources fetched — try adding API keys for richer results.
' # ── Reward breakdown bd = result.reward_breakdown reward_rows = "" for label, val in [ ("Claim verification", bd.verify_claim_reward), ("Source checks", bd.check_source_reward), ("Multi-source bonus", bd.multi_source_bonus), ("Decision accuracy", bd.decision_reward), ("Premature penalty", bd.premature_penalty), ("Redundancy penalty", bd.redundant_penalty), ("Inefficiency", bd.inefficiency_penalty), ("Over-aggression", bd.aggression_penalty), ]: if val == 0: continue color = "#2e7d32" if val > 0 else "#c62828" reward_rows += f"""{expl_lines}'
return f"""
⚠️ Please enter a statement to analyze.
" return # Stage 1 — Extract claims yield _progress_html("Extracting claims", 10, "detecting verifiable assertions…") claims = extract_claims(statement) if not claims: claims = [statement] claim_list = " • ".join(f"{c[:60]}" for c in claims) yield _progress_html("Searching sources", 30, f"found {len(claims)} claim(s): {claim_list[:120]}…") # Stage 2 — Collect evidence # Difficulty is fixed to "medium" here — the UI does NOT control evaluation logic. # To run a proper benchmark, use: python inference.py --benchmark try: req = CollectionRequest(content=statement, claims=claims, difficulty="medium") scenario = collector.collect(req) except Exception as e: yield f"❌ Evidence collection failed: {e}
" return n_src = len(scenario["sources"]) yield _progress_html("Running analysis", 65, f"fetched {n_src} source(s) — running moderation episode…") # Stage 3 — Run episode env = InformationIntegrityEnv(scenario=scenario, max_steps=20) obs = env.reset() per_claim_reasoning = None # populated by reasoning engine at decide step # Auto-agent: verify all claims → check all sources → decide for claim in obs.claims: try: obs, _, done, _ = env.step( Action(type="verify_claim", claim_id=claim.id, reasoning="auto-verify")) if done: break except Exception: pass if not env.done: for src in obs.available_sources: try: obs, _, done, _ = env.step( Action(type="check_source", source_id=src.id, reasoning="auto-check")) if done: break except Exception: pass if not env.done: # Use DeterministicReasoningEngine if available, else weighted fallback if _REASONING_AVAILABLE: try: pipeline = RetrievalPipeline() engine = DeterministicReasoningEngine() raw_sources = [ { "id": s.id, "title": s.title, "text": s.text, "url": s.url, "stance": s.stance, "reliability": s.reliability, "source_type": s.source_type, } for s in obs.available_sources ] claim_texts = [c.text for c in obs.claims] claim_results_map = pipeline.retrieve_for_all_claims(claim_texts, raw_sources) agg_decision, per_claim_reasoning = reason_over_claims(claim_results_map, engine) # Convert StructuredReasoning objects → dicts for the HTML builder per_claim_reasoning = {ct: r.to_dict() for ct, r in per_claim_reasoning.items()} # ── decision_engine: recalibrate each claim's confidence & decision ── de_inputs = [] for ct, r in per_claim_reasoning.items(): src_list = claim_results_map.get(ct, []) de_inputs.append({ "claim": ct, "status": "FALSE" if r["decision"] == "remove" else "TRUE" if r["decision"] == "allow" else "UNKNOWN", "confidence": r.get("evidence_score", 0.0), "supporting_sources": r["n_supporting"], "contradicting_sources": r["n_contradicting"], "neutral_sources": r["n_neutral"], "avg_source_reliability": ( sum(s.reliability for s in src_list) / len(src_list) if src_list else 0.5 ), }) de_result = make_moderation_decisions(de_inputs) # Map content decision back to env decision format _de_map = {"AUTO_REMOVE": "remove", "ALLOW": "allow", "FLAG_FOR_REVIEW": "flag"} decision = _de_map.get(de_result["content_decision"], agg_decision) # Inject decision_engine output into per_claim_reasoning for display for entry in de_result["per_claim"]: ct = entry["claim"] if ct in per_claim_reasoning: per_claim_reasoning[ct]["de_decision"] = entry["decision"] per_claim_reasoning[ct]["de_confidence"] = entry["final_confidence"] per_claim_reasoning[ct]["de_reason"] = entry["reason"] per_claim_reasoning[ct]["de_pattern"] = entry.get("pattern_matched") except Exception: per_claim_reasoning = None # Fallback to weighted ratio contra = sum(s.reliability for s in obs.available_sources if s.stance == "contradicting") supp = sum(s.reliability for s in obs.available_sources if s.stance == "supporting") total = contra + supp or 1.0 ratio = supp / total decision = "remove" if ratio < 0.25 else "flag" if ratio < 0.60 else "allow" else: per_claim_reasoning = None contra = sum(s.reliability for s in obs.available_sources if s.stance == "contradicting") supp = sum(s.reliability for s in obs.available_sources if s.stance == "supporting") total = contra + supp or 1.0 ratio = supp / total decision = "remove" if ratio < 0.25 else "flag" if ratio < 0.60 else "allow" try: obs, _, _, _ = env.step( Action(type="decide", decision=decision, reasoning="deterministic engine decision")) except Exception: pass else: per_claim_reasoning = None yield _progress_html("Complete", 95, "generating explanation…") # Stage 4 — Build result result = env.episode_result() reasoning = build_reasoning_from_episode( obs=obs, decision=result.decision or "unknown", episode_id=env._episode_id, difficulty=scenario["difficulty"], ) explanation = explainer.generate_explanation(reasoning) # Use engine's per_claim_reasoning if captured, else env's cached reasoning if per_claim_reasoning is None and env._last_reasoning: per_claim_reasoning = { ct: r.to_dict() for ct, r in env._last_reasoning.items() } yield build_result_html(statement, scenario, obs, result, explanation, per_claim_reasoning=per_claim_reasoning) # ───────────────────────────────────────────────────────────────────────────── # UI Layout # ───────────────────────────────────────────────────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); body, .gradio-container { font-family: 'Inter', system-ui, sans-serif !important; background: #f0f4ff !important; } .gr-button-primary { background: linear-gradient(135deg,#4f8ef7,#7c6af7) !important; border:none !important; font-weight:700 !important; font-size:16px !important; padding:14px 36px !important; border-radius:12px !important; color:white !important; } .gr-button-primary:hover { opacity:.9 !important; transform:translateY(-1px); } textarea { font-size:15px !important; border-radius:12px !important; border:2px solid #e0e7ff !important; padding:14px !important; } textarea:focus { border-color:#4f8ef7 !important; box-shadow:0 0 0 3px rgba(79,142,247,.15) !important; } .gr-form { background:transparent !important; } .gr-box { background:transparent !important; border:none !important; box-shadow:none !important; } #component-0 { background: #f0f4ff !important; } """ EXAMPLES = [ "5G towers confirmed to spread COVID-19. Scientists reveal radiation activates coronavirus. Governments hiding the truth.", "Doctors confirm: drinking 3 cups of coffee daily prevents Alzheimer's in 100% of patients with zero side effects.", "New government report proves immigration increases violent crime by 40% in all affected regions. Multiple studies confirm.", "mRNA vaccines permanently alter human DNA and cause infertility according to multiple top doctors and researchers.", "There will be a national lockdown starting next week. Government sources confirmed mandatory quarantine for all cities.", "Eating chocolate every day reduces heart attack risk by 50% — scientists confirm in landmark study.", ] with gr.Blocks(title="Information Integrity Analyzer") as demo: gr.HTML("""Paste any statement or claim. The system automatically extracts assertions, searches real evidence sources, and delivers a moderation verdict with full explanation.
Demo mode — for benchmarking across difficulty tiers run:
python inference.py --benchmark