Skip to content
evoclockPublic

About

Graph-RAG that earns trust. Treats sources as primitives, because garbage in == garbage out. Bayesian SBM clustering, community-aware MCP retrieval with routing keys on every hit, strict schema validation, and honest per-tool token-economy accounting. A persistent pipeline, not a one-shot HTML dump that builds a knowledge base unfit for purpose.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Nuthatch

Nuthatch logo

CI License Python 3.12+

Local-first knowledge-graph tool for structured corpora, with principled clustering, strict ingest-time schema validation, and honest LLM token-economy accounting.

What this is

Nuthatch turns a curated corpus (research papers, patents, internal documents, technical reports, notes, Repomix-preprocessed codebases, or any text-based source) into a navigable knowledge graph and serves it to an MCP-aware agent (Claude Code, Codex, OpenCode, Aider, Pi, Hermes, an Obsidian plugin, or your own client). The agent queries the graph through nine read-only MCP tools; the operator drives the build pipeline through a five-stage CLI.

The rendered corpus also opens as an Obsidian-compatible vault: per-paper cards live under cards/, per-community pages under communities/, with dashboard.md / index.md / log.md at the top. Wikilinks between cards drive Obsidian's graph view; Dataview queries in the dashboard filter by tag / year / community.

The corpus type is a schema profile, not a category constraint. Nuthatch is designed for any structured-corpus problem where graph-augmented retrieval helps.

Why it exists

The graph-augmented retrieval space has working open-source implementations (Microsoft GraphRAG, graphify, LightRAG, HippoRAG, nano-graphrag) plus adjacent tools (Cognee, PaperQA2, Khoj, Verba). We surveyed them and built Nuthatch anyway because we wanted a specific combination of choices none of them makes:

  • Community-aware retrieval, not just clustering. Every chunk hit carries community_id, community_path (the nested SBM chain), and community_label so an agent can route directly into the relevant cluster without paying for a card fetch first. Plus community_search ranks communities semantically by query-to-centroid cosine; flat modularity-based clustering (Leiden, Louvain) cannot do this. See docs/Design_Decisions.md § Community-aware retrieval.
  • Bayesian Stochastic Block Model as the principled clustering ceiling, implemented via graph-tool from Tiago Peixoto's lab (Professor of Complex Systems and Network Science, Goethe University Frankfurt). Leiden is a graceful fallback when graph-tool is unavailable. The SBM tier emits a nested hierarchy that flat methods cannot, and Nuthatch persists every level so agents can zoom from leaf clusters up to coarser super-clusters.
  • Honest per-tool token-economy accounting (BM25 baseline for search, card-token-sum baseline for subgraph and community) instead of whole-corpus headline ratios.
  • Build pipeline triggered out-of-band by the operator with quality gates between stages, never end-to-end unattended.
  • Strict schema quarantine on ingest with reasoned per-file failure sidecars.
  • Authoritative metadata fetched from publisher APIs (arxiv, bioRxiv) rather than parsed from PDF body text.
  • User-extensible schema profiles supporting papers, patents, internal documents, and arbitrary user-defined types.
  • A read-only MCP query surface that the agent cannot mutate.

Quick start

See docs/HOWTO.md for the full operator guide covering install, corpus initialisation, the five-stage build pipeline, MCP registration per agent host, and stop / resume semantics. The short version:

sfw uv tool install "nuthatch @ git+https://github.com/evoclock/nuthatch.git@main"
nuthatch init ~/my-corpus --register-as my-corpus --set-default

# Drop sources anywhere under the corpus root (inbox/, arxiv/,
# bioarxiv/, notes/, root-level: your choice). Then trigger each
# pipeline stage in sequence and inspect between stages.

scripts/ops/launch-stage.sh ingest  my-corpus
scripts/ops/launch-stage.sh embed   my-corpus
scripts/ops/launch-stage.sh graph   my-corpus
scripts/ops/launch-stage.sh cluster my-corpus
nuthatch render --corpus my-corpus

# Then serve it to an MCP-aware agent:
nuthatch serve --corpus my-corpus

A pre-built reference corpus (nuthatch-kb-demo) is available to explore before building your own.

Hosted-provider credentials

Hosted LLM stages (OpenAI, Anthropic) read their API keys from your operating-system credential store via Python keyring — never from environment variables. Store each key once, before the first hosted stage:

keyring set nuthatch openai-api-key
keyring set nuthatch anthropic-api-key

