agentsclimarketplace

Muninn graphrag

Skill neozenith/sqlite-muninn/skills/muninn-graphrag

Builds end-to-end GraphRAG retrieval over a text corpus entirely in SQLite: chunk → embed → extract entities and relations → build knowledge graph → detect communities → label clusters → retrieve via vector seed + graph expansion + centrality ranking. Composes muninn_embed, hnsw_index, muninn_extract_ner_re_batch, graph_leiden, muninn_label_groups, and muninn_extract_er. Use when the user mentions "GraphRAG", "Graph RAG", "knowledge graph RAG", "retrieval-augmented generation", "RAG pipeline in SQLite", "KG retrieval", "entity resolution", "muninn_extract_er", "community labeling", "KG indexing", or wants to build Microsoft GraphRAG- style retrieval on a corpus.From its SKILL.md

Install
npx -y skills add neozenith/sqlite-muninn --skill muninn-graphrag

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

9.2 KB, as published. Nobody here has run it

muninn-graphrag — Composite retrieval pipeline in SQL

GraphRAG combines vector retrieval (for recall) with knowledge-graph structure (for reasoning/explanation) and community summaries (for coverage). muninn implements every stage in SQL — no Python model server, no extra extensions.

This skill is the orchestration layer. It assumes you already know the building blocks:

Pipeline shape

Build phase:
  corpus → chunk → embed → HNSW index
                     ↓
                   NER+RE → knowledge-graph edges → Leiden → labels

Retrieve phase:
  query → embed → KNN seed → BFS expand → rank by centrality → top-k

Prerequisites

.load ./muninn

INSERT INTO temp.muninn_models(name, model)
  SELECT 'MiniLM', muninn_embed_model('models/all-MiniLM-L6-v2.Q8_0.gguf');

INSERT INTO temp.muninn_chat_models(name, model)
  SELECT 'Qwen3.5-4B', muninn_chat_model('models/Qwen3.5-4B-Instruct.Q4_K_M.gguf');

For long corpora, Qwen3.5-4B Q4_K_M on Metal is a good speed/quality trade-off.

Build phase

1 — Source corpus

CREATE TABLE documents (id INTEGER PRIMARY KEY, content TEXT);

INSERT INTO documents(content) VALUES
  ('Tesla acquired Maxwell Technologies in 2019 for $218 million.'),
  ('SpaceX, founded by Elon Musk in 2002, launched Falcon 9 in 2010.'),
  ('Apple released the iPhone in 2007; Steve Jobs unveiled it at Macworld.'),
  ('Tim Cook succeeded Steve Jobs as Apple CEO in 2011.'),
  ('Elon Musk became CEO of Tesla in 2008.'),
  ('Maxwell Technologies was based in San Diego.'),
  ('Steve Jobs co-founded Apple with Steve Wozniak in 1976.'),
  ('SpaceX is headquartered in Hawthorne, California.');

2 — Chunk (if needed) and embed

-- Short docs; skip chunking. Check length first:
SELECT id, muninn_token_count('MiniLM', content) AS tokens FROM documents;

CREATE VIRTUAL TABLE doc_vec USING hnsw_index(dimensions=384, metric='cosine');
INSERT INTO doc_vec(rowid, vector)
  SELECT id, muninn_embed('MiniLM', content) FROM documents;

For longer documents, chunk by paragraph in your host language or via a recursive CTE that splits on sentence boundaries.

3 — Extract entities + relations

CREATE TABLE kg_triples(
  doc_id INTEGER, head TEXT, rel TEXT, tail TEXT, score REAL
);

-- Batch NER+RE is ~3-5× faster than per-row calls
WITH batches AS (
  SELECT json_group_array(content) AS texts,
         json_group_array(id) AS ids
  FROM documents
)
INSERT INTO kg_triples(doc_id, head, rel, tail, score)
SELECT
  json_extract(ids, '$[' || b.key || ']'),
  json_extract(r.value, '$.head'),
  json_extract(r.value, '$.rel'),
  json_extract(r.value, '$.tail'),
  json_extract(r.value, '$.score')
FROM batches,
     json_each(muninn_extract_ner_re_batch(
       'Qwen3.5-4B',
       (SELECT texts FROM batches),
       'person,organization,date,location,product',
       'founded,acquired,launched,released,leads,based_in',
       4
     )) b,
     json_each(json_extract(b.value, '$.relations')) r;

4 — Build KG edge table

CREATE TABLE kg_edges(src TEXT, dst TEXT, rel TEXT, weight REAL);

INSERT INTO kg_edges(src, dst, rel, weight)
  SELECT head, tail, rel, AVG(score)
  FROM kg_triples
  GROUP BY head, tail, rel;

5 — Leiden communities

CREATE TABLE kg_communities AS
SELECT node, community_id FROM graph_leiden
  WHERE edge_table = 'kg_edges' AND src_col = 'src' AND dst_col = 'dst'
    AND direction = 'both' AND resolution = 1.0;

