Skip to content

Repository files navigation

Provenote

A local-first research assistant that answers questions about your own documents (PDF, EPUB, HTML, DOCX, Markdown) with inline, page-level citations, and measures whether those answers are any good. Hybrid retrieval (BM25 + vector + cross-encoder rerank) over Chroma and SQLite on your disk; Claude API or fully-local Ollama for generation.

A fluent answer with a confident citation is not the same as a correct one. So every answer carries a provenance record, separates what your sources say from what the model infers, and can be re-graded by a separate reviewer. The RAG techniques here are established ones; what this adds is the integrity layer and the measurement behind it.

Provenote demo: ask a question, watch a cited answer stream in, open the passage behind a citation, filter the library, read a document as five ordered blocks, open one of its figures, then explore the concept graph

Why it's built this way

  • Settings are locked by experiment, not intuition. TOP_K, parent-child retrieval, chunk sizes and the BM25/vector mix were each chosen by measuring alternatives with the in-repo eval harness. What didn't make the cut is recorded too, in docs/decisions.md.
  • Benchmarks anyone can re-run. The headline numbers come from a public corpus pinned by arXiv ID and SHA-256, fetched by a script, reported with variance and caveats (evals/).
  • Growth by addition. Every derived layer (citations, figures, tables, keywords, wiki, concept graph) is an idempotent sidecar that never mutates the chunk store. New capability is a new module, not a rewrite.

What it does

  • Grounded answers with inline citations. Page numbers and sections, every passage inspectable.
  • Evidence vs. interpretation. Each answer separates what your sources say from the model's synthesis, with per-claim grounding markers you can accept, reject or edit, so an inference is never mistaken for a fact (how answers work).
  • Citation and concept graphs. Resolved reference edges, plus a deterministic concept skeleton (the LLM only annotates existing edges, it never invents structure) with gap detection that surfaces single-source concepts and thin bridges as leads to read next.
  • Knowledge-currency markers — built, and currently opt-in. Advisory contested and superseded trend chips derived from cross-document stance and publication years; they inform, they never gate. They ship off (EPISTEMICS_MARKERS_ENABLED=true enables them) because the stance pass behind them judges without seeing the document text — see Limitations.
  • Library workspace. Browsable grid with filters and folders; each document opens as five ordered blocks — metadata, connections, passages, figures, references — with the full bibliography and the figures extracted from the paper, readable at full size. Editable metadata that survives re-ingest, safe delete (OS trash first), selective ingestion, derived corpus wiki.
  • A chat history you can keep tidy. Conversations are searchable and renameable, exportable as one markdown file, and removable in bulk — a soft delete that the same control undoes.
  • Measurable quality. Eval harness with six scorers (deterministic plus LLM judge), DuckDB result store, per-turn cost tracking.

Architecture

flowchart LR
    subgraph Ingest
        A["data/sources/<br/>PDF · EPUB · HTML · DOCX · MD"] --> B["extract to markdown<br/>(cached, page-marked)"]
        B --> C["chunk<br/>(parent-child + baseline)"]
        C --> D["embed<br/>(bge-base, local)"]
    end
    D --> E[("Chroma ×2<br/>vector stores")]
    C --> F[("SQLite<br/>documents")]
    B -. "idempotent sidecar runners:<br/>citations · figures · tables ·<br/>keywords · doc vectors" .-> F
    subgraph Query
        Q["question"] --> R["hybrid retrieve<br/>(BM25 + vector)"]
        R --> RR["cross-encoder rerank"]
        RR --> G["LLM synthesis<br/>(Claude API or Ollama)"]
        G --> H["answer + inline citations<br/>+ provenance record"]
        H -. "flagged" .-> V["reviewer agent<br/>(separate context)"]
    end
    E --> R
    F --> R
Loading

bge-base-en-v1.5 embedder and bge-reranker-base cross-encoder, both local and swappable; Chroma for vectors, SQLite for documents; Tauri + Svelte 5 desktop app over a FastAPI/SSE backend, plus a CLI. Data flow and module contracts: docs/architecture.md.

Benchmarks

Quality is measured, not asserted. The eval harness runs the full pipeline (retrieve, rerank, generate) over a fixed question set on a public 10-paper arXiv corpus that anyone can rebuild. 5 trials on bge-base, latest run 2026-08-01, reported as mean ± trial-mean std:

Scorer Mean (n=5) Trial-mean std What it measures
citation_overlap (0-1) 1.000 0.000 retrieval cited the correct source
contains_all (0-1) 0.932 0.014 answer surfaces the required facts
llm_judge (1-5) 3.694 0.258 reference-graded answer quality

citation_overlap is 1.000 with zero variance because retrieval depends only on the deterministic index; the generated-answer scorers wobble run-to-run around stable means. Cases are deliberately strict, not tuned to score 1.0. Two caveats travel with these numbers: citation_overlap is saturated on a 10-paper corpus, so it shows no regression at the available resolution rather than ranking quality — on the 97-document library the same scorer spans 0.877-0.946 and does discriminate; and this run's llm_judge band is wide enough that only changes larger than about ±0.5 would be visible. Full results, including the embedder comparison, the chunk-size sweep, the weight sweep and reproduction steps, live in evals/.

Cost is measured separately from quality: launch and per-turn latency, ingest throughput, memory, disk, and what each of those does as the corpus grows are in docs/performance.md.

