GraphRAG layer for AgentContextOS, shipped in Step 2.9. Provides
community detection, LLM-backed sub-graph summarisation, and a
community-aware graph retriever that plugs into
HybridRetriever as the graph source.
GraphRAG (Edge et al. 2024) is a retrieval style that uses community structure in a knowledge graph to drive retrieval — instead of asking the LLM "which entities are relevant to this query?", we precompute communities and their summaries at index time, then at query time pick the most relevant communities and follow their key entities into the graph.
The rag-graphrag package implements three pieces:
- Community detection — partition the graph into dense
sub-graphs. Two algorithms ship; both produce identical
Communityobjects. - Sub-graph summarisation — turn each
Communityinto aCommunitySummaryvia theLLMSPI. - Graph-aware retrieval — score the query against community
summaries, pick the top communities, use their key entities as
graph-expansion seeds, and emit
ChunkReflists ready for RRF fusion.
Domain types and the GraphRAGError exception live in rag_core so
downstream packages can consume them without depending on
rag_graphrag.
# Default — pure-Python Louvain detector.
uv pip install rag-graphrag
# Optional — higher-modularity Leiden via igraph + leidenalg.
uv pip install 'rag-graphrag[leiden]'import networkx as nx
from rag_graphrag import (
CommunitySummarizer,
InMemoryCommunityStore,
LouvainDetector,
tenant_subgraph,
)
# Pull a tenant-scoped sub-graph out of the multi-tenant graph store.
sub = tenant_subgraph(networkx_store.graph, str(ctx.tenant_id))
# 1) Detect communities.
communities = await LouvainDetector().detect(ctx, sub)
# 2) Summarise each community via the LLM SPI.
store = InMemoryCommunityStore()
summariser = CommunitySummarizer(llm)
for c in communities:
node_props = {n: dict(sub.nodes[n]) for n in c.node_ids if n in sub}
summary = await summariser.summarize(ctx, c, node_properties=node_props)
await store.upsert_community(ctx, c)
await store.upsert_summary(ctx, summary)from rag_graphrag import GraphRAGConfig, GraphRAGRetriever
retriever = GraphRAGRetriever(
store,
graph_backend, # any GraphRetrievalBackend
config=GraphRAGConfig(community_top_k=3, graph_hops=2),
)
refs = await retriever.retrieve(ctx, "Who manages compliance at Acme?")refs is a list[ChunkRef] — pass it through HybridRetriever (Step
2.5) for fusion with vector + keyword paths, then reranker + packer.
from rag_retrieval import HybridRetriever
from rag_graphrag import graphrag_adapter
retriever = HybridRetriever(
vector_backend=vector_store,
keyword_backend=keyword_store,
graph_backend=graph_store,
graph_adapter=graphrag_adapter, # entity → chunk_ids unwrap
)| Symbol | Type | Notes |
|---|---|---|
LouvainDetector(min_community_size=2) |
class | Default; pure-Python NetworkX. |
LeidenDetector(min_community_size=2) |
class | [leiden] extra; higher modularity. |
tenant_subgraph(graph, tenant_id) -> nx.Graph |
helper | Tenant-scoped subgraph copy. |
CommunityDetector |
Protocol | Implement to plug a custom detector. |
Both detectors expose:
async def detect(
ctx: RequestContext,
graph: nx.Graph,
*,
resolution: float = 1.0,
seed: int | None = 0,
) -> list[Community]CommunitySummarizer(llm: LLM, *, config: SummariserConfig | None = None)
async summarize(
ctx: RequestContext,
community: Community,
*,
node_properties: dict[str, dict[str, object]] | None = None,
) -> CommunitySummarySummariserConfig(max_tokens=512, temperature=0.0, max_key_entities=8)
tunes per-call cost and determinism.
CommunityStore is a Protocol; V1 ships InMemoryCommunityStore.
Methods:
upsert_community(ctx, community) -> None
upsert_summary(ctx, summary) -> None
get_community(ctx, community_id) -> Community | None
get_summary(ctx, community_id) -> CommunitySummary | None
list_communities(ctx) -> list[Community]
list_summaries(ctx) -> list[CommunitySummary]
community_for_node(ctx, node_id) -> Community | NoneCross-tenant access raises GraphRAGError.
GraphRAGRetriever(
store: CommunityStore,
graph_backend: GraphRetrievalBackend,
*,
config: GraphRAGConfig | None = None,
adapter: GraphAdapter | None = None,
)
async retrieve(ctx, query: str) -> list[ChunkRef]
async score_communities(ctx, query: str) -> list[tuple[CommunitySummary, float]]GraphRAGConfig(community_top_k=3, graph_hops=1, seeds_per_community=5, expand_limit=50) tunes the breadth of the search.
async def graphrag_adapter(
ctx: RequestContext,
neighbors: list[NeighborResult],
) -> list[ChunkRef]Reads neighbor.properties["chunk_ids"] and emits one ChunkRef per
chunk. Nodes without the property are silently skipped (they're
legitimate isolated-entity nodes).
ragctl graphrag "Who manages compliance?"Builds a 10-node toy graph, runs the full detect → summarise → retrieve
flow against NoopLLM + NoopGraphStore, prints communities + chunks.
No infra or LLM credentials required.
- All public types are frozen Pydantic v2 models — create new instances rather than mutating.
- Each pipeline stage opens its own OTel span via
span_from_trace_context:graphrag.detect,graphrag.summarize,graphrag.retrieve. Attribute keys are namespacedrag.graphrag.*mirroring Step 2.7/2.8 conventions. LouvainDetectorcollapses directed graphs to undirected before community detection. Multi-graphs are accepted but parallel edges collapse to one for modularity scoring.graphrag_adapterdoes not deduplicate chunks across entities — that isrrf_fuse's job (Step 2.5)._overlap_scoreis set-overlap with a 2× weight onkey_entitiesvs. summary text. This is intentionally simple; a real deployment will swap it for embedder-cosine via a constructor argument in Phase 3.
| Want to | Approach |
|---|---|
| Use Leiden instead of Louvain | Construct with LeidenDetector(min_community_size=...) (requires [leiden] extra). |
| Plug a custom detector | Implement the CommunityDetector Protocol; pass it where you'd pass LouvainDetector. |
| Use a real LLM backend | Pass any LLM SPI implementation (OpenAI / Anthropic / vLLM) into CommunitySummarizer. |
| Replace the community store | Implement the CommunityStore Protocol — V1 ships in-memory only; persistent stores arrive with Phase 3 / Step 3.5. |
| Use embedder cosine for community scoring | Override GraphRAGRetriever.score_communities in a subclass or pass a custom adapter (the public scorer signature will become pluggable in Phase 3 alongside the corpus router). |
- Persistent community store — Phase 3 / Step 3.5 corpus router decides where it lives.
- Hierarchical communities —
LeidenDetectorsupports hierarchical output via repeated calls with reduced graphs; V1 returns flat partitions to keep the API surface narrow. - Embedder-cosine community scoring — V1 uses token overlap; cosine path arrives once Phase 3 wires per-tenant embedder selection.
- LLM-driven contradiction reconciliation across communities — Step 5.x hallucination guard.
/v1/communitiesREST endpoint — Phase 3 gateway.