Cache your knowledge. Channel the Akashic.
A compiler for knowledge. Turn a folder of documents into a self-contained GraphRAG agent you can docker run.
📄 Documents → ⚡ kash build → 🐳 One Container → 🚀 Ship Anywhere
RAG usually means a Python service, a hosted vector database, a graph server, and glue to hold them together. Kash is a compiler instead. All the expensive work happens once, at build time. What ships is a single binary with the databases baked in.
| Typical RAG Stack | ⚡ Kash | |
|---|---|---|
| Runtime | Python + dependency tree | One Go binary |
| Vector DB | Hosted service | Embedded (chromem-go) |
| Graph DB | Neo4j server | Embedded (cayley) |
| Keyword search | Elasticsearch cluster | Embedded (pure-Go BM25) |
| Deploy | Multi-service orchestration | One ~50MB container |
| Share an agent | "clone the repo, install…" | docker run |
Works with any OpenAI-compatible API — OpenAI, Voyage, Ollama, LiteLLM, OneAPI. Bring your own model. 🔑
# 1. Install
go install github.com/akashicode/kash/cmd/kash@latest
# 2. Scaffold an agent
kash init my-expert
# 3. Drop in your knowledge (PDF · Markdown · TXT)
cp ~/docs/*.pdf my-expert/data/
# 4. Compile
cd my-expert && kash build
# 5. Serve
kash serve🎉 Live at http://localhost:8000 — REST API, MCP server, and a dashboard.
Important
kash init writes ~/.kash/config.yaml on first run. Put your API keys there before kash build. See Configuration.
Most RAG is cosine similarity and hope. Kash runs four independent retrieval routes and fuses them with Reciprocal Rank Fusion — so a question phrased any way still lands.
flowchart LR
Q["❓ Query"] --> D["✂️ Decompose<br/><i>entities · concepts</i>"]
D --> V["🧮 Vector<br/><i>semantic</i>"]
D --> L["🔤 BM25<br/><i>keyword</i>"]
D --> X["🎯 Exact ref<br/><i>“clause 7.2”</i>"]
D --> G["🕸️ Graph<br/><i>facts → passages</i>"]
V --> F["⚖️ RRF Fusion"]
L --> F
X --> F
G --> F
F --> RR["📊 Rerank"]
RR --> A["💬 Answer<br/><i>with citations</i>"]
| Route | Catches what the others miss |
|---|---|
| 🧮 Vector | Meaning. "techniques for stilling the mind" → passages that never use those words |
| 🔤 BM25 | Exact terms. A rare proper noun that embeddings smooth away |
| 🎯 Exact reference | Structure. "dhāraṇā 49", "§ 4.2", "Article 12" — numbering detected from your corpus |
| 🕸️ Graph | Connections. Facts one hop away, resolved back to the passages that state them |
Why fusion beats any single route: each returns a ranked list, and RRF (k=60) rewards documents that several routes agree on. A chunk ranked 3rd by vectors and 2nd by BM25 outranks one ranked 1st by vectors alone.
Measured on the test corpus: recall@5 of 1.00 fused vs 0.40 vector-only.
The knobs, if you want them
- Candidate depth scales with the request —
top_k × 20, floor 200, ceiling 2000. Not a fixed pool that silently caps recall. - Reranking is a cascade — the top 100 candidates go to the reranker (Cohere-compatible), the rest keep similarity order behind them. Bounded so a paid API call stays one billing unit.
- Diversity is capped per work, not per file — three editions of one book don't crowd out everything else, but a question genuinely concentrated in one text can still be answered from it.
- Near-duplicate chunks collapse before they reach the model.
Kash splits on document structure, not every N characters. Each chunk carries a breadcrumb baked into its text at build time — so it's part of the embedding, part of the BM25 index, and part of what the model reads:
[Vigyāna Bhairava Tantra > Dhāraṇā 49]
Focus on the space between two breaths…
That single line is why "what is dharana 49" works: the exact-reference route can route to it, BM25 can match it, and the answer can cite it.
- 🔢 Numbered items stay individually addressable — a new verse or clause number starts a new chunk
- 📊 Table headers carry into every chunk of a long table
- 🧾 Corrupt PDF text is rejected, not embedded — some PDFs decode to valid UTF-8 that is actually a substitution cipher, and a quality gate catches it before it reaches the index
Note
PDFs are read through their embedded text layer. Kash does not OCR — run a scanned PDF through OCR first, or feed it as Markdown.
Every fact records the chunk it came from, not just the document. That makes the whole chain walkable: entity → chunk id → the actual passage.
Knowledge Graph Facts:
- Abhinavagupta commented on Tantrāloka (source: tantraloka.md [passage 1])
↳ Tantrāloka is part of Trika (source: malini.md) [connected via Tantrāloka]
A fact whose passage was retrieved cites [passage N] — pointing at text the reader can see. One whose passage wasn't cites its document and nothing more. Provenance is never invented.
And you can audit it:
kash verifyFacts: 2202 Fold mode: iast
Traceable to a passage:
both endpoints found 1854 84.2%
one endpoint found 276 12.5%
Not traceable:
passage found neither 72 3.3%
chunk no longer exists 0 0.0%
no chunk recorded 0 0.0%
✓ 96.7% of facts can be shown to a reader in the passage they came from
How the graph gets built well
- Gleaning — after the first extraction pass the model is shown its own output and asked what it missed. Dense passages give up more than one pass gets out of them. Stops as soon as a round finds nothing new, so cost tracks recovery.
- Closed predicate vocabulary — derived from your corpus and unioned with a generic set. Without it, extraction invents a new phrasing every few facts and nothing ever matches.
- Passage isolation — batched excerpts are explicitly delimited and the prompt forbids crossing them, so a title-page translator credit can't bind to a text merely mentioned nearby.
- Evidential weight — a triple attested across many chunks outranks one seen once. A corpus-time quality signal, not just query-time relevance.
- Entity resolution —
Kármán/Karman,Dr. Feynman/Feynmanmerge so chains stop breaking at spelling boundaries.
The settings that depend on your subject matter are measured from your documents, not asked for. kash build profiles the corpus and writes data/domain.profile.json.
kash profile # what was measured, and the evidence for it• resolution.fold_diacritics = iast
IAST marks: 3597866 in 56/60 docs; Latin marks: 1439 in 37/60 docs
• resolution.strip_final_vowel = true
1566 stem-vowel variant pairs, e.g. lakṣya/lakṣyam, tāmasa/tāmasam
• chunker.ref_patterns = 6 detected + 2 generic
"dhāraṇā" (105 hits, sequence 0.97); "śloka" (3209 hits, sequence 0.67)
Point Kash at legal contracts and it finds Clause 4.2. At a research library and it finds Figure 3. Nobody writes a regex.
Configuration is layered, and agent.yaml always wins:
built-in defaults < data/domain.profile.json < agent.yaml
So you only add a block when you disagree with the measurement. Setting a list there replaces the derived one rather than merging.
Note
The model never emits a regex, a number, or a boolean during profiling — it picks from lists it was given and returns words. Your documents are untrusted input to a prompt whose output becomes configuration, so everything it returns is re-validated against what it was offered.
kash build tracks every document in data/build.manifest.json — content hash, chunk and triple counts, and how far each phase got.
kash build # v1 — compiles everything
cp new-books/*.pdf data/
kash build # v2 — only the new documents are processed- ⏭️ Unchanged documents are skipped — no embedding calls, no LLM calls
- ♻️ Changed documents are replaced — old vectors and triples removed first
- 🔌 Interrupted builds resume at the exact batch they stopped on — including a build stopped after its last document, whose lexical index, entity descriptions and MCP description are completed by the next run
- 🏷️ Each change bumps the corpus version, exposed at
GET /health
| Flag | Purpose |
|---|---|
--rebuild |
Discard the databases and manifest, start from scratch |
--prune |
Remove data for documents deleted from data/ |
--refresh-profile |
Re-derive the corpus profile |
Note
Changing the embedder or its dimensions against an existing corpus is a hard error — mixed embeddings fail silently otherwise. Same for structural rules, which are baked into chunk metadata. Use --rebuild.
kash serve hosts a dashboard at / — one self-contained page embedded in the binary, so it works offline in a container with no CDN.
| Tab | What It Shows |
|---|---|
| 📚 Books | Every document: chunks, triples, build status, date |
| 🕸️ Knowledge Graph | Force-directed explorer — zoom, filter by source, click a node for its facts |
| 🔍 Retrieval Tester | Run a query, see exactly what the pipeline retrieved and which routes fired |
The retrieval tester is the fastest way to diagnose a bad answer — it shows you what the model actually received.
| Interface | Endpoint | Use It For |
|---|---|---|
| 🌐 REST API | POST /v1/chat/completions |
Drop-in OpenAI replacement with RAG context injected |
| 🧩 MCP Server | GET /mcp |
Expose your knowledge as tools to Claude, Cursor, Windsurf via MCP |
| 🤝 A2A Protocol | POST /rpc/agent |
JSON-RPC for multi-agent frameworks — WIP |
🔒 Secure every endpoint with the
AGENT_API_KEYenvironment variable.
🐳 Docker Compose (recommended)
# Fill in .env with your runtime API keys, then:
kash build
docker compose up --build🐳 Docker Run
docker build -t my-agent:latest .
docker run -p 8000:8000 --env-file .env my-agent:latest💻 Local (no Docker)
kash build && kash serve
# Falls back to ~/.kash/config.yaml when env vars aren't setAuto-generated by kash init. Used by kash build for embedding and extraction:
build_providers:
llm:
base_url: "https://api.openai.com/v1"
api_key: "sk-..."
model: "gpt-4o"
# reasoning_effort: medium # low | medium | high — reasoning models only
embedder:
base_url: "https://api.voyageai.com/v1"
api_key: "pa-..."
model: "voyage-3"| Variable | Required | Purpose |
|---|---|---|
LLM_BASE_URL / LLM_API_KEY / LLM_MODEL |
✅ | The model that answers queries |
LLM_REASONING_EFFORT |
➖ | low │ medium │ high; unset disables it |
EMBED_BASE_URL / EMBED_API_KEY / EMBED_MODEL |
✅ | Embeds queries for vector search |
RERANK_BASE_URL / RERANK_API_KEY / RERANK_MODEL |
➖ | Optional reranker (Cohere-compatible /rerank) |
AGENT_API_KEY |
➖ | Auth for all endpoints |
Generated per project. Persona, dimensions, chunk sizes — and that's genuinely all you need to write:
agent:
name: "my-agent"
system_prompt: |
You are a knowledgeable expert assistant...
runtime:
embedder:
dimensions: 1024 # must match at build AND serve time
llm:
reasoning_effort: medium # optional
build:
chunk_size: 1000 # characters per chunk (800–2000 works best)
chunk_overlap: 200
retrieval:
top_k: 5 # chunks injected as context
graph_facts: 10 # graph facts injected| Command | What It Does |
|---|---|
kash init <name> |
Scaffold a project (data/, agent.yaml, Dockerfile) |
kash build |
Compile documents into vector + graph + BM25 indexes |
kash profile |
Show the domain settings derived from your corpus, and why |
kash verify |
Audit how much of the graph traces back to a source passage |
kash resolve-entities |
Merge entity spelling variants so graph chains connect |
kash serve |
Start the HTTP server + dashboard |
kash version |
Print version, commit and build date |
git clone https://github.com/akashicode/kash.git
cd kash
make build # or: make build-all for every platform
# Windows without make: powershell -File scripts\build.ps1
go test ./... # full suiteRelease history and upgrade notes live in CHANGELOG.md — including which changes need a kash build --rebuild.
MIT — do whatever, just keep the notice.
If Kash saves you an infra headache, ⭐ star the repo — it helps others find it.