import re from urllib.parse import urlparse from datetime import datetime from typing import List, Dict import gradio as gr import plotly.graph_objects as go import numpy as np # --------- Heuristic Scoring Logic --------- SENTENCE_SPLIT_PATTERN = re.compile(r'(?<=[.!?])\s+') URL_PATTERN = re.compile(r'(https?://[^\s]+)') YEAR_PATTERN = re.compile(r'\b(19[5-9]\d|20[0-4]\d|2050)\b') BIAS_WORDS = [ "obviously", "definitely", "certainly", "undeniably", "everyone knows", "no doubt", "clearly", "always", "never" ] def domain_quality(domain: str) -> int: """Very rough heuristic for domain quality.""" if not domain: return 50 domain = domain.lower() if domain.endswith(".edu") or domain.endswith(".ac.uk"): return 90 if domain.endswith(".gov") or domain.endswith(".gov.uk"): return 90 if domain.endswith(".org"): return 75 if "wikipedia.org" in domain: return 70 if "blog" in domain or "medium.com" in domain: return 55 return 60 # default def label_from_score(score: int) -> str: if score >= 80: return "High Trust" if score >= 60: return "Medium Trust" if score >= 40: return "Low–Medium Trust" return "Low Trust" def make_gauge(score: int): # clamp score between 0–100 score = max(0, min(100, score)) fig = go.Figure( go.Indicator( mode="gauge+number", value=score, number={"suffix": "%", "font": {"size": 34}}, gauge={ "shape": "angular", "axis": {"range": [0, 100], "visible": False}, "bar": { "color": "#2563eb", # solid blue fill for the active progress "thickness": 0.35 }, "bgcolor": "white", "borderwidth": 0, # Three blue segments (background arc) "steps": [ {"range": [0, 33], "color": "#c7d2fe"}, # light blue {"range": [33, 66], "color": "#60a5fa"}, # medium blue {"range": [66, 100], "color": "#1d4ed8"} # dark blue ], }, domain={"x": [0, 1], "y": [0, 1]}, ) ) fig.update_layout( margin=dict(t=30, b=0, l=20, r=20), height=220, ) return fig def analyze_text(text: str, is_ai_generated: bool): text = text.strip() if not text: return ( "—", "No input", "Please paste some content to analyze.", "- No issues (no text)\n", "- Add some content to see suggestions.\n", "_No sentences to analyze._", ) # --- Sentence splitting --- sentences = [s.strip() for s in SENTENCE_SPLIT_PATTERN.split(text) if s.strip()] urls = URL_PATTERN.findall(text) domains = [] for url in urls: try: parsed = urlparse(url) domains.append(parsed.netloc) except Exception: continue unique_domains = set(domains) # Sentence-level metadata sentence_data: List[Dict] = [] sentences_with_citation = 0 sentences_with_bias = 0 for s in sentences: lower_s = s.lower() has_citation = bool( URL_PATTERN.search(s) or re.search(r'\[\d+\]', s) or "according to" in lower_s ) has_bias = any(bias in lower_s for bias in BIAS_WORDS) years = YEAR_PATTERN.findall(s) sentence_data.append( { "text": s, "has_citation": has_citation, "has_bias_indicator": has_bias, "years": years, } ) if has_citation: sentences_with_citation += 1 if has_bias: sentences_with_bias += 1 total_sentences = len(sentences) # ---- SQS: Source Quality ---- if urls: domain_scores = [domain_quality(d) for d in unique_domains] avg_domain_score = sum(domain_scores) / len(domain_scores) sqs = int(avg_domain_score) sqs_rationale = f"Based on {len(unique_domains)} cited domain(s); average domain quality ≈ {avg_domain_score:.1f}." else: sqs = 55 sqs_rationale = "No explicit sources detected; defaulting to moderate source quality." # ---- CSS: Claim Support ---- citation_ratio = sentences_with_citation / total_sentences if total_sentences else 0 if citation_ratio == 0: css = 25 css_rationale = "No citations detected for any sentences." else: css = int(40 + 60 * citation_ratio) # 40–100 css_rationale = f"{sentences_with_citation}/{total_sentences} sentence(s) appear to have citations or references." # ---- BDS: Bias & Diversity ---- bias_ratio = sentences_with_bias / total_sentences if total_sentences else 0 diversity_bonus = min(len(unique_domains) * 5, 20) # up to +20 for multiple domains base_bds = 70 + diversity_bonus bias_penalty = int(bias_ratio * 40) # up to -40 bds = max(20, min(100, base_bds - bias_penalty)) bds_rationale_parts = [] if unique_domains: bds_rationale_parts.append(f"{len(unique_domains)} unique source domain(s) detected.") if sentences_with_bias: bds_rationale_parts.append( f"{sentences_with_bias}/{total_sentences} sentence(s) with bias-indicating phrases." ) if not bds_rationale_parts: bds_rationale_parts.append("No citations or explicit bias indicators detected.") bds_rationale = " ".join(bds_rationale_parts) # ---- RCS: Recency & Context ---- all_years = YEAR_PATTERN.findall(text) current_year = datetime.now().year if not all_years: rcs = 70 rcs_rationale = "No specific years detected; assuming moderate recency for educational content." else: years_int = [int(y) for y in all_years] newest = max(years_int) age = current_year - newest if age <= 5: rcs = 90 rcs_rationale = f"Newest year mentioned is {newest}, which is recent." elif age <= 15: rcs = 70 rcs_rationale = f"Newest year mentioned is {newest}, which is moderately old." else: rcs = 50 rcs_rationale = f"Newest year mentioned is {newest}, which appears quite old for some topics." # ---- MRS: Model Reliability ---- mrs = 80 if not is_ai_generated else 75 mrs_rationale = "Assuming competent human or general-purpose AI author; can be refined later." # ---- Final weighted score ---- weight_sqs = 0.30 weight_css = 0.30 weight_bds = 0.15 weight_mrs = 0.15 weight_rcs = 0.10 overall = ( sqs * weight_sqs + css * weight_css + bds * weight_bds + mrs * weight_mrs + rcs * weight_rcs ) overall_int = int(round(overall)) label = label_from_score(overall_int) # ---- Build markdown outputs ---- component_md = f""" ### Component Breakdown | Component | Weight | Score | Rationale | |----------|--------|-------|-----------| | SQS (Source Quality) | 30% | {sqs} | {sqs_rationale} | | CSS (Claim Support) | 30% | {css} | {css_rationale} | | BDS (Bias & Diversity) | 15% | {bds} | {bds_rationale} | | MRS (Model Reliability) | 15% | {mrs} | {mrs_rationale} | | RCS (Recency & Context) | 10% | {rcs} | {rcs_rationale} | """ issues: List[str] = [] suggestions: List[str] = [] if sentences_with_citation == 0: issues.append("No explicit citations or references detected.") suggestions.append("Add at least 2–3 citations from reliable educational or academic sources.") elif citation_ratio < 0.3: issues.append("Only a small portion of statements appear to be supported by citations.") suggestions.append("Support more factual claims with references to textbooks, encyclopedias, or academic articles.") if sentences_with_bias > 0: issues.append("Some sentences use strong or absolute language that may indicate bias.") suggestions.append("Rephrase strongly worded claims into more neutral, evidence-based statements.") if rcs <= 60: issues.append("The information relies on relatively old dates, which may be outdated for some topics.") suggestions.append("Check if there are more recent sources or data to support your claims.") if not issues: issues.append("No major credibility issues detected by the heuristic checks.") suggestions.append("You can still improve credibility by adding more high-quality sources and clarifying complex claims.") issues_md = "\n".join(f"- {i}" for i in issues) suggestions_md = "\n".join(f"- {s}" for s in suggestions) sentence_lines = [] for s in sentence_data: tags = [] if s["has_citation"]: tags.append("📚 citation") if s["has_bias_indicator"]: tags.append("⚠️ possible bias") if s["years"]: tags.append("📅 years: " + ", ".join(s["years"])) tag_str = " — " + ", ".join(tags) if tags else "" sentence_lines.append(f"- {s['text']}{tag_str}") sentences_md = "\n".join(sentence_lines) if sentence_lines else "_No sentences found._" overall_md = f""" # Trust Score: **{overall_int}/100** Label: **{label}** """ return overall_int, overall_md, label, component_md, issues_md, suggestions_md, sentences_md # --------- Gradio UI --------- with gr.Blocks(title="Educational Content Credibility Checker") as demo: gr.Markdown( """ # 📚 Educational Content Credibility Checker (MVP) Paste any student essay or AI-generated explanation. This tool estimates a **Trust Score (0–100)** based on: - Source quality - Claim support (citations) - Bias & diversity - Model reliability - Recency & context > ⚠️ MVP: This is a **heuristic prototype**, not a perfect fact-checker. """ ) with gr.Row(): with gr.Column(): text_input = gr.Textbox( label="1. Paste content here", lines=15, placeholder="Paste student content or AI-generated text...", ) is_ai_checkbox = gr.Checkbox( label="Content is AI-generated", value=False ) analyze_btn = gr.Button("Analyze Credibility", variant="primary") with gr.Column(): gauge_plot = gr.Plot(label="trust.AI Score") overall_out = gr.Markdown(label="Overall Trust Score") components_out = gr.Markdown(label="Component Breakdown") issues_out = gr.Markdown(label="Issues Detected") suggestions_out = gr.Markdown(label="Suggestions for Improvement") sentences_out = gr.Markdown(label="Sentence-level Analysis") def gradio_wrapper(text, is_ai_generated): overall_int, overall_md, label, component_md, issues_md, suggestions_md, sentences_md = analyze_text( text, is_ai_generated ) fig = make_gauge(overall_int) return fig, overall_md, component_md, issues_md, suggestions_md, sentences_md analyze_btn.click( fn=gradio_wrapper, inputs=[text_input, is_ai_checkbox], outputs=[gauge_plot, overall_out, components_out, issues_out, suggestions_out, sentences_out], ) if __name__ == "__main__": demo.launch()