Metadata-Version: 2.4
Name: trikedb
Version: 0.8.1
Summary: The DuckDB of graph databases: a knowledge graph that lives in a single YAML file, queried with full SPARQL 1.1. Built for LLM agents.
Author: Ryuto Yoda
License: MIT
Project-URL: Homepage, https://github.com/RyutoYoda/trikedb
Keywords: knowledge-graph,rdf,triple-store,yaml,llm,agents,ontology
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Requires-Dist: rdflib>=7
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.2; extra == "mcp"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: mcp<2,>=1.2; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="docs/logo.svg" width="220" alt="trikedb — a triceratops whose three horns are the triple: subject, predicate, object">
</p>

# trikedb

**The DuckDB of graph databases.** You query it like a real triple store — full SPARQL 1.1, reads *and* writes. Underneath, it's a single YAML file. Built for LLM agents.

```yaml
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.**

## Why

RDF graph databases are like Oracle: 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.

DuckDB proved the pattern: keep the *interface* of the big system (full SQL, in DuckDB's case) and shrink the *machinery* down to an embedded library over a file. trikedb applies the same move to RDF graph databases — the interface is real SPARQL 1.1 (rdflib's engine, not a homegrown subset), the storage is YAML 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) |
| Writes | SPARQL Update | SPARQL Update — persisted back to the YAML |
| Schema | OWL + reasoners | a list of allowed predicates |
| Agent integration | a service to operate | the agent reads the file, or `trikedb mcp` (stdio, embedded) |
| Setup time | an afternoon (or a sprint) | `pip install trikedb` |

If you need inference engines, 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

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

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

## Install

```bash
pip install trikedb           # library + CLI
pip install 'trikedb[mcp]'    # + MCP server for AI agents
```

## Quickstart (Python)

```python
from trikedb import TrikeDB

db = TrikeDB("pipeline.yaml", ontology={
    "PROVIDES": "SaaS vendor -> ingestion job",
    "INGESTS_TO": "ingestion job -> warehouse table",
    "MIGRATED_TO": "deprecated table -> its replacement",
})

db.add("salesflow-crm", "PROVIDES", "crm-sync-job")
db.add("crm-sync-job", "INGESTS_TO", "RAW_CRM_CONTACTS", schedule="hourly")
db.add("crm-sync-job", "OWNS", "x")   # OntologyError: predicate not declared

# Pattern matching — None is a wildcard, '*' globs
for t in db.triples(p="INGESTS_TO", o="RAW_*"):
    print(t.s, "->", t.o, t.attrs)

# Multi-pattern queries with variable joins (zero dependencies)
db.query(["?vendor PROVIDES ?job", "?job INGESTS_TO ?table"])
# [{'vendor': 'salesflow-crm', 'job': 'crm-sync-job', 'table': 'RAW_CRM_CONTACTS'}]

# Or real SPARQL 1.1 — FILTER, OPTIONAL, aggregates, the lot.
# Delegated to rdflib, not hand-rolled. The prefix t: is pre-bound.
db.sparql("""
  SELECT ?vendor ?table WHERE {
    ?vendor t:PROVIDES ?job .
    ?job t:INGESTS_TO ?table .
    FILTER(STRSTARTS(STR(?table), "urn:trikedb:RAW_"))
  }
""")
db.sparql("ASK { ?x t:MIGRATED_TO ?y }")  # True

# Writes go through SPARQL too, DuckDB-style — and land back in the YAML
db.sparql("INSERT DATA { t:figly t:PROVIDES t:figly-export-job }")
db.sparql("DELETE WHERE { ?job t:INGESTS_TO t:LEGACY_CONTACTS_DUMP }")
db.save()  # or pass autosave=True and skip this

db.to_rdflib()               # plain rdflib.Graph, if you want to go further
db.to_html("pipeline.html")  # interactive graph workbench (see demos below)
db.to_jsonld()               # best-effort export for real RDF tooling
```

## Quickstart (CLI)

```bash
trikedb add pipeline.yaml salesflow-crm PROVIDES crm-sync-job
trikedb add pipeline.yaml crm-sync-job INGESTS_TO RAW_CRM_CONTACTS -a schedule=hourly

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 }"

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:

```bash
# 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
```

```markdown
<!-- 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`](examples/acme_design_doc.md) and [`examples/acme_new_vendors.csv`](examples/acme_new_vendors.csv).

## The file format

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

```yaml
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`](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)`.

## An ontology layer for AI agents (MCP)

Like the database it takes its analogy from, trikedb is embedded, not hosted. For agents, "embedded" means MCP over stdio — the graph runs inside the agent session, no server to operate:

```bash
claude mcp add kg -- uvx --from 'trikedb[mcp]' trikedb mcp /absolute/path/to/graph.yaml
```

The agent gets `sparql`, `match`, `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 (e.g. in `CLAUDE.md` / your 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](https://github.com/RDFLib/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/acme_pipeline.yaml`](examples/acme_pipeline.yaml) — a fictional company's data platform: vendors, ingestion jobs, warehouse tables, change events, migrations. The use case trikedb was born from.
- [`examples/python_ecosystem.yaml`](examples/python_ecosystem.yaml) — dependencies and deprecations in the Python packaging world, with free-form predicates.

```bash
trikedb html examples/acme_pipeline.yaml -o acme.html && open acme.html
```

**Live demos (GitHub Pages):**

- [acme knowledge graph](https://ryutoyoda.github.io/trikedb/) — the fictional data platform
- [python ecosystem](https://ryutoyoda.github.io/trikedb/python_ecosystem.html) — dependencies and deprecations

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](https://github.com/oxigraph/oxigraph) compiled to WASM, loaded from CDN on first use. Change events render as red diamonds with a timeline bar at the bottom.

## Development

Uses [uv](https://docs.astral.sh/uv/):

```bash
uv sync --extra dev
uv run pytest
```

## License

MIT
