Spaces:
Sleeping
Sleeping
Download ui.py from infoclassifier/InfoClassifier: direct link, hf CLI and curl.
- Browser
- Download file 32.3 kB
-
https://huggingface.co/spaces/infoclassifier/InfoClassifier/resolve/main/ui.py
- Command line
-
hf download hf://spaces/infoclassifier/InfoClassifier/ui.py
-
curl -L -o ui.py https://huggingface.co/spaces/infoclassifier/InfoClassifier/resolve/main/ui.py
32.3 kB
| """ | |
| 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'<span style="display:inline-block;width:10px;height:10px;border-radius:50%;' | |
| f'margin:0 4px;background:{"#4f8ef7" if i <= stages.index(stage) else "#dde3ee"}' | |
| f'"></span>' | |
| for i, s in enumerate(stages) | |
| if (i <= stages.index(stage)) or True | |
| ) | |
| return f""" | |
| <div style="font-family:'Inter',sans-serif;padding:28px 32px;background:#f8faff; | |
| border-radius:16px;border:1px solid #e0e7ff;"> | |
| <div style="font-size:15px;color:#5c6bc0;margin-bottom:14px;font-weight:600;"> | |
| {stage}β¦ <span style="font-weight:400;color:#8e99c5">{msg}</span> | |
| </div> | |
| <div style="background:#e8edf8;border-radius:8px;height:8px;overflow:hidden;"> | |
| <div style="background:linear-gradient(90deg,#4f8ef7,#7c6af7);height:100%; | |
| width:{pct}%;transition:width 0.4s ease;border-radius:8px;"></div> | |
| </div> | |
| </div>""" | |
| def _verdict_color(decision: str) -> tuple[str, str, str]: | |
| """Returns (bg_gradient, text_color, icon).""" | |
| return { | |
| "remove": ("linear-gradient(135deg,#ff4444,#d32f2f)", "#fff", "π«"), | |
| "flag": ("linear-gradient(135deg,#ff9800,#e65100)", "#fff", "β οΈ"), | |
| "allow": ("linear-gradient(135deg,#43a047,#1b5e20)", "#fff", "β "), | |
| }.get(decision, ("linear-gradient(135deg,#607d8b,#37474f)", "#fff", "β")) | |
| def _reliability_bar(score: float) -> str: | |
| filled = int(score * 12) | |
| color = "#e53935" if score < 0.4 else "#fb8c00" if score < 0.7 else "#43a047" | |
| bar = f'<span style="color:{color}">{"β"*filled}</span>' \ | |
| f'<span style="color:#e0e0e0">{"β"*(12-filled)}</span>' | |
| return f'<span style="font-family:monospace;font-size:12px">{bar}</span> ' \ | |
| f'<span style="font-size:12px;color:#666">{score:.2f}</span>' | |
| def _stance_badge(stance: str) -> str: | |
| cfg = { | |
| "contradicting": ("#fdecea", "#c62828", "β Contradicts"), | |
| "supporting": ("#e8f5e9", "#2e7d32", "β Supports"), | |
| "neutral": ("#f3f4f6", "#555", "Β· Neutral"), | |
| }.get(stance, ("#f3f4f6", "#555", stance)) | |
| bg, fg, label = cfg | |
| return (f'<span style="background:{bg};color:{fg};padding:2px 8px;' | |
| f'border-radius:12px;font-size:12px;font-weight:600">{label}</span>') | |
| def _claim_badge(status: str) -> str: | |
| cfg = { | |
| "false": ("#fdecea", "#c62828", "FALSE"), | |
| "true": ("#e8f5e9", "#2e7d32", "TRUE"), | |
| "partially_true": ("#fff8e1", "#e65100", "PARTIAL"), | |
| "unknown": ("#f3f4f6", "#555", "UNKNOWN"), | |
| "unverified": ("#f3f4f6", "#9e9e9e", "UNVERIFIED"), | |
| }.get(status.lower() if isinstance(status, str) else "unverified", | |
| ("#f3f4f6", "#555", status)) | |
| bg, fg, label = cfg | |
| return (f'<span style="background:{bg};color:{fg};padding:3px 10px;' | |
| f'border-radius:12px;font-size:11px;font-weight:700;' | |
| f'letter-spacing:.5px">{label}</span>') | |
| def build_result_html(statement: str, scenario: dict, obs, result, explanation: str, | |
| per_claim_reasoning: dict | None = None) -> str: | |
| decision = result.decision or "unknown" | |
| grad, txt_col, icon = _verdict_color(decision) | |
| correct_badge = ( | |
| '<span style="background:#e8f5e9;color:#2e7d32;padding:3px 10px;' | |
| 'border-radius:12px;font-size:12px;font-weight:600">β Correct</span>' | |
| if result.correct else | |
| '<span style="background:#fdecea;color:#c62828;padding:3px 10px;' | |
| 'border-radius:12px;font-size:12px;font-weight:600">β Incorrect</span>' | |
| ) | |
| # ββ Claims section | |
| claims_html = "" | |
| for c in obs.claims: | |
| status = c.verification_status | |
| if hasattr(status, "value"): | |
| status = status.value | |
| conf_pct = int(c.confidence * 100) | |
| claims_html += f""" | |
| <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 0; | |
| border-bottom:1px solid #f0f0f0;"> | |
| <div style="flex:1;font-size:14px;color:#333;line-height:1.5">{c.text}</div> | |
| <div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px; | |
| white-space:nowrap"> | |
| {_claim_badge(status)} | |
| <span style="font-size:11px;color:#999">conf {conf_pct}%</span> | |
| </div> | |
| </div>""" | |
| # ββ Sources section (top 6 by reliability) | |
| sources = sorted(scenario["sources"], key=lambda s: -s["reliability"])[:6] | |
| sources_html = "" | |
| for s in sources: | |
| domain = re.sub(r"https?://(www\.)?", "", s["url"]).split("/")[0] | |
| sources_html += f""" | |
| <div style="padding:12px;border-radius:10px;background:#fafafa; | |
| border:1px solid #eee;margin-bottom:8px"> | |
| <div style="display:flex;justify-content:space-between;align-items:center; | |
| flex-wrap:wrap;gap:6px"> | |
| <span style="font-size:13px;font-weight:600;color:#333">{s['title'][:65]}</span> | |
| {_stance_badge(s['stance'])} | |
| </div> | |
| <div style="font-size:12px;color:#888;margin:4px 0">{domain} Β· {s['source_type']}</div> | |
| <div style="font-size:12px;color:#555;line-height:1.5;margin:6px 0"> | |
| {s['text'][:180]}{'β¦' if len(s['text'])>180 else ''} | |
| </div> | |
| <div>Reliability: {_reliability_bar(s['reliability'])}</div> | |
| </div>""" | |
| if not sources_html: | |
| sources_html = '<p style="color:#999;font-style:italic">No sources fetched β try adding API keys for richer results.</p>' | |
| # ββ 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""" | |
| <div style="display:flex;justify-content:space-between;padding:5px 0; | |
| border-bottom:1px solid #f5f5f5;font-size:13px"> | |
| <span style="color:#555">{label}</span> | |
| <span style="color:{color};font-weight:600">{val:+.2f}</span> | |
| </div>""" | |
| # ββ Grader sub-scores | |
| grader = GraderFactory.get(scenario["difficulty"]) | |
| gbd = grader.score_breakdown( | |
| decision = result.decision or "none", | |
| ground_truth = result.ground_truth, | |
| sources_checked = list(range(result.sources_checked)), | |
| claims_verified = list(range(result.claims_verified)), | |
| step_count = result.steps_taken, | |
| all_source_ids = [s["id"] for s in scenario["sources"]], | |
| all_claim_ids = [c["id"] for c in scenario["claims"]], | |
| ) | |
| grader_bars = "" | |
| for sub_label, sub_val in [ | |
| ("Decision", gbd["decision_sub_score"]), | |
| ("Evidence", gbd["evidence_sub_score"]), | |
| ("Reasoning", gbd["reasoning_sub_score"]), | |
| ("Efficiency", gbd["efficiency_sub_score"]), | |
| ]: | |
| pct = int(sub_val * 100) | |
| colr = "#e53935" if pct < 50 else "#fb8c00" if pct < 75 else "#43a047" | |
| grader_bars += f""" | |
| <div style="margin-bottom:8px"> | |
| <div style="display:flex;justify-content:space-between;font-size:12px; | |
| color:#555;margin-bottom:3px"> | |
| <span>{sub_label}</span><span style="color:{colr};font-weight:600">{pct}%</span> | |
| </div> | |
| <div style="background:#eee;border-radius:4px;height:6px"> | |
| <div style="background:{colr};width:{pct}%;height:100%;border-radius:4px"></div> | |
| </div> | |
| </div>""" | |
| # ββ Deterministic Reasoning breakdown (new) | |
| reasoning_html = "" | |
| if per_claim_reasoning: | |
| reasoning_html = """ | |
| <div style="background:#fff;border-radius:14px;border:1px solid #e8ecf4; | |
| padding:20px 24px;margin-bottom:20px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:14px"> | |
| π§ Deterministic Reasoning Engine | |
| </div>""" | |
| for claim_text, r in per_claim_reasoning.items(): | |
| score = r["evidence_score"] | |
| conf = r["confidence"] | |
| dec = r["decision"] | |
| n_sup = r["n_supporting"] | |
| n_con = r["n_contradicting"] | |
| n_neu = r["n_neutral"] | |
| dec_col = {"allow": "#2e7d32", "flag": "#e65100", "remove": "#c62828"}.get(dec, "#555") | |
| score_col = "#c62828" if score < -0.3 else "#2e7d32" if score > 0.3 else "#e65100" | |
| reasoning_html += f""" | |
| <div style="background:#f8faff;border-radius:10px;padding:14px 18px;margin-bottom:12px; | |
| border-left:4px solid {dec_col}"> | |
| <div style="font-size:13px;color:#444;font-style:italic;margin-bottom:8px"> | |
| "{claim_text[:100]}{'β¦' if len(claim_text)>100 else ''}" | |
| </div> | |
| <div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px"> | |
| <span style="background:{dec_col};color:#fff;padding:2px 10px;border-radius:10px; | |
| font-size:12px;font-weight:700;text-transform:uppercase">{dec}</span> | |
| <span style="font-size:12px;color:{score_col};font-weight:600"> | |
| Score: {score:+.3f}</span> | |
| <span style="font-size:12px;color:#888">Confidence: {conf}</span> | |
| <span style="font-size:12px;color:#2e7d32">β² {n_sup} supporting</span> | |
| <span style="font-size:12px;color:#c62828">βΌ {n_con} contradicting</span> | |
| <span style="font-size:12px;color:#888">Β· {n_neu} neutral</span> | |
| </div>""" | |
| # Safety rules fired | |
| if r.get("safety_rules_fired"): | |
| for rule in r["safety_rules_fired"]: | |
| rule_name = rule.split(":")[0] | |
| rule_desc = rule[len(rule_name)+1:].strip() | |
| reasoning_html += f""" | |
| <div style="font-size:11px;color:#e65100;background:#fff8e1;padding:4px 10px; | |
| border-radius:6px;margin-bottom:4px"> | |
| β <b>{rule_name}</b>: {rule_desc} | |
| </div>""" | |
| # Source breakdown (top 4) | |
| sb = r.get("source_breakdown", [])[:4] | |
| if sb: | |
| reasoning_html += '<div style="margin-top:8px">' | |
| for src in sb: | |
| contrib = src["contribution"] | |
| c_col = "#c62828" if contrib < 0 else "#2e7d32" if contrib > 0 else "#888" | |
| reasoning_html += f""" | |
| <div style="display:flex;justify-content:space-between;font-size:11px; | |
| color:#555;padding:2px 0;border-bottom:1px solid #eee"> | |
| <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; | |
| max-width:420px">{src['title'][:70]}</span> | |
| <span style="margin:0 8px">{_stance_badge(src['stance'])}</span> | |
| <span style="font-weight:600;color:{c_col};white-space:nowrap"> | |
| {contrib:+.3f}</span> | |
| </div>""" | |
| reasoning_html += "</div>" | |
| reasoning_html += "</div>" | |
| reasoning_html += "</div>" | |
| # ββ Explanation (pre-formatted) | |
| expl_lines = explanation.replace("<", "<").replace(">", ">") | |
| expl_html = f'<pre style="font-family:inherit;font-size:13px;line-height:1.7;' \ | |
| f'white-space:pre-wrap;margin:0;color:#333">{expl_lines}</pre>' | |
| return f""" | |
| <div style="font-family:'Inter',system-ui,sans-serif;max-width:960px;margin:0 auto"> | |
| <!-- VERDICT BANNER --> | |
| <div style="background:{grad};border-radius:16px;padding:28px 32px; | |
| margin-bottom:24px;display:flex;align-items:center;gap:20px"> | |
| <div style="font-size:52px">{icon}</div> | |
| <div> | |
| <div style="color:rgba(255,255,255,.75);font-size:13px; | |
| font-weight:600;letter-spacing:1px;text-transform:uppercase"> | |
| Moderation Verdict | |
| </div> | |
| <div style="color:{txt_col};font-size:36px;font-weight:800; | |
| line-height:1.1;text-transform:uppercase">{decision}</div> | |
| <div style="color:rgba(255,255,255,.85);font-size:13px;margin-top:4px"> | |
| Difficulty: <b>{scenario['difficulty']}</b> Β· | |
| Steps: <b>{result.steps_taken}</b> Β· | |
| Reward: <b>{result.total_reward:+.2f}</b> Β· | |
| Grader: <b>{result.grader_score:.0%}</b> | |
| {correct_badge} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- STATEMENT --> | |
| <div style="background:#fff;border-radius:14px;border:1px solid #e8ecf4; | |
| padding:20px 24px;margin-bottom:20px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:8px">Statement Reviewed</div> | |
| <div style="font-size:15px;color:#1f2937;line-height:1.6; | |
| font-style:italic">"{statement}"</div> | |
| </div> | |
| <!-- CLAIMS + SOURCES (two col) --> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px"> | |
| <!-- Claims --> | |
| <div style="background:#fff;border-radius:14px;border:1px solid #e8ecf4; | |
| padding:20px 24px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:12px"> | |
| Claims Verified ({len(obs.claims)}) | |
| </div> | |
| {claims_html} | |
| </div> | |
| <!-- Scores --> | |
| <div style="background:#fff;border-radius:14px;border:1px solid #e8ecf4; | |
| padding:20px 24px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:14px">Grader Score Breakdown</div> | |
| {grader_bars} | |
| <div style="border-top:1px solid #eee;padding-top:12px;margin-top:4px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:8px">Reward Breakdown</div> | |
| {reward_rows} | |
| <div style="display:flex;justify-content:space-between;padding:8px 0; | |
| font-size:14px;font-weight:700"> | |
| <span>Total Reward</span> | |
| <span style="color:{'#2e7d32' if result.total_reward>0 else '#c62828'}"> | |
| {result.total_reward:+.2f} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SOURCES --> | |
| <div style="background:#fff;border-radius:14px;border:1px solid #e8ecf4; | |
| padding:20px 24px;margin-bottom:20px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:14px"> | |
| Evidence Sources ({len(scenario['sources'])} found) | |
| </div> | |
| {sources_html} | |
| </div> | |
| {reasoning_html} | |
| <!-- EXPLANATION --> | |
| <div style="background:#fff;border-radius:14px;border:1px solid #e8ecf4; | |
| padding:20px 24px"> | |
| <div style="font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1px; | |
| text-transform:uppercase;margin-bottom:14px"> | |
| π Full Reasoning & Explanation | |
| </div> | |
| <div style="background:#f8faff;border-radius:10px;padding:16px 20px"> | |
| {expl_html} | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main pipeline (streaming generator) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze(statement: str) -> Generator: | |
| """ | |
| Difficulty is NOT a UI input. | |
| The UI is a demo tool; evaluation difficulty is controlled exclusively | |
| by the backend API (/reset?difficulty=...) and inference.py. | |
| DataCollector always uses 'medium' here β it only affects evidence | |
| retrieval depth, not grading. | |
| """ | |
| statement = statement.strip() | |
| if not statement: | |
| yield "<p style='color:#c62828;padding:20px'>β οΈ Please enter a statement to analyze.</p>" | |
| 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"<em>{c[:60]}</em>" 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"<p style='color:#c62828;padding:20px'>β Evidence collection failed: {e}</p>" | |
| 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(""" | |
| <div style="text-align:center;padding:40px 20px 20px;font-family:'Inter',sans-serif"> | |
| <div style="font-size:36px;margin-bottom:8px">π‘οΈ</div> | |
| <h1 style="font-size:28px;font-weight:800;color:#1e2a4a;margin:0 0 8px"> | |
| Information Integrity Analyzer | |
| </h1> | |
| <p style="color:#6b7280;font-size:15px;max-width:520px;margin:0 auto"> | |
| Paste any statement or claim. The system automatically extracts assertions, | |
| searches real evidence sources, and delivers a moderation verdict with full explanation. | |
| </p> | |
| <p style="color:#9ca3af;font-size:12px;margin-top:8px"> | |
| Demo mode β for benchmarking across difficulty tiers run: | |
| <code>python inference.py --benchmark</code> | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(elem_id="main-row"): | |
| with gr.Column(scale=1, min_width=640): | |
| statement_box = gr.Textbox( | |
| label="", | |
| placeholder=( | |
| "Paste any statement, post, or claim hereβ¦\n\n" | |
| "e.g. \"5G towers confirmed to spread COVID-19. " | |
| "Governments hiding the truth.\"" | |
| ), | |
| lines=4, | |
| max_lines=8, | |
| show_label=False, | |
| ) | |
| analyze_btn = gr.Button( | |
| "π Analyze Statement", | |
| variant="primary", | |
| ) | |
| gr.HTML("<div style='margin:8px 0 4px;font-size:12px;color:#9ca3af;font-weight:600;letter-spacing:.5px;text-transform:uppercase'>Try an example</div>") | |
| with gr.Row(): | |
| for ex_text in EXAMPLES[:3]: | |
| btn = gr.Button(ex_text[:45] + "β¦", size="sm", scale=1) | |
| btn.click(fn=lambda t=ex_text: t, outputs=[statement_box]) | |
| with gr.Row(): | |
| for ex_text in EXAMPLES[3:]: | |
| btn = gr.Button(ex_text[:45] + "β¦", size="sm", scale=1) | |
| btn.click(fn=lambda t=ex_text: t, outputs=[statement_box]) | |
| result_html = gr.HTML( | |
| value="<div style='padding:32px;text-align:center;color:#9ca3af;font-size:15px'>" | |
| "Results will appear here after analysis.</div>" | |
| ) | |
| analyze_btn.click( | |
| fn=analyze, | |
| inputs=[statement_box], | |
| outputs=[result_html], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_error=True, | |
| css=CSS, | |
| theme=gr.themes.Soft( | |
| primary_hue="indigo", | |
| secondary_hue="purple", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| ), | |
| ) | |