Skip to main content

English  ·  日本語  ·  简体中文

TrikeDB — a triceratops carrying a knowledge graph on its frill

tests

trikedb

A knowledge graph your agent can read, in one file you can diff.

Your agent already reads your code. What it cannot read is everything that is not in the code: which job feeds which table, who owns what, which of two similar-looking services is the live one. So it guesses, and the guess is a plausible name that does not exist.

trikedb is where you write that down — one YAML file, in the repo, next to the code it describes:

triples:
  - {s: salesflow-crm, p: PROVIDES, o: crm-sync-job}
  - {s: crm-sync-job, p: INGESTS_TO, o: RAW_CRM_CONTACTS, schedule: hourly}
  - {s: LEGACY_DUMP, p: MIGRATED_TO, o: RAW_CRM_CONTACTS, deprecated: true}

That file is the database. No server, no daemon, no deployment. It diffs in git like any other file, a human can read it, and an agent can query it with real SPARQL 1.1 — executed by Oxigraph, not a homegrown subset — or just open the file and read it.

trikedb HTML workbench — 600 Freebase facts as force-directed clusters, with a node detail panel open

Live demos — a company as five graphs  ·  a data platform with an action log  ·  600 real facts, filterable, with an in-browser SPARQL console

Install

pip install trikedb          # library + CLI
pip install 'trikedb[mcp]'   # + MCP server, so an agent can use it
pip install 'trikedb[all]'   # + serve, OAuth, SHACL, OWL, semantic search, S3/warehouse graphs

Every optional feature is an extra, so the core stays PyYAML + rdflib + pyoxigraph. The full list is in the reference.

Start with three facts

No schema, no modelling session. Write facts, look at them:

from trikedb import TrikeDB

db = TrikeDB("graph.yaml")               # the file is created on first write
db.add("salesflow-crm", "PROVIDES", "crm-sync-job")
db.add("crm-sync-job", "INGESTS_TO", "RAW_CRM_CONTACTS", schedule="hourly")
db.set_node("RAW_CRM_CONTACTS", type="table", pii=True)

db.query(["?vendor PROVIDES ?job", "?job INGESTS_TO ?table"])
db.to_html("graph.html")                 # a clickable page for your teammates

Or from a blank repo, without writing any Python:

trikedb init graph.yaml --template agent-memory   # a starting graph with a real shape
trikedb add graph.yaml salesflow-crm PROVIDES crm-sync-job
trikedb query graph.yaml -w "?vendor PROVIDES ?job"
trikedb ui graph.yaml                             # open it in a browser

The four you actually need

There are more, but a graph that earns its keep is usually built out of these:

db.add(s, p, o, **attrs) state a fact. Any keyword becomes an edge attribute — prov= is the one worth standardizing on, so every fact can be traced back to its source
db.act(s, p, o, by=…, state=…) record something that happened: it is stamped with a time, appended to the node's history, and moves the node to the state it left behind
db.find(question, where=…) the retrieval an agent wants: search by meaning, then filter on exact properties
db.sparql(query) the full query language, when patterns are not enough

Everything else — inference, SHACL, workspaces, S3 and warehouse storage, the HTTP server — is there when you need it and costs nothing until then. See the reference.

Then lock it down

A graph is only worth reading if it cannot fill up with junk. Declare what may be said, and every write path — yours, the CLI's, an agent's — is held to it. A shape is checked wherever the node's type is known, in either direction:

db = TrikeDB("graph.yaml", ontology={
    "PROVIDES":   "SaaS vendor -> ingestion job",
    "INGESTS_TO": {"description": "ingestion job -> warehouse table",
                   "domain": "job", "range": "table"},
})

db.set_node("crm-sync-job", type="job")
db.set_node("RAW_CRM_CONTACTS", type="table")