6 — Label each community

CREATE TABLE kg_labels AS
SELECT group_id AS community_id, label, member_count FROM muninn_label_groups
  WHERE model = 'Qwen3.5-4B'
    AND membership_table = 'kg_communities'
    AND group_col = 'community_id'
    AND member_col = 'node'
    AND min_group_size = 2
    AND max_members_in_prompt = 10;

Now you have: a vector index over chunks, a KG edge table, community membership per entity, and a human-readable label per community.

Retrieve phase

A — VSS seed

WITH query AS (
  SELECT muninn_embed('MiniLM', 'Who leads Apple today?') AS q
)
SELECT rowid, distance FROM doc_vec, query
  WHERE vector MATCH query.q AND k = 3;

B — Graph expansion

For each seed document, pull entities mentioned, then BFS 1 hop in the KG to gather related entities.

WITH query AS (
  SELECT muninn_embed('MiniLM', 'Who leads Apple today?') AS q
),
seeds AS (
  SELECT v.rowid AS doc_id
  FROM doc_vec v, query
  WHERE v.vector MATCH query.q AND k = 3
),
seed_entities AS (
  SELECT DISTINCT head AS entity FROM kg_triples WHERE doc_id IN (SELECT doc_id FROM seeds)
  UNION
  SELECT DISTINCT tail FROM kg_triples WHERE doc_id IN (SELECT doc_id FROM seeds)
)
SELECT DISTINCT node FROM graph_bfs
  WHERE edge_table = 'kg_edges' AND src_col = 'src' AND dst_col = 'dst'
    AND start_node = (SELECT entity FROM seed_entities LIMIT 1)
    AND max_depth = 2 AND direction = 'both';

C — Rank by centrality

WITH bc AS (
  SELECT node, centrality FROM graph_node_betweenness
    WHERE edge_table = 'kg_edges' AND src_col = 'src' AND dst_col = 'dst'
      AND direction = 'both' AND normalized = 1
)
SELECT node, centrality FROM bc ORDER BY centrality DESC LIMIT 10;

D — Assemble context

Join the ranked entities with their document text and community labels to form a prompt payload for a downstream LLM:

SELECT
  e.node            AS entity,
  l.label           AS community,
  group_concat(d.content, ' || ') AS evidence
FROM kg_communities c
JOIN kg_labels l USING (community_id)
JOIN kg_edges e ON (e.src = c.node OR e.dst = c.node)
JOIN kg_triples t ON (t.head = c.node OR t.tail = c.node)
JOIN documents d ON d.id = t.doc_id
WHERE c.node IN (SELECT node FROM /* your ranked seed set */ LIMIT 5)
GROUP BY e.node, l.label;

Entity resolution shortcut

muninn_extract_er runs the full deduplication cascade (KNN blocking → Jaro-Winkler × cosine scoring → LLM refinement → optional edge-betweenness bridge removal → Leiden) in a single call. Use it after NER when the same entity appears under variants.

-- Build an entity-name vector index first
CREATE VIRTUAL TABLE entity_vec USING hnsw_index(dimensions=384, metric='cosine');
INSERT INTO entity_vec(rowid, vector)
  SELECT rowid, muninn_embed('MiniLM', head) FROM kg_triples;

SELECT muninn_extract_er(
  'entity_vec',   -- HNSW with entity embeddings
  'head',         -- name column on kg_triples
  10,             -- KNN neighbors
  0.3,            -- max cosine distance for candidate pairs
  0.5,            -- Jaro-Winkler weight (cosine gets 1-0.5 = 0.5)
  0.1,            -- LLM refinement band (0 disables LLM)
  'Qwen3.5-4B',   -- chat model, required when band > 0
  NULL,           -- edge_betweenness_threshold (NULL = skip)
  'same_source'   -- type guard
);

Returns {"clusters": {"entity_name": cluster_id, ...}}. Cluster ids are 0-indexed.

Pipeline performance targets

StageThroughput (M1 Metal, MiniLM + Qwen3.5-4B Q4_K_M)
Embed~5k docs/sec
NER+RE (single)~1-3 docs/sec
NER+RE (batch 4)~4-10 docs/sec
Leiden on 10k-node KGseconds
Betweenness on 10k-node KG10s-minutes (O(VE))

WASM: expect 5-10× slower across the board; not recommended for full pipelines over >1k docs.

Common pitfalls

  • Qwen3.5 <think> leaks into relations — set skip_think = 1 in muninn_extract_ner_re_batch calls for throughput.
  • Community labels look generic — raise min_group_size to 5+ and provide a system_prompt scoped to your domain.
  • KG too dense to Leiden cleanly — increase resolution in graph_leiden; run edge-betweenness pruning before clustering for very dense graphs.
  • Re-running is slow — none of these tables are temporary. Persist the DB and incrementally append new docs; only re-run Leiden/betweenness when the graph structure materially changes.

See also

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.