Server data from the Official MCP Registry
Graph memory for AI agents: entities, cause-effect links, cross-session recall, time travel.
About
Graph memory for AI agents: entities, cause-effect links, cross-session recall, time travel.
Remote endpoints: streamable-http: https://memory.primaxiom.ai/mcp
Security Report
Valid MCP server (1 strong, 1 medium validity signals). No known CVEs in dependencies. Imported from the Official MCP Registry.
Endpoint verified · Requires authentication · 1 issue found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
Permissions Found in Source Code
Found by scanning the linked source code. This listing connects to a hosted endpoint, so none of this runs on your machine: it describes what the server software does where it is hosted.
How to Connect
Remote Plugin
No local installation needed. Your AI client connects to the remote endpoint directly.
Add this to your MCP configuration to connect:
{
"mcpServers": {
"ai-primaxiom-memory-reasongraph": {
"url": "https://memory.primaxiom.ai/mcp"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
ReasonGraph
A graph-based memory for AI agents: it ingests facts, auto-extracts entities and cause->effect relations, and discovers connections across independent documents and across agent sessions -- with conflict resolution, time-travel, causal tracing, and counterfactuals.
Why ReasonGraph?
Standard RAG retrieves documents similar to your query. ReasonGraph is a persistent, updatable memory that discovers connections between facts that were written independently.
When you feed text into add_texts(), ReasonGraph automatically extracts entities (via GLiNER) and cause-effect relations (via a dedicated causal model) that become nodes and typed edges in a graph. Facts that share entities or causal chains get connected -- even if they never reference each other. Multi-hop traversal then walks these connections to build reasoning chains that span multiple sources.
On top of retrieval it works as agent memory: scopes/sessions (agents discover into each other's memory through shared entities), contradiction resolution (a new fact soft-supersedes what it contradicts), time-travel (query(as_of=...)), causal tracing (trace_effects / root_causes / causal_chain), counterfactuals (what_if), and a shippable MemoryService over HTTP and MCP.
Zero config, strong defaults. ReasonGraph() picks the best available entity extractor, causal model, embedder, and reranker automatically -- the eval numbers below come from these defaults. For the SOTA causal model (~0.70 F1) use pip install reasongraph[causal] and the graph uses it automatically. The configuration sections are optional depth, not required reading.
Use it in 60 seconds
Claude Code / Cursor / any MCP client, hosted (EU, no LLM in the loop):
claude mcp add --transport http memory https://memory.primaxiom.ai/mcp \
--header "Authorization: Bearer rgm_YOUR_KEY"
Local MCP server (stdio, everything on your machine, SQLite):
pip install "reasongraph[service,gliner,fastembed,sqlite]"
claude mcp add memory -- reasongraph-mcp
Python, in-process:
pip install "reasongraph[all]"
from reasongraph import ReasonGraph
graph = ReasonGraph()
graph.initialize_sync()
graph.add_texts_sync(["TSMC is building a chip fab in Phoenix, Arizona.",
"Arizona ordered water cuts for industrial users in Maricopa County."])
print(graph.discover_sync("water and chips")) # a path: water cuts -> Arizona -> TSMC fab
Any language, over HTTP (self-hosted or hosted):
curl -X POST https://memory.primaxiom.ai/sessions/notes/memory \
-H "Authorization: Bearer rgm_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"text": "Apple sources M-series chips from TSMC in Arizona."}'
Ready-to-copy agents (Groq/OpenAI-compatible research agent, two agents sharing one
memory, Claude Code with persistent memory, LangGraph) live in
examples/agents/.
Hosted: ReasonGraph Cloud
memory.primaxiom.ai runs this library as a service: sign in, get a free key (10k requests a month), remote MCP endpoint, browser console and playground. Extraction runs with small models on servers PrimAxiom operates (currently in the EU); facts are only sent to an LLM provider if you ask for a synthesized answer. Early access.
JavaScript and TypeScript
Agents that are not written in Python talk to the hosted service through a dependency-free client
(clients/typescript). It uses the platform fetch, so it runs on Node 18+,
Bun, Deno, Cloudflare Workers and the browser. npm install reasongraph.
import { Memory, memoryTools } from "reasongraph";
const mem = new Memory({ apiKey: process.env.REASONGRAPH_API_KEY });
await mem.remember("scout", "TSMC is building a chip fab in Phoenix, Arizona.");
const facts = await mem.discover("What could disrupt the Phoenix fab?");
memoryTools(mem) returns OpenAI-style tool definitions with their executors attached, for agents
that call functions directly without a framework: remember, recall and why.
Installation
pip install reasongraph[all] # everything included
Or install only what you need:
pip install reasongraph # core: in-memory backend, NER extraction, embeddings
pip install reasongraph[gliner] # + GLiNER entity extraction + hybrid causal (default, recommended)
pip install reasongraph[causal] # + SOTA span-pointer causal model (~0.70 F1) + hybrid fallback
pip install reasongraph[gliner2] # + GLiNER2 alternative (single model does entities + causal)
pip install reasongraph[sqlite] # + SQLite backend with sqlite-vec
pip install reasongraph[postgres] # + PostgreSQL + pgvector backend
pip install reasongraph[service] # + HTTP + MCP memory service
pip install reasongraph[fastembed] # + pure-ONNX embedder / reranker (faster cold start)
Cross-Source Discovery
Two reports about different topics. Source A covers TSMC's semiconductor plant. Source B covers Arizona's water crisis. Neither mentions the other's subject.
import asyncio
from reasongraph import ReasonGraph
source_a = [ # Tech industry report
"TSMC announced plans to build a $40 billion semiconductor fabrication plant in Phoenix, Arizona.",
"The Phoenix fab requires 10 million gallons of purified water daily to cool wafers during the chip etching process.",
"TSMC signed a long-term supply agreement with Apple to manufacture next-generation M-series processors at the Arizona facility.",
"Construction delays at the Phoenix site pushed first production to late 2025, raising concerns among TSMC's major customers.",
]
source_b = [ # Environmental report -- never mentions TSMC, semiconductors, or chips
"Arizona declared a water emergency after Lake Mead dropped to its lowest level since the 1930s, threatening water supply for millions.",
"The Arizona Department of Water Resources ordered mandatory water cuts for all industrial users in Maricopa County, where Phoenix is located.",
"Intel paused expansion of its Chandler, Arizona chip plant citing water availability concerns and rising operational costs.",
"Apple warned investors that component shortages from its Asian and North American suppliers could impact iPhone production timelines through 2026.",
]
async def main():
async with ReasonGraph() as graph:
await graph.add_texts(source_a)
await graph.add_texts(source_b)
results = await graph.query("How does the Arizona water crisis affect semiconductor manufacturing?")
for i, text in enumerate(results, 1):
source = "A" if text in source_a else "B"
print(f"{i}. [Source {source}] {text}")
asyncio.run(main())
1. [Source B] Intel paused expansion of its Chandler, Arizona chip plant citing water availability concerns and rising operational costs.
2. [Source B] The Arizona Department of Water Resources ordered mandatory water cuts for all industrial users in Maricopa County, where Phoenix is located.
3. [Source A] The Phoenix fab requires 10 million gallons of purified water daily to cool wafers during the chip etching process.
4. [Source B] Arizona declared a water emergency after Lake Mead dropped to its lowest level since the 1930s.
5. [Source A] TSMC announced plans to build a $40 billion semiconductor fabrication plant in Phoenix, Arizona.
6. [Source A] TSMC signed a long-term supply agreement with Apple to manufacture M-series processors at the Arizona facility.
Results come from both sources. No single document contains this chain. Here is what happens under the hood:
ReasonGraph extracts entities and causal relations from each text (requires an entity+causal extractor, e.g. pip install reasongraph[gliner] or [all]):
| Text (abbreviated) | Entities | Causal relations |
|---|---|---|
| TSMC to build fab in Phoenix, Arizona... | TSMC, Phoenix, Arizona | -- |
| Phoenix fab requires 10M gallons water... | Phoenix | -- |
| TSMC supply agreement with Apple... | TSMC, Apple, Arizona | -- |
| Construction delays at Phoenix site... | TSMC, Phoenix | Construction delays -> first production |
| Arizona water emergency, Lake Mead... | Arizona, Lake Mead | Lake Mead dropped -> water emergency |
| Mandatory water cuts in Maricopa County... | Arizona Dept. of Water Resources, Phoenix, Maricopa County | -- |
| Intel paused Arizona chip plant... | Intel, Chandler, Arizona | -- |
| Apple warned of component shortages... | Apple | component shortages -> iPhone production timelines |
Three entities appear in both sources, creating bridge nodes:
| Bridge entity | Source A connections | Source B connections |
|---|---|---|
| Arizona | TSMC fab, TSMC-Apple deal | water emergency, Intel pause, water cuts |
| Phoenix | TSMC fab, water usage, delays | water cuts for industrial users |
| Apple | TSMC supply agreement | component shortage warning |
The query traversal path:
Water crisis query -> finds water-related texts from both sources via embeddings -> follows Arizona and Phoenix entity edges to discover TSMC's water-intensive fab -> follows Apple entity edge from TSMC supply agreement to Apple's component shortage warning. The causal relation Lake Mead dropped -> water emergency connects the environmental trigger to the industrial impact.
Full demo: uv run python examples/cross_source_discovery.py
Quick Start
Using a built-in dataset
from reasongraph import ReasonGraph
graph = ReasonGraph()
graph.initialize_sync()
graph.load_dataset_sync("financial")
results = graph.query_sync("What caused the 2008 financial crisis?")
for i, text in enumerate(results, 1):
print(f"{i}. {text}")
graph.close_sync()
Output -- a connected reasoning chain, not just keyword matches:
1. Lehman Brothers filed for bankruptcy in September 2008 after massive MBS losses.
2. Loose lending standards fueled a housing price bubble across the United States.
3. Lehman's collapse triggered a global credit freeze as interbank lending stopped.
4. Mortgage-backed securities built on subprime loans collapsed when defaults surged.
5. The U.S. government enacted TARP, a $700 billion bailout to stabilize the financial system.
6. Banks issued subprime mortgages to borrowers with poor credit histories.
Async API
import asyncio
from reasongraph import ReasonGraph
async def main():
async with ReasonGraph() as graph:
await graph.load_dataset("financial")
results = await graph.query("What caused the 2008 crisis?")
for text in results:
print(text)
asyncio.run(main())
Features
- Cross-source discovery -- connect facts across independent documents through shared entities and causal relations
- Automatic extraction -- entities (GLiNER
gliner_small-v2.5by default) and cause->effect relations (a dedicated span-pointer / hybrid causal model) are extracted on add, both on by default; falls back to GLiNER2 then BERT NER whenglineris not installed - Agent memory -- scopes/sessions with cross-session discovery, contradiction resolution (soft-supersede), time-travel (
as_of), semantic dedup, and auto-forget - Causal reasoning -- trace downstream effects, root causes, and directed causal paths; ask counterfactual
what_if - Hybrid search -- combine embedding similarity, keyword (trigram) matching, or both
- Multi-hop traversal -- follow graph edges to discover connected reasoning chains
- Cross-encoder reranking -- rerank results at each hop with a cross-encoder (
ms-marco-MiniLM-L-6-v2by default, multilingual mMARCO in the hosted service) - Memory service -- ready HTTP + MCP server so agents share and query memory
- Built-in datasets -- load curated reasoning graphs for immediate use
- Async-first -- native async API with sync convenience wrappers
- Pluggable backends -- in-memory (zero-config default), SQLite, or PostgreSQL with pgvector
Causal eval cases
tests/data/causal_cases.jsonl (40 reviewed cases), tests/data/causal_cases_batch2.jsonl
(40 more, 8 domains x 5, 12 non-English) tests/data/causal_cases_batch3.jsonl (60 more:
20 each German, Spanish, French) and tests/data/causal_cases_batch4_trnl.jsonl (20 Turkish,
20 Dutch) each hold multi-source why-questions with a gold
chain. Run python tests/eval_causal_cases.py --cases tests/data/causal_cases_batch2.jsonl.
Baseline on batch2 with the default models: chain recovered 100%, ordered 47%, answer 100%,
causal chain 65%.
Models
Every model slot is pluggable; these are the defaults and what ReasonGraph Cloud runs. All of them are small and run on CPU.
| Step | Library default | ReasonGraph Cloud | Notes |
|---|---|---|---|
| Sentence splitting | off (split="sat" or "regex" to enable) | SaT sat-3l-sm (wtpsplit) | 84% boundary recovery on messy text vs 40% for the regex splitter |
| Entities | GLiNER gliner-community/gliner_small-v2.5 | same | zero-shot, multilingual; 97% recall on a 6-language check, ~19 ms/call. REASONGRAPH_ENTITY_NORMALIZE=1 (or canonicalizer=EntityNormalizer()) merges surface forms ("Sabah"/"sabah", "Bulk Export's"/"Bulk Export") into one node, and REASONGRAPH_ENTITY_CONTAINMENT=1 (or link_contained_entities=True) also links a fact to an existing entity that its entity whole-word-prefixes or extends ("malzeme" / "malzeme eksikliği"), through an indexed first-word lookup. Both are off by default: measured on the 80-case busy-tenant eval they gave no root-recall gain and containment linked across unrelated cases 79% of the time; they remain for experiments |
| Cause → effect | Berk/causal-span-pointer-v2 (fine-tuned mDeBERTa-v3, open weights) | v3 (private for now; purpose-clause direction 97% vs 20%, CNC 0.705), plus its token gate at threshold 0.1 | 0.70 F1 on CausalNewsCorpus dev; the gate keeps plain statements out of the causal graph. REASONGRAPH_CAUSAL_ONNX=hf://owner/repo/file.onnx runs the same model through onnxruntime, 2x faster on CPU with identical spans |
| Embeddings | all-MiniLM-L12-v2 | paraphrase-multilingual-MiniLM-L12-v2 (fastembed) | switch when your facts are not only English |
| Span linking | embedding cosine ≥ span_link_threshold (0.85) | a span-link cross-encoder (REASONGRAPH_SPAN_LINKER, logit ≥ REASONGRAPH_SPAN_LINK_LOGIT) once measured; ties paraphrased hops cosine misses (17/20 vs 7/20 recovered at zero false links on held-out cases) | |
| Reranker | cross-encoder/ms-marco-MiniLM-L-6-v2 | cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 | the multilingual reranker lifted German discovery from 62% to 88% in our eval |
| Contradiction check | off (resolve_conflicts=True needs a resolver) | Berk/reasongraph-extractor-1.7b (fine-tuned Qwen3 1.7B, open weights) on llama.cpp, with an embedding pre-filter | 0.95 F1 on the hand-checked pairs; ~0.4 s per pair on two CPU threads |
| Chat / written answers | none (bring your own call_model) | an outside provider, currently gpt-oss-120b on Groq | the only step that uses a large model, and only when you use chat or ask for an answer |
Evaluation scripts for each slot are in tests/ (eval_causal_extraction.py,
eval_causal_cases.py) and results are quoted next to the options below.
Built-in Datasets
| Dataset | Description |
|---|---|
syllogisms | Classical syllogistic reasoning chains |
causal | Cause-effect reasoning with entity annotations |
taxonomy | Hierarchical concept taxonomy |
financial | Financial crisis causal chains (2008 crisis, dot-com, inflation, eurozone) |
medical | Medical causal chains (heart disease, diabetes, infectious disease, cancer) |
analysis_patterns | Data analysis reasoning: scenario detection, technique selection, implementation patterns |
graph.load_dataset_sync("financial")
Search Modes
embedding (the default) finds seeds by meaning. hybrid fuses that with a word-level
trigram channel (pg_trgm strict word similarity, index-assisted on Postgres): a name, a code,
a number or a compound in the question matches the fact that contains it even when the
embedder never saw the word. keyword is the trigram channel alone, for known-term lookups.
The memory loop takes the same option (MemoryLoop(search_mode="hybrid"), or
REASONGRAPH_LOOP_SEARCH=hybrid), and stays embedding by default.
Measured, so you do not have to guess: on the causal-root eval (180 cases, six languages, one busy tenant) hybrid does not help and slightly hurts: root recall 9% -> 7%, English 15% -> 8%, p95 latency +22%. The reason is structural, not a tuning failure: a root cause is phrased nothing like the question, so matching the question's words surfaces mid-chain filler that then competes for the context budget. Hybrid's own case (exact names, codes, part numbers, identifiers) is real but is a lookup task, which that eval does not measure. Turn it on for lookup-shaped workloads; leave it off for "why" questions.
How far the walk goes, and in which direction
hops bounds the walk's depth. causal_hops=(forward, backward) bounds it per direction:
forward follows cause -> effect (consequences), backward follows effect -> cause (what led here).
Entity bridges are unaffected. "Why" is a backward question, so a walk that spends its depth
asymmetrically can reach a root cause a symmetric one misses:
loop = MemoryLoop(graph, session="chat", causal_hops=(3, 2)) # or REASONGRAPH_CAUSAL_HOPS=3,2
The default is symmetric (None), and on our own causal eval the split makes no difference at all:
no path there runs more than two causal edges in one direction before the overall hops limit binds, so a
per-direction cap never fires. Reach for this only when your graph has genuinely long one-directional
chains, four or more hops of pure cause-to-cause. Raising hops itself was also measured: depth 5 buys
two more correct answers in 180 for roughly twice the database work, which is why the default stays 3.
Linking two wordings of one event
The same event turns up written two ways: "costs were reduced" in one sentence, "the cost reduction" in the next. Cosine over a general retrieval embedder does not put those together, and when it fails the causal chain breaks, which is measurably where root causes get lost.
That decision is its own job, so it can use its own model. span_linker accepts either shape:
ReasonGraph(span_linker="bi:my-org/same-event-multilingual") # a similarity model, scored by cosine
ReasonGraph(span_linker="my-org/same-event-cross-encoder") # a cross-encoder, scored pairwise
or REASONGRAPH_SPAN_LINKER with the same values. Both see only direction-aware candidates: an effect
span is only ever compared with a cause span. span_link_top_k sets how many near neighbours a span is compared against before anything judges them.
It defaults to 6. It used to widen to 10 whenever a linker was configured, and that was measurably wrong:
on 341 cases the wider shortlist floods the walk with look-alikes, halving the gain on rephrased chains and
turning a small gain on ordinary chains into a small loss. Narrowing it back was the single largest
improvement in this whole line of work, and it costs nothing.
By default a linker replaces the embedder's own cosine, which means it can also lose links cosine was
right about. span_link_floor makes it add instead: cosine keeps every link it would have made, and the
linker only speaks for pairs whose cosine falls in the band between the floor and the threshold.
ReasonGraph(span_linker="bi:my-org/same-event", span_link_floor=0.6) # or REASONGRAPH_SPAN_LINK_FLOOR
On our own corpus the floor turned out to be a no-op at every setting: within the linker's candidate set, cosine and the trained model never disagree in the band the floor arbitrates, so there is nothing for it to keep or cut. It stays available because another corpus may well contain that disagreement, but do not expect it to help without measuring.
Keeping a long conversation inside its context window
Three things hold a long chat together: the recent messages verbatim, a summary of what came before, and recall of anything older that turns out to be relevant. The loop does the third by default, and the first two are configured on the session.
loop = MemoryLoop(
graph, session="support-chat",
max_history_tokens=6000, # fold once the transcript passes this
keep_tail_tokens=2000, # this much of the newest talk stays word for word
summarizer=my_model, # fn(messages, max_tokens=None) -> str; omit and old turns are dropped
summary_length_target=False, # cap the summary at its share of the budget; costs detail
summarize_in_background=True, # summarise after the reply, never before it
)
Why a token budget rather than the last N messages. A fixed count is wrong in both directions: ten one-line exchanges are nothing, and ten pasted stack traces overflow the window. The budget counts what actually costs you.
Why fold a block at a time. When the transcript outgrows the budget, everything older than the tail becomes one summary and the tail stays verbatim. Summarising a block rather than a message means the summarizer runs rarely, and a conversation that never reaches the budget never summarises at all. The next fold takes the previous summary in with the newly-aged messages, so summaries merge instead of stacking.
What the summary costs, and why it is not capped by default. The summary and the verbatim tail
share max_history_tokens, so what is left once the tail is counted is the room the summary has:
loop.summary_budget_tokens. Nothing makes the model respect it. Measured on qwen2.5 7B over eight
folds with a 110-token share, keeping a list of 17 details that a later reply could need:
| summary after 8 folds | details kept | |
|---|---|---|
| no length asked for (default) | 374 tokens | 17 of 17 |
summary_length_target=True | 228 tokens | 11 of 17 |
Told a length, the model holds near it and drops detail to get there; told nothing, it keeps
everything and overruns its share instead. Neither is free, so the choice is yours: the default
keeps the detail, and summary_length_target=True passes summary_budget_tokens to the summarizer
as max_tokens when the prompt has to fit. A summarizer that takes only messages still works
either way. One model, one transcript, one seed — worth re-measuring on yours.
The summarizer is never on the hot path. Folding uses the summary the session already has and
records what still needs summarising. With summarize_in_background the work happens after the reply
is sent, so the summary lands one turn later; without it the fold waits. Either way a summarizer that
is slow, down, or returns nothing leaves the conversation working, because losing the wording of old
turns is survivable and stalling the answer is not.
What a summary is not. It is a model's paraphrase with no source behind it, so it arrives as its
own system message and never mixes with the recalled facts, which carry theirs. And it matters less
here than elsewhere: messages that fall out of the window were stored as facts by observe, so they
come back by meaning when they are relevant. The summary is for continuity, not for remembering.
Notes that point backwards
People chain causes by pointing rather than repeating: "this broke checkout", "because of that we rolled back". Extraction reads one sentence at a time, so "this" refers to nothing it can see and the link is lost. On real incident write-ups about a fifth of the causal links between sentences are of this kind.
ReasonGraph(resolve_back_references=True) # or REASONGRAPH_RESOLVE_BACK_REFERENCES=1
A sentence that opens with a back-reference and asserts a cause is linked to the note before it. The device differs by language and is often not a pronoun at all: German and Dutch carry the reference inside an adverb ("dadurch", "daardoor"), Turkish marks it with case endings ("bu nedenle", "bundan dolayı"), and a connective like "as a result" or "por ello" asserts the link on its own.
Off by default, and here is the honest state. On generated cases it recovers a third of these otherwise lost links with no wrong links at all, and it changes nothing on the standing evaluation. What is not established is its precision on ordinary traffic, so turn it on deliberately and check. Two limits no rule can fix: Spanish and Turkish often drop the subject entirely, leaving no marker to find, and Turkish tends to express cause inside a single sentence, so it has less of this to recover in the first place.
Measured, so you can skip what we tried. A general cross-encoder scored below plain cosine. A purpose-trained same-event model, on the other hand, triples root-cause recall on exactly the cases where the two sides are phrased differently (1.6% to 4.9%), while slightly hurting the cases that never needed it (6.0% to 5.0%). Whether that trade is worth it depends on your text: it breaks even when about a quarter of your causal links are phrased differently on each side, and in ordinary domain notes roughly two-thirds are, so it usually pays about three times over. Measure your own mix before assuming it.
Repeatable answers at scale
Measured on a 100k-fact tenant, and the answer is reassuring: a running service is already repeatable. Ask the same question twice against the same index and you get the same facts, every time, on every version. The order within a result is fixed too, at no cost.
What drifts is a rebuild. Two indexes built independently over the same data return slightly different neighbours, because the search is approximate. On that tenant, 30 of 80 questions differed between two fresh builds, and 4 of those changed the root cause itself, so it is not merely cosmetic.
Making two rebuilds agree requires exact search, which costs roughly 3.6x on recall latency. That is a migration setting, not a serving one:
REASONGRAPH_PG_DETERMINISTIC=1 # exact search: rebuild-stable, much slower
REASONGRAPH_PG_EF_SEARCH=<n> # a wider candidate window, if you want it too
Leave both off for serving. Turn the first on when you rebuild a store and need the answers to match the one it replaces.
Walking in levels
A recall walks the graph outward from its seeds. Each level is fetched in one backend call
(nearest_neighbors_many), not one call per node, because over a network a recall's cost is its
round trips: measured at 110 round trips per recall, a 5 ms hop to the database nearly quadrupled
recall latency. Backends that cannot batch inherit a default that loops, so this is transparent.
Within one recall, a fact's scopes, causal relations and timestamps are fetched once
(ReasonGraph.request_cache(), opened by MemoryLoop.recall): a recall makes several passes over
overlapping facts, and over a network every repeat is a round trip. The cache is dropped when the
request ends, so nothing is stale across requests, and only read-only metadata is memoised.
Concurrency, for deployments that run several writers (an API plus extraction workers): node and edge upserts are ordered by key so writers cannot deadlock on the same rows, schema creation is serialised with an advisory lock, and transient write failures (deadlock, serialization failure, an aborted pipeline) are retried with backoff.
# Pure embedding similarity (default)
results = graph.query_sync("credit freeze", search_mode="embedding")
# Pure keyword/trigram matching
results = graph.query_sync("credit freeze", search_mode="keyword")
# Hybrid: Reciprocal Rank Fusion of embedding + trigram rankings
results = graph.query_sync("credit freeze", search_mode="hybrid")
# Tune the RRF smoothing constant (default 60, lower = more weight to top ranks)
results = graph.query_sync("credit freeze", search_mode="hybrid", rrf_k=30)
Entity and Causal Extraction
Entity extraction and causal extraction are two independent, both-on-by-default capabilities. add_text() / add_texts() use gliner_small-v2.5 for entities (fast, multilingual, highest entity recall) when gliner is installed, falling back to GLiNER2 then BERT NER. Override per call with the extractor argument -- e.g. gliner_large-v2.5 for higher precision.
from reasongraph import ReasonGraph, NERExtractor, GLiNER2Extractor
graph = ReasonGraph()
graph.initialize_sync()
# Default: GLiNER gliner_small-v2.5 for entities (+ the default causal model),
# falling back to GLiNER2 then BERT NER
entities = graph.add_text_sync("Apple released the iPhone in 2007.")
print(entities) # ['Apple', 'iPhone']
# Explicit: force BERT NER even if a GLiNER model is installed
entities = graph.add_text_sync("Apple released the iPhone in 2007.", extractor=NERExtractor())
# Explicit: GLiNER2 with custom entity types
gliner = GLiNER2Extractor(entity_types=["company", "product", "date"])
entities = graph.add_text_sync("Apple released the iPhone in 2007.", extractor=gliner)
# Conversational memory: ChatExtractor also captures preference/plan/topic,
# so "hard techno" or "visit" become bridgeable nodes -- not just people/places
from reasongraph import ChatExtractor
entities = graph.add_text_sync(
"I love hard techno and plan to visit Berlin.", extractor=ChatExtractor()
) # ['Berlin', 'hard techno', 'visit']
# Any callable works
entities = graph.add_text_sync("some text", extractor=lambda t: ["custom"])
Causal reasoning (default on)
Causality is the headline feature, so causal extraction runs by default
(opt out per call with causal=False). Directed cause->effect relations become
first-class typed edges (label="causes") in the graph, distinct from
anonymous entity bridges, and discover() returns them per fact:
graph.add_text_sync("Heavy rainfall caused severe flooding.")
# -> typed edge heavy rainfall --causes--> severe flooding
for fact in graph.discover_sync("flooding"):
print(fact["content"], fact["causes"]) # [{'cause': 'Heavy rainfall', 'effect': 'severe flooding'}]
Causal chain tracing
Because cause->effect edges are directed and first-class, you can walk the causal graph -- something a flat vector store cannot do. Trace downstream impact, trace back to root causes, or find a directed causal path between two facts:
graph.add_texts_sync([
"Heavy rainfall caused flooding.",
"Flooding caused power outages.",
"Power outages caused hospital disruptions.",
])
graph.trace_effects_sync("Heavy rainfall caused flooding.")["terminals"]
# e.g. -> ['hospital disruptions'] # downstream impact
graph.trace_causes_sync("Power outages caused hospital disruptions.")["terminals"]
# e.g. -> ['rainfall'] # upstream causes (same as root_causes_sync)
graph.causal_chain_sync("Heavy rainfall caused flooding.",
"Power outages caused hospital disruptions.")
# -> ordered causal hops, each cited to the fact that asserted it
The exact spans depend on the causal extractor; a hop chains when one fact's effect
span matches the next fact's cause span. Each hop is tagged with the fact that
asserts it, its scopes, and a cross_session flag; with a conflict_resolver
configured, retired (superseded) facts are skipped by default (include_superseded=True
keeps them). Tunable with max_depth (default 6) and max_visited (default 1000).
Exposed to agents as the trace_memory MCP tool and the /trace HTTP endpoint.
Counterfactual: what breaks if a fact were false
Because the causal edges are first-class, you can ask the inverse of a trace:
if one fact were false, which downstream effects collapse? what_if prunes a
fact hypothetically (no graph mutation), re-walks reachability, and reports which
effect spans lost all causal support versus which survived via an alternate
path. Only edges the pruned fact solely supports are removed -- an effect another
fact also explains still stands.
graph.what_if_sync("Flooding caused power outages.")
# {
# 'pruned': 'Flooding caused power outages.',
# 'origin': 'Flooding caused power outages.', # walk start (== pruned unless origin= given)
# 'pruned_edges': [{'cause': 'flooding', 'effect': 'power outages'}],
# 'collapsed': [ # lost their only causal path
# {'span': 'power outages', 'fact': 'Flooding caused power outages.', 'depth': 0, ...},
# {'span': 'hospital disruptions', 'fact': 'Power outages caused hospital disruptions.', 'depth': 1, ...},
# ],
# 'survived': [], # spans an alternate path rescued
# }
Pass origin= to measure collapse relative to an upstream fact, or
direction='causes' to see which upstream causes become orphaned. Exposed as the
what_if_memory MCP tool and the /what_if HTTP endpoint.
The default causal extractor picks the best available backend. When the
causal-span-model package is installed it uses the span-pointer model
(CausalPointerExtractor): a fine-tuned mDeBERTa-v3 that scores ~0.70 F1 on the
Causal News Corpus Subtask-2 official scorer -- beating the 0.627 organizer baseline,
the hybrid, and a few-shot LLM baseline (~0.24-0.41). It is trained on English but
multilingual at inference (script-aware segmentation, verified on es/fr/de/pt/tr/ru/ar
and zh/ja) and has a built-in causal gate, so it returns nothing on non-causal text.
The built-in gate can be replaced by a decoupled embedding gate: a small
classifier on sentence embeddings (train one with scripts/train_embed_gate.py in
causal-span-model; it saves a .joblib). It costs a millisecond per sentence, is
retrained on any negative mix without touching the span heads, and on our causal
eval it gave fewer, more precise edges than the built-in gate. Pass a local path or
an hf://owner/repo/file.joblib reference:
ReasonGraph(causal_extractor=CausalPointerExtractor(
model="Berk/causal-span-pointer-v2", gate_threshold=1.0, # built-in gate off
embed_gate="hf://Berk/causal-span-pointer-v2/embed_gate_mlp.joblib",
embed_gate_threshold=0.9)) # keep P(causal) >= 0.9
causal_chain also bridges facts that phrase one event differently ("the system
throttles performance" -> "Throttling performance", or a plain root fact whose
words reappear in the next cause span), so directed chains survive wording changes
even without span_link_threshold. trace_causes(..., bridge=True) walks back the same
way: from a cause span to the effect spans of other facts that name its event ("the billing
job ran twice whenever a payment retried" -> "The billing job ran twice"), and from a plain
fact with no relation of its own to the effect spans that describe it, so a symptom someone
noted ("the billing job started double-charging") leads to the explanation recorded later.
Otherwise it falls back to the hybrid (HybridCausalExtractor): a fast,
model-free multilingual cue pass handles explicit and reversed phrasing with
correct direction, and sentences with no causal connective (implicit causality)
fall through to gliner-relex-multi (Apache-2.0, mDeBERTa, ~100 languages).
On a four-regime probe set (explicit / multilingual / implicit / reversed) the
hybrid reached 100% directed-pair recall vs 61-79% for either part alone --
each covers the other's blind spot -- and most sentences never touch the model,
so the average cost is low. Reproduce with tests/bench_causal_extractors.py.
from reasongraph import CausalPointerExtractor, HybridCausalExtractor, GlinerRelexExtractor
ReasonGraph() # best available (pointer if installed, else hybrid)
ReasonGraph(causal_extractor=CausalPointerExtractor()) # force the span-pointer model
ReasonGraph(causal_extractor=HybridCausalExtractor()) # force the hybrid
ReasonGraph(causal_extractor=GlinerRelexExtractor()) # relex model only
ReasonGraph(causal_extractor=False) # disable causal extraction
pip install reasongraph[causal] installs both the pointer model
(causal-span-model) and the hybrid (gliner), so the graph uses the SOTA pointer
by default and falls back to the hybrid automatically. If neither is available the
default warns once rather than silently dropping causality; add_text(..., causal=True)
raises when no causal extractor can be resolved.
Sentence splitting at ingest
Every model in the pipeline is trained on single sentences, so a paragraph pushed as one fact hurts entities, causal spans and retrieval alike (on our 39-case causal eval, chain recall drops from 79% to 10%). Pass a splitter and each text becomes one fact per sentence:
ReasonGraph(sentence_splitter="sat") # Segment-any-Text, 85 languages: pip install reasongraph[split]
ReasonGraph(sentence_splitter="regex") # dependency-free fallback (punctuation + newlines)
graph.add_texts([paragraph], split=True) # or per call; split=False keeps a text whole
The service reads REASONGRAPH_SPLIT_SENTENCES=sat|regex; pushes accept split: true/false.
Deep memory integration: the memory loop
No tools, no prompts to write: wrap any chat model and every exchange becomes memory, and whatever is relevant comes back by itself before the next call.
from reasongraph import ReasonGraph, MemoryLoop
graph = ReasonGraph() # or your Postgres-backed graph
loop = MemoryLoop(graph, session="support-chat", max_facts=8)
history = [{"role": "user", "content": "Why did the Rotterdam warehouse lose power?"}]
reply, context = loop.chat_sync(call_model, history, system="You are a careful assistant.")
# call_model is any fn(messages) -> str: OpenAI-compatible, Claude, Ollama, llama.cpp
# context.facts -> what was recalled (with sources and cause->effect links)
# context.roots -> for why-questions, the root cause(s) the chain walks back to;
# they are spelled out in the injected block so a small model
# answers with the root, not only the nearest cause
# the question and the reply are now remembered in "support-chat"
The loop also remembers the conversation itself. Those stored turns are recalled like any
other fact, but judged after everything else: the question you just asked and an earlier
"I don't know" would otherwise score highest and take every slot.
graph.forget(scopes) erases a session (or a tenant, or a test run): facts and the entities
only they linked are deleted; a sentence also held elsewhere is detached, not deleted.
Documentation truncated — see the full README on GitHub.
Reviews
No reviews yet
Be the first to review this server!
More Developer Tools MCP Servers
Rea
Freeby Morluto · Developer Tools
Reverse engineer anything from your terminal or agent with one CLI and MCP server.
Paperclip
Freeby Paperclipai · Developer Tools
Trending hip-hop artist momentum scores across four cultural dimensions.
Git
Freeby Modelcontextprotocol · Developer Tools
Read, search, and manipulate Git repositories programmatically
Toleno
Freeby Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
MCP Marketplace
Freeby mcp-marketplace · Developer Tools
Search and install MCP servers from inside your AI client.
mcp-creator-python
Freeby mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.
