#!/usr/bin/env python3 """ Independently cross-verify a single candidate citation. This is the anti- hallucination check: it never trusts a single source. If a check cannot be run at all (e.g. no network), that check is reported as "skipped" -- never silently counted as a pass. CLI usage: python3 verify_citation.py --title "Compensation of magnetic ..." \\ --arxiv-id 2401.12345 --doi 10.1109/TGRS.2024.1234567 Importable: from verify_citation import verify """ import sys import json import argparse import difflib import urllib.request import urllib.parse import urllib.error import xml.etree.ElementTree as ET ATOM_NS = {"atom": "http://www.w3.org/2005/Atom"} def title_similarity(a, b): if not a or not b: return 0.0 return difflib.SequenceMatcher(None, a.lower().strip(), b.lower().strip()).ratio() def check_arxiv_id(arxiv_id, timeout=20): """Confirm an arXiv ID actually resolves to a real paper.""" try: url = f"http://export.arxiv.org/api/query?id_list={urllib.parse.quote(arxiv_id)}" with urllib.request.urlopen(url, timeout=timeout) as resp: data = resp.read() root = ET.fromstring(data) entry = root.find("atom:entry", ATOM_NS) if entry is None: return {"status": "fail", "reason": "arXiv ID not found"} title_el = entry.find("atom:title", ATOM_NS) title = " ".join(title_el.text.split()) if title_el is not None else None return {"status": "pass", "canonical_title": title} except Exception as e: return {"status": "skipped", "reason": str(e)} def check_doi(doi, timeout=20): """Confirm a DOI actually resolves via Crossref.""" try: url = f"https://api.crossref.org/works/{urllib.parse.quote(doi)}" req = urllib.request.Request( url, headers={"User-Agent": "literature-search-verify-skill/1.0"} ) with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read()) titles = data.get("message", {}).get("title") or [] return {"status": "pass", "canonical_title": titles[0] if titles else None} except urllib.error.HTTPError as e: if e.code == 404: return {"status": "fail", "reason": "DOI not found in Crossref"} return {"status": "skipped", "reason": f"HTTP {e.code}"} except Exception as e: return {"status": "skipped", "reason": str(e)} def check_title_cross_source(title, timeout=20): """Independently re-search by title on a different source (Semantic Scholar) and require a near-exact title match. This is what catches a plausible-sounding but entirely invented title/author combination.""" try: params = {"query": title, "limit": 3, "fields": "title"} url = f"https://api.semanticscholar.org/graph/v1/paper/search?{urllib.parse.urlencode(params)}" with urllib.request.urlopen(url, timeout=timeout) as resp: data = json.loads(resp.read()) candidates = data.get("data", []) or [] if not candidates: return {"status": "fail", "reason": "no matching title found on Semantic Scholar"} best = max(candidates, key=lambda p: title_similarity(title, p.get("title", ""))) sim = title_similarity(title, best.get("title", "")) if sim >= 0.9: return {"status": "pass", "similarity": round(sim, 3), "matched_title": best.get("title")} return {"status": "fail", "similarity": round(sim, 3), "matched_title": best.get("title")} except Exception as e: return {"status": "skipped", "reason": str(e)} def verify(title=None, arxiv_id=None, doi=None): checks = {} if arxiv_id: checks["arxiv_id_check"] = check_arxiv_id(arxiv_id) if doi: checks["doi_check"] = check_doi(doi) if title: checks["title_cross_source_check"] = check_title_cross_source(title) passed = [c for c in checks.values() if c["status"] == "pass"] failed = [c for c in checks.values() if c["status"] == "fail"] if failed: verdict = "suspect" # something actively contradicted it elif passed: verdict = "verified" # at least one independent check passed else: verdict = "unverified" # everything skipped (e.g. no network) -- NOT the same as verified return {"title": title, "arxiv_id": arxiv_id, "doi": doi, "verdict": verdict, "checks": checks} if __name__ == "__main__": ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--title", default=None) ap.add_argument("--arxiv-id", default=None) ap.add_argument("--doi", default=None) args = ap.parse_args() if not any([args.title, args.arxiv_id, args.doi]): print(json.dumps({"error": "provide at least one of --title/--arxiv-id/--doi"})) sys.exit(1) result = verify(args.title, args.arxiv_id, args.doi) print(json.dumps(result, ensure_ascii=False, indent=2))