Spaces:
Running
Running
| """Test script for Wikipedia REST API web search integration. | |
| Uses the Wikipedia REST API (free, no key required) to fetch entity summaries | |
| and extract temporal information for verification in C3b. | |
| Usage: | |
| python scripts/test_web_search.py | |
| python scripts/test_web_search.py --entity "Matt Gaetz" | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| import sys | |
| import time | |
| from dataclasses import dataclass, field | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Optional | |
| import requests | |
| _PROJECT_ROOT = Path(__file__).parent.parent | |
| if str(_PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_PROJECT_ROOT)) | |
| # section: constants | |
| WIKIPEDIA_API = "https://en.wikipedia.org/api/rest_v1/page/summary/{name}" | |
| WIKIPEDIA_SEARCH = "https://en.wikipedia.org/w/api.php" | |
| REQUEST_TIMEOUT = 10 | |
| QUERY_DELAY = 0.5 | |
| YEAR_PATTERN = re.compile(r"\b(1[89]\d{2}|20[012]\d)\b") | |
| MONTH_YEAR_PATTERN = re.compile( | |
| r"\b(January|February|March|April|May|June|July|" | |
| r"August|September|October|November|December)" | |
| r"\s+(1[89]\d{2}|20[012]\d)\b", | |
| re.IGNORECASE, | |
| ) | |
| HEADERS = {"User-Agent": "FakeNews-XAI/1.0 (UTCN Bachelor Thesis)"} | |
| # section: test cases | |
| # Format: (entity, position, claimed_year, expected_result) | |
| TEST_CASES = [ | |
| # should be CONFIRMED — correct facts | |
| ("Barack Obama", "President", 2012, "confirmed"), | |
| ("Joe Biden", "Senator", 2005, "confirmed"), | |
| ("Hillary Clinton", "Secretary of State", 2011, "confirmed"), | |
| ("Donald Trump", "President", 2019, "confirmed"), | |
| ("Nancy Pelosi", "Speaker", 2019, "confirmed"), | |
| ("Matt Gaetz", "Representative", 2020, "confirmed"), | |
| ("Angela Merkel", "Chancellor", 2015, "confirmed"), | |
| # should detect CONFLICT — wrong years | |
| ("Barack Obama", "President", 2005, "conflict"), | |
| ("Joe Biden", "Senator", 2015, "conflict"), # was VP 2009-2017 | |
| ("Bill Clinton", "President", 2004, "conflict"), # term ended 2001 | |
| ("Matt Gaetz", "Congressman", 2013, "conflict"), # elected 2017 | |
| ("Stacey Abrams", "Governor", 2019, "conflict"), # lost election | |
| ] | |
| class WikipediaResult: | |
| """Result from Wikipedia REST API.""" | |
| entity: str | |
| wiki_title: str | |
| extract: str | |
| years_found: list[int] = field(default_factory=list) | |
| dates_found: list[str] = field(default_factory=list) | |
| def _normalize_name(name: str) -> str: | |
| """Convert entity name to Wikipedia URL format.""" | |
| return name.strip().replace(" ", "_") | |
| def search_wikipedia_title(name: str) -> Optional[str]: | |
| """Search Wikipedia for the best matching article title.""" | |
| try: | |
| resp = requests.get( | |
| WIKIPEDIA_SEARCH, | |
| params={ | |
| "action": "query", | |
| "list": "search", | |
| "srsearch": name, | |
| "srlimit": 1, | |
| "format": "json", | |
| }, | |
| headers=HEADERS, | |
| timeout=REQUEST_TIMEOUT, | |
| ) | |
| data = resp.json() | |
| results = data.get("query", {}).get("search", []) | |
| if results: | |
| return results[0]["title"].replace(" ", "_") | |
| except Exception: | |
| pass | |
| return _normalize_name(name) | |
| def fetch_wikipedia_summary(entity_name: str) -> Optional[WikipediaResult]: | |
| """Fetch Wikipedia summary for an entity and extract temporal info.""" | |
| # try direct URL first | |
| wiki_name = _normalize_name(entity_name) | |
| url = WIKIPEDIA_API.format(name=wiki_name) | |
| try: | |
| resp = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT) | |
| if resp.status_code == 404: | |
| # try search fallback | |
| wiki_name = search_wikipedia_title(entity_name) | |
| if not wiki_name: | |
| return None | |
| url = WIKIPEDIA_API.format(name=wiki_name) | |
| resp = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| except Exception as e: | |
| print(f" [ERROR] Wikipedia fetch failed for '{entity_name}': {e}") | |
| return None | |
| extract = data.get("extract", "").strip() | |
| title = data.get("title", entity_name) | |
| if not extract: | |
| return None | |
| # extract years and dates from the summary | |
| years = sorted(set( | |
| int(y) for y in YEAR_PATTERN.findall(extract) | |
| if 1900 <= int(y) <= 2030 | |
| )) | |
| dates = [ | |
| f"{m.group(1)} {m.group(2)}" | |
| for m in MONTH_YEAR_PATTERN.finditer(extract) | |
| ] | |
| return WikipediaResult( | |
| entity=entity_name, | |
| wiki_title=title, | |
| extract=extract, | |
| years_found=years, | |
| dates_found=dates[:5], | |
| ) | |
| # position synonyms for matching | |
| POSITION_SYNONYMS: dict[str, list[str]] = { | |
| "president": ["president", "presidency", "presidential", "commander"], | |
| "senator": ["senator", "senate", "senatorial"], | |
| "representative": ["representative", "congressman", "congresswoman", "congress", "house"], | |
| "congressman": ["congressman", "congresswoman", "representative", "congress", "house"], | |
| "governor": ["governor", "gubernatorial"], | |
| "speaker": ["speaker", "house"], | |
| "secretary of state": ["secretary", "state department", "foreign"], | |
| "chancellor": ["chancellor", "bundeskanzler"], | |
| "prime minister": ["prime minister", "premier"], | |
| "vice president": ["vice president", "vp"], | |
| } | |
| def _position_in_extract(position: str, extract: str) -> bool: | |
| """Check if a position (or its synonyms) is mentioned in the Wikipedia extract.""" | |
| extract_lower = extract.lower() | |
| position_lower = position.lower() | |
| if position_lower in extract_lower: | |
| return True | |
| synonyms = POSITION_SYNONYMS.get(position_lower, []) | |
| if any(s in extract_lower for s in synonyms): | |
| return True | |
| skip = {"of", "the", "a", "an", "us", "u.s.", "united", "states"} | |
| words = [w for w in position_lower.split() if w not in skip and len(w) > 2] | |
| return any(w in extract_lower for w in words) | |
| def _extract_position_interval(position: str, extract: str) -> tuple[int, int] | None: | |
| """Try to extract explicit start-end interval for a position from Wikipedia text. | |
| Looks for patterns like: | |
| - "served as X from 2009 to 2017" | |
| - "X from 2017 until his resignation in 2024" | |
| - "elected X in 2016" | |
| """ | |
| extract_lower = extract.lower() | |
| position_lower = position.lower() | |
| synonyms = POSITION_SYNONYMS.get(position_lower, [position_lower]) | |
| all_terms = [position_lower] + synonyms | |
| # pattern: "from YEAR to YEAR" or "from YEAR until YEAR" | |
| from_to = re.compile( | |
| r"from\s+(1[89]\d{2}|20[012]\d)\s+(?:to|until|through)\s+(1[89]\d{2}|20[012]\d)" | |
| ) | |
| # pattern: "from YEAR" (open ended) | |
| from_only = re.compile(r"from\s+(1[89]\d{2}|20[012]\d)") | |
| # pattern: "since YEAR" | |
| since = re.compile(r"since\s+(1[89]\d{2}|20[012]\d)") | |
| # pattern: "in YEAR" (point in time) | |
| in_year = re.compile(r"in\s+(1[89]\d{2}|20[012]\d)") | |
| # search in sentences that mention the position | |
| sentences = re.split(r"[.!?]", extract) | |
| for sent in sentences: | |
| sent_lower = sent.lower() | |
| if not any(t in sent_lower for t in all_terms): | |
| continue | |
| # try from-to first | |
| m = from_to.search(sent) | |
| if m: | |
| return int(m.group(1)), int(m.group(2)) | |
| # try from only | |
| m = from_only.search(sent) | |
| if m: | |
| return int(m.group(1)), 2030 # open-ended | |
| # try since | |
| m = since.search(sent) | |
| if m: | |
| return int(m.group(1)), 2030 | |
| # try in YEAR | |
| m = in_year.search(sent) | |
| if m: | |
| y = int(m.group(1)) | |
| return y, y # point in time | |
| return None | |
| def evaluate_result( | |
| result: Optional[WikipediaResult], | |
| position: str, | |
| claimed_year: int, | |
| tolerance: int = 2, | |
| ) -> str: | |
| """Evaluate whether Wikipedia confirms, conflicts, or is inconclusive. | |
| Strategy: | |
| 1. Try to extract explicit "from YEAR to YEAR" interval for the position | |
| 2. If found, check if claimed_year falls within interval | |
| 3. If not found, fall back to year proximity check with ±2 tolerance | |
| """ | |
| if result is None or not result.years_found: | |
| return "not_found" | |
| if not _position_in_extract(position, result.extract): | |
| return "not_found" | |
| # strategy 1: explicit interval extraction | |
| interval = _extract_position_interval(position, result.extract) | |
| if interval: | |
| start, end = interval | |
| if start <= claimed_year <= end + 1: # +1 for transition year | |
| return "confirmed" | |
| elif claimed_year < start - 2 or claimed_year > end + 2: | |
| return "conflict" | |
| return "inconclusive" | |
| # strategy 2: proximity to any year in extract | |
| political_years = [y for y in result.years_found if 1940 <= y <= 2030] | |
| if not political_years: | |
| return "not_found" | |
| if any(abs(y - claimed_year) <= tolerance for y in political_years): | |
| return "confirmed" | |
| closest = min(political_years, key=lambda y: abs(y - claimed_year)) | |
| if abs(closest - claimed_year) >= 3: | |
| return "conflict" | |
| return "inconclusive" | |
| def main(args: argparse.Namespace) -> None: | |
| if args.entity: | |
| # single entity lookup | |
| print(f"\nFetching Wikipedia summary for: '{args.entity}'") | |
| result = fetch_wikipedia_summary(args.entity) | |
| if result: | |
| print(f"Title : {result.wiki_title}") | |
| print(f"Years : {result.years_found}") | |
| print(f"Dates : {result.dates_found}") | |
| print(f"Extract :\n {result.extract[:500]}") | |
| else: | |
| print("No result found.") | |
| return | |
| # run all test cases | |
| print("\n" + "=" * 78) | |
| print(" WIKIPEDIA REST API — VERIFICATION TEST") | |
| print("=" * 78) | |
| print(f"\n {'Entity':<25} {'Position':<22} {'Year':>5} " | |
| f"{'Expected':<12} {'Got':<12} {'Years found'}") | |
| print(f" {'-'*90}") | |
| correct = 0 | |
| total = len(TEST_CASES) | |
| results = [] | |
| for entity, position, year, expected in TEST_CASES: | |
| result = fetch_wikipedia_summary(entity) | |
| got = evaluate_result(result, position, year) | |
| match = "✓" if got == expected else "✗" | |
| if got == expected: | |
| correct += 1 | |
| years_display = result.years_found[:6] if result else [] | |
| print( | |
| f" {match} {entity:<24} {position:<22} {year:>5} " | |
| f"{expected:<12} {got:<12} {years_display}" | |
| ) | |
| results.append({ | |
| "entity": entity, | |
| "position": position, | |
| "claimed_year": year, | |
| "expected": expected, | |
| "got": got, | |
| "match": got == expected, | |
| "years_found": years_display, | |
| "wiki_title": result.wiki_title if result else None, | |
| "extract": result.extract[:300] if result else None, | |
| }) | |
| time.sleep(QUERY_DELAY) | |
| # summary | |
| print(f"\n Accuracy: {correct}/{total} = {correct/total:.1%}") | |
| print(f"\n Breakdown:") | |
| for outcome in ["confirmed", "conflict", "not_found", "inconclusive"]: | |
| n = sum(1 for r in results if r["got"] == outcome) | |
| correct_in = sum(1 for r in results if r["got"] == outcome and r["match"]) | |
| print(f" {outcome:<15}: {n:>2} (correct: {correct_in})") | |
| # save results | |
| out_path = (_PROJECT_ROOT / "evaluation" / "results" | |
| / f"web_search_test_{datetime.now().strftime('%Y-%m-%d')}.json") | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(out_path, "w", encoding="utf-8") as f: | |
| json.dump({ | |
| "generated_at": datetime.now().isoformat(), | |
| "source": "wikipedia_rest_api", | |
| "accuracy": correct / total, | |
| "results": results, | |
| }, f, ensure_ascii=False, indent=2) | |
| print(f"\n Results saved: {out_path}") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Test Wikipedia REST API for temporal fact verification" | |
| ) | |
| parser.add_argument( | |
| "--entity", type=str, default=None, | |
| help="Fetch Wikipedia summary for a single entity (e.g. 'Matt Gaetz')" | |
| ) | |
| args = parser.parse_args() | |
| main(args) |