Skip to main content

Your agent cannot assert without quoting the source. A claim store with verbatim-evidence enforcement, for LLM extraction pipelines.

Project description

xrad

ci pypi python licence

Your agent cannot assert here without quoting the source.

Extraction pipelines produce knowledge graphs that assert. Acme ACQUIRED Beta, as a fact. That is fine until the source only implies it, attributes it to someone, or hedges it — and then the graph has silently turned a rumour into a fact, and nothing downstream can tell.

xrad stores claims: who said it, with what commitment, and the verbatim span that proves it. Three mechanisms enforce that, and all three are refusals.

pip install xrad

# see it refuse things in 60 seconds
python examples/demo.py

xrad serve --db graph.db --source contract.txt --vocab contracts.yaml

1. A quote that is not in the source is refused

>>> record_claim(subject="Acme", relation="ACQUIRED", object="Beta",
...              quote="Acme purchased Beta", modality="assertion",
...              asserter="the contract")
{"ok": false, "error": "quote_not_found",
 "message": "quote not found in source: 'Acme purchased Beta'.
             Quote verbatim from the text, including original spelling..."}

The document says "Acme Ltd hereby acquires Beta Inc". The paraphrase is plausible, would have passed any review, and is rejected — because models are asked for the exact words and finding them is arithmetic, done here.

There is no fuzzy matching. A near-miss means the model paraphrased, and a paraphrase pointing at real offsets is exactly the silent corruption this exists to prevent.

2. A relation outside the vocabulary is refused

# contracts.yaml
entity_types: [Party, Obligation, Instrument]
relations:
  ACQUIRED:    {subject: [Party], object: [Party]}
  GOVERNED_BY: {subject: [Obligation], object: [Instrument]}
{"ok": false, "problems": ["unknown relation 'SORT_OF_RELATED_TO' —
                           use one of: ACQUIRED, GOVERNED_BY"]}

Closed by design. An open vocabulary lets a model invent four synonyms for one relation, and the graph stops being queryable. vocab.prompt_fragment() generates the prompt text from the same file the code enforces, so the two cannot drift.

3. Modality is fixed, and the graph keeps disagreements

A source asserts, reports, hedges, denies, declines to decide, or asks. Six, and they do not vary by domain. Everything else is yours.

That is what lets two sources contradict each other without one being deleted:

$ xrad disagreements
  subject  relation  object  says                    against
  Acme     ACQUIRED  Beta    the filing (assertion)  the auditor (rejected)

And what makes this question askable at all:

$ xrad trace Acme Gamma
  1. Acme --ACQUIRED--> Beta          the filing
  2. Beta --OWNS--> Gamma  (reported) Reuters
  2 hops · 1 asserted · 1 reported or hedged

$ xrad trace Acme Gamma --modality assertion
  no chain  the connection may exist when reported claims are allowed

trace, not path, because what comes back is not a route. It is the argument a source built, with who committed to each hop and how strongly. The second form strips everything the source merely reports and asks whether the argument survives on its own authority — here it does not. In a graph of edges that question has no meaning.

Building the graph

Recording claims gives you a claim log. resolve makes it a graph:

$ xrad resolve
9 mentions -> 3 entities
  Acme Limited  <- Acme Ltd, Acme Limited, ACME
  Beta Inc      <- Beta, Beta Inc

2 merges refused, ranked by what the answer moves:
    5  Acme  into 'Acme Limited'
       type conflict: Party vs Product
    3  Acme Products  into 'Acme Limited'
       'acme products' extends 'acme' with a word that is not a known empty suffix

Before it, Acme Ltd, Acme Limited and ACME are three nodes and xrad trace "Acme Ltd" Gamma finds nothing though the document plainly connects them. After it they are one node with three surface forms, and a query finds it by any of them.

The design is in what it refuses. Clustering on string similarity is easy and wrong in a specific, expensive way: the pairs that look most alike are often the ones the source is deliberately distinguishing.

  • A type boundary stops a merge. Osiris the deity and Osirite the stone differ by two characters; merging them moved 322 claims about a mineral onto a god.
  • An explicit discriminator stops a merge. Acme Ltd (UK) against Acme Ltd, Belus minor against Belus priscus — the source telling you these differ is the most reliable evidence available.
  • An unknown extra word stops a merge. Acme against Acme Products is as often a parent and a subsidiary as it is two spellings. Known empty suffixes (Ltd, GmbH, Holdings) are a documented equivalence and are stripped; anything else is a judgement and is queued.

Refusals are ranked by how many claims the answer moves, because a wrong merge puts every claim about one thing onto another.

Auditing what was recorded

locate guarantees the quote is real. The audit asks the harder question: does it establish the claim?

xrad audit --emit batches.json     # hand these to a model or an agent fleet
xrad audit --ingest verdicts.json
#   14 supported · 4 refuted
#   4/18 refuted = 22.2%

A genuine quotation can fail to support what was hung on it, and nothing else in a pipeline catches it:

claim:   Ferdinand III IDENTIFIED_WITH Mercurius Trismegistus
span:    "FERDINANDVS III Caesar, / Vivat Mercurius Trismegistus redivivus,"
verdict: refuted — two separate acclamation lines. The second carries its own
         verb and need not predicate of Ferdinand. Identification is adjacency,
         not statement.

Both names are in the quotation. The extractor saw two names near each other in a dedication and inferred an equation.

The auditor is shown the span and nothing else — not the document, not the neighbouring claims, not how it was extracted. With the context around it, a verifier reconstructs plausibility from the surroundings and confirms nearly everything; that measures agreement, not evidence. It is also told to try to refute and to default to unsupported, because an agreeable verifier finds nothing.

A refuted claim is not deleted. The source still said it, and the refutation is a fact about the citation rather than about the source.

Exporting

xrad export jsonld -o graph.jsonld    # rdf:Statement reification
xrad export graphml -o graph.graphml  # Gephi, yEd, Cytoscape
xrad export cypher -o graph.cypher    # Neo4j
xrad export csv -o graph              # nodes + edges, for pandas

Every format carries modality, asserter and the evidence quote on each claim. JSON-LD uses rdf:Statement, and Cypher reifies — (a)<-[:SUBJECT]-(:Claim)-[:OBJECT]->(b) — rather than writing (a)-[:ACQUIRED]->(b) and losing who said it. An export that flattens a claim into an edge produces a file asserting things no source asserted.

Searching

xrad search "sacrificium"

FTS5 has no stemmer for most languages, and inflected corpora are nothing but stems. Measured on the source project: sacrificium returned 0 hits where 9 rows contained sacrific-, and Aegyptius returned 4 against 110. A reader would conclude the source never mentions it.

So each term is searched as written and as a prefix on its stem. This over-matches by design — BM25 sorts it out, and a false positive you can see beats a silent absence you cannot. Results carry modality and asserter, because a quotation shown alone invites reading it as a fact.

Widening a citation that stopped too early

The commonest way a correct claim fails an audit is not fabrication. It is a citation that ends a clause too soon:

>>> widened_quote(text, "We hold it to be the same instrument", ["Acme Ltd"])
"Acme Ltd entered the agreement on 1 March. ... We hold it to be the same
 instrument"

We hold it to be the same cites a real identification whose subject is it. Measured: 49% of claims were refused and 76% of those refusals were referential rather than substantive. Widening turns a right claim that fails an audit into a right claim that passes one — and the budget stops it making a wrong claim right by swallowing the document.

Why it exists

Extracted from a pipeline that put 5,646 claims from a 1652 folio into a graph where every one resolves to a rectangle on a scanned page. The audit is the reason to trust any of it: a second model, shown only the cited span, tries to refute each claim. Given the surrounding page it would reconstruct plausibility and confirm everything — starved of it, the question becomes answerable.

23% of claims did not survive. The largest single failure was an attribution the quote itself did not contain: the source names Strabo once and writes for three paragraphs, and the extractor quotes paragraph three while still crediting him. One prompt rule fixed it — the attribution must be inside the cited span — and refutation fell to 11.5%, replicated on a second volume (z = 5.05).

That is why record_claim warns when a reported claim names an asserter its own quote does not.

What it is not

Not a vector index — no embeddings, no similarity search. A claim store you traverse. Not an extraction model: bring your own agent, xrad decides what it is allowed to record. Not a fact database. Sources are wrong; a source being wrong is recorded, with the modality showing how it held the statement.

Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xrad-0.2.0.tar.gz (56.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xrad-0.2.0-py3-none-any.whl (52.7 kB view details)

Uploaded Python 3

File details

Details for the file xrad-0.2.0.tar.gz.

File metadata

  • Download URL: xrad-0.2.0.tar.gz
  • Upload date:
  • Size: 56.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for xrad-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f0d70c3c1027b180fb5b3fdbe61241ae0758dcd57d0f18c8d5d254335d1c3673
MD5 ae48e77d74a9c523e5944486a5d87654
BLAKE2b-256 fe74e03344a16d236a24e15183e25d3221f7214797480ed20acc720b9a49c623

See more details on using hashes here.

Provenance

The following attestation bundles were made for xrad-0.2.0.tar.gz:

Publisher: release.yml on narimannemo/xrad

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file xrad-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: xrad-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 52.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for xrad-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 25e26e5f729fa4a2712d578bc6f1f739cc4d1af361aa9c8197ff44f813c8037f
MD5 44029d20250203df8bdbd8f9e48493d5
BLAKE2b-256 a78c4a9437e2e5055cded0873a2ce5bf92288b4559502da0396a74a4d860bb2c

See more details on using hashes here.

Provenance

The following attestation bundles were made for xrad-0.2.0-py3-none-any.whl:

Publisher: release.yml on narimannemo/xrad

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page