db.add("crm-sync-job", "OWNS", "anything")                # OntologyError: undeclared predicate
db.add("RAW_CRM_CONTACTS", "INGESTS_TO", "crm-sync-job")  # OntologyError: written backwards

Actions can declare when they may run and who may sign them — the half a type check cannot reach. An order delivered before it ever shipped breaks no type; a price change approved by nobody is type-correct. Both are refused here:

db.declare_link("DELIVERED_TO", domain="order", range="region",
                requires="SHIPPED_FROM",   # this has to have happened first
                by="courier")              # and this is who may do it

db.act("ORD-25101", "DELIVERED_TO", "Riverside", by="Kai")   # OntologyError: never shipped

Hand it to an agent

Register the graph as an MCP server and the agent gets sixteen tools — sparql, match, search, find, get_node, history, ontology, stats to read, add_triple, act, set_node, remove_triples, import_source to write, and extraction_prompt, preview_triples, add_triples to turn a document into facts without guessing at the vocabulary:

{
  "mcpServers": {
    "kg": {
      "command": "uvx",
      "args": ["--from", "trikedb[mcp]", "trikedb", "mcp", "/absolute/path/to/graph.yaml"]
    }
  }
}

Writes autosave to the YAML, so an agent's contribution arrives as a reviewable git diff, and the ontology rejects any predicate it tries to invent. That is the answer to "just throw the docs at it": the agent is the extractor, trikedb is the validated write path. Extraction stays flexible; the vocabulary does not.

No MCP client? Then the whole integration is one line in your agent's project instructions — "before any task touching the pipeline, read graph.yaml" — and the file does the rest.

Point it at a document

You have a model already. trikedb writes the prompt and judges the answer; the call in between is yours, so there is no SDK to install and no key to hand over. The prompt is built from the graph being written into — its declared predicates, its existing node names — which is why the model does not invent EMPLOYED_BY beside WORKS_AT, or open a second node for a company already in the file:

rows = db.extract(open("press-release.md").read(), llm=my_model)

for f in db.preview(rows):          # nothing is written yet
    print(f["verdict"], f["triple"], f["detail"])

# new       Acme BASED_IN Osaka
# new       Sato WORKS_AT Acme
# conflict  Tanaka WORKS_AT Globex
#           └ WORKS_AT is declared functional and Tanaka already holds 'Acme'

llm is any callable taking the prompt and returning text — three lines around whichever SDK you use, and five of them are written out in examples/extract_providers.py.

Or skip the API entirely and run the two halves from a shell, with a person or a chat window in the middle:

trikedb extract graph.yaml report.md > prompt.txt   # paste it anywhere
trikedb import graph.yaml answer.md --dry-run       # what it would do
trikedb import graph.yaml answer.md                 # what it did

--dry-run is worth having on its own, and works on any source — CSV, Markdown, another graph. Every row comes back as new, same, update, rejected or conflict, with the reason, and nothing is written until you have read them. A conflict is not a guess: a predicate declared functional may hold one object per subject, so a second one is a contradiction the graph can prove.

How well does the constrained prompt actually do? Run it and see — the cases, the answer sheets and the scorer are in evals/, along with the unconstrained baseline to compare against. No scores are committed there, because a committed score is one model on one day.

What people put in it

  • An agent's memory of your systems. Which warehouse role can read what, which ingestion job is the live one, which repo a change belongs in. None of it is in the code, all of it is what an agent gets wrong.
  • A service and ownership map. Who calls whom, who is on call, which of the three similarly named services is deprecated.
  • A decision and incident log with preconditions. act() plus requires means "deployed" cannot be recorded for something that was never approved — enforced on write, not in a review checklist.
  • A data governance ledger. PII tables, who may access them, retention. by: and requires: are an authorization model that diffs in a pull request.

The common thread: a few hundred to a few thousand facts that somebody curates on purpose. That is the size where a graph is both possible and worth trusting.

Why not just a Markdown file?

