Blog / How-to guides

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.

Rivera et al supports the spacing effect; Okafor and Lind contradicts it and cites Rivera et al, so one walk from the claim finds both sides.Okafor and LindRivera et alSpacing effect (claim)Campus vocabularystudyCITESCONTRADICTSSUPPORTSUSES

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 AUTHORED a paper; a paper CITES a paper; a paper SUPPORTS or CONTRADICTS a claim; a paper USES a 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:

bash
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:

bash
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:

bash
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):

Your extraction step reads each paper and decides its claims; the loader writes one paper per call into the review's scope; a question then returns the papers on each side of a claim with the sentences that put them there.A paperYour extraction stepThe loader: one paperper callThe review's scopeA question about theclaimBoth sides, with theirsentencesclaims and sentences
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.
  • SUPPORTS and CONTRADICTS are 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.
  • 401 for a missing or wrong token; 422 "missing scope" for a call without one; "state": "nothing_stored" when nothing matched.

Next

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.

Related