Skip to content

Latest commit

Β 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🧠 LocalBrain

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.


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        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)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Agent loop (graph.py)

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

Workspace model

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.


Quick Start

Prerequisites

  • Python 3.11+
  • Ollama running locally (or set an OPENAI_API_KEY in .env)

Models

# 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

Setup

# 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 needed

Run

cd backend
python main.py                   # starts on http://localhost:8000

All 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.

Index your vault

curl -X POST http://localhost:8000/api/v1/vault/ingest

Or set VAULT_PATH in .env to point at your Obsidian vault.

Query

curl -X POST http://localhost:8000/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{"question": "What decisions were made?", "workspace": "work"}'

Vault Format

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.


Ingestion Sources

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)
PDF 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 (file CRUD)

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.


Configuration

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)

Testing

cd backend
pytest ../tests/ -v

Eval benchmarks

cd 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 timing

The 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 LocalAnswerRelevancy subclass of RAGAS's AnswerRelevancy β€” local judges often mark every answer noncommittal, which zeroes the cosine score; the subclass replaces that flag with a deterministic regex gate so relevancy stays meaningful offline.


Desktop App (Tauri + React)

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 installer

See desktop/README.md for the full setup.


Docker

Run the full stack with Docker Compose:

docker compose up -d

This 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 -v

Project Structure

LocalBrainNotes/
β”œβ”€β”€ 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

Roadmap

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

License

MIT

About

Local-first, Obsidian-compatible agentic RAG assistant over markdown vaults (LangGraph + ChromaDB + Ollama)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages