Which papers support a claim, and which contradict it: a research graph
12 October 2026 · Semurg · 5 min read · Markdown
Verified: every command below ran on a fresh node of r11.78.20261012.bc0d94f (how)
build-on-semurgresearchknowledge-graphagentsblueprint
A literature review is a graph that most people keep in their head. This paper makes that claim; that one tested it on a larger sample and found nothing; a third cites both and argues the first measured the wrong thing. Search tools find papers that share words with a question. They do not tell you which ones support a claim and which ones contradict it, because that is a relationship, not a keyword. This blueprint keeps the relationships.
Everything below uses only what the published release does.
The shape of the data
- One scope per review or project, so each review's graph stays its own, and a group-wide scope for what everyone relies on.
- Entities: papers, authors, claims, datasets and methods. A claim gets a short, stable name, so the papers that make it and the papers that dispute it all point at the same node.
- Relationships: an author
AUTHOREDa paper; a paperCITESa paper; a paperSUPPORTSorCONTRADICTSa claim; a paperUSESa dataset. - Evidence as passages: the sentence in which a paper makes or disputes the claim, naming the paper and the claim, so the answer comes back with the words that justify it.
A claim made in forty papers is one node with forty edges; the papers do not each carry a copy of it.
Step one: write what your pipeline extracted
Your extraction step (a model you run, or a person) reads each paper and decides the claims and relationships; the node stores them with the sentences they came from:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/remember \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scope":"review-spacing","source":"literature extraction, batch one",
"facts":{"entities":[{"name":"Rivera et al","type":"Paper"},{"name":"Okafor and Lind","type":"Paper"},
{"name":"Spacing effect","type":"Claim"},
{"name":"Campus vocabulary study","type":"Dataset"}],
"relations":[{"source":"Rivera et al","target":"Spacing effect","type":"SUPPORTS"},
{"source":"Okafor and Lind","target":"Spacing effect","type":"CONTRADICTS"},
{"source":"Okafor and Lind","target":"Rivera et al","type":"CITES"},
{"source":"Rivera et al","target":"Campus vocabulary study","type":"USES"}]},
"passages":[{"text":"Rivera et al report that learners who spaced their sessions recalled more words a week later, supporting the Spacing effect.",
"mentions":["Rivera et al","Spacing effect"],"key":"rivera-abstract"},
{"text":"Okafor and Lind found no benefit from spacing once total study time was held equal, and argue that Rivera et al did not control for it; this disputes the Spacing effect as usually stated.",
"mentions":["Okafor and Lind","Rivera et al","Spacing effect"],"key":"okafor-lind-discussion"}]}'Expected: a JSON answer with "ok": true
Step two: ask about the claim
Context starts from every name in the question, walks out from it, and returns the papers on each side with the sentences that put them there:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/context \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scope":"review-spacing","query":"What supports or contradicts the Spacing effect?","hops":2}'Expected: "state": "answered", with Rivera et al supporting and Okafor and Lind contradicting, and both sentences
From a paper outward, recall returns every connection in both directions: what it supports, what it cites, and what cites it:
export SEMURG_API_TOKEN=<your token>
curl -sS http://localhost:4000/v1/memory/recall \
-H "Authorization: Bearer $SEMURG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scope":"review-spacing","subject":"Rivera et al"}'Expected: "state": "answered": Rivera et al supports the Spacing effect, uses the Campus vocabulary study, and is cited by Okafor and Lind
The loader, in Python
Your extraction step produces records; this loader writes them, one paper per call, so each call names both ends of every relationship it writes. With the Python client from https://one.semurg.io/dl/clients.tar.gz (unpack it, then pip install -e clients/python):
import json
import os
import sys
from semurg import Client
# papers.json is a list of papers, each:
# {"paper": "...",
# "claims": {"supports": [...], "contradicts": [...]},
# "cites": [...],
# "evidence": [{"text": "...", "mentions": [...]}]}
papers = json.load(open(sys.argv[1]))
with Client("http://localhost:4000", token=os.environ["SEMURG_API_TOKEN"]) as memory:
for p in papers:
claims = p["claims"]["supports"] + p["claims"]["contradicts"]
entities = [{"name": p["paper"], "type": "Paper"}]
entities += [{"name": c, "type": "Claim"} for c in claims]
entities += [{"name": cited, "type": "Paper"} for cited in p["cites"]]
relations = [{"source": p["paper"], "target": c, "type": "SUPPORTS"} for c in p["claims"]["supports"]]
relations += [{"source": p["paper"], "target": c, "type": "CONTRADICTS"} for c in p["claims"]["contradicts"]]
relations += [{"source": p["paper"], "target": cited, "type": "CITES"} for cited in p["cites"]]
reply = memory.remember("review-spacing", facts={"entities": entities, "relations": relations},
passages=p["evidence"], source="literature extraction")
print(p["paper"], reply.get("ok"))An agent that reviews with you
A research agent connected over MCP with the review's scope in the X-Semurg-Scope header can ask the graph before it summarises, so its summary says which papers disagree instead of averaging them. Tell it to quote the sentences that come back and to say when nothing does. The agents page has the configuration for each agent and framework we test.
Scale and cost
A single node is free, with no licence key, and it is the full engine. A cluster is licensed per node; talk to us about the one you need. We have not measured this blueprint's workload, so it states no speed.
What it does not do today
- It does not read papers. The node stores a document (
memory_document) and links its passages to the names already stored, but no model on the node turns text into claims; your extraction step does that and sends the facts. - It matches meaning only with the node's embedding model installed: until then a question about "distributed practice" does not reach a claim named "Spacing effect"; name claims the way your group talks about them, and add the other names as entities related to them.
SUPPORTSandCONTRADICTSare what your pipeline asserted. The node keeps them and shows them; it does not judge whether a paper really supports a claim.- The evidence comes back cited to the sentence you stored and its source; a citation into the original paper needs the paper itself stored as a document.
When something is refused
- A passage whose mention does not appear in its text, letter case aside, is refused and the refusal names the mention: write the paper's name the way the sentence does.
- A relationship whose far end is not listed in the same call is not written on today's release, although the call answers ok; the loader above lists every end in each call for that reason.
401for a missing or wrong token;422"missing scope" for a call without one;"state": "nothing_stored"when nothing matched.
Next
- Give your agent persistent memory with MCP in five minutes: the node and the seven memory tools.
- Follow the money: a connection graph for fraud investigators.
How these commands were checked
Our test farm ran each of the 3 command blocks marked runnable, exactly as written, with the answer checked against the expectation printed under it, against a fresh node of the published release r11.78.20261012.bc0d94f. A post is published only when every one of them passed there.