Quick start

uv sync --extra cu130 --extra dev        # or --extra cpu on a GPU-less box
uv run python -m scripts.download_corpus --demo   # no corpus yet? 28 papers from arXiv
uv run python -m doc_assistant.ingest
just app                                 # backend + desktop UI

Then open Settings → Getting started and pick an answer engine: paste an Anthropic API key (checked before it is saved, stored on your machine only) or point at a local Ollama server for a free, fully offline run. Both paths are configurable in-app, so there is no file to edit; .env still works and takes precedence if you prefer it.

First run, step by step: docs/QUICKSTART.md. Full install, hardware guidance and Docker: docs/setup.md. Everyday commands, enrichment passes and tests: docs/usage.md.

Limitations

Re-read for this release; the full ledger lives in .claude/KNOWN_ISSUES.md.

  • An API key entered in the app is stored in plain text in your data folder — weaker than an OS keychain, which is the recorded upgrade path (ADR-034). Use .env, which takes precedence, if you would rather manage the key yourself.

  • A scanned page with no text layer at all is unreachable. Documents whose text hides behind a page image are now read correctly, but a pure image has nothing to fall back to. Recovering those needs OCR, which is designed and deliberately not built until its quality is measured — text that is wrong is worse than text that is absent, because absence is honest while garbage is retrievable and citable. One document of 97 in the development library.

  • Most reference links into your own library are withheld, on purpose. A document's bibliography is shown in full, but the links from a reference to the copy in your library are re-checked before being offered, and only exact-DOI or title-agreeing matches survive. On the development library that is 4 links where 16 are stored: the matcher resolves on first-author surname and year with no title comparison, and it runs once at ingest, so it is frozen at whatever your library looked like that day. Withholding is the honest half of the fix; the matcher itself is next (KI-45).

  • Validated at ~100 documents, not yet at thousands. Retrieval quality is benchmarked and holds. Memory used to be the limit and no longer is: both search indexes now live on disk, so backend RAM measures flat at about 2 GB regardless of corpus size (ADR-036). What binds now is the first ingest, which is dominated by PDF extraction at roughly 15 seconds per document, single-threaded, and disk at about 6 MB per document. Numbers and projections: docs/performance.md. The enrichment layer still has its own corpus-linear hot paths and corpus-tuned thresholds, catalogued with a prioritized fix plan in the scale review, so don't bulk-ingest thousands of documents before those land.

  • Local-model ceilings are real, and measured. A local model cites far less of what it writes: across 27 questions on a 97-document library — same prompt, same retrieval — llama3.1:8b carried inline citations on 36% of its sentences and qwen2.5:7b on 14%, against 81% for Claude Haiku. Answers stay grounded either way; more claims simply show as uncited. Small local models also place documents into a taxonomy at 70-87% precision, and their self-reported confidence carries almost no signal — on one model it was anti-correlated with correctness. Never auto-accept on it. Nothing is gated: the app states this where you choose the engine.

  • Document metadata extraction is imperfect. A handful of documents still yield no title, or publisher furniture instead of one, and downstream layers that key on the title inherit that — the reference-link limitation above is the visible consequence.

  • Per-source "epistemic assessment" is off by default. The chips labelling a source contested / corroborated / single-source are withheld: they came from a stance pass that judges without ever seeing the document text and whose verdict moves with list position (one document, identical inputs, position varied alone → four different verdicts). Nothing was deleted — EPISTEMICS_MARKERS_ENABLED=true opts back in — and the rebuild is planned. Document year, relevance score and graph freshness are unaffected and still shown.

  • Single-user, local-first by design. The FastAPI backend serves one desktop app on localhost; multi-client serving would need threadpool offloading (documented, not built).

  • Tested primarily on Windows plus CI on Linux; macOS (MPS) paths work but are unbenchmarked.

Status

v0.5.0 (2026-08-11) — the library became somewhere to read, not just a list. Phase 6 + 7 in progress. Shipped: core RAG, the eval harness, the document store and library workspace, citation and doc-similarity graphs, the research-integrity layer (provenance, evidence/interpretation split, separate-context reviewer), a provider-agnostic LLM layer with in-app setup and live switching between Claude API and local Ollama, figures and tables, the corpus wiki, and the full concept-graph stack with gap detection. 1,647 tests · ruff / mypy / bandit clean.

Next: keyword quality — the extracted keyword layer is measured and does not yet partition a corpus (1,376 keywords, 98% of them on a single document) — then opt-in LLM-assisted ingestion. Release notes: CHANGELOG.md. Full roadmap: docs/ROADMAP.md.

Documentation

Quickstart First run in ~10 minutes: API key or Ollama, then your documents
60-second walkthrough What to look at first
Setup · Usage Install, hardware, Docker · commands, enrichment, tests
Architecture Data flow and module contracts
Decisions ADR index, and why each non-obvious choice was made
How answers work Evidence/interpretation split, grounding markers
Evals Quality benchmark write-ups and reproduction
Performance Speed, memory, disk, the trade each optimisation made, and what happens at 10x

Agent-facing coordination lives in AGENTS.md, deliberately separate from this README.

License

Apache-2.0, see LICENSE.txt.

About

Local-first RAG over your own document library (PDFs, papers, books, notes). Grounded answers with inline citations, eval harnesses that measures retrieval quality, epistemics and per-answer provenance.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages