Metadata-Version: 2.4
Name: mimir-ontology
Version: 0.3.2
Summary: A lightweight, local-first ontology engine for AI agents.
License-Expression: AGPL-3.0-only
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: fastmcp>=3.4.4
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6.0
Requires-Dist: sqlite-vec<0.2,>=0.1.9
Requires-Dist: typer>=0.12
Provides-Extra: gemini
Requires-Dist: google-genai>=1; extra == 'gemini'
Provides-Extra: local
Requires-Dist: sentence-transformers>=5; extra == 'local'
Description-Content-Type: text/markdown

# Mimir

Mimir is a small, local-first ontology engine for AI agents. You describe your domain in a YAML
schema (types, properties, links), feed the engine claims from your data sources, and it
materializes a typed graph that an agent can query over MCP — with every value traceable to the
claim, and therefore the source, it came from.

The engine is event-sourced. The only source of truth is an append-only log of *claims* ("source
X asserts fact Y"); the queryable graph is a deterministic projection of that log. The same
claims always materialize into the byte-identical graph, nothing is ever mutated in place, and
conflicts between sources are resolved by explicit rules instead of overwrites — the losing
claims stay reachable.

Everything runs in one process on your machine: one SQLite file, one YAML schema, one MCP server.
There is no LLM inside the engine and no network access in the core, by design.

## Install

```
uv add mimir-ontology             # or: pip install mimir-ontology
```

The distribution is named `mimir-ontology`; the package you import and the CLI command are
`mimir`.

## Quickstart

The repository ships a demo world: a fictional plumbing business asserted by two fixture sources
that overlap, disagree and occasionally make mistakes — on purpose. The full walkthrough with
expected output and explanations is in [`examples/plumber/demo.md`](examples/plumber/demo.md);
the short version:

```console
$ uv run mimir init demo-ws
$ uv run mimir schema load examples/plumber/schema.yaml --dir demo-ws
$ uv run mimir ingest examples/plumber/claims.jsonl --dir demo-ws
Ingested claims.jsonl: 88 accepted, 0 duplicates, 0 quarantined.
$ uv run mimir materialize --dir demo-ws
Materialized 88 claims: 43 entities, 35 links, 1 extra properties, 2 quarantined.
Graph hash: grf_42d6a5d8cb69ff50
```

Along the way the engine merged the customers both sources asserted (natural keys), resolved a
phone-number disagreement by observation time, kept an undeclared property in the entity's
`extra` pocket instead of dropping it, and quarantined two unprocessable claims with
machine-readable reasons. Ingest is idempotent: run it again and you get `88 duplicates`.

Inspect the graph from the shell (`--json` for machine output):

```console
$ uv run mimir search Job --filter status=done --limit 3 --dir demo-ws
$ uv run mimir show ent_6f65b3bf159ff099 --dir demo-ws
$ uv run mimir query '{"type": "Job",
    "where": [{"property": "status", "op": "eq", "value": "done"},
              {"link": "invoice", "exists": false}],
    "aggregate": {"function": "sum", "property": "estimated_value"}}' --dir demo-ws
sum(estimated_value) = 40800.0
  (3 matched, 0 missing the property)
```

Serve it to an agent:

```console
$ uv run mimir serve-mcp --dir demo-ws
```

The server speaks MCP over stdio and exposes seven read-only tools: `describe_schema`,
`describe_type`, `search`, `query`, `traverse`, `get`, `find`. In a clone of this repository,
`.mcp.json` already registers the server for Claude Code — run the quickstart above, start
`claude`, and ask:

> Which finished jobs have no invoice, and what is the total uninvoiced amount?

The agent reads the schema, narrows with a query, and lets the engine compute the sum in the
same call — every figure traceable to a specific claim. When it guesses a link name, an
operator or an enum value that does not exist, the server replies with a structured error
naming the valid options.

## How agents should query

The read surface is designed around how an LLM agent actually works: small working memory, a
tendency to hallucinate structure, and no patience for inventing pagination. The intended
pattern:

1. **Overview first.** `describe_schema` returns the map — every type with its description,
   entity count and links, but no property definitions. It stays small even at a hundred types.
2. **Then the chapter.** `describe_type(type_name)` returns one type in full: properties with
   enums and descriptions, outgoing links, incoming links, entity count.
3. **Narrow.** `search` for simple property filters; `query` for link predicates (both
   directions), ordering and pagination. Results are compact cards; every list response carries
   the true `total`, and a `cursor` continues exactly where the page ended.
4. **Walk.** `traverse` follows declared links from one entity. Expansion is bounded and every
   truncation is reported (`complete: false`) — nothing is ever cut silently.
5. **Let the engine count.** Every count, sum, average, minimum, maximum and group breakdown is
   one `query` call with an `aggregate` block. An agent adding up figures it paged through is
   the failure mode this surface exists to prevent; aggregation walks are never truncated, and
   float sums are bit-stable.
6. **Drill to provenance.** List views show plain values; `get(type_name, entity_id)` returns
   the full dossier — every value with its `observed_at` and `claim_id`. Every aggregate names
   the filter that reproduces its input set, so every number can be re-derived and audited.

Wrong turns fail loudly and teach: an unknown field, an inapplicable operator or a typo'd enum
value returns a structured error listing the valid options instead of silently matching
nothing.

## Semantic search

Exact reads answer questions you can spell precisely. `find` answers the other kind — "which
jobs were about water damage?" — by ranking entities by meaning. It needs an embedding model
and a vector index:

```console
$ uv add 'mimir-ontology[local]'      # local model (downloads on first use), or:
$ uv add 'mimir-ontology[gemini]'     # Gemini API (key from GEMINI_API_KEY)
$ uv run mimir embed --dir demo-ws    # build the index; re-runs embed only what changed
$ uv run mimir find "water damage" --type Job \
    --where '[{"link": "invoice", "exists": false}]' --dir demo-ws
```

The provider is configured per workspace in `mimir.toml` (`provider = "local"` uses
`Qwen/Qwen3-Embedding-0.6B` by default; `provider = "gemini"` uses `gemini-embedding-2`).
Results are the k nearest entities with cosine distances, and the optional `type`/`where`
filters use the same predicate language as `query` — applied *before* the ranking, so you get
the nearest entities among those that match. `find` is also the seventh MCP tool; the intended
agent pattern is *semantic first to locate, exact to verify*.

If a workspace has no use for semantic search, say so once in `mimir.toml`:

```toml
[embedding]
enabled = false
```

`serve-mcp` then exposes six tools instead of seven — the `find` tool is not registered at
all, so an agent never sees a capability this workspace does not have. (That is different
from an index that simply has not been built yet: there the tool stays visible and answers
with `NO_INDEX` and the command to fix it. Hiding a *failure* would be the silent lie the
surface is engineered against; withholding a capability you switched off is configuration.)

The index is a projection, never truth: embeddings are derived from the materialized graph,
live in the same SQLite file, and can be dropped and rebuilt at any time. Every index carries
a passport — which model, which dimensions, which graph — and `find` answers only when all
three match what is being served: a stale or mismatched index is a structured error naming
the fix (`mimir embed`), never a silently wrong ranking. Embeddings are cached by content, so
re-materializing and re-embedding a large workspace costs only the entities whose rendered
text actually changed.

## How it works

```
claims.jsonl ──ingest──▶  claim log            append-only, content-addressed (SQLite)
                             │
                        materialize            pure function: (schema, claims) → graph;
                             │                 same input, byte-identical output
                             ▼
                        typed graph            entities + links, every value with provenance
                             │
                     ┌───────┴───────┐
                embed│               │serve-mcp
                     ▼               ▼
               vector index     read-only MCP tools for agents
               (sqlite-vec)     (find searches the index)
```

Five primitives: **Claim** (an atomic assertion by a source), **Schema** (your ontology, loaded
from YAML — the engine hardcodes no domain type), **Entity** and **Link** (the materialized
graph), **Provenance** (who said it, based on what, when). Identity is content-derived
throughout: the same natural keys land on the same entity no matter which source asserted them,
and re-materialization can rebuild the whole graph from the log at any time.

This release adds the semantic layer: a vector index over the materialized entities
(sqlite-vec, in the same workspace file), pluggable embedding providers behind a small port
(a local sentence-transformers model or the Gemini API), the `find` tool, and query v2 —
link predicates that filter on the linked entities' properties. The claim log, canonical
identity and materialization are untouched; earlier workspaces open as-is.

## Development

Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).

```
uv sync                  # install dependencies
uv run pytest            # run tests
uv run ruff check .      # lint
uv run ruff format .     # format
uv run mypy .            # strict type check
uv run mimir --help      # CLI
```

## License

AGPL-3.0-only. See `LICENSE`.
