Local first
Documents, Qdrant storage, embeddings, and responses stay on the developer machine with the default settings. Hosted providers are explicit opt-ins.
A private-by-default, Ollama-first RAG workspace that keeps retrieval visible, opens cited PDF pages, and can optionally audit claim support with TypeSafe Jev.
Documents, Qdrant storage, embeddings, and responses stay on the developer machine with the default settings. Hosted providers are explicit opt-ins.
The RAG loop is plain Python: load, chunk, embed, retrieve, assemble context, generate. No hidden agent graph.
Small protocols isolate chat and embeddings. Each can use a different provider without changing ingestion or retrieval.
A web UI, OpenAPI endpoints, architecture notes, tests, CI, releases, and observable source scores demonstrate the whole system.
No accounts, tenancy, distributed workers, or cloud database. These only belong after usage creates a real need.
Ollama, Qdrant local mode, Python libraries, GitHub Actions, and GitHub Pages are available without a paid runtime.
The FastAPI process owns orchestration and the browser workspace. Ollama owns model inference. Qdrant runs in-process, while vectors and retained originals persist beneath RAG_DATA_DIR; no database server or Docker container is required. The Jev edge is inactive by default.
The interface keeps the question, grounded answer, and exact source passage in one reading flow. It exposes system state early and moves implementation detail behind deliberate disclosure.
The document library controls query scope, numbered citations activate the matching source, and the source header distinguishes cited passages from the complete retrieved set. PDF citations open the retained original on the cited page.
The question uses an editorial serif face, answer copy has a restrained reading measure, and labels describe outcomes in plain language. Copy and source actions sit beside the content they affect.
The header shows whether Jev is off, observing, or repairing. Inline claim states and the audit summary communicate the result first; probability bars, repair attempts, privacy disclosure, and the technical record remain available on demand.
Staged status text explains long local-model work, an active request can be stopped, and a failed or cancelled question leaves the previous answer intact. The three-column layout collapses into a usable mobile reading order.
approved_gold cases into a versioned release dataset.| Concern | Choice | Why it fits | Alternative / trigger |
|---|---|---|---|
| Runtime | Python 3.13 + uv | Typed, cross-platform, reproducible lockfile, fast project commands. | Python 3.12 remains compatible if a dependency requires it. |
| HTTP/UI | FastAPI + vanilla HTML/CSS/JS | OpenAPI for free and one small browser client without a Node toolchain. | Add a frontend framework only when client state becomes complex. |
| Chat | Ollama /api/chat | Local inference, no token bill, simple documented JSON API. | OpenAI-compatible adapter for another local or hosted endpoint. |
| Embeddings | Ollama /api/embed + embeddinggemma | Batch inputs, local execution, and independent model configuration. | Swap only the embedding provider and rebuild the collection. |
| Vector database | Qdrant client local mode | Persistent vector search with the same client API available for a later server migration. | Run Qdrant server when concurrent writers or remote access matter. |
| Retrieval | Cosine + BM25Plus + RRF | Combines semantic and exact-term ranks without mixing incompatible raw score scales. | Add a cross-encoder only after a frozen dataset proves the gain. |
| Document parsing | PyMuPDF4LLM + RapidOCR | Extracts layout-aware Markdown and automatically OCRs image-only pages locally. | Add language models only when non-English scans are required. |
| Claim audit | Optional TypeSafe Jev | Checks each generated claim against its cited excerpts and can verify a replacement citation. | Keep disabled for a fully local path; calibrate thresholds before enforcement. |
| Model transport | HTTPX | One async HTTP client covers Ollama and OpenAI-compatible endpoints. | Provider SDK only when it exposes required behavior missing from HTTP. |
| Evaluation | Deterministic gates + optional Ragas | Human-approved gold drives repeatable release metrics; local judges add semantic diagnostics. | Use a stronger local judge when hardware permits. |
| Quality | Ruff, mypy, pytest, nox, pre-commit | Fast local checks mirror CI; configuration lives in pyproject.toml. | None until project scale justifies more specialized tooling. |
Swappability is two structural typing contracts, not a dependency-injection framework. Configuration selects one implementation for each contract at startup.
class EmbeddingProvider(Protocol):
async def embed(self, texts: list[str]) -> list[list[float]]: ...
class ChatProvider(Protocol):
async def answer(self, question: str, context: str,
citation_count: int) -> GroundedResponse: ...RAG_EMBEDDING_PROVIDER=ollamaRAG_CHAT_PROVIDER=ollama
Uses independently configured models against the local Ollama base URL.
openai-compatible targets any compatible endpoint. Base URL, model, and API key are environment settings; neither OpenAI nor a paid account is required.
The embedding is stored with cosine distance. Collection dimensionality is discovered from the first embedding and checked thereafter.
document_id, source_sha256, source, page, page_count, chunk_index, and the actual chunk text support stable evidence anchors, citations, hybrid retrieval, document filtering, deletion, and replacement on re-ingestion.
Uploads are retained beneath RAG_DATA_DIR/sources/<collection-hash> by content-addressed document ID. The UI can download a source or render a PDF page without re-uploading it.
BM25Plus ranks stored chunk text while Qdrant ranks embeddings. Reciprocal-rank fusion combines positions, not incompatible raw score magnitudes.
PyMuPDF4LLM preserves PDF layout as Markdown; RapidOCR handles scanned pages locally; LangChain's standalone splitter preserves headings and natural text boundaries while enforcing configurable character limits.
Changing the embedding model or provider requires rebuilding the collection. Mixing vector spaces produces invalid similarity results.
| Area | Design |
|---|---|
| Configuration | Environment variables with safe local defaults; .env is ignored and .env.example documents every setting. |
| Health | /health reports readiness, provider choices, Jev mode, upload limit, and whether both model endpoints are local without exposing keys or document contents. |
| Privacy | The default path sends no document data to a third party. A remote model endpoint or Jev mode is an explicit operator decision; Jev receives generated claims and selected excerpt text, never retained originals or filenames. |
| Prompt safety | Retrieved documents are untrusted data. The system prompt tells the model to ignore instructions inside context and answer only from evidence. |
| Input boundary | Extensions and upload size are validated; filenames are metadata only and never used as filesystem destinations. Original downloads and PDF previews use non-cacheable, same-origin endpoints. |
| Failure modes | Unavailable models, malformed documents, dimension changes, invalid structured output, and empty retrieval return actionable API errors. |
| Quality signals | The UI reports cited and retrieved counts, source scores, answer latency, and optional claim-support distributions; deterministic JSONL metrics gate releases while local Ragas judges remain diagnostic. |
| Backup | Stop the app and copy RAG_DATA_DIR, including its sources subtree. Keep external originals when a rebuild path independent of application storage is required. |
CI checks formatting, typing, tests, and commit messages. Feature pull requests target dev; release pull requests target main.
Conventional Commits drive semantic versions. Pushes to main can update the changelog, tag the release, and create a GitHub Release.
GitHub Pages publishes this static document at no cost. Application deployment remains local to preserve the zero-spend promise.