What a flat graph forgets
Retrieval-augmented generation has two classic memories. Vector RAG fetches relevant text chunks.
Knowledge graphs store (subject, predicate, object) triples. Both are good at what. Neither
remembers why.
Take a pricing decision. A normal knowledge graph records:
With the fourth element in place, you can ask questions a flat graph simply cannot represent:
- "Who approved discounts above 15% in Q3 2024, and are those approvals still valid?"
- "Find all pricing exceptions approved via Slack in the last 6 months."
- "Are there precedents for waiving standard payment terms for a renewal commitment?"
A triple is a headline. A quadruple is the headline plus the byline, the dateline, and the sources.
The fourth element: rc
Context Graph extends every edge from a triple (h, r, t) to a quadruple
(h, r, t, rc). RelationContext is an 11-field dataclass — the decision record attached to the
relationship.
| Field | Answers |
|---|---|
| decision_trace | the why — rationale, exception, override, approval reasoning |
| approved_by · approved_via | who authorized it, and through which channel (slack · zoom · email · in_person · jira · system) |
| valid_from · valid_until · temporal_info | the validity window — is this still in force today? |
| policy_ref | the policy followed (or overridden) |
| quantitative_data | the numbers — discount %, budget, counts |
| supporting_sentences · provenance · confidence_score | verbatim evidence, source reference, and extraction reliability (0–1) |
Two ways into the graph
RelationContext arrives through two paths that feed the same graph and the same vector indexes.
Extraction suits historical records — call transcripts, email threads, meeting notes. The LLM pulls
entities, relations, and an rc JSON object per relationship.
Emission suits the live moment a decision is made — no document, no re-ingestion. An agent or workflow writes the context atomically:
from lightrag.context_graph_types import RelationContext
rc = RelationContext(
decision_trace="VP approved 20% discount; 5-yr relationship + competitive pressure",
approved_by="Sarah Chen", approved_via="in_person",
valid_until="2024-12-31", policy_ref="DiscountPolicy_Standard",
quantitative_data="20% discount", confidence_score=0.97,
)
await cg.emit_decision_trace("Sarah Chen", "MegaCorp", "discount_approval", rc)
CGR3: retrieve, rank, reason
Single-shot retrieval answers single-hop questions. Real decision questions are multi-hop — the answer to "are there precedents…" depends on entities you haven't found yet. CGR3 loops until it has enough.
Edges come back with their RelationContext attached, and the answer is grounded in it:
answer = await cg.cgr3_query(
"Are there precedents for waiving standard payment terms for a renewal?")
# cites approval chains, decision traces, temporal validity, and provenance
CGR3 implements the Retrieve→Rank→Reason paradigm (Liang et al., 2024) over the context-enriched graph.
One question, the right algorithm
A knowledge graph offers several ways to retrieve — entity lookup, community/pattern analysis, vector search, multi-hop reasoning. Each is best for a different shape of question. Most systems make you pick the mode. Context Graph picks it for you.
The auto query router (POST /query/auto) makes one lightweight LLM call to classify the question's
intent, routes it to the best retrieval mode, runs it, and returns the answer plus which mode it chose and why.
Every response says which mode ran and why — so routing is transparent and debuggable rather than a black box. A one-line request replaces a mode-picking decision:
POST /query/auto { "query": "compare the H1H and CurQD-4 on battery and price" }
→ { "mode": "cgr3", "mode_reason": "explicit multi-product comparison",
"response": "…", "latency_ms": 1840 }
Precedents & validity — your graph as case law
Once every edge carries authority, a policy basis, a validity window, and a rationale, the graph starts to behave like a body of case law: decisions you can cite, check for currency, and trace to who made them.
Precedent search
find_precedents() does semantic search over decision traces — "have we done
something like this before, and how did it go?"
Temporal validity
valid_from/until turns "is this still in force today?" into a filter, not a guess —
expired approvals stop counting.
Approval chains
approved_by + policy_ref reconstruct who authorized what under which
rule — an audit trail by construction.
Does surfacing rc actually help?
Storing context is only useful if exposing it to the LLM produces better answers. Context Graph renders RelationContext inline in the prompt (the annotated context format) and was benchmarked head-to-head against the legacy format that hides it.
| Dimension | Annotated – Legacy |
|---|---|
| Completeness | 10 – 2 |
| Usefulness | 10 – 2 |
| Synthesis | 9 – 3 |
| Grounding | 6 – 0 |
Comparative LLM-judged benchmark over the project's question set. Surfacing the decision context inline — not just storing it — is what moves the needle, most strongly on grounding (answers tied to real evidence).
Standing on LightRAG
Context Graph is a fork of LightRAG (HKUDS). It inherits a production-grade RAG substrate and adds the decision layer on top.
Inherited from LightRAG
- Pluggable storage: JSON, NetworkX, Neo4j, PostgreSQL, Mongo, Redis, Milvus, Qdrant, Faiss
- Query modes: local · global · hybrid · naive · mix · bypass
- LLM bindings: OpenAI, Ollama, Azure, Gemini, Bedrock
- Chunking, entity/relation extraction, embeddings
Added by Context Graph
RelationContextextraction + storage on every edge- CGR3 iterative reasoning + annotated context format
emit_decision_trace/find_precedents+ decision endpoints- Multi-tenant workspaces (per-request isolation) and an MCP server (8 tools)
Business Rules: from memory to policy
Once every edge carries why a decision was made, the next question writes itself: should this decision have been allowed at all? Context Graph now ships a business rules engine that evaluates every decision the moment it's recorded.
A flat graph stores facts; the rc layer made it remember decisions; this layer lets it act on
them — pass, flag for review, or reject — before they're ever written.
The catch: you can't exact-match free text
The fields a rule wants to read — the relation type, the approver, the channel — are LLM-extracted prose. The same
decision surfaces as APPROVES, GRANTED_APPROVAL, "signed off on", "authorized". A classic
rule engine matches strings exactly and would miss all but one spelling. So rules get a soft predicate,
sim(), backed by fast static embeddings (model2vec,
numpy-only, sub-millisecond on CPU). It matches by meaning against a small concept catalog — and it's
cheap enough to run on every write. Numbers, dates and flags stay on exact predicates, where you want them.
# a rule reads like the policy a human would state
rule "large discount needs finance review" priority 10
when
sim(relation_type, "APPROVAL") > 0.4 # soft — meaning, not spelling
and percent > 0.15 # hard
and approved_via == "slack" # hard
then
flag("Discount >15% approved over Slack — route to Finance for review")
end
On the real model, sim("GRANTED_APPROVAL", "APPROVAL") scores 1.00 while unrelated
relations sit near 0.1 — a wide, calibratable gap. Polarity (approved vs. rejected) stays on structured fields,
where static embeddings are weak.
Three outcomes, one audit trail
The gate sits at the single point every decision flows through. A decision passes and persists, gets
flagged (persisted, marked needs_review), or is rejected (HTTP 422, nothing written).
Either way it leaves a record as explainable as a numeric check — matched concept · score · threshold:
POST /graph/decision/emit → 200 {
"outcome": "FLAG",
"audit": { "matched_concept": "APPROVAL", "score": 1.0, "threshold": 0.4,
"rule": "large discount needs finance review" }
} # …or 422 if a rule rejects the decision
Authored over REST, per workspace
Policies are managed through a small /rules API and validated before they're stored — the DSL must
parse and every concept a rule references must be defined. Each tenant (workspace) carries its own rule set and concept
catalog, with a pinned embedding model so a rule that passed yesterday behaves identically today.
→ Full walkthrough: the Business Rules Engine guide — the DSL, the
fields you can match on, semantic sim(), the pre-emit gate, and the plain-English rule author.
rc layer) to a
system of policy — one where an agent's actions are checked against the institution's own rules, with a
defensible reason for every pass, flag, and block.
Filling the graph — from any website
A decision graph is only as good as what's in it, and in most real deployments the primary source is the customer's own website. So Context Graph now includes a smart web ingester: point it at a URL and it turns the site into grounded, cited knowledge.
Any format, judged by content-type
The crawler never guesses by file extension. It fetches each link and classifies it by content-type — HTML pages are crawled; any data payload (PDF, Word, PowerPoint, Excel, JSON, CSV, Markdown…) is downloaded and handed to Context Graph's existing document pipeline. A new format needs no new code.
It reads what the browser reads
Modern sites hide their data: content rendered by JavaScript, documents inside embedded viewers, catalogs fetched from back-end APIs. So the ingester renders pages in a headless browser and captures the network responses a page makes — finding data that never appears as a link in the HTML. On a real school-district site, the entire policy catalog lived only in a JSON API the page called at load time — invisible to ordinary scraping, captured here.
Connectors — and letting the LLM pick the right one
Some platforms don't just render with JavaScript; they hide every document behind a recursive or paged API. A Finalsite document container lists files only through a folder-tree service you have to walk; a WordPress site keeps its PDFs in a media endpoint you have to page; a BoardDocs board portal serves agendas through a chain of AJAX calls. Capturing one response isn't enough — you have to replay the platform's API. So Context Graph ships connectors: small, platform-generic plugins that recognise a technology and replay its API to enumerate and download every file. Each keys off the platform's signature, never a specific site.
But which connector fits a given site? Rather than try a fixed list in config order, Context Graph asks the
LLM. Every connector carries a one-line self-description; at load time the ingester summarises the real page's
signals — its generator meta tag, tell-tale markers, the API URLs it actually called — and the LLM reads
those against the connector descriptions and names the plugin(s) that apply. Selection is reasoning about the real
site, not pattern-matching a hard-coded rule. If the LLM is unsure it says so, and the engine safely falls back to
letting each connector self-check.
It works end-to-end on real sites. On a school district's Finalsite portal it walked the document service and pulled
the full policy binder as clean, named PDFs. On creativecommons.org — a WordPress site — the selector
reasoned "the api.w.org tag means WordPress", paged the media API, and downloaded the annual report
and issue briefs, which Context Graph turned into a graph of over a hundred cited entities.
Polite by default, cited by design
It honours robots.txt, rate-limits per host, and stays on-domain — and every fact it extracts carries
the page URL as provenance, so the mentor can cite the exact page it learned something from.
POST /scrape { "url": "https://a-district.org/policies", "render_js": true }
→ crawl + render + capture → documents ingested → graph, with citations
→ Full walkthrough: the Web Ingester guide — the polite crawler, the
connector framework, LLM-driven connector selection, the site analyst, and the /scrape job API.
The road from memory to operational ontology
The decision gate, the web ingester, and the typed ontology are all in; the plan turns Context Graph into a system you don't just reason from, but operate from. (The AI rule author — plain-English policy → validated DSL, dry-run before it goes live — already ships with the rules engine.)
LLM site analyst
The brain on top of the web ingester: given a page, its links, and its captured API responses, an
LLM judges relevance (policy catalog vs. tracking beacon) and extracts the document URLs to fetch — so the
graph fills with what matters, on any site. Live behind analyze=true — see the
web ingester guide.
Ontology layer
A typed schema — object types, typed properties, link types, cardinality — authored top-down and
AI-assisted. It gives rules a real vocabulary (Order.value, Employee.role) and validates
and coerces what extraction writes. Now live, with a plain-English schema author — see the
ontology guide.
Action layer
Executable operations bound to object types (ApproveOrder), invoked over REST,
authorized by the rules gate and written back as an audit edge — turning a graph you reason from into one
you operate from. Live now; next come object-level access control and agent action-discovery — see the
action layer guide.
The arc is deliberate: facts → decisions → policy → an operational ontology, fed from real sources. Each stage stands on its own and sharpens the next — the rules engine gets dramatically stronger once the ontology gives it typed values to reason over.
AI agents that remember the why
The sharpest place a decision-aware graph earns its keep is AI-driven software development. A coding agent has perfect recall inside a session and near-total amnesia between them — so it rebuilds what already exists, "fixes" deliberate choices whose reasoning it lost, and drifts from the team's methodology. Point it at Context Graph and the project gets a shared memory and a process that runs.
Ask the graph "does this already exist, and why is it like this?" before writing a line. On our first live run it stopped a duplicate persistence module cold.
Every design choice — including the rejected option — becomes a
durable, queryable ADR. The threading.Lock-over-asyncio.Lock reasoning a later session
would have "fixed" is now on record.
Typed actions run a five-guard pipeline — RBAC → lifecycle → rules gate → side effect → audit — so the methodology is enforced, not merely documented.
It's the same engine from the rest of this post, pointed at a new object graph: modules, APIs, tasks, change
requests, ADRs, commits. The methodology itself is data — an agentic-dev preset of ontology,
rules, actions, RBAC, and lifecycle — so the core stays generic while the agent gets a project that governs itself.
How this differs from the "code graph" crowd. Almost every tool that pairs graph or context with coding agents is a code parser: it indexes your symbols, imports, and call sites and hangs that index off the IDE for better autocomplete and retrieval. Useful — but it's still an index of what the code is, rebuilt from the source each time. Context Graph sits a level up: it's a management system on a generic decision-aware platform that remembers the decisions made across the project, enforces how the next one gets made (rules, RBAC, lifecycle), and keeps the audit trail. A code index is just one input we can ingest — not the product.
Where it fits · try it
Any place an AI system reasons over decisions rather than just documents: deal desks, policy and compliance, ops runbooks, support escalations.
The use case that drove these extensions is AI sales. A negotiating agent needs more than product facts — it needs the precedent it can cite ("we waived setup fees for a comparable renewal in Q2"), the discount approval still in force, and the policy it's operating under. Context Graph is the memory that makes those answers defensible — the decision layer behind agents built at Sellence.
Getting started is a few lines:
from lightrag.context_graph import ContextGraph
from lightrag import QueryParam
cg = ContextGraph(working_dir="./cg_storage", llm_model_func=..., embedding_func=...)
await cg.initialize_storages()
await cg.ainsert("Your text with decisions, approvals, and context…")
result = await cg.aquery("Your question", param=QueryParam(mode="hybrid"))
answer = await cg.cgr3_query("A multi-hop precedent question?")