Skip to main content

Index a PDF library and answer queries with page-accurate citations. Backend-agnostic; ships with a QMD adapter.

Project description

proactive-librarian

Python License: MIT Status

Turn a directory of PDFs into a searchable index and get back page-accurate citations like Report.pdf:p.27.

proactive-librarian indexes a large document library once, then answers queries with the exact file and page number — ready to paste into a memo, brief, paper, or deck. It is a sharp, single-purpose CLI tool: not a chatbot, not a RAG framework, not a vector database you have to operate. Backend-agnostic, with a QMD adapter shipped in the box.

It replaces the brutal friction of citing your own research at scale — folder grep is slow, filenames lie, and Ctrl+F across thousands of PDFs is unworkable.

Features

  • 📄 Page-accurate citations — results come back as file.pdf:p.N, the page number derived from the cache path, not parsed (and mis-parsed) from a snippet.
  • 🔍 Hybrid retrieval — keyword + semantic search via the backend (QMD adapter included; bring your own).
  • ♻️ Deterministic, reproducible cache — rebuildable from the PDFs at any time; safe to delete.
  • 🧱 Page-per-file layout — pure page text with no metadata leakage into snippets (see why).
  • 🔐 Robust ingest — handles AES-encrypted PDFs, sanitises surrogate/4-byte characters, and logs every failure with a reason to _failures.json.
  • 🗂️ Optional taxonomy enforcement — constrain the library to a known subject/geography scheme and fail loudly on stragglers.
  • Incremental — sha1 manifest means re-ingest only touches changed PDFs.
  • Tested — 36+ pytest cases over hashing, path mapping, manifest persistence, atomic writes, failure logging, and taxonomy validation.

Who it's for

Built originally for one person's research-citation workflow, but it solves a general problem — anyone who maintains a large PDF/document library and needs to quote it with precision:

  • researchers and analysts citing a corpus of papers/reports,
  • policy, legal, and compliance teams working from regulatory documents,
  • technical writers and maintainers grounding docs in source material,
  • AI agents/assistants that need citation-grounded retrieval they can shell out to (librarian query "…").

Install

Not yet on PyPI. Install from source:

pip install git+https://github.com/davidorban/proactive-librarian
# or, for local development:
git clone https://github.com/davidorban/proactive-librarian
cd proactive-librarian
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

You also need a search backend. The shipped adapter targets QMD — install it from https://github.com/eatonphil/qmd (or implement another backend; see ADR-001).

Quickstart

# 1. Point at your PDF library
cat > proactive-librarian.yaml <<'EOF'
pdf_root: /path/to/your/pdfs
collection_name: research
EOF

# 2. Index (first run; ~30 min for ~1,400 PDFs, then incremental)
librarian ingest --full

# 3. Tell your backend about the new cache (one-time; see backend docs)

# 4. Query
librarian query "stablecoin regulation in the UAE"

Example output:

1. Digital-Assets-Regulatory-Landscape.pdf:p.27   (0.81)
   "...the payment-token services regulation establishes a licensing regime for fiat-backed..."
2. ADGM-Virtual-Asset-Framework-2025.pdf:p.12      (0.74)
   "...issuers must maintain reserves on a 1:1 basis, audited monthly by an approved..."
3. Stablecoin-Market-Structure.pdf:p.4             (0.69)
   "...redemption-at-par guarantees are the load-bearing assumption behind..."

Paste the file.pdf:p.N straight into your draft.

What it produces

<pdf_root>/.derived/
├── _manifest.json                    # sha1 cache for incremental re-ingest
├── _failures.json                    # last run's failed PDFs (with reasons)
└── <rel-pdf-stem-tree>/
    ├── _meta.json                    # per-PDF metadata sidecar (NOT indexed)
    ├── p0001.md                      # pure page text — no frontmatter
    ├── p0002.md
    └── ...

The cache lives next to the PDFs in .derived/ (gitignored), is reproducible from the source PDFs, and is safe to delete and rebuild. The PDFs stay canonical; the markdown is a disposable index (ADR-004).

Why page-per-file?

Single-file-per-PDF chunking has two failure modes:

  1. Metadata leaks into snippets. YAML frontmatter gets indexed with the content, so citation snippets end up containing sha1: abc… and source_pdf: ….
  2. Page numbers become unreliable. When a chunk spans page boundaries, parsing ## Page N markers collapses everything to p.1 and citations lose precision.

Page-per-file fixes both: each markdown file holds only one page's text (nothing to leak), and the page number is a property of the path (p0042.md → page 42). The trade-off is a high file count (~1,377 PDFs × ~50 pages ≈ 67k files); modern filesystems (APFS, ext4) handle it fine. Full rationale in ADR-002.

Configuration

proactive-librarian.yaml in the working directory, or --config /path/to/config.yaml:

# Required
pdf_root: /path/to/pdfs

# Optional (defaults shown)
derived_dir: .derived              # relative to pdf_root
collection_name: research          # backend collection key

taxonomy:                          # optional subject-category enforcement
  enabled: false
  allowed_subjects: []             # first path component under pdf_root must match

backend:
  type: qmd
  binary: qmd                      # on PATH, or an absolute path
  timeout_seconds: 30

With no config file present, it runs with permissive defaults (pdf_root: ., no taxonomy). See examples/ for annotated config and taxonomy files.

Architecture

Five load-bearing decisions, each documented as an ADR:

ADR Decision
ADR-001 QMD as the search substrate, not a standalone vector DB
ADR-002 Page-per-file granularity, not paragraph chunks
ADR-003 Explicit CLI invocation, not an ambient hook (yet)
ADR-004 PDFs canonical; markdown is a disposable cache
ADR-005 A Python package + adapter, not a long-running service

Development

pip install -e ".[dev]"
pytest          # 36+ cases
ruff check .    # lint

Roadmap

  • Additional backends beyond QMD (the adapter boundary already exists).
  • Publish to PyPI for a plain pip install proactive-librarian.
  • Optional ambient/agent invocation surface (the explicit-CLI decision in ADR-003 is revisitable as adoption grows).
  • Richer query output formats (JSON, BibTeX-style citation export).

Contributing

Issues and pull requests welcome. The codebase is small and well-tested — run pytest and ruff check . before opening a PR. If you're adding a backend, start from the QMD adapter and the ADR-001 rationale.

License

MIT © David Orban.

Status

v0.1.0 — in single-user production use; the API will stabilise as more backends and surfaces are added. The cache format and CLI are stable. See CHANGELOG.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

proactive_librarian-0.1.0.tar.gz (21.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

proactive_librarian-0.1.0-py3-none-any.whl (17.3 kB view details)

Uploaded Python 3

File details

Details for the file proactive_librarian-0.1.0.tar.gz.

File metadata

  • Download URL: proactive_librarian-0.1.0.tar.gz
  • Upload date:
  • Size: 21.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for proactive_librarian-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8929c9d789d0374656955a898beaad507b58fd9c2cbacbb96edd25316e4705e1
MD5 5ba65758c06185ad97e03ae69eb432c8
BLAKE2b-256 cafeb8815e1865e969a49a69ce87de7718108fb941fe079e151a31ae0165592c

See more details on using hashes here.

File details

Details for the file proactive_librarian-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for proactive_librarian-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1f1191f3f8ce6c6f1147576a359a64b30bf82a5fa4fbc1e7700eb74024da945b
MD5 2c7e00dc278f374c4f0dc3adc069e6a1
BLAKE2b-256 f89c8f1b1b16118239da666a8e316c64c604a96071955a8af20a525e92518626

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page