A fully local, private personal knowledge assistant. It reads your own notes, READMEs and PDFs, builds a knowledge graph (entities + relations) alongside a vector index, and answers questions with GraphRAG (multi-hop retrieval over the graph) or plain vector RAG. Runs on Ollama, so no data leaves your machine.
files -> chunk -> embed -> ChromaDB ──────────────┐
└─> LLM extraction (JSON, validated, │
retried) -> NetworkX graph ├─> question -> entity linking -> k-hop subgraph
(edges remember source chunks) │ + vector hits -> local LLM answer + cited facts
- Schema-validated entity/relation extraction with retries and fallback (small local models emit messy JSON)
- Case-insensitive entity merging across documents, so facts from different files connect
- Entity linking + 2-hop graph expansion; each edge keeps its supporting chunk for citations
- Community detection (greedy modularity) to see topic clusters
- Built-in benchmark: plain RAG vs GraphRAG, split by single-hop vs multi-hop questions
- FastAPI service (
/ask,/graph,/health), CLI, 10 offline unit tests + GitHub Actions CI - Low-load mode for laptops: capped threads/context, cooldown between LLM calls, macOS thermal guard, resumable extraction cache, character-budgeted prompts
ollama pull llama3.2:1b && ollama pull nomic-embed-text
pip install -r requirements.txt
export PYTHONPATH=src
python -m sbrain.cli --store store_large index data/readmes # resumable; Ctrl-C and rerun any time
python -m sbrain.cli --store store_large ask "Which projects use MLflow?"
python -m sbrain.evaluate eval/questions_large.json store_large [plain_rag|graph_rag]
uvicorn sbrain.api:appTuning knobs (env vars): SB_CHAT_MODEL, SB_NUM_THREAD (2), SB_NUM_CTX (2048), SB_COOLDOWN (4 s rest after each LLM call), SB_MIN_CPU_SPEED (pause while macOS throttles the CPU). Indexing 62 chunks with llama3.2:1b takes ~25 min at SB_COOLDOWN=8 and kept a laptop from overheating.
Same questions, same model (llama3.2:1b), keyword-graded. Graph built from 4 project READMEs: 62 chunks, 198 entities, 165 relations.
Large corpus (eval/questions_large.json: 8 single-hop + 8 multi-hop questions)
| single-hop | multi-hop | overall | |
|---|---|---|---|
| Plain RAG | 0.75 (6/8) | 0.00 (0/8) | 0.375 |
| GraphRAG | 0.625 (5/8) | 0.125 (1/8) | 0.375 |
| GraphRAG + context budget | 0.50 (4/8) | 0.125 (1/8) | 0.312 |
Small corpus (eval/questions.json: 12 questions over 6 chunks): plain RAG 0.75 overall (single 1.00, multi 0.57), GraphRAG 0.75 (single 0.80, multi 0.71). Plain RAG already sees nearly the whole corpus here, so this only works as a smoke test.
What I take from this:
- At this scale and model size the graph added complexity without a measurable gain. Differences of one question are within noise on 16 questions.
- I suspected GraphRAG's longer prompt was being truncated by the 2048-token window. Fitting the prompt to a character budget did not recover any multi-hop answers (and single-hop dipped by one), so truncation is not the main cause.
- Likely causes, not yet tested: the 1B model can't combine facts across several passages (e.g. list every project that uses Docker); the grader requires every project name in the answer, so partial answers score zero; extraction noise (small models mislabel or drop relations) means the graph misses edges.
- Keyword grading is crude; an LLM judge or manual review would be fairer.
- Add a retrieval-only metric (did the right chunks/edges get retrieved?) so graph quality is measured independently of the answering model.
- Try a larger model (
SB_CHAT_MODEL) for answering and extraction; a hybrid router that uses the graph only when a question links several entities. - The corpus is four READMEs written by one author, so questions are easy to answer from a single passage. A bigger, messier corpus is where GraphRAG should matter.