If a key is missing, the pipeline fails closed with openai credential error: absent_store_entry (plus the provisioning hint above) rather than silently falling back. Other credential failures map to closed kinds too: store_unavailable (no usable keyring backend on this host), access_denied (locked or denied keychain), invalid_credential (blank entry). Non-sensitive settings such as OLLAMA_HOST and NUTHATCH_ACCELERATOR remain ordinary environment configuration.

Documentation

  • docs/Design_Decisions.md: architectural choices and the reasoning behind each, including the peer-landscape survey and the token-economy methodology
  • docs/HOWTO.md: operator runbook for the five-stage pipeline plus the MCP query surface
  • docs/SPEC.md: the architecture spine
  • docs/extraction-benchmarks/ocr-comparison.md: evidence for the OCR routing decisions (Chandra vs Docling vs EasyOCR vs Granite vs SmolDocling across three representative scanned papers)
  • Per-agent skill files under agent_skills/ (skill-claude-code.md, skill-codex.md, skill-aider.md, skill-opencode.md, skill-pi.md, skill-hermes.md) for MCP registration details and recommended multi-step query flows

Extraction and model stack

This section documents the exact tools and models used to build the reference corpus. It is here so others can replicate the setup or substitute equivalents at each stage.

PDF triage

Every source PDF goes through a cheap pre-flight pass with pdftotext (poppler) before any extraction cost is paid. If the text layer is present and dense enough the document is classified as born-digital; otherwise it is routed to one of the OCR paths below.

Extraction routing

Input type Condition Extractor
Born-digital PDF always Docling (no OCR)
Scanned PDF GPU available, max quality Chandra OCR 2
Scanned PDF GPU available, not max quality Granite-Docling 258M VLM
Scanned PDF CPU-only Docling + EasyOCR
Math span retry per document, broken spans only Chandra OCR 2

Math span retry. Post-extraction validation records every broken inline math span per document (position, broken-ratio, sample). For each document that has broken spans, all of its broken inline math spans are consolidated into a single page; Chandra OCR 2 resolves them in one pass against that page and the corrected expressions are traced back to their original positions. Only the span-level corrections are applied and the deferred record is closed. Math-span retry is a Chandra-specific capability; documents processed under the other extraction backends surface broken spans without the deferred-retry pass.

SmolDocling is documented as a fallback (small VLM, CPU-capable) but is not yet wired into the routing logic.

Chandra OCR — supported, not shipped silently

Nuthatch supports Chandra OCR as a backend because it produces the best results for research-grade math-aware extraction. Chandra-ocr's model weights are distributed under the OpenRAIL-M licence, which carries use restrictions that users adopt when they install Chandra. We surface this directly rather than treat the dependency as silent.

Chandra is therefore an opt-in extra, not a default dependency. To install Nuthatch with the Chandra backend:

sfw uv add "nuthatch[chandra]"

Or, as a standalone tool install:

sfw uv tool install "nuthatch[chandra]"

The sfw prefix routes the install through Socket Firewall's threat- intel scan before any tarball lands on disk. We recommend it for every package install regardless of source.

The dependency-licence compatibility holds at the package level: chandra-ocr the package is Apache 2.0; only the model weights downloaded at runtime carry OpenRAIL-M obligations, and those attach to whoever downloads them.

Embedding models

Profile Model Notes
scientific_paper SPECTER2 (AllenAI) trained on the scientific citation graph; planned default for this profile
All other profiles BGE-M3 (BAAI) general-purpose multilingual; current default for all profiles

Eval stack

Evaluation runs fully locally via Ollama except where noted.

Role Model Source
RAGAS generator / answerer gemini-3-flash-preview:cloud Ollama (local)
RAGAS judge / graph eval / cluster eval granite3-dense:8b Ollama (local)
Retrieval embeddings (eval) BGE-M3 same checkpoint as ingest, via NuthatchEmbeddings RAGAS adapter
Cluster label relabelling granite3-dense:8b Ollama (local), --relabel-llm pass

All models listed as "Ollama (local)" run on-device with no outbound traffic. Chandra OCR 2 and BGE-M3 likewise run locally; SPECTER2 can be served locally via the HuggingFace transformers backend.

Architecture

Nuthatch data lifecycle

The full module graph and architecture diagrams are at docs/architecture/.

The corpus graph

Nuthatch D3 topology viz: full SBM partition with some filters dropped
SBM partition with a baseline subset of filters active. Communities coloured via Catppuccin Mocha; floating boxes (Communities, Filters) are draggable and persist position to localStorage. Interactive version lives at docs/graph.html in every nuthatch-published KB; open it in any browser, no backend.

Same partition, most filters off, citations off
Most relations toggled off and the cites relation off: the visible structure is the SBM community as the clustering inferred it from non-citation edges (authorship, topic / method co-mentions, embedding-driven adjacency).

