Local-first, Obsidian-compatible agentic RAG assistant over markdown vaults.
LocalBrain indexes an Obsidian-style markdown vault using a LangGraph agent loop (router β retrieval β grading β rewrite β generate β reflect), backed by a unified ChromaDB vector store and optionally powered by a local Ollama LLM.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FastAPI (main.py) β
β POST /api/v1/query POST /api/v1/vault/ingest GET /api/v1/health β
ββββββββ¬βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββ ββββββββββββββββββ
β LangGraph β β Vault β
β Agent Loop βββββββββββββ Ingester β
β (graph.py) β β (vault.py) β
ββββββββ¬ββββββββ βββββββββ¬βββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Unified ChromaDB Vector Store β
β (single collection, workspace = metadata field) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
START β router
βββ "fastpath" β retrieve β generate (skips grade/rewrite/reflect for simple facts)
βββ "retrieve" β retrieve β filter_docs β grade_docs
β βββ any relevant β generate β reflect
β βββ grounded β END (return answer)
β βββ ungroundedβ guard_answer (one in-place repair) β END
β βββ none relevant β query_rewrite β retrieve (cycle, max 3)
βββ "direct" β generate (chit-chat, no context) β END
βββ "tool" β tool_search β generate (live web search context) β END
Workspaces are a metadata field on each chunk (not a separate collection).
Query-time: WHERE workspace = "work".
This fixes the orphan-chunk bug in the affine-lite prototype.
- Python 3.11+
- Ollama running locally (or set an
OPENAI_API_KEYin.env)
# Pull the LLM + embedding model, then create the project's named modelfile
# (an alias of phi4-mini that `config.py` expects by default).
ollama pull phi4-mini
ollama pull nomic-embed-text
ollama create phi4-mini-localbrain -f Modelfile
# Verify
ollama list# Clone
git clone <repo> && cd LocalBrainNotes
# Create venv
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate # Windows
# Install deps
cd backend
pip install -r requirements.txt
# Copy env config
cp ../.env.example ../.env # edit as neededcd backend
python main.py # starts on http://localhost:8000All API routes are versioned under /api/v1/. The legacy /api/ prefix is kept for backward compatibility.
Open http://localhost:8000 in a browser for the chat UI.
curl -X POST http://localhost:8000/api/v1/vault/ingestOr set VAULT_PATH in .env to point at your Obsidian vault.
curl -X POST http://localhost:8000/api/v1/query \
-H "Content-Type: application/json" \
-d '{"question": "What decisions were made?", "workspace": "work"}'Every markdown note can include a YAML frontmatter block. The workspace field becomes the workspace label used to scope queries.
---
title: "Q3 Design Review"
workspace: work
tags: [design, meeting, q3]
created: 2026-08-01
---
# Q3 Design Review
## Decisions
- Key decision here.| Field | Type | Default | Purpose |
|---|---|---|---|
title |
string | filename | Display name / citation in answers |
workspace |
string | "default" |
Retrieval scope label |
tags |
list | β | Metadata; drives the tag-aware pre-filtering step (filter_docs) |
created |
date | β | Ordering / context |
Notes without frontmatter default to workspace = "default". A YAML scalar
tags: project-alpha (without the list brackets) is coerced during ingestion
to ["project-alpha"], so string tags are handled the same as list tags.
| Source | Route | Description |
|---|---|---|
| Markdown vault | POST /api/v1/vault/ingest |
Walk vault, parse frontmatter, chunk by headings |
| Single markdown file | POST /api/v1/vault/ingest/file |
Index one note by path β must resolve inside VAULT_PATH (escapes rejected) |
POST /api/v1/vault/ingest/pdf |
PyMuPDF extraction + chunking (+ Tesseract OCR fallback for image-only PDFs) | |
| YouTube | POST /api/v1/vault/ingest/youtube |
Transcript API + timestamped chunks |
Notes are real .md files in the vault; the API reads/writes them directly and
re-indexes on write so edits are immediately searchable.
| Method | Route | Description |
|---|---|---|
GET |
/api/v1/notes |
List notes (?workspace=X&offset=0&limit=50) |
GET |
/api/v1/notes/{path} |
Read note content + metadata |
POST |
/api/v1/notes |
Create a note file |
PUT |
/api/v1/notes/{path} |
Overwrite content + re-index |
DELETE |
/api/v1/notes/{path} |
Delete file + drop vector chunks |
Path safety: note paths are resolved inside VAULT_PATH and escapes are rejected.
All settings live in backend/config.py and are overridable via environment variables (see .env.example).
| Setting | Default | Description |
|---|---|---|
LLM_PROVIDER |
ollama |
ollama, openai, anthropic, groq, nvidia |
LLM_TEMPERATURE |
0.1 |
Sampling temperature for generation |
EMBEDDING_PROVIDER |
ollama |
ollama, openai, huggingface |
VAULT_PATH |
./vaults/sample |
Root of Obsidian vault to index |
SEARCH_TYPE |
mmr |
similarity or mmr (maximal marginal relevance) |
TOP_K |
5 |
Documents returned per retrieval step |
SIMILARITY_THRESHOLD |
0.70 |
Relevance cutoff for retrieved context |
FASTPATH_ENABLED |
true |
Route simple factual queries straight to retrieveβgenerate |
CONTEXT_MAX_CHARS |
6000 |
Compress retrieved context above this budget (fastpath) |
WEB_SEARCH_ENABLED |
true |
Allow the tool route (DuckDuckGo HTML, no API key) |
MEMORY_WINDOW_SIZE |
10 |
Messages kept per workspace in SQLite |
OLLAMA_BASE_URL |
http://localhost:11434 |
Local model server |
LOG_LEVEL |
INFO |
Python logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
EVAL_JUDGE_PROVIDER |
ollama |
RAGAS judge backend (or openai for any /v1 endpoint) |
cd backend
pytest ../tests/ -vcd backend
# RAGAS evaluation (agentic vs naive on golden_dataset.json)
python ../evals/run_eval.py # --only=agentic|naive to run one pipeline
python ../evals/benchmark_speed.py # keyword vs semantic retrieval timingThe judge defaults to the local Ollama model (EVAL_JUDGE_PROVIDER=ollama) for
consistent, dependency-free scoring. Point EVAL_JUDGE_PROVIDER=openai +
EVAL_LLM_BASE_URL at any /v1 endpoint (OpenRouter, NVIDIA NIM, etc.) to use a
cloud judge. Metrics: faithfulness, answer_relevancy, context_recall,
context_precision.
Note: the harness uses a
LocalAnswerRelevancysubclass of RAGAS'sAnswerRelevancyβ local judges often mark every answernoncommittal, which zeroes the cosine score; the subclass replaces that flag with a deterministic regex gate so relevancy stays meaningful offline.
desktop/ is a native Tauri v2 shell around the FastAPI backend. The Rust
sidecar spawns backend/main.py on startup, waits for /api/v1/health, opens the
native window, and kills the backend on exit. The React UI provides Chat and
Notes views.
cd desktop
npm install
# one-time: install Rust toolchain + WebView2
# Windows: powershell -ExecutionPolicy Bypass -File bootstrap-toolchain.ps1
# macOS/Linux: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
npm run tauri dev # dev window (Vite :1420 + Rust shell)
npm run tauri build # production installerSee desktop/README.md for the full setup.
Run the full stack with Docker Compose:
docker compose up -dThis starts:
- Backend on
http://localhost:8000(FastAPI + ChromaDB) - Ollama on
http://localhost:11434(with auto-pull of models) - Frontend on
http://localhost:3000(nginx serving static files)
First run pulls Ollama images (~2 GB) and downloads the LLM + embedding models.
# Stop
docker compose down
# Stop and remove volumes
docker compose down -vLocalBrainNotes/
βββ backend/
β βββ main.py FastAPI app (+ notes CRUD routes)
β βββ config.py pydantic-settings
β βββ rag/
β β βββ graph.py LangGraph agent wiring (fastpath / retrieve / tool routes)
β β βββ nodes.py router / retrieve / filter / grade / rewrite / generate / reflect / guard_answer / tool_search
β β βββ state.py AgentState schema
β β βββ llm_factory.py multi-provider LLM factory
β β βββ vectorstore.py unified ChromaDB (single collection, workspace filter)
β β βββ embedder.py Ollama/OpenAI/HF embeddings with cache
β β βββ web_search.py dependency-free DuckDuckGo search for the tool route
β β βββ ingester/
β β βββ vault.py frontmatter + markdown header splitter
β β βββ pdf.py PyMuPDF + Tesseract OCR fallback
β β βββ youtube.py youtube-transcript-api
β β βββ watcher.py watchdog incremental indexing
β β βββ memory.py workspace-scoped sqlite chat memory
β βββ models/schemas.py Pydantic request/response schemas
β βββ db/store.py JSON-backed vault metadata index
βββ frontend/index.html full-featured vanilla UI (chat + notes editor, no build step)
βββ desktop/ Tauri v2 + React native app (chat + notes editor, cross-platform)
βββ evals/
β βββ run_eval.py RAGAS faithfulness / relevancy / recall / precision (agentic vs naive)
β βββ benchmark_speed.py keyword vs semantic retrieval timing
β βββ golden_dataset.json 20 vault-aware test questions
βββ tests/ pytest suite (ingester, vectorstore, graph, API, PDF, integration)
βββ vaults/sample/ Obsidian vault for testing
βββ .env.example
βββ README.md
Done:
- β PDF OCR fallback (Tesseract for image-only PDFs)
- β Tauri + React native desktop shell (chat + notes editor, cross-platform)
- β Notes file CRUD API + JSON metadata index (atomic writes, path-safety guards)
- β Web-search tool route (dependency-free)
- β Groundedness guard + context compression + fast-path router
- β Local Ollama-based RAGAS judge (consistent offline evals)
- β Vanilla frontend parity (full Notes CRUD, conversation history, health indicator)
- β Security hardening (path traversal guards on all CRUD endpoints)
- β Human-in-the-loop review (preview β approve/reject)
- β GitHub Actions CI (backend tests + tsc lint)
- β Structured logging (Python logging, configurable LOG_LEVEL)
- β Memory improvements (thread-safe SQLite, conversation export/import)
Planned:
- Phase G: Obsidian plugin for in-vault chat
- Phase H: CRDT-based sync layer for multi-device vaults
MIT