Skip to content

Python SDK

The same engine, from Python — no server, no IPC

The uniko Python package is a PyO3 binding over the exact same Rust engine that powers the core library. It links into your Python process like SQLite: one in-process handle, no sidecar service, no network hop, no serialization tax on the hot path. You get $0-cost local ingest, graph-native reasoning, and provenance-backed recall — all from import uniko.

The SDK is async-first. Every facade verb returns an asyncio awaitable and runs the real Rust work on a shared tokio runtime. When you need to call from synchronous code, every verb also ships a *_sync twin that blocks the calling thread and releases the GIL across the Rust work — so a blocking call never starves the rest of your interpreter.


When to reach for the Python SDK

Reach for it when your agent lives in Python and you want cognitive memory without standing up infrastructure:

  • You're building on a Python agent framework and want recall, facts, goals, and reasoning in the same process — not behind an HTTP boundary.
  • You want the in-process, embedded model (no Neo4j, no Qdrant, no Postgres) with a Pythonic async surface.
  • You need provenance: every recalled item traces back to the message that grounds it, and you want those handles in Python.

If you're writing Rust, use the core library directly — the Python SDK is a thin skin over the same types.

The Python SDK does not re-explain the memory model

Entities, Facts, Procedures, Topics, and Episodes — and how they derive from Messages with full provenance — are the same here as in the core engine. This page documents the Python surface; for the model itself, read the Rust Concepts and Pipelines pages. They apply verbatim.


Package and module layout

The distribution is named uniko (import uniko). The native extension is an abi3 cdylib at uniko._uniko; the package re-exports its public surface, so you import everything straight from the top-level package:

import uniko

uni = await uniko.Uniko.in_memory()
agent = uni.agent("assistant")
  • uniko — the package you import. Re-exports the full public API (30 names).
  • uniko._uniko — the compiled native extension. You don't import this directly.
  • Ships py.typed and a hand-written type stub (_uniko.pyi), so editors and type checkers see the full signature surface, including the *_sync twins. Requires Python ≥ 3.10.

The public surface is one Uniko handle from which you mint Agents, open Sessions, observe Turns, and read memory back through Data and Goals. Builders (Turn, IngestSource, Scope, LlmSpec, UnikoBuilder, AssumeBuilder) construct inputs; frozen view types (ContextBundle, Answer, MessageView, GoalView, …) carry results back.


Alpha status — read this first

The Python SDK is in active development. Here's the honest state of the world:

Shipped now, and what isn't

Shipped: the full async facade, the synchronous *_sync twins (GIL released across Rust work), py.typed + a complete type stub, and the structured exception hierarchy.

Not yet: prebuilt wheels. You build from source with maturin (a C/C++ toolchain and protoc are required; ONNX is statically linked). The native module does not set __version__ — the package reports the literal "0.1.0".

To build and install from the binding crate:

# from bindings/uniko-py/
maturin develop

A handful of design-doc surfaces did not ship in this alpha — notably a UnikoConfig object (config() returns a plain dict), Python-authored extractors, and visibility scoping on Scope. The API reference documents only what actually ships.


A minimal example

The core loop is three calls: open an instance, observe a turn, recall a context bundle. Pick your skin — the async surface and the synchronous *_sync twins are line-for-line equivalent.

import asyncio
import uniko


async def main() -> None:
    uni = await uniko.Uniko.in_memory()
    agent = uni.agent("assistant")
    session = agent.session("conversation-1")

    # Observe a message. Local ONNX extraction runs, then one atomic
    # transaction commits the Message, Entities, Observations, and edges
    # — durable and read-after-write before this returns.
    result = await session.observe(
        uniko.Turn("alice", "I love hiking in the mountains.")
    )
    assert result.message_node_id > 0

    # Recall compiled knowledge. No LLM in this path; each item carries
    # its kind, score, and the sources it traces back to.
    bundle = await agent.recall("hiking")
    for item in bundle.items:
        print(f"[{item.kind}] {item.content}")


asyncio.run(main())
import uniko

uni = uniko.Uniko.in_memory_sync()
agent = uni.agent("assistant")
session = agent.session("conversation-1")

result = session.observe_sync(
    uniko.Turn("alice", "I love hiking in the mountains.")
)
assert result.message_node_id > 0

bundle = agent.recall_sync("hiking")
for item in bundle.items:
    print(f"[{item.kind}] {item.content}")

# Shut down cleanly: drop derived handles first, then shut down the store.
del session, agent
uni.shutdown_sync()

Construction helpers stay synchronous

Only the verbs that do real engine work have _sync twins. agent(...), session(...), builder setters, and the value-object constructors (Turn, Scope, IngestSource, LlmSpec) are plain synchronous calls in both skins.


Errors are structured

Every failure raises a uniko.UnikoError (or a subclass), and every instance carries a .kind string you can branch on. Subclasses cover the cases you'll catch by type — ConfigError, LlmError, TimeoutError, ConflictError (a transient, retriable SSI abort), and UnsupportedError. Storage-domain failures fold into the base UnikoError with distinct kinds ("storage", "search", "schema", "pipeline", "locy", "embedding", "internal"), so branch on e.kind for those.

try:
    answer = await agent.answer("Where does Alice like to hike?")
except uniko.ConfigError as e:
    # No LLM configured — answer()/answer_in() need an LlmSpec on the builder.
    print(f"configure an LLM first ({e.kind})")

What you can do

Observe and recall

Feed turns with session.observe(...), pull a ContextBundle with agent.recall(...), and get grounded answers from agent.answer(...) once an LLM is configured. Every recalled item carries its sources.

Goals and tasks

agent.goals is a full lifecycle surface — create goals and tasks, drive them through planned → active → completed, and pull a GoalContext that gathers the goal, its tasks, sessions, facts, and entities.

Reasoning in the database

Define and run Locy rules, run abduce(...) for hypothesis generation, and use agent.assume(...) to test hypothetical mutations that roll back — the real graph is never touched.

By-id retrieval and Cypher

agent.data resolves messages, artifacts, and raw binary payloads by id (unknown id → None, never raises). agent.query(...) runs read-only Cypher and rejects mutations.


Next steps

Quickstart

Build the SDK, open an instance, and run the observe → recall loop end to end.

Python API reference

Every handle, builder, and view type — with the exact async and *_sync signatures.

Concepts

The five kinds of memory, the data model, and how facts carry bitemporal provenance.

Pipelines

What actually happens inside observe and recall — the ingest, consolidation, and recall paths.