Skip to main content
Glama
README.md
# AI Lab - Local-First RAG Second Brain

![CI](https://github.com/bunlongheng/ai-lab/actions/workflows/ci.yml/badge.svg)

A retrieval-augmented knowledge base that runs entirely on your machine. It
ingests markdown and an Obsidian vault, embeds every chunk locally with no API
key, stores the vectors in SQLite, and answers semantic queries over hybrid
keyword + vector search. It plugs into Claude Code over MCP so an AI assistant
can pull your own notes before it answers.

No cloud. No API key for retrieval. Your notes never leave the machine.

```
$ npm run search "how do I keep my notes private without a cloud API"

[note] Local Embeddings and Local LLMs  (score 0.550)
  You do not have to send your notes to a cloud API to embed them. transformers.js
  runs sentence-embedding models directly in Node with no API key and no network...
```

That match came from pure semantic similarity - the query shares no keywords with
the note title.

## What it demonstrates

| Capability | How |
|---|---|
| **RAG** | Ingest -> chunk by heading -> embed -> retrieve -> assemble context. `src/ingest.ts`, `src/search.ts` |
| **Vector DB** | Embeddings stored as BLOBs in SQLite; brute-force cosine scan + FTS5 keyword index in one file. `src/db.ts` |
| **Local embeddings (key-free)** | `all-MiniLM-L6-v2` (384-dim) via transformers.js - runs on CPU, no network after first download. `src/embeddings.ts` |
| **Second brain -> Obsidian** | Point `OBSIDIAN_VAULTS` at a vault root; every note becomes retrievable. Content-hashing re-embeds only what changed. `src/ingest.ts` |
| **Exposed to Claude Code (MCP)** | `kb_search` and `kb_context` tools an assistant calls on its own. `src/mcp-server.ts` |

## Quickstart

```bash
npm install                       # native build needs Python <= 3.13
npm run ingest                    # embeds the bundled sample notes
npm run search "vector search"    # semantic + keyword retrieval
npm run ask "what is a second brain"   # assembled context block
npm run stats
```

Index your own Obsidian vault:

```bash
OBSIDIAN_VAULTS="$HOME/Documents/My Vault" npm run ingest
```

Wire it into Claude Code as a tool:

```bash
claude mcp add ai-lab -- npx tsx /absolute/path/to/ai-lab/src/mcp-server.ts
```

## How retrieval works

1. **Ingest** - markdown is split on H1-H3 boundaries so each chunk stays
   topically coherent, with a hard cap (~1600 chars) on long sections.
2. **Embed** - each chunk is mean-pooled and L2-normalized into a 384-dim vector,
   so a raw dot product equals cosine similarity.
3. **Store** - vectors live as BLOBs next to their text in SQLite; an FTS5 virtual
   table indexes the same chunks for keyword recall.
4. **Retrieve (hybrid)** - the query is embedded and scored by cosine against
   every chunk; FTS5 keyword hits get a small boost. Keyword catches exact terms,
   vectors catch paraphrase - the blend beats either alone.
5. **Assemble** - top chunks are packed into a token-budgeted context block ready
   to ground a prompt.

## Testing

```bash
npm test    # node:test unit tests - pure functions only, no model download, CI gate
npm run eval  # retrieval eval harness - downloads the MiniLM model, not part of the CI gate
```

## Retrieval quality

`eval/` runs `search()` against a fixed 10-document corpus (`eval/corpus/`)
covering 10 unrelated topics, against 12 natural-language queries
(`eval/queries.json`), and reports recall@1, recall@5, and MRR
(`eval/run.ts`). It ingests into a throwaway temp SQLite db, so it never
touches `data/ai-lab.db`. Full per-query breakdown: `eval/results.md`.

| Metric | Value |
|---|---|
| recall@1 | 100.0% |
| recall@5 | 100.0% |
| MRR | 1.000 |

12/12 queries, including 2 paraphrase queries with no keyword overlap with
their target doc. The corpus is small and deliberately made of maximally
distinct topics (sourdough bread, TCP handshakes, Roman aqueducts, ...), so
a perfect score here says the hybrid pipeline works end to end and catches
regressions - it is not a claim about recall on a real, topically-overlapping
personal knowledge base, where scores will be lower.

## Architecture decisions

- [0000 - ADR template](docs/adr/0000-adr-template.md)
- [0001 - Brute-force cosine over SQLite, hybridized with FTS5](docs/adr/0001-brute-force-cosine-over-sqlite.md) -
  why linear cosine scan + FTS5 instead of an ANN index, and what corpus
  size would change that answer.

## Stack

- TypeScript + Node (ESM, run directly with `tsx`)
- `@huggingface/transformers` - local sentence embeddings
- `better-sqlite3` - vectors + FTS5, single-file store
- `@modelcontextprotocol/sdk` - MCP server for Claude Code

## More AI projects

Part of a wider set of AI tooling:

- [nexus](https://github.com/bunlongheng/nexus) - 12-agent system for Claude Code, structured multi-agent roles
- [diagrams](https://github.com/bunlongheng/diagrams) - describe a diagram in plain English, get rendered Mermaid
- [decks](https://github.com/bunlongheng/decks) - AI slide-deck generator from a topic prompt
- [score-card](https://github.com/bunlongheng/score-card) - technical-interview rubric with Claude feedback
- [mimi](https://github.com/bunlongheng/mimi) - live meeting transcription with an AI summary
- [local-apps](https://github.com/bunlongheng/local-apps) - self-healing dev monitor with AI fix agents
- [automations](https://github.com/bunlongheng/automations) - visual node-based automation flow builder
- [claude-live](https://github.com/bunlongheng/claude-live) - Rust WebSocket server streaming Claude Code sessions