---
title: "Which papers support a claim, and which contradict it: a research graph"
slug: build-research-assistant-claims-graph
date: 2026-10-12
author: Semurg
section: how-to
tags: [build-on-semurg, research, knowledge-graph, agents, blueprint]
summary: A blueprint for a research group: papers, authors and the claims they make or dispute as one graph, so the question "what supports this, and what contradicts it" is a walk with the evidence attached.
status: published
covers: [route:POST /v1/memory/remember, route:POST /v1/memory/recall, route:POST /v1/memory/context, sdk:remember]
---
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.

> Diagram claim-graph: 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.

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 runnable kind=shell-node needs=token
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"}]}'
```

```expect
expect: a JSON answer with "ok": true
assert: json:.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 runnable kind=shell-node needs=token
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}'
```

```expect
expect: "state": "answered", with Rivera et al supporting and Okafor and Lind contradicting, and both sentences
assert: json:.state=="answered" and ([..|strings|ascii_downcase|select(contains("total study time was held equal"))]|length>0)
```

From a paper outward, recall returns every connection in both directions: what it supports, what it cites, and what cites it:

```bash runnable kind=shell-node needs=token
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"}'
```

```expect
expect: "state": "answered": Rivera et al supports the Spacing effect, uses the Campus vocabulary study, and is cited by Okafor and Lind
assert: json:.ok==true and .state=="answered" and ([..|strings|ascii_downcase|select(contains("okafor and lind"))]|length>0)
```

## 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`):

> Diagram claims-loader: 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.

```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](/agents) 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

- [Give your agent persistent memory with MCP in five minutes](/blog/agent-memory-mcp-five-minutes): the node and the seven memory tools.
- [Follow the money](/blog/build-fraud-investigation-graph): a connection graph for fraud investigators.
