Skip to main content

TrikeDB โ€” a triceratops carrying a knowledge graph on its frill

๐Ÿฆ• Live demo โ€” 600 real Freebase facts, click around, run SPARQL in the browser ย ยทย  workspace demo โ€” the same facts as 6 domain graphs, tiled and filterable ย ยทย  PyPI

trikedb

The single-file graph database. You query it like a real triple store โ€” full SPARQL 1.1, reads and writes. Underneath, it's one YAML file. Built for LLM agents.

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 cloud deployment. It diffs cleanly in git, survives in a repo next to your code, and โ€” the part trikedb is actually designed around โ€” an LLM agent can Read it directly and reason over your domain without hallucinating entity names.

And it renders as an interactive workbench (live demo โ€” 600 real Freebase facts):

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

Why

RDF graph databases are powerful, correct โ€” and heavy. SPARQL endpoints, OWL reasoners, enterprise semantic layers: great at scale, overkill when what you need is a curated map of a few hundred facts that your AI agents (and teammates) can trust.

trikedb keeps the interface of the big system โ€” real SPARQL 1.1 (rdflib's engine, not a homegrown subset) โ€” and shrinks the machinery down to an embedded library over a file you can read, diff, and commit:

A full triple-store deployment trikedb
Storage server / cloud service one YAML file
Query SPARQL 1.1 SPARQL 1.1 (same language, rdflib engine)
Graph model usually pick one: RDF or property graph (two systems) both from one file โ€” SPARQL/RDF (to_rdflib) and property graph (to_networkx, via [networkx])
Writes SPARQL Update SPARQL Update โ€” persisted back to the YAML
Schema OWL + reasoners a predicate whitelist, plus SHACL shapes via [shacl]
Inference DL reasoning engines OWL-RL materialization via [owl] โ€” inferred facts land in the YAML, reviewable
Agent integration a service to operate the agent reads the file, trikedb mcp (stdio), or trikedb serve (remote MCP + UI + REST)
Setup time an afternoon (or a sprint) pip install trikedb

If you need full OWL-DL reasoning at scale, named graphs, and multi-tenant governance, you want a full enterprise semantic platform. If you want a knowledge graph today, in a file, in git โ€” that's trikedb. And because the storage maps cleanly onto RDF, graduating to a bigger system later is an export, not a rewrite: each team keeps its own YAML graph, and stitching them together (or migrating them wholesale) is just merging triples.

Curation-first, not extraction-first

Most "AI knowledge graph" tools use an LLM to extract triples from text. That's great for bootstrapping, but extracted graphs inherit hallucinations. trikedb takes the opposite stance: the graph is curated data (by humans, or by agents you supervise), the ontology constrains what can be said, and LLMs consume the graph rather than invent it. When an agent reads

- {s: crm-sync-job, p: INGESTS_TO, o: RAW_CRM_CONTACTS}

there is no step where a table name can be made up.

Install

From PyPI:

pip install trikedb             # library + CLI (PyYAML + rdflib only)
pip install 'trikedb[all]'      # everything below in one shot

pip install 'trikedb[mcp]'      # + MCP server for AI agents (stdio)
pip install 'trikedb[serve]'    # + UI / REST / remote MCP over HTTP
pip install 'trikedb[oauth]'    # + OAuth 2.1 for the claude.ai / ChatGPT UIs
pip install 'trikedb[remote]'   # + s3:// gs:// graphs
pip install 'trikedb[snowflake]' # + snowflake:// graphs (the warehouse is the store)
pip install 'trikedb[shacl]'    # + SHACL validation
pip install 'trikedb[owl]'      # + OWL-RL inference
pip install 'trikedb[semantic]' # + semantic search (numpy + model2vec, no torch)
pip install 'trikedb[networkx]' # + property-graph projection (to_networkx)

Quickstart (Python)

from trikedb import TrikeDB

# A typed knowledge graph that lives in one YAML file. The predicates you declare
# are the schema โ€” that whitelist catches typos and junk on write.
db = TrikeDB("pipeline.yaml", ontology={
    "PROVIDES":   "SaaS vendor -> ingestion job",
    "INGESTS_TO": "ingestion job -> warehouse table",
    "MIGRATED_TO": "deprecated table -> its replacement",
})

# Add facts. Any keyword becomes an edge attribute โ€” and `prov` is the one to
# standardize on: cite where each fact came from so the graph stays verifiable.
db.add("salesflow-crm", "PROVIDES", "crm-sync-job")
db.add("crm-sync-job", "INGESTS_TO", "RAW_CRM_CONTACTS",
       schedule="hourly", prov="https://runbook.example/crm#sync")
db.add("LEGACY_DUMP", "MIGRATED_TO", "RAW_CRM_CONTACTS", deprecated=True)

# The ontology is a guardrail: db.add("crm-sync-job", "OWNS", "x") would raise
# OntologyError โ€” 'OWNS' isn't a declared predicate, so the typo never lands.

# Describe nodes: `type` colors the graph and is queryable; attach anything else.
db.set_node("RAW_CRM_CONTACTS", type="table", pii=True,
            url="https://catalog.example/raw_crm_contacts")

# Ask questions โ€” join patterns with zero dependencies โ€ฆ
db.query(["?vendor PROVIDES ?job", "?job INGESTS_TO ?table"])
# [{'vendor': 'salesflow-crm', 'job': 'crm-sync-job', 'table': 'RAW_CRM_CONTACTS'}]

# โ€ฆ or full SPARQL 1.1 (FILTER, OPTIONAL, aggregates โ€” delegated to rdflib, t: pre-bound)
db.sparql('SELECT ?t WHERE { ?t t:type "table" ; t:pii true }')   # every PII table
db.sparql('SELECT ?s ?o WHERE { ?st rdf:subject ?s ; rdf:object ?o ; t:schedule "hourly" }')  # edge attrs, too

# Let the graph classify itself โ€” declare RDFS/OWL semantics and materialize what
# follows (pip install 'trikedb[owl]'). Inferred facts land in the YAML, reviewable.
db.declare("INGESTS_TO", "domain:job")    # subjects of INGESTS_TO are jobs
db.declare("INGESTS_TO", "range:table")   # objects are tables
db.infer(apply=True)   # -> crm-sync-job a job, RAW_CRM_CONTACTS a table (tagged inferred: true)

# Check it before you trust it โ€” validate against SHACL shapes (pip install 'trikedb[shacl]')
ok, report = db.validate('''@prefix sh: <http://www.w3.org/ns/shacl#> . @prefix t: <urn:trikedb:> .
  t:IngestShape a sh:NodeShape ; sh:targetObjectsOf t:INGESTS_TO ;
    sh:property [ sh:path t:type ; sh:minCount 1 ] .''')   # does every landed table declare a type?

# Find facts by meaning, not spelling (pip install 'trikedb[semantic]')
db.search("what syncs the CRM?", k=5)

# Hybrid retrieval for agents โ€” semantic recall + a hard structured filter, in one
# call: cast a wide net by meaning, then keep only what precisely matches.
db.find("where is the customer CRM data?", where={"type": "table", "pii": True})
# -> [{'node': 'RAW_CRM_CONTACTS', 'props': {'type': 'table', 'pii': True, ...}, 'facts': [...]}]

# Writes go through SPARQL too and autosave straight back to the YAML
db.sparql("INSERT DATA { t:figly t:PROVIDES t:figly-export-job }")

# Ship one self-contained HTML file your team can actually click through
db.to_html("pipeline.html")     # searchable graph + node details + in-browser SPARQL console
db.to_rdflib(); db.to_jsonld()  # RDF/SPARQL view โ€” or graduate to any RDF tool
db.to_networkx()                # property-graph view: run networkx algorithms on the
                                # same file (shortest path, centrality) โ€” 'trikedb[networkx]'

Quickstart (CLI)

trikedb add pipeline.yaml salesflow-crm PROVIDES crm-sync-job
# `prov` is just an edge attribute, but the one to standardize on: cite each fact's source.
trikedb add pipeline.yaml crm-sync-job INGESTS_TO RAW_CRM_CONTACTS -a schedule=hourly -a prov=https://runbook.example/crm#sync

trikedb query pipeline.yaml -w "?vendor PROVIDES ?job" -w "?job INGESTS_TO ?table"
# vendor         job           table
# -------------  ------------  ----------------
# salesflow-crm  crm-sync-job  RAW_CRM_CONTACTS

trikedb sparql pipeline.yaml \
  "SELECT ?v ?t WHERE { ?v t:PROVIDES ?j . ?j t:INGESTS_TO ?t }"

# updates persist straight back to the file
trikedb sparql pipeline.yaml \
  "INSERT DATA { t:figly t:PROVIDES t:figly-export-job }"

# semantic search: meaning, not spelling ([semantic] extra)
trikedb search pipeline.yaml "what syncs the CRM?" -k 5

trikedb stats pipeline.yaml
trikedb html pipeline.yaml -o pipeline.html
trikedb jsonld pipeline.yaml

Importing from CSV and Markdown docs

The YAML file is the store, but triples can come from wherever your team already writes:

# CSV/TSV with an s,p,o header โ€” extra columns become edge attributes
trikedb import pipeline.yaml new_vendors.csv

# Markdown: every table whose header has s/p/o columns is picked up;
# prose and other tables are ignored. Your design docs are data.
trikedb import pipeline.yaml design_doc.md
<!-- anywhere inside an ordinary design doc: -->
| s                 | p          | o                  | schedule  |
|-------------------|------------|--------------------|-----------|
| clickpath-pa      | PROVIDES   | clickpath-webhook  |           |
| clickpath-webhook | INGESTS_TO | RAW_PRODUCT_EVENTS | streaming |

Imports are deterministic โ€” no LLM extraction, so nothing gets invented. The ontology is enforced on the way in, and "true"/"false" cells become booleans. See examples/acme_design_doc.md and examples/acme_new_vendors.csv.

Validation and inference (SHACL / OWL)

The predicate whitelist is the seatbelt; when you want real schema validation, use SHACL (pip install 'trikedb[shacl]' โ€” delegated to pySHACL, not hand-rolled):

conforms, report = db.validate("""
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix t:  <urn:trikedb:> .
t:BotShape a sh:NodeShape ;
  sh:targetSubjectsOf t:USES_ROLE ;
  sh:property [ sh:path t:type ; sh:hasValue "bot" ; sh:minCount 1 ] .
""")
trikedb validate graph.yaml shapes.ttl   # exit code 1 on violations โ€” CI-friendly

For inference, declare RDFS/OWL semantics on your predicates (and classes) and materialize what follows (pip install 'trikedb[owl]', OWL-RL via owlrl):

# OWL property characteristics
db.declare("INHERITS", "transitive")     # stored as a reviewable triple
db.add("admin", "INHERITS", "editor")
db.add("editor", "INHERITS", "viewer")
db.infer(apply=True)                     # adds (admin, INHERITS, viewer) โ€” marked inferred: true

# RDFS class hierarchy + typing
db.declare("Cat", "subclass_of:Animal")        # rdfs:subClassOf
db.declare("authored", "domain:Person")        # rdfs:domain  โ†’ subjects get typed
db.declare("authored", "range:Book")           # rdfs:range   โ†’ objects get typed
db.declare("bornIn", "subproperty_of:locatedIn")  # rdfs:subPropertyOf
db.add("felix", "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "Cat")
db.infer()   # โ†’ (felix, rdf:type, Animal)  via subClassOf; domain/range typing; etc.

infer() surfaces classifications and hierarchy (rdf:type, subClassOf, subPropertyOf) as well as OWL edges (transitive / symmetric / inverse), while suppressing the reasoner's rdf/owl bookkeeping noise.

Inference is materialization, not magic: derived facts land in the YAML tagged inferred: true, so the git diff shows exactly what the reasoner concluded and a human can review it like any other change. (For ad-hoc transitivity you often don't need OWL at all โ€” SPARQL property paths like t:INHERITS+ already walk chains at query time.)

Where the graph lives: your storage, your choice

The file doesn't have to be local, and it doesn't have to be a file. Everything above storage only ever asks for one whole document, so the destination swaps out and nothing else changes โ€” SPARQL, the MCP tools, SHACL and to_networkx behave identically wherever the bytes are.

Object storage (pip install 'trikedb[remote]'):

db = TrikeDB("s3://team-bucket/kg/pipeline.yaml")   # read and write
trikedb sparql s3://team-bucket/kg/pipeline.yaml "SELECT ?s WHERE { ?s ?p ?o } LIMIT 5"
trikedb mcp s3://team-bucket/kg/pipeline.yaml       # whole team's agents share one graph

Auth is delegated to the standard AWS credential chain (env vars, ~/.aws/credentials profiles, SSO, IAM roles) via fsspec/s3fs โ€” trikedb stores no credentials, and your bucket policy is the access control: readers get s3:GetObject, writers get s3:PutObject, per-prefix policies give each team its own graph. gs://, az:// and plain https:// (read-only) work through the same mechanism with the matching fsspec backend installed.

A warehouse table (pip install 'trikedb[snowflake]') โ€” for teams whose governance says data lives in the warehouse:

db = TrikeDB("snowflake://ANALYTICS.PUBLIC.TRIKE_GRAPHS/sales/crm")

One graph is one row (name, doc, version, updated_at), and one table holds many graphs โ€” adopting trikedb costs a company one table, not one per graph. There's no local copy and nothing to synchronise: the row is the graph. Create the table first (trikedb won't run DDL in your warehouse on its own):

trikedb sql-init snowflake://ANALYTICS.PUBLIC.TRIKE_GRAPHS/sales/crm --print   # review the DDL
trikedb sql-init snowflake://ANALYTICS.PUBLIC.TRIKE_GRAPHS/sales/crm           # or just run it

Connection settings come from the environment (SNOWFLAKE_ACCOUNT, SNOWFLAKE_USER, SNOWFLAKE_PRIVATE_KEY_PATH or SNOWFLAKE_PASSWORD, plus role/warehouse/database as needed), or name an entry in your connections.toml with SNOWFLAKE_CONNECTION_NAME and let your existing Snowflake tooling own it. Same as S3: trikedb stores no credentials, and your grants are the access control.

And the warehouse can read it back. sql-init also creates four views, so the same graph answers SPARQL from memory and SQL from the warehouse โ€” with no second copy to keep in step:

-- does the graph still match reality?
SELECT k.NODE_ID
FROM MYDB.PUBLIC.KG_NODE k
LEFT JOIN MYDB.INFORMATION_SCHEMA.TABLES t ON t.TABLE_NAME = k.NODE_ID
WHERE k.NODE_TYPE = 'table' AND t.TABLE_NAME IS NULL;   -- claimed, but gone

KG_NODE and KG_EDGE carry the node/edge column shape conventionally used for property graphs on Snowflake, so a Cortex Analyst semantic model or query written against that shape works here too; KG_PREDICATE exposes the ontology, and KG_TRIPLE the same rows as plain s/p/o. Node properties and edge attributes stay in VARIANT columns, so adding a predicate never needs a DDL change. They're views, not tables โ€” nothing stored twice, nothing to drift, zero cost, and AT(TIMESTAMP => โ€ฆ) reads the past through them. (That column shape is an intended byproduct, not a dependency: nothing is imported from anyone, the SQL is generated from trikedb's own model, and trikedb is not affiliated with or endorsed by Snowflake.)

Reading without a write path. Pass read_only=True and every mutation raises, reload() included:

db = TrikeDB("snowflake://DB.SCHEMA.T/sales/crm", read_only=True)

Use it when writes belong somewhere else โ€” a reviewed file in git, say โ€” and the warehouse is there for distribution and SQL access. An app that only reads shouldn't be holding a capability a bug could spend.

Bringing your own connection. Some hosts have a session and no way to make another โ€” inside Streamlit in Snowflake there are no credentials to find and no outbound connection to open. Pass what you have:

from snowflake.snowpark.context import get_active_session

db = TrikeDB("snowflake://DB.SCHEMA.T/sales/crm",
             connection=get_active_session(), read_only=True)

A DB-API connection works too; dispatch is on what the object can do, not on an imported type, so neither driver has to be installed for the other path to work.

Concurrent writes are safe on both. A save is conditional on the stored graph still being the one it was read from, so a write that would clobber someone else is refused with ConcurrentWriteError rather than silently winning โ€” S3 does it with an ETag precondition, a warehouse with a version column and an affected-row count. Ten concurrent writers land ten triples in either. gs://, az:// and local files have no conditional write, so they stay last-write-wins: point writers through a single MCP process or keep writes in git-reviewed batches.

Adding a backend happens in one place. A warehouse is four SQL templates and a connect function.

Workspaces: many graphs, one view

Real teams have more than one graph โ€” finance, data platform, HR. A workspace file unions them:

# workspace.yaml
graphs:
  finance:  finance.yaml
  platform: s3://team-bucket/kg/platform.yaml   # local and remote mix freely
  warehouse: ../infra/ontology/warehouse.yaml

Every command accepts it (trikedb sparql workspace.yaml ..., trikedb html workspace.yaml, trikedb serve workspace.yaml). In the HTML view each project tiles into its own cluster with a per-graph filter bar; every triple carries a graph: attribute naming its source.

The payoff is automatic joins: because RDF triples merge on shared names, (tanaka, OWNS_BUDGET, project-atlas) in finance and (project-atlas, USES, ACME_DWH) in platform become one SPARQL-walkable path โ€” no foreign keys, no schema negotiation. Unions are read-only views; each member graph stays owned (and permissioned) by its team, and writes go to the member file. Members can be warehouse rows too, and they inherit the connection โ€” which is what makes a union usable somewhere that cannot open one of its own:

# workspace.yaml, itself stored as a row
graphs:
  ontology: snowflake://DB.SCHEMA.T/kg/ontology
  skills:   snowflake://DB.SCHEMA.T/kg/skills
db = TrikeDB("snowflake://DB.SCHEMA.T/kg/workspace",
             connection=get_active_session(), read_only=True)

Let TrikeDB build the union rather than reading the members and merging them yourself. Three details decide what a union contains, and getting one wrong is silent โ€” the graph just comes out slightly poorer than the files it was built from:

  • Node properties merge per key, not per node. A node declared in two members keeps the first value of each key, so a description only the second member carries still survives. Taking the whole dict from the first member drops it with no error anywhere.
  • Ontologies merge per predicate, first member wins the description.
  • A triple's graph attribute is the workspace key, not the member's filename or path.

content_hash() is the cheap way to prove a union you built matches one trikedb built: same hash, same graph.

Keeping a growing graph healthy

Ontologies accumulate facts from many hands (and agents). Two commands keep that sustainable:

# CI / pre-commit: does the graph parse, and is the exported HTML current?
# Generated HTML embeds a content hash of the graph, so staleness is detectable.
trikedb check graph.yaml --html docs/index.html   # exit 1 if stale

# health findings: duplicate triples across workspace members, Tokyo-vs-tokyo
# name collisions, near-duplicate free-text facts, orphan node props,
# declared-but-unused predicates
trikedb audit workspace.yaml            # exit 1 on errors; --strict fails on warnings too

audit is deterministic by design โ€” for semantic dedup beyond these heuristics, hand the --json report to an LLM agent and let it propose merges as a reviewable PR.

The review gate depends on where the graph lives. A file in git gives you the strongest story: every change is a diff, check and audit run in CI, history comes free. An s3:// or snowflake:// graph has no pull request โ€” writes land immediately โ€” so review moves to the ontology guard at the write boundary, audit on a schedule instead of per-change, and the backend's own history (object versions, warehouse time travel, the updated_at column). Some teams run both on purpose: the reviewed graph in git, a shared graph agents write to, unioned with a workspace file so curation and accumulation don't block each other.

Do you have to write YAML by hand?

No โ€” YAML is the storage format, not the authoring interface. It's what the graph is written down as, chosen so a human can read a diff. Every write path produces the same document and passes the same ontology check:

db.add(s, p, o, **attrs) Python โ€” scripts, notebooks, ETL
trikedb add FILE S P O -a k=v one fact from a shell
trikedb import FILE data.csv a spreadsheet or Markdown table already has the facts
db.sparql("INSERT DATA {...}") you think in SPARQL
MCP add_triple / set_node an agent is writing โ€” the usual case
db.infer(apply=True) materialize what already follows
editing the YAML a text editor is a legitimate client too

The guard applies to all of them equally, so "an agent wrote it" and "a human wrote it" can't diverge in vocabulary.

The HTML workbench is a rendering, and where the graph lives never decides where the page goes: a local graph renders next to itself, a remote one into the working directory, and -o takes a path or an object URL (-o s3://site/kg.html publishes it). It's one self-contained file โ€” no build step, no server โ€” so publishing is just putting it somewhere.

Serving a graph (UI + REST + remote MCP)

One process, three doors (pip install 'trikedb[serve]'):

trikedb serve workspace.yaml --port 8080 --token $SECRET
  • / โ€” the workbench UI, always showing the current graph
  • /sparql โ€” minimal REST: POST {"query": "..."} โ†’ JSON, for apps
  • /mcp โ€” MCP over Streamable HTTP, for agents anywhere:
claude mcp add kg https://kg.internal:8080/mcp --transport http \
  --header "Authorization: Bearer $SECRET"

Same eleven MCP tools as stdio โ€” the server definition is shared, only the transport differs. Pair it with an s3:// graph and the server is stateless โ€” run it anywhere.

OAuth 2.1, for the claude.ai and ChatGPT UIs

A static token is fine for a script, but the web UIs want a real login. Point trikedb at an IdP you already run and it becomes an OAuth 2.1 resource server โ€” the thing both connector UIs know how to talk to:

pip install 'trikedb[serve,oauth]'
trikedb serve graph.yaml --public-url https://kg.example.com \
  --oauth-issuer https://idp.example.com/ --required-scope kg:read

Then add https://kg.example.com/mcp as a custom connector and log in as yourself. trikedb verifies tokens; it never issues them. There is no authorization server here, no user table, no password โ€” just a JWKS lookup against your issuer and a check that the token's signature, expiry, and audience are right. Your IdP stays the only place identity lives, and the graph stays a file.

Three things to get right:

  • --public-url must be the HTTPS URL clients actually reach. Tokens are bound to <public-url>/mcp as their audience (RFC 8707), so a token minted for another service can't open your graph. Override with --oauth-audience if your IdP issues a fixed API identifier instead.
  • The IdP needs to register the connector. Dynamic Client Registration is the smooth path (Auth0, Okta, Keycloak, WorkOS all support it); if yours doesn't, claude.ai also accepts a Client ID Metadata Document or a client ID/secret you paste in.
  • It has to be publicly reachable over HTTPS. Neither UI can connect to localhost โ€” use a tunnel while you're developing.

Discovery is served for you at /.well-known/oauth-protected-resource/mcp, and an unauthenticated request gets the RFC 9728 challenge that starts the login flow.

The file format

A trikedb file is ordinary YAML with three top-level keys (only triples is required):

ontology:            # optional โ€” omit it for free-form predicates
  predicates:
    PROVIDES: "SaaS vendor -> ingestion job"
    AFFECTED_BY: "table -> change event"

nodes:               # optional โ€” free-form node properties
  salesflow-crm: {type: saas, url: "https://salesflow.example", plan: enterprise}
  RAW_CRM_CONTACTS: {type: table, schema: ACME_RAW, pii: true}

triples:
  # compact form for plain facts
  - {s: adastra-ads, p: PROVIDES, o: ads-spend-collector}

  # any extra keys become edge attributes
  - s: RAW_AD_SPEND_DAILY
    p: AFFECTED_BY
    o: "2025-04-01 adastra API v3: spend now in micros (was cents)"

Three conventions worth stealing (see examples/acme_pipeline.yaml):

  • Change events as objects. AFFECTED_BY edges pointing at dated event strings give your graph a memory โ€” "why did this number change in April?" becomes a query.
  • deprecated: true on edges renders them dashed in the HTML view and lets agents filter dead paths.
  • via: / schedule: attributes carry operational detail without polluting the node set.
  • Node properties keep growing. That's the RDF promise: attach type, url, schema, owners โ€” whatever your team needs โ€” without a schema migration. type drives color grouping in the HTML view, and node properties are queryable in SPARQL (?x t:type "table"). Set them from code with db.set_node("RAW_CRM_CONTACTS", pii=True).

Hybrid retrieval for agents

Semantic search is great at recall (finds what you mean) but not precision โ€” the score is uncalibrated and it never says "no match." SPARQL is the opposite: exact, but only if you already know the names. find() combines them in one call โ€” semantic recall, then a hard structured filter โ€” which is the retrieval an agent actually wants:

# "cast a wide net by meaning, then keep only what precisely matches"
db.find("where is the customer CRM data?",
        where={"type": "table", "pii": True})   # dict of required node props โ€ฆ
db.find("customer data", where=lambda name, props: props.get("pii"))  # โ€ฆ or a predicate

# each result is a ready-to-use payload: the node, its properties, its facts
# [{"node": "RAW_CRM_CONTACTS", "props": {"type": "table", "pii": True, ...},
#   "facts": [["INGESTS_TO", ...], ...]}]

Recall casts a wide net (search, cross-lingual, synonym-tolerant); the where filter drops the false positives with no fuzz and pulls exact structured facts. Use the recall stage for candidates and the filter for correctness โ€” never gate on the raw similarity score. The same two-stage move is available to LLM agents as the find MCP tool below, or hand-rolled from search + sparql/match when you want full control.

An ontology layer for AI agents (MCP)

trikedb is embedded, not hosted. For agents, "embedded" means MCP over stdio โ€” the graph runs inside the agent session, no server to operate. Register it with any MCP client:

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

The agent gets sparql, match, search, find, get_node, ontology, stats to read, and add_triple, set_node, remove_triples, import_source to write. Every write autosaves to the YAML โ€” so agent contributions arrive as reviewable git diffs.

This is also the answer to "just throw docs at it": the agent is the extractor, trikedb is the validated write path. Point your agent at a pile of documents and ask it to record the facts; it reads them (any format โ€” it's an LLM), calls add_triple for each fact, and the ontology rejects any predicate it tries to invent. Extraction stays flexible, the graph stays clean, and a human reviews the diff.

Using it with LLM agents (no MCP)

The zero-setup loop:

  1. Keep graph.yaml in your repo, next to the code it describes.

  2. Tell your agent about it once (in your agent's project instructions / system prompt):

    Before any task touching the data pipeline, read pipeline.yaml. It is the source of truth for which jobs feed which tables. Predicates are limited to the ontology declared in the file.

  3. Agents propose edits as diffs to the YAML โ€” reviewable in a PR like any other change. The ontology check (trikedb.add raises on unknown predicates) keeps generated edits inside the vocabulary you chose.

  4. Humans browse the same graph via trikedb html.

One source of truth, two projections: YAML for machines, HTML for people.

What trikedb is not

  • Not a SPARQL implementation of its own. The SPARQL surface is deliberately not hand-rolled โ€” your YAML is loaded into rdflib and queried/updated by rdflib's battle-tested engine. Mapping rule: subjects/predicates become URIs under urn:trikedb:; objects with whitespace (change events, notes) become literals. Triples inserted via SPARQL start without edge attributes; surviving triples keep theirs. The lighter query()/triples() API also exists for quick pattern matching.
  • Not an extraction pipeline. It won't turn your PDFs into a graph. Pair it with an extractor if you want that โ€” then curate what comes out.
  • Not for millions of triples. Everything is in memory and scans are linear. The sweet spot is the hundreds-to-thousands range, where a curated graph is even possible.

Examples

  • examples/freebase_sample.yaml โ€” real-world data: ~600 facts from the Freebase knowledge graph (CC BY, extracted from the WebQSP benchmark subgraphs) around Tupac Shakur, Agatha Christie, Nikola Tesla and more. Node types are inferred from predicate domains. This powers the live demo.
  • examples/freebase_workspace.yaml โ€” the same facts split into 6 domain graphs (film / music / books / people / places / misc) and unioned back as a workspace: each member renders as its own island with a filter chip. This powers the workspace demo.
  • examples/acme_pipeline.yaml โ€” a fictional data platform showing the operational conventions: ontology, deprecations, change events.
  • examples/python_ecosystem.yaml โ€” free-form predicates, no ontology.
  • examples/trikedb_quickstart.ipynb โ€” runnable notebook quickstart with an inline graph.

Live demo: https://ryutoyoda.github.io/trikedb/ ยท Workspace demo: https://ryutoyoda.github.io/trikedb/workspace.html

The exported HTML is a small workbench, not just a picture: click a node for a right-hand panel with all its properties (URLs become links), search nodes top-right, and open the SPARQL console to run real SPARQL 1.1 in the browser โ€” powered by Oxigraph compiled to WASM, loaded from CDN on first use. Change events render as red diamonds with a timeline bar at the bottom; the initial layout adapts to graph shape (--layout flow|free|auto). Filter the view by toggling node-type checkboxes (with all / none shortcuts) โ€” the legend slides horizontally when types get numerous โ€” and, in a workspace, toggle member graphs the same way.

Benchmark

On WebQSP (knowledge-graph QA), the same small LLM answers 60% alone vs 83% with a trikedb graph as context โ€” a +23-point delta under a deterministic, reproducible protocol. Scripts, method, and an honest scoring-sensitivity analysis live in benchmarks/.

Documentation

Development

Uses uv:

uv sync --extra dev
uv run pytest

License

MIT

Download files

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

Source Distribution

trikedb-0.25.0.tar.gz (113.5 kB view details)

Uploaded Source

Built Distribution

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

trikedb-0.25.0-py3-none-any.whl (71.0 kB view details)

Uploaded Python 3

File details

Details for the file trikedb-0.25.0.tar.gz.

File metadata

  • Download URL: trikedb-0.25.0.tar.gz
  • Upload date:
  • Size: 113.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.5

File hashes

Hashes for trikedb-0.25.0.tar.gz
Algorithm Hash digest
SHA256 2e5849483caa9df7009f11438565e8aa70c1d48857ece2706a1bff70b7a66b6f
MD5 4ff901b7c0bf7f21868ba5d11526809b
BLAKE2b-256 22377bffa5dda518880ad66e12676beb72874b78546a23226191e8965e6272de

See more details on using hashes here.

File details

Details for the file trikedb-0.25.0-py3-none-any.whl.

File metadata

  • Download URL: trikedb-0.25.0-py3-none-any.whl
  • Upload date:
  • Size: 71.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.5

File hashes

Hashes for trikedb-0.25.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e3c61fb64aa6c478df3cc28bc7330e16e6acfddb8208a2a8affd3f4d0065b40d
MD5 ca9880734f6022003ea642a77fa8ab32
BLAKE2b-256 5ea48a5abd68cec06d32b42ac7db5129a1082dede38ab8993867419f1272af33

See more details on using hashes here.

Supported by

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