fix: validate PDF content and filter phantom papers from fallback chain - #93
fix: validate PDF content and filter phantom papers from fallback chain#93heliowap wants to merge 1 commit into
Conversation
Three complementary fixes for the 'phantom paper' bug where search and
download fallbacks returned unrelated or non-citable items as if they
were papers.
Fix 1 — PDF content verification (server.py):
After downloading a PDF via fallback, extract text from the first 3
pages and check token overlap with the expected title (>=40%) or DOI
verbatim match. Reject and continue to next fallback when mismatch.
Catches the case where Unpaywall/CORE/OpenAIRE resolve to a wrong
document with the same DOI/title hint.
Fix 2 — CrossRef non-paper type filtering (crossref.py):
CrossRef returns peer-review material, figures, dataset components,
and other sub-components with real DOIs. These pollute search results
as 'phantom papers' that have a DOI but no citable content. Added
NON_PAPER_TYPES denylist and short-circuit in _parse_crossref_item.
Fix 3 — Repository fallback title matching (server.py):
Before downloading a candidate from a fallback repository, check
title similarity (difflib SequenceMatcher, threshold 0.6). Skip
candidates whose title is topically unrelated to the requested paper.
Prevents the gross-mismatch case (e.g. solar-cell PDF returned for a
myodural-bridge search).
Tests:
- 4 new tests in test_crossref.py covering the type filter
- 14 new tests in test_fallback.py covering _title_similarity,
_pdf_matches_expected, title-matching in fallback, and backward
compatibility for numeric paper_id (issue openags#57)
- Regression test using real PDF fixtures reproduces the original
phantom-PDF bug (chemistry PDF returned for myodural-bridge query)
and confirms it is now rejected
Validation:
- 30/30 tests in test_fallback.py + test_crossref.py pass
- Full suite: 145 tests, 0 regressions (2 pre-existing errors in
biorxiv/medrxiv download tests due to 403 from upstream, unrelated)
- End-to-end smoke test: download_with_fallback for PMC10912660 now
logs 'mismatch (sim=0.18)' for the wrong chemistry PDF, skips it,
and returns the correct myodural-bridge paper PDF
|
Independent confirmation of the same failure mode, from a different source in the chain. On v0.1.4 this call returned a file and reported success: download_with_fallback(
source="semantic",
paper_id="DOI:10.3233/jsa-200411",
doi="10.3233/jsa-200411",
title="A football player rating system",
)What landed on disk was Your description names OpenAIRE and Europe PMC, so CORE is one more source that produces this. The part that worries me is not the wrong file, it is that the call reported success. We only noticed because we read the first page of every PDF before citing it. In a literature review that file would have been cited as Wolf et al. 2020 and nobody would have looked again. With this PR applied the same call returns an explicit failure and writes nothing. Tested on a branch with #93 and #96 merged together, both apply cleanly, 76 tests pass in |
Problem
The MCP fallback chain (search results + download_with_fallback) currently returns unrelated or non-citable items as if they were papers. Two failure modes observed in production:
CrossRef returns sub-components as papers: a search for
myodural bridgereturns entries likeReview for "The morphology of the suboccipital region"(type=peer-review) andFigure 5: The myodural bridge(type=figure) as if they were citable papers. These have real DOIs but are not papers.Repository/Unpaywall fallback returns wrong PDF: when the primary downloader fails,
_try_repository_fallbackand the Unpaywall resolver pick the first paper with apdf_urland download it — without checking that the PDF actually corresponds to the requested paper. Reproduction:download_with_fallback('europepmc', 'PMC10912660', doi='10.1038/s41598-024-55069-7', title='Evidence for chronic headaches induced by pathological changes of myodural bridge complex')returned a PDF about solar-cell chemistry ([NH3(CH2)2NH3]CuBr4).This is the root cause of the 'phantom paper' bug class where LLMs cite papers that don't exist or attribute wrong content to real PMIDs/DOIs.
Fix — three complementary changes
Fix 1: PDF content verification (
server.py)After downloading a PDF via
_download_from_url, extract text from the first 3 pages and check:When mismatch, the PDF is deleted and
Noneis returned, so the caller continues to the next fallback. Conservative thresholds chosen to tolerate editorial front-matter that pushes title tokens to page 2–3, while still rejecting gross mismatches (a chemistry paper will not containmyodural,bridge,headache,chronic,pathological).Fix 2: CrossRef non-paper type filtering (
crossref.py)Added
NON_PAPER_TYPESdenylist covering:peer-review,peer-review-material,review,component,figure,dataset,report,report-component,standard,standard-series._parse_crossref_itemshort-circuits toNonefor these types, so they are excluded from search results. Real citable types (journal-article,book-chapter,posted-content, etc.) pass through unchanged.Fix 3: Repository fallback title matching (
server.py)Before downloading a candidate from a fallback repository (OpenAIRE/CORE/Europe PMC/PMC), check title similarity via
difflib.SequenceMatcher(threshold 0.6, case-insensitive, whitespace-normalized). Skip candidates whose title is topically unrelated to the requested paper. Prevents the gross-mismatch case where a repo search for a DOI returns a topically-unrelated hit.Backward compatibility
_download_from_urland_try_repository_fallbackgained optionalexpected_title/expected_doiparams. Existing callers (and existing tests) that don't pass them continue to work — no filtering is applied when no title claim is made.download_with_fallbackpropagates its existingtitlearg asexpected_titleto both helpers, so the validation is active by default without changing the public tool signature.paper_id) preserved and passing.Tests
test_crossref.py: +4 tests covering the type filter (peer-review/figure/dataset rejected, journal-article passes, denylist covers observed phantom types).test_fallback.py: +14 tests covering_title_similarity,_pdf_matches_expected(including a regression test using a real PDF fixture that reproduces the chemistry-vs-myodural mismatch), title-matching in fallback (dissimilar skipped, similar downloaded, empty-title backward-compat), and propagation ofexpected_titlefromdownload_with_fallback.assertIn→any(... in ...)for author substring).Validation
Files changed