Same partition, most filters off, citations on
Same filter set as above, but with cites toggled on. The new edges show how one paper citing another can extend a community beyond what semantic / co-mention signals alone would produce. The contrast between the previous frame and this one is the citation-extension effect made visible.

Demo

Nuthatch demo

Token economy

Nuthatch token-economy report: per-tool actual cost vs BM25 and card-sum baselines
Per-tool token-economy report from token_econ_report. Each tool is measured against two honest baselines: BM25 (what a flat keyword search over the whole corpus would cost) for search tools, and card-token-sum (the cost of fetching every card) for subgraph and community tools. The ratio shows how much of the corpus a query actually touches. Whole-corpus headline ratios inflate the savings figure by comparing against a ceiling nobody would pay; Nuthatch compares against what a reasonable alternative would actually cost.

Nuthatch token-economy report: all tools including community graph tools
Full report after wiring counterfactuals for all community tools. The numbers are worth reading carefully.

What each ratio actually measures - and where to trust it:

The community_hierarchy (138×) and community_core_nodes (139×) figures are the cleanest in the report. Without nuthatch, finding a document's community path means loading and traversing the full communities.json index; getting the high-degree members of a community means loading every member card and ranking by degree. There is no cheaper alternative - these operations are intrinsically whole-index or whole-community reads. The ratios reflect that directly.

corpus_search (4×) is the most methodologically principled number in the report. Its counterfactual is BM25 over the same chunks at the same k - the only variable is whether the retrieval is semantic (dense) or lexical (sparse). It measures exactly the contribution of vector similarity over keyword matching, nothing more. A 4× reduction means the dense retrieval returns materially more relevant chunks per token than BM25 would for the same query budget.

community_get (21×) and community_brief (8×) are likewise honest. The community page bundles information that would otherwise require fetching every member card individually; the brief is a strict subset of the full page and the ratio is exact by construction.

community_search at 22,000× is a known upper bound and should be read as such. The counterfactual used is n_total_chunks × avg_chunk_tokens - the total token cost of scanning every chunk in the corpus to do brute-force cosine grouping by community. That ceiling is real but nobody would actually pay it: no agent working with a 123-paper corpus loads all 123 papers into its context window per query just to find relevant communities. A smarter fallback would be corpus_search(query, k=large), which already returns community_id metadata, at a cost of a few thousand tokens. The honest ratio for community_search is therefore somewhere in the 50-150× range. The 22,000× figure is preserved in the report because it is the correct answer to the specific counterfactual question posed ("what if you had no centroid index at all and had to read every chunk?"), but it is not a number to put in a headline. Some tools in this space make such claims without compunction; this project does not, because the whole point is to keep the numbers honest.

subgraph_extract at 0.57× is correctly negative and stays in the report. Depth-2 BFS on this corpus returns more tokens than the card-sum baseline it is compared against - it fans out too aggressively. The default depth has been changed to 1 and a max_nodes cap added; at depth 1 the ratio becomes positive, and the negative result from the earlier session is kept as a reminder that graph tools can inflate context just as easily as they compress it.

Status and roadmap

The build pipeline is operational. The five build stages (ingest, embed, graph, cluster, render) chain into a queryable corpus, plus three post-pipeline commands serve and ship it: serve (the MCP server), publish (export a shareable KB directory), and viz d3 (interactive force-atlas2 + Catppuccin topology viz). Clustering supports an optional --relabel-llm pass that replaces the heuristic word-frequency labels with topical 2-4 word names produced by an LLM (default granite3-dense:8b via Ollama).

The MCP server exposes nine read-only tools: corpus_search, subgraph_extract, card_get, community_get, community_brief, community_search, community_core_nodes, community_hierarchy, and token_econ_report. The community tools, plus the routing keys (community_id, community_path, community_label) carried inside every search hit and every card's frontmatter, are what make graph-RAG actually work without per-card lookups.

The test suite covers every contract (routing thresholds, model defaults, chunk coverage, reranker invocation, schema validation, end-to-end pipeline integration, suffix-rename invariants, publish-side canonical promotion, --relabel-llm round-trip with mock LLM). PyPI publication is in progress.

