Skip to main content

grag

LLM-first graph knowledgebase. One embedded Cypher engine (LadybugDB, the Kuzu successor), one file per database, zero daemons — wrapped in the tool contract LLMs actually need: schema introspection that anchors text-to-Cypher, idempotent upserts with provenance, hybrid FTS/vector search, and token-budgeted subgraph context for grounded, low-hallucination answers.

(G(raph)RAG — retrieval-augmented generation grounded in a graph.)

Not an enterprise platform. pip install, point an MCP client at it, done.

Why

LLM answers hallucinate when retrieval returns isolated chunks. grag stores knowledge as a graph — entities, documents, and their relationships — so retrieval returns a connected, cited subgraph an LLM can reason over. The LLM can also build the graph: define_schema + upsert_nodes/edges are first-class tools, so "turn these docs into a knowledge graph" is a normal conversation, not a pipeline project.

Install

From PyPI (ships the web UI):

pip install gragdb

Python 3.10–3.14; 3.13 recommended (faster interpreter for the Python-side packing/serialization paths, and 3.10 reaches end-of-life in October 2026).

From source (for development). Build the UI firstpip install needs the built bundle at src/grag/api/static (the wheel's force-include; see pyproject.toml):

cd ui && npm ci && npm run build && cd ..   # builds the UI into src/grag/api/static/
pip install -e .            # core: engine, REST, MCP, FTS — no torch, no GPU stack
pip install -e ".[dev]"     # tests
pip install -e ".[code]"          # optional: tree-sitter code parsing (ts/js/cs/tf)
pip install -e ".[embed-local]"   # optional: local embeddings (fastembed/ONNX, still no torch)
pip install -e ".[embed-remote]"  # optional: OpenAI-compatible remote embeddings

Without an embedder, everything works FTS-only (BM25 is native to the engine).

Enabling semantic search: install embed-local, then set GRAG_EMBED_PROVIDER=fastembed when serving. This uses ONNX Runtime — no PyTorch — so grag stays light (~50-100MB, model downloads once then works offline). Nodes are (re)embedded lazily on the next search whenever their embedding is NULL. First query downloads the model + embeds all nodes (seconds); steady state is ~300ms/query on CPU.

pip install -e ".[embed-local]"
GRAG_EMBED_PROVIDER=fastembed grag --db knowledge.lbdb serve
# optional: GRAG_EMBED_MODEL=BAAI/bge-base-en-v1.5 GRAG_EMBED_DIM=768

Quickstart

# build the demo knowledgebase (fictional company handbook, entities + relations)
python examples/build_example.py

# serve REST + the graph UI at http://127.0.0.1:8471
# (note: start it from a normal terminal — servers launched inside an agent
# sandbox get torn down and can't be reached from your browser)
grag --db examples/knowledge.lbdb serve

# single-process mode: UI + REST + MCP on one live .lbdb (recommended for
# dogfooding — the UI sees MCP writes the moment they land)
grag --db examples/knowledge.lbdb serve --with-mcp
#   UI  → http://127.0.0.1:8471/
#   MCP → http://127.0.0.1:8471/mcp   (streamable-http; point MCP clients here)

# or answer 3 demo questions end-to-end in the terminal
python examples/demo_e2e.py

The UI: force-graph explorer (click = inspect, double-click = expand neighbors), Cypher console (Ctrl+Enter, graph/table results), schema sidebar, and a search bar that shows the exact grounding text an LLM would receive. Click a label in the legend (bottom-left) to view just that label and its 1-hop relationships — e.g. click Decision to see only your Decisions and what they document/motivate; run a query or reload to reset the canvas.

One process, one live file. LadybugDB is single-writer, so serve and mcp can't share a .lbdb as separate processes. serve --with-mcp mounts the MCP endpoint inside the REST/UI server, so UI + REST + MCP share one registry and one write connection — the UI watches the AI's writes land live instead of reading a stale copy. Use --mcp-path to change the MCP mount path (default /mcp).

Use from an LLM harness (MCP)

grag --db knowledge.lbdb mcp

Cursor / .cursor/mcp.json:

{
  "mcpServers": {
    "grag": {
      "command": "grag",
      "args": ["--db", "/absolute/path/knowledge.lbdb", "mcp"]
    }
  }
}

Any MCP client gets these 8 tools:

tool purpose
describe_schema prompt-shaped schema: tables, properties, row counts, sample keys. Call before writing Cypher — kills hallucinated labels.
define_schema create node/rel tables (LLM designs the graph for a domain)
upsert_nodes / upsert_edges idempotent MERGE writes; _source provenance automatic
cypher_query read-only Cypher; errors come back with correction hints
search_knowledge hybrid BM25 + vector seeds → RRF fusion → per-label diversity cap → k-hop expansion → cited, token-budgeted context
get_context re-pack chosen node ids into a token budget
ingest_code index a repo's code STRUCTURE (Repo/Module/Class/Function + CONTAINS/IMPORTS/CALLS/INHERITS) — never source bodies

Errors are returned as ERROR: ... HINT: ... tool output so the model self-corrects in-loop.

Ingest code

Point ingest_code at a repo and structural questions become cheap Cypher instead of file-reading spelunking. Two entry points, same engine:

# CLI
grag --db knowledge.lbdb ingest-code src/ ../other-repo [--no-calls] [--max-file-kb 2048]
# MCP (8th tool) — an agent indexes a repo on demand
ingest_code(paths=["src/"], calls=true, max_file_kb=1024)
graph LR
  R[Repo] -->|CONTAINS_REPO_MODULE| M[Module]
  M -->|CONTAINS_MODULE_CLASS| C[Class]
  M -->|CONTAINS_MODULE_FUNCTION| F[Function]
  C -->|CONTAINS_CLASS_FUNCTION| F
  M -->|IMPORTS| M
  C -->|INHERITS| C
  F -->|CALLS| F

Nodes carry path, line range, signature and docstring — structure only, no source bodies — with ids like Module:repo:src/a.py and Function:repo:src/a.py#Class.method. Re-ingesting the same tree is idempotent (MERGE by key). Three recipes:

// what imports module X?
MATCH (m:Module)-[:IMPORTS]->(x:Module) WHERE x.id = 'pkg:core.py' RETURN m.id
// what calls function Y?
MATCH (f:Function)-[:CALLS]->(y:Function) WHERE y.id = 'pkg:core.py#helper' RETURN f.id
// cross-repo imports (multiple paths ingested into one db)
MATCH (r1:Repo)-[:CONTAINS_REPO_MODULE]->(a:Module)-[:IMPORTS]->(b:Module)<-[:CONTAINS_REPO_MODULE]-(r2:Repo)
WHERE r1.id <> r2.id RETURN a.id, b.id

Python parses via stdlib ast in every install. TypeScript/JavaScript/C#/Terraform (.ts/.tsx/.js/.jsx/.mjs/.cjs/.cs/.tf) parse via tree-sitter and need pip install "gragdb[code]"; without it those files raise a hint-carrying error. CALLS/INHERITS edges are Python-only for now; IMPORTS is best-effort (path/namespace-based) for the tree-sitter languages.

Multiple projects / shared server

One .lbdb = one isolated universe — no shared entities, no cross-db queries. Per-project DBs is the default pattern; multi-db serving is opt-in via --db-dir (single-db is unchanged).

grag --db-dir ~/kb serve    # one process serves every .lbdb in ~/kb

Every /api/* endpoint accepts ?db=<name> or an x-grag-db: <name> header (query param wins). GET /api/dbs returns {"dbs": ["alpha","beta"], "default": "alpha"} ({"dbs": [], "default": null} in single-db mode). Without a selector the server prefers the file matching db_path's name, else a lone .lbdb, else 400 with a hint; unknown name → 404 listing available DBs.

For MCP, several IDE windows on one DB collide: stdio spawns a grag mcp process per client and LadybugDB allows only ONE process to write a given .lbdb ("Could not set lock"). One shared HTTP server avoids it — each window sends its project name via x-grag-db:

grag --db-dir ~/kb mcp --transport streamable-http --host 127.0.0.1 --port 8472

Cursor / .cursor/mcp.json (per window, one header per project):

{
  "mcpServers": {
    "grag": {
      "url": "http://127.0.0.1:8472/mcp",
      "headers": { "x-grag-db": "project-a" }
    }
  }
}

The server is localhost-only by default, and db names are routing hints, not auth — resolution rejects absolute paths and ... Single-db stdio (grag --db knowledge.lbdb mcp) remains the simple default.

Python API

from grag import GragConfig
from grag.service import GragService
from grag.core.types import SearchRequest

svc = GragService(GragConfig(db_path="knowledge.lbdb"))
res = svc.search_knowledge(SearchRequest(query="who owns the ingestion gateway?", hops=1))
print(res.context)        # cited subgraph text, ready for a prompt

Everything is also mirrored over REST: POST /api/{query,search,context,ingest,ingest/code}, GET /api/{schema,graph/sample,health}, POST /api/{schema/define,nodes/upsert,edges/upsert}.

Retrieval: hybrid + polar-split vectors

  1. Text properties get a native BM25 FTS index per searchable table.
  2. With an embedder configured, embeddings are written with a polar decomposition: magnitude r in one float property, direction u quantized by a swappable codec. Codes only generate candidates; final scores are exact fp32 rescore + graph rerank, so recall loss is bounded and measurable.
  3. Seeds (RRF-fused FTS+vector) expand k hops through the graph — structure compensates for aggressive quantization.

Codec ladder (grag bench reproduces these numbers on a synthetic 1500-doc corpus):

codec bytes/vec (dim 64) recall@10 note
fp32 256 0.998 baseline; native HNSW index
int8 68 0.998 4x smaller, near-zero loss
binary 8 0.476 32x, hamming scan + rescore
polar 14 0.766 experimental PolarQuant-style angular codes (sine-power-law bit allocation, training-free)

Select with GRAG_VECTOR_CODEC / GragConfig.vector_codec. polar is opt-in; int8 is the sweet spot today.

Configuration

Env vars: GRAG_DB_PATH, GRAG_BUFFER_POOL_MB (default 256), GRAG_VECTOR_CODEC, GRAG_TOKEN_BUDGET, GRAG_SEARCH_LABEL_CAP, GRAG_EMBED_PROVIDER (fastembed|remote), GRAG_EMBED_MODEL, GRAG_EMBED_DIM, GRAG_EMBED_BASE_URL, GRAG_EMBED_API_KEY_ENV.

GRAG_SEARCH_LABEL_CAP (default 2) is the per-label diversity cap on search_knowledge: no single node label may occupy more than this many of the fused top_k seeds before other labels get a turn (leftover slots then backfill by rank). It stops a large table — e.g. an ingested repo's Function nodes — from crowding out knowledge tables (Decision/Concept) on a general query. Set 0 to disable and get pure RRF rank order.

Performance budget

Measured, not assumed — tests/test_perf.py guards cold start (< 2s), search latency, and RSS; grag bench reports recall + p50/p95 + RSS per codec. Design rules: no heavy deps in the default install, one process for API+UI, lazy embedder loading, default LIMITs, hop caps, statement timeouts, token budgets everywhere.

Storage conventions

  • One .lbdb file per database. Properties starting with _ are grag-internal.
  • Provenance: _source, _created_at on every table created via define_schema.
  • Vector columns (embedding, _emb_r, _emb_code, _emb_model) are added lazily by the retrieval layer.
  • _grag_tables registry powers introspection and canonical Label:key node ids.

Develop

python -m pytest tests/          # 160+ tests, ~10s
grag bench                        # codec recall/latency/RSS table
cd ui && npm run build            # rebuilds the UI into src/grag/api/static/

See CONTRIBUTING.md for the branching model (Gitflow-lite: main + develop + feature/release/hotfix), PR rules, and how releases are cut and published to PyPI.

Known limits: embedded engine = single-writer; LadybugDB reserves a large virtual address space per open database (actual RSS stays within the buffer pool) — close Engines you create; polar codec encode is Python-speed (fine at query time, slower at write time).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gragdb-0.2.0.tar.gz (356.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gragdb-0.2.0-py3-none-any.whl (332.6 kB view details)

Uploaded Python 3

File details

Details for the file gragdb-0.2.0.tar.gz.

File metadata

  • Download URL: gragdb-0.2.0.tar.gz
  • Upload date:
  • Size: 356.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gragdb-0.2.0.tar.gz
Algorithm Hash digest
SHA256 577203f7f3f62c5d539cbaf12c6626ce2f6653ea278e23c543a577235d0bfa08
MD5 f5c7229a70fb3a14d928ccfdeeee782a
BLAKE2b-256 59764e1792cbc1fa05029f84129533b4d5ba96d67d4ae795404ee7d452e29d44

See more details on using hashes here.

Provenance

The following attestation bundles were made for gragdb-0.2.0.tar.gz:

Publisher: publish.yml on Krokz/grag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gragdb-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: gragdb-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 332.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gragdb-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 55655060e16b554414931fe43b56c2f5cc602a2621f7592f5de59cd99ef59017
MD5 0f48262b3e543643e59bb177bfe50fb6
BLAKE2b-256 b4fd8e990352667ba3783be12df5366fd343db3d6f333cd13ad20d5136290655

See more details on using hashes here.

Provenance

The following attestation bundles were made for gragdb-0.2.0-py3-none-any.whl:

Publisher: publish.yml on Krokz/grag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page