Because a Markdown file cannot refuse a bad write, and neither can the agent writing to it. Every fact here passes the same guard — predicate declared, direction right, precondition met — whoever writes it. And once the facts are structured you can ask questions no grep answers: everything two hops from this table, every PII column with no owner, what changed since March.

It also measurably helps the model. On WebQSP (knowledge-graph QA), the same local model answers 42.7% alone vs 77.7% with a trikedb graph as context — Hits@1 over 300 questions, paired McNemar p = 9e-20, 0.59 s per question. Method, caveats, and an honest scoring-sensitivity analysis: benchmarks/.

What trikedb is not

  • Not an extraction pipeline. It will not call a model, hold your key, or parse your PDFs. It writes the prompt from your ontology and judges every row against it before the write — the model and the decision stay yours. Extracted graphs inherit hallucinations; this is the part that makes them visible before they land.
  • Not for millions of triples. Everything is in memory and scans are linear. Hundreds to thousands is the range where a curated graph is even possible.
  • Not its own SPARQL engine. Reads run on Oxigraph, updates and OWL/SHACL on rdflib. Graduating to a full triple store later is an export, not a rewrite.
  • Not a replacement for Obsidian if what you want is human notes. This is for facts a machine has to be right about.

Where to go next

License

MIT. Copyright (c) 2026 Ryuto Yoda.

Bundled data

One third-party dataset is shipped, and one is not:

  • Freebase — the examples/freebase_*.yaml graphs are a small extract of the Freebase dump, licensed CC BY 2.5. They are in the repository so the demo pages can be rebuilt from their source.
  • WebQSP — the benchmark questions and gold answers come from The Value of Semantic Parse Labeling for KBQA (Yih et al., 2016), via the rmanluo/RoG-webqsp repack. No dataset content is committed here: benchmarks/webqsp_bench.py prepare downloads the test split at run time. What is tracked is benchmarks/*_data.json — the scored results the charts and the number above are read from.

Neither is required to use trikedb.

Release files for trikedb 0.42.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for trikedb 0.42.1
File Size Uploaded
trikedb-0.42.1.tar.gz 581.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for trikedb 0.42.1
File Interpreter ABI Platform
trikedb-0.42.1-py3-none-any.whl Python 3 none any Details

Total release size: 713.3 kB

Release files / trikedb-0.42.1.tar.gz

Download URL trikedb-0.42.1.tar.gz
Size 581.8 kB
Tags Source
SHA-256 checksum
How to use checksums
fad27cec8346d2b58c76e104517be900a2e1f2014177de3dc74b3952721b0126
BLAKE2b-256 checksum
How to use checksums
7b2a02f099731e6f37b28092b6c4dcbe6fbc55b965509fbd5413ff192733c02f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / trikedb-0.42.1-py3-none-any.whl

Download URL trikedb-0.42.1-py3-none-any.whl
Size 131.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ea80aadf4a6ed94920d9f637c6a260f2a7533e04debfaaa370e9e7405c736223
BLAKE2b-256 checksum
How to use checksums
b2040b6665677342e844d085dae51462c53b6209056657a9bbc30039be6909a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.42.1 This release

2 release files

0.42.0

2 release files

0.41.0

2 release files

0.40.0

2 release files

0.39.6

2 release files

0.39.5

2 release files

0.39.4

2 release files

0.39.3

2 release files

0.39.2

2 release files

0.39.1

2 release files

0.39.0

2 release files

0.38.1

2 release files

0.38.0

2 release files

0.37.0

2 release files

0.35.0

2 release files

0.34.0

2 release files

0.33.3

2 release files

0.33.2

2 release files

0.33.1

2 release files

0.33.0

2 release files

0.32.1

2 release files

0.32.0

2 release files

0.31.2

2 release files

0.31.1

2 release files

0.31.0

2 release files

0.30.2

2 release files

0.30.1

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.1

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.0

2 release files

0.23.0

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page