Planned work, not yet landed:

  • Air-gapped / corporate environments. A Dockerfile + container entrypoint so the published KB can be lifted into enterprise DevOps environments (AWS, GCP, Azure DevOps, and similar internal container runtimes) and queried via MCP without any outbound traffic. The published KB is already a self-contained bundle (markdown cards + JSON indexes + chroma archive); this task wraps it in a runtime so an internal deployment can reach a full MCP server inside the firewall.
  • Routing hardening for heterogeneous inputs. The current ingest pipeline routes by filename profile (arXiv preprint, bioRxiv, internal doc, patent); next is per-content-type detection and per-profile thresholding so mixed corpora (scanned PDFs + born-digital papers + plain-text notes) route cleanly without manual triage.
  • Docling as the general-purpose extraction default. Docling becomes the default extractor for born-digital text, with SmolDocling and EasyOCR as the documented fallbacks per the OCR benchmark (docs/extraction-benchmarks/ocr-comparison.md).
  • SPECTER2 as the scientific-paper embedding default. The current default embedding model is BAAI/bge-m3 (general purpose, all profiles). For the scientific_paper profile, SPECTER2 (AllenAI; trained on the scientific-citation graph and identified during the PhD knowledge-base build as superior to general-purpose embeddings for paper retrieval) becomes the documented default. All other profiles keep BGE-M3.

Licence

GNU Affero General Public License v3 only (AGPL-3.0-only) plus a Section 7(b) author-attribution clause. See LICENSE for the full text.

The plain-English version:

  • If you are using Nuthatch in an open-source project: you are asked to credit Julen Gamboa as the original author in your README or equivalent primary documentation, with a link back to this repository. It's a courtesy ask; the project will not chase you to the ends of the earth to enforce it. The AGPL terms still apply (source disclosure on conveyance and network use), and that is the part with real teeth. We support genuine open-source use without friction.
  • Commercial use, including forks and substantial modifications, is permitted under the AGPL when all AGPL obligations and the Section 7(b) attribution requirements are followed. This includes offering covered source to network users as required by Section 13. A separate commercial licence is required only when an organisation wants proprietary modifications, alternative attribution terms, or otherwise cannot or does not wish to comply with those obligations. Contact the author for details; pricing is flexible and case-by-case.
  • The split exists because we do not have a problem with open-source contributors and we do have a problem with the pattern of enterprises that exploit open-source projects without contributing back. AGPLv3 plus a commercial offering is the standard, OSI-approved pattern (Nextcloud, Plausible, Cal.com, iText) for distinguishing the two populations cleanly.

A commercial licence template will be published in this repository at COMMERCIAL.md once finalised. Until then, reach out directly.

Why this licence (and why it changed twice in two days)

If you saw this repository's commit log, the licence moved from Apache 2.0 (initial release) to MIT plus a Commercial Attribution Rider (0.0.2) to AGPLv3 plus §7(b) (0.0.3) over two days. Faster than is comfortable, worth explaining once instead of leaving as a thread on the issue tracker.

Each step sharpened the same question: what mechanism actually distinguishes open-source use from commercial use cleanly, without putting friction on either side?

Apache 2.0 was the default and asked nothing of either side, which left no leverage for the commercial-fair-use case. The MIT + Rider asked for attribution but was non-OSI (which scares off the compliant commercial adopters, the ones who would have respected the ask anyway) and was a social-pressure mechanism with weak enforcement teeth against the rest. AGPLv3 + §7(b) + a separate commercial licence is the standard dual-licence pattern used by Nextcloud, Plausible, Cal.com, and iText. AGPLv3 is OSI-approved so genuine open-source adoption sees no friction; Section 13's network-use source-disclosure obligation is the structural lever that gives the licence real teeth; Section 7(b) preserves author attribution as an enforceable term rather than a courtesy ask. The commercial licence is for adopters who cannot or do not want to comply with AGPL, priced case-by-case rather than triggered by a revenue threshold.

The intent is plain. Open-source use should be friction-free. Larger commercial adopters should either contribute back through AGPL source disclosure or pay through the commercial licence. The previous two attempts got most of the way there; this one settles it.

Acknowledgements

Designed and built by Julen Gamboa, who drove system design, implementation, agent orchestration, and code review. Claude Code and Hermes operated as spec-driven agents, executing implementation tasks under that direction.

Specs and decisions are pinned in docs/SPEC.md and docs/Design_Decisions.md so every implementation points back to a spec entry.

The SBM clustering tier is built on graph-tool, developed by Tiago Peixoto (Center for Critical Computational Studies, Goethe University Frankfurt). The nested SBM hierarchy and the principled model-selection criterion it provides are the reason Nuthatch can offer a clustering ceiling that goes beyond what flat modularity methods produce.

About

Graph-RAG that earns trust. Treats sources as primitives, because garbage in == garbage out. Bayesian SBM clustering, community-aware MCP retrieval with routing keys on every hit, strict schema validation, and honest per-tool token-economy accounting. A persistent pipeline, not a one-shot HTML dump that builds a knowledge base unfit for purpose.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages