FakeNews-XAI / scripts /test_web_search.py
Marius16's picture
Testes + refactor
f22b61b
Raw
History Blame Contribute Delete
12.5 kB
"""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
]
@dataclass
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)