reference-audit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reference_audit/__init__.py +8 -0
- reference_audit/alignmentcheck.py +136 -0
- reference_audit/bookcheck.py +98 -0
- reference_audit/cache/__init__.py +1 -0
- reference_audit/cache/db.py +108 -0
- reference_audit/cache/store.py +128 -0
- reference_audit/cli.py +190 -0
- reference_audit/config.py +179 -0
- reference_audit/fieldcheck.py +599 -0
- reference_audit/inputs.py +96 -0
- reference_audit/llm/__init__.py +6 -0
- reference_audit/llm/client.py +85 -0
- reference_audit/llm/prompts.py +206 -0
- reference_audit/llm/schemas.py +38 -0
- reference_audit/matching/__init__.py +6 -0
- reference_audit/matching/adjudicate.py +63 -0
- reference_audit/matching/features.py +185 -0
- reference_audit/matching/names.py +238 -0
- reference_audit/matching/pool.py +344 -0
- reference_audit/matching/sameobject.py +150 -0
- reference_audit/matching/scoring.py +75 -0
- reference_audit/matching/verdict.py +56 -0
- reference_audit/matching/webcheck.py +189 -0
- reference_audit/models.py +412 -0
- reference_audit/parsing/__init__.py +1 -0
- reference_audit/parsing/bib.py +392 -0
- reference_audit/parsing/context.py +55 -0
- reference_audit/parsing/entities.py +22 -0
- reference_audit/parsing/identifiers.py +165 -0
- reference_audit/parsing/latex.py +83 -0
- reference_audit/parsing/tex.py +159 -0
- reference_audit/pdf/__init__.py +16 -0
- reference_audit/pdf/embedded.py +117 -0
- reference_audit/pdf/grobid.py +234 -0
- reference_audit/pdf/tei.py +470 -0
- reference_audit/pipeline.py +1692 -0
- reference_audit/report.py +330 -0
- reference_audit/sources/__init__.py +6 -0
- reference_audit/sources/arxiv.py +81 -0
- reference_audit/sources/base.py +54 -0
- reference_audit/sources/clickhouse.py +575 -0
- reference_audit/sources/crossref.py +90 -0
- reference_audit/sources/dblp.py +140 -0
- reference_audit/sources/google_books.py +101 -0
- reference_audit/sources/http.py +260 -0
- reference_audit/sources/normalize.py +464 -0
- reference_audit/sources/openalex.py +93 -0
- reference_audit/sources/openlibrary.py +158 -0
- reference_audit/sources/publisher.py +186 -0
- reference_audit/sources/registry.py +117 -0
- reference_audit/sources/render.py +115 -0
- reference_audit/sources/semantic_scholar.py +68 -0
- reference_audit/sources/titlewords.py +31 -0
- reference_audit/sources/web.py +256 -0
- reference_audit/versioning.py +125 -0
- reference_audit-0.1.0.dist-info/METADATA +715 -0
- reference_audit-0.1.0.dist-info/RECORD +60 -0
- reference_audit-0.1.0.dist-info/WHEEL +4 -0
- reference_audit-0.1.0.dist-info/entry_points.txt +2 -0
- reference_audit-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Runtime configuration for the reference auditor.
|
|
2
|
+
|
|
3
|
+
All settings load from the environment / `.env` (pydantic-settings). Secrets live only in
|
|
4
|
+
`.env` (git-ignored); never hard-code keys. The LLM model defaults to `gpt-6-luna` per
|
|
5
|
+
the README but is configurable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import AliasChoices, Field
|
|
14
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuditConfig(BaseSettings):
|
|
18
|
+
"""Central config: LLM, data-source credentials, matching thresholds, cache."""
|
|
19
|
+
|
|
20
|
+
model_config = SettingsConfigDict(
|
|
21
|
+
env_file=".env",
|
|
22
|
+
env_file_encoding="utf-8",
|
|
23
|
+
extra="ignore",
|
|
24
|
+
protected_namespaces=(),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# --- LLM (OpenAI SDK) ---
|
|
28
|
+
model: str = "gpt-6-luna"
|
|
29
|
+
# None ⇒ omit `temperature` and use the model default. gpt-6-luna rejects any value but its default
|
|
30
|
+
# (1), so a fixed 0.0 would fail every call; set LLM_TEMPERATURE=0 for models that accept it.
|
|
31
|
+
llm_temperature: float | None = None
|
|
32
|
+
openai_api_key: str | None = Field(default=None, alias="OPENAI_API_KEY")
|
|
33
|
+
openai_base_url: str | None = Field(default=None, alias="OPENAI_BASE_URL")
|
|
34
|
+
|
|
35
|
+
# --- Data-source credentials (all already present in .env) ---
|
|
36
|
+
crossref_mailto: str = Field(
|
|
37
|
+
default="reference-audit@example.org", alias="CROSSREF_MAILTO"
|
|
38
|
+
)
|
|
39
|
+
s2_api_key: str | None = Field(default=None, alias="S2_API_KEY")
|
|
40
|
+
ncbi_api_key: str | None = Field(default=None, alias="NCBI_API_KEY")
|
|
41
|
+
nasa_ads_api_key: str | None = Field(default=None, alias="NASA_ADS_API_KEY")
|
|
42
|
+
core_api_key: str | None = Field(default=None, alias="CORE_API_KEY")
|
|
43
|
+
unpaywall_email: str | None = Field(
|
|
44
|
+
default=None, alias="PAPER_SEARCH_MCP_UNPAYWALL_EMAIL"
|
|
45
|
+
)
|
|
46
|
+
openlibrary_email: str | None = Field(default=None, alias="OPENLIBRARY_EMAIL")
|
|
47
|
+
google_books_api_key: str | None = Field(default=None, alias="GOOGLE_BOOKS_API_KEY")
|
|
48
|
+
|
|
49
|
+
# --- Source backend: where Semantic Scholar, OpenAlex and DBLP are read from ---
|
|
50
|
+
# "api": their public APIs (rate-limited; needs no setup). "clickhouse": a local ClickHouse mirror
|
|
51
|
+
# of the three databases (no rate limits; must carry the full-text title indexes, see README).
|
|
52
|
+
# Crossref, arXiv, Open Library, Google Books and the web/publisher fetches are API-only.
|
|
53
|
+
source_backend: Literal["api", "clickhouse"] = Field(
|
|
54
|
+
default="api", validation_alias=AliasChoices("SOURCE_BACKEND", "source_backend")
|
|
55
|
+
)
|
|
56
|
+
clickhouse_host: str = Field(default="127.0.0.1", alias="CLICKHOUSE_HOST")
|
|
57
|
+
clickhouse_port: int = Field(default=8123, alias="CLICKHOUSE_PORT")
|
|
58
|
+
clickhouse_user: str = Field(default="default", alias="CLICKHOUSE_USER")
|
|
59
|
+
clickhouse_password: str = Field(
|
|
60
|
+
default="",
|
|
61
|
+
validation_alias=AliasChoices("CLICKHOUSE_DEFAULT_USER_PASSWORD", "CLICKHOUSE_PASSWORD"),
|
|
62
|
+
)
|
|
63
|
+
clickhouse_s2_db: str = Field(
|
|
64
|
+
default="s2ag", validation_alias=AliasChoices("CLICKHOUSE_S2_DB", "CLICKHOUSE_DB")
|
|
65
|
+
)
|
|
66
|
+
clickhouse_openalex_db: str = Field(default="openalex", alias="CLICKHOUSE_OPENALEX_DB")
|
|
67
|
+
clickhouse_dblp_db: str = Field(default="kb", alias="CLICKHOUSE_DBLP_DB")
|
|
68
|
+
clickhouse_concurrency: int = 8 # queries in flight against the local server
|
|
69
|
+
clickhouse_query_timeout: float = 60.0 # seconds; a slower query is an error, never "not found"
|
|
70
|
+
|
|
71
|
+
# --- Matching thresholds (calibrated against the pilot; see plan risk #3) ---
|
|
72
|
+
title_accept: float = 0.92 # auto_accept title floor (entry has an identifier)
|
|
73
|
+
title_backfill: float = 0.95 # stricter title floor when entry has NO identifier
|
|
74
|
+
author_accept: float = 0.80 # auto_accept author-overlap floor
|
|
75
|
+
composite_reject: float = 0.40 # auto_reject ceiling
|
|
76
|
+
prefix_trap_tail_jaccard: float = 0.34 # V3: tail-token Jaccard below this ⇒ distinct
|
|
77
|
+
author_set_distinct_jaccard: float = 0.60 # V4: author-set Jaccard below this ⇒ distinct
|
|
78
|
+
|
|
79
|
+
# --- LLM adjudication ---
|
|
80
|
+
use_llm: bool = True # CLI --no-llm sets this False (deterministic CI)
|
|
81
|
+
llm_concurrency: int = 8
|
|
82
|
+
llm_max_candidates: int = 8 # cap CAN_CORRESPOND calls per entry (cost control)
|
|
83
|
+
|
|
84
|
+
# --- Step 3: field correctness ---
|
|
85
|
+
check_fields: bool = True # verify each field of an exactly-one match is correct
|
|
86
|
+
# A cited author list that names only some of the work's authors without saying so ('and
|
|
87
|
+
# others'): `ignore` it, `warn` (an `uncertain` author finding) or report an `error`. Many
|
|
88
|
+
# bibliographies shorten long lists; HALLMARK counts an unmarked omission as a hallucination.
|
|
89
|
+
# CLI --partial-authors.
|
|
90
|
+
partial_authors: Literal["ignore", "warn", "error"] = Field(
|
|
91
|
+
default="warn", alias="PARTIAL_AUTHORS"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# --- Citation alignment (advisory; opt-in) ---
|
|
95
|
+
# Compare each citing context against the cited work's abstract. Off by default: it needs the LLM
|
|
96
|
+
# and an abstract, and adds a per-citation cost. CLI --check-citations enables it.
|
|
97
|
+
check_alignment: bool = False
|
|
98
|
+
|
|
99
|
+
# --- Web artifacts (URL-only @misc): HTML-metadata check before the LLM fallback ---
|
|
100
|
+
web_title_accept: float = 0.85 # page meta-title vs cited-title floor for a deterministic confirm
|
|
101
|
+
|
|
102
|
+
# --- Web artifacts: headless rendering of client-side-rendered (single-page-app) pages ---
|
|
103
|
+
web_render_enabled: bool = True # render SPA shells in a headless browser before judging
|
|
104
|
+
web_render_browser_path: str | None = Field(default=None, alias="WEB_RENDER_BROWSER_PATH")
|
|
105
|
+
web_render_timeout: float = 30.0 # seconds before a render is treated as a (retryable) error
|
|
106
|
+
web_render_virtual_time_ms: int = 15000 # JS virtual-time budget given to the headless browser
|
|
107
|
+
|
|
108
|
+
# --- PDF input (GROBID) ---
|
|
109
|
+
# The service is NOT managed by this package. Point this at a running instance; a PDF input with
|
|
110
|
+
# no reachable GROBID is a reported failure, never a silently empty reference list.
|
|
111
|
+
# podman run -d --replace --name grobid -p 8070:8070 docker.io/grobid/grobid:0.9.1-crf
|
|
112
|
+
# Intentionally NOT declared with alias="GROBID_URL": a non-aliased field already reads its
|
|
113
|
+
# upper-cased env var, whereas an aliased one silently ignores by-name construction here
|
|
114
|
+
# (`populate_by_name` is not set), which would make AuditConfig(grobid_url=...) a no-op in tests.
|
|
115
|
+
grobid_url: str = "http://localhost:8070"
|
|
116
|
+
# A PDF that embeds the BibTeX records of its cited works (`pdf.embedded`, e.g. one made with the
|
|
117
|
+
# AI4X template) is audited from them, without GROBID. CLI --ignore-embedded turns this off.
|
|
118
|
+
use_embedded_references: bool = True
|
|
119
|
+
|
|
120
|
+
# --- Cache / pipeline ---
|
|
121
|
+
cache_path: Path | None = None # default: <bib_dir>/.reference_audit/cache.db
|
|
122
|
+
# Bump when thresholds/prompts/rules change, or when a new verdict-producing path lands.
|
|
123
|
+
# 0.16: PDF input via GROBID — TEI entry-type inference changes source routing, and an entry with
|
|
124
|
+
# neither a title nor an identifier is now reported unresolved instead of being searched on
|
|
125
|
+
# (a title-less query could previously score an arbitrary paper as a match).
|
|
126
|
+
# 0.17: default LLM model gpt-5.4-mini → gpt-6-luna, and temperature is no longer pinned to 0.0
|
|
127
|
+
# (model default unless LLM_TEMPERATURE is set) — adjudication behavior changes.
|
|
128
|
+
# 0.18: DBLP is queried through its SPARQL endpoint (QLever word search, shortest title first)
|
|
129
|
+
# instead of the search API, which now serves automated clients a bot-challenge page. DBLP's
|
|
130
|
+
# candidates and their ranking change, and DBLP no longer errors on every query, so entries
|
|
131
|
+
# that were left unresolved can now reach a verdict.
|
|
132
|
+
# 0.19: a second source backend — Semantic Scholar, OpenAlex and DBLP read from a local ClickHouse
|
|
133
|
+
# mirror (full-text title index, all title words required, shortest title first). The
|
|
134
|
+
# verdict cache is now also keyed by backend.
|
|
135
|
+
# 0.20: the local ClickHouse title search retries with each word left out when no all-words hit is
|
|
136
|
+
# a near-exact title, and OpenAlex rows gain their best open-access location as a version
|
|
137
|
+
# link (candidates change). A cited DOI the matched work does not carry is checked and
|
|
138
|
+
# reported as a `doi` field finding; an unresolved entry records why; a `none` on a local
|
|
139
|
+
# snapshot that may predate the work carries a coverage caveat.
|
|
140
|
+
# 0.21: HTML character references (`d'Amore`, `&`) are decoded in .bib fields and in
|
|
141
|
+
# author/title normalization, so author checks and title scores change for such entries.
|
|
142
|
+
# 0.22: the HALLMARK run's work list. Field checks compare against the version the entry cites
|
|
143
|
+
# (the pooled record keeps its member records); a pooled venue is never a preprint
|
|
144
|
+
# server while a member names the venue; pooling no longer fuses a paper with a journal
|
|
145
|
+
# extension that has other authors; the field-check LLM sees the matched work, not the
|
|
146
|
+
# entry, as context; authors are checked person by person against every source, as an
|
|
147
|
+
# `author` field finding (with the partial-list option); the cited DOI is checked with the
|
|
148
|
+
# doi.org Handle API, which answers 404 for an unregistered prefix.
|
|
149
|
+
# 0.23: regressions the 0.22 run exposed. A field's canonical value is the one most sources of the
|
|
150
|
+
# cited version agree on (an OpenAlex title defect won on source rank); a pooled record's
|
|
151
|
+
# author list comes from the most reliable source, not the citation-richest (S2's defective
|
|
152
|
+
# list made the LLM reject a real paper); members are deduplicated; given-name forms
|
|
153
|
+
# ('Tim' / 'Timothy') and dropped middle initials match; the field-check prompt calls a
|
|
154
|
+
# different journal or conference series an error (the LLM hedged on ACL vs ICLR).
|
|
155
|
+
# 0.24: bibtexparser 2. Field values are read by `parsing.bib` itself and LaTeX is decoded with
|
|
156
|
+
# pylatexenc (`parsing.latex`), so some values change: `\url{x}` is `x` (v1: `r̆lx`),
|
|
157
|
+
# `\textit{X}` is `X`, `\href{u}{X}` is `X <u>`. An undefined `@string`, a duplicate field or
|
|
158
|
+
# an undecodable value now makes only that entry unparsed (v1 failed the whole file on an
|
|
159
|
+
# undefined `@string` and kept the first of duplicate fields); commented twins are read
|
|
160
|
+
# from the uncommented text.
|
|
161
|
+
# 0.25: HTML character references in source values are decoded (`SourceRecord`), as they already
|
|
162
|
+
# were in `.bib` values: a Crossref title "Allocation & Regularization" no longer folds
|
|
163
|
+
# to the word "amp", so it is settled by rule instead of going to the field-check LLM, and
|
|
164
|
+
# the canonical value is reported decoded.
|
|
165
|
+
# 0.26: a cited OpenAlex Work id is looked up on its own even when the entry also carries a DOI or
|
|
166
|
+
# arXiv id (OpenAlex was queried by those instead, so the Work id was never checked), and
|
|
167
|
+
# can then be pinned as the match; a cited DOI that is another Work is reported by the
|
|
168
|
+
# cited-DOI check. A cited Work id that resolves to a different work, to nothing, or could
|
|
169
|
+
# not be looked up is now reported as an issue, whatever the verdict.
|
|
170
|
+
pipeline_version: str = "0.26"
|
|
171
|
+
|
|
172
|
+
def llm_enabled(self) -> bool:
|
|
173
|
+
return self.use_llm and bool(self.openai_api_key)
|
|
174
|
+
|
|
175
|
+
def resolved_mailto(self) -> str:
|
|
176
|
+
"""Crossref polite-pool contact; fall back to the Unpaywall email if set."""
|
|
177
|
+
if self.crossref_mailto and self.crossref_mailto != "reference-audit@example.org":
|
|
178
|
+
return self.crossref_mailto
|
|
179
|
+
return self.unpaywall_email or self.crossref_mailto
|