Python SDK Reference¶
The uniko Python package is a thin, typed binding over the same Rust engine the
facade exposes. You program against the same verbs — observe, recall,
answer, forget — from one owning Uniko handle, with native asyncio support and a blocking
twin for every call. Ships py.typed and a hand-written stub, so your editor and type checker see
the full surface.
import uniko
uni = await uniko.Uniko.in_memory()
agent = uni.agent("assistant")
session = agent.session("conversation-1")
await session.observe(uniko.Turn("alice", "I love hiking in the mountains."))
bundle = await agent.recall("hiking")
for item in bundle.items:
print(item.score, item.content)
Install
Requires Python ≥ 3.10. From the binding crate, maturin develop builds the native extension
(uniko._uniko). A C/C++ toolchain and protoc are needed at build time; ONNX is statically
linked. See Installation.
The async / sync contract¶
Every facade verb ships two skins:
- the bare name (
recall,observe,answer, …) returns an awaitable youawaitinside an event loop; - the
*_synctwin (recall_sync,observe_sync, …) blocks the calling thread on the shared tokio runtime and returns the resolved value directly.*_syncreleases the GIL across the Rust work, so other Python threads keep running.
Construction helpers and builder setters are synchronous — there is no _sync twin for
agent(), session(), Turn(...), Scope().sessions(...), or any UnikoBuilder setter. Only the
async I/O verbs (and terminal build) get the twin. Throughout this page, tables mark which methods
have a _sync twin in the Sync column; assume the async signature shown and append _sync to
the name for the blocking form.
__version__
uniko.__version__ is "0.1.0" during the v1 line.
Uniko — the instance¶
Uniko owns the store, the model runtime, and the ingest pipeline. Build one, mint agents from it,
and shutdown when done. repr(uni) is Uniko(open) or Uniko(shut down).
| Method | Signature | Sync | Description |
|---|---|---|---|
open |
@staticmethod open(path: str) -> Awaitable[Uniko] |
open_sync |
Persistent, zero-config defaults. |
in_memory |
@staticmethod in_memory() -> Awaitable[Uniko] |
in_memory_sync |
Ephemeral, zero-config defaults. |
builder |
@staticmethod builder() -> UnikoBuilder |
— (sync) | Configure capabilities, then build(). |
agent |
agent(agent_id: str) -> Agent |
— (sync) | Mint an agent identity (cheap). |
config |
config() -> dict[str, Any] |
— (sync) | The resolved configuration as a dict. |
purge |
purge() -> Awaitable[DeletionReport] |
purge_sync |
Erase the whole graph (dev/test reset). |
shutdown |
shutdown() -> Awaitable[None] |
shutdown_sync |
Drain workers and close. Consumes the handle. |
config() returns a plain dict with keys: embedding_model_id, embedding_dimensions,
recall_limit, recall_token_budget, recall_min_score.
shutdown needs sole ownership
shutdown consumes the handle and requires that all derived Agent/Session objects are
dropped first (a store ref-count guard), or it raises. Drop them explicitly, then shut down:
Any subsequent use of the Uniko handle after shutdown raises ConfigError.
UnikoBuilder¶
Chainable, synchronous setters; build is the terminal async step. Returns self from every setter
so you can fluently compose, and re-using a builder after build() raises
RuntimeError("UnikoBuilder already consumed by build()").
| Method | Signature | Sync | Description |
|---|---|---|---|
path |
path(path: str) -> UnikoBuilder |
— (sync) | File-backed store at path. |
in_memory |
in_memory() -> UnikoBuilder |
— (sync) | Ephemeral store (default). |
llm |
llm(spec: LlmSpec) -> UnikoBuilder |
— (sync) | Generation model — required for answer(). |
streaming |
streaming(on: bool) -> UnikoBuilder |
— (sync) | Enables the fire-and-forget submit/flush lane. |
scope_to_agent |
scope_to_agent() -> UnikoBuilder |
— (sync) | Reads default to the agent's visibility. |
build |
build() -> Awaitable[Uniko] |
build_sync |
Terminal: produce the Uniko instance. |
LlmSpec¶
Describes the generation model that powers answer() / answer_in(). All constructors are static
and synchronous.
| Constructor | Signature | Notes |
|---|---|---|
openai |
@staticmethod openai(alias: str, model_id: str, base_url: str \| None = None) |
Key read from OPENAI_API_KEY. |
openai_with_key_env |
@staticmethod openai_with_key_env(alias: str, model_id: str, key_env: str, base_url: str \| None = None) |
Key read from the named env var. |
mistralrs |
@staticmethod mistralrs(alias: str, model_id: str) |
Local model, in-situ Q4K quantization. |
spec = uniko.LlmSpec.openai("answerer", "gpt-4o-mini")
uni = await uniko.Uniko.builder().llm(spec).build()
Agent¶
An Agent binds an identity to the store. It is Arc-backed and clonable, so passing it around is
cheap. Read through the agent (recall / answer / query), retrieve by id through agent.data,
and plan through agent.goals.
agent = uni.agent("assistant")
bundle = await agent.recall("what does the user prefer?")
answer = await agent.answer("when is the deadline?") # needs .llm(...)
Properties¶
| Property | Type | Description |
|---|---|---|
agent_id |
str |
The agent's identity. |
data |
Data |
By-id retrieval surface. |
goals |
Goals |
Goals & tasks surface. |
Recall, answer, query¶
| Method | Signature | Sync | Description |
|---|---|---|---|
recall |
recall(query: str) -> Awaitable[ContextBundle] |
recall_sync |
Ranked, prompt-ready context. |
recall_in |
recall_in(query: str, scope: Scope) -> Awaitable[ContextBundle] |
recall_in_sync |
Recall confined to a Scope. |
answer |
answer(question: str) -> Awaitable[Answer] |
answer_sync |
Recall + the configured LLM. |
answer_in |
answer_in(question: str, scope: Scope) -> Awaitable[Answer] |
answer_in_sync |
Scoped answer. |
query |
query(cypher: str) -> Awaitable[list[dict[str, Any]]] |
query_sync |
Read-only Cypher; mutations rejected. |
query_in |
query_in(cypher: str, scope: Scope) -> Awaitable[list[dict[str, Any]]] |
query_in_sync |
Binds the scope allow-set as $allow. |
query_in is a raw escape hatch
query_in binds the scope's allow-set as the $allow parameter for you to reference in your
Cypher, then returns the raw rows with no visibility filtering applied. You enforce the
filter in your query.
answer() requires an LLM
Calling answer() / answer_in() on an instance built without .llm(...) raises ConfigError
(e.kind == "config").
Logic & reasoning¶
| Method | Signature | Sync | Description |
|---|---|---|---|
define_rule |
define_rule(name: str, source: str) -> Awaitable[int] |
define_rule_sync |
Register a Locy rule; returns its node id. |
run_rule |
run_rule(name: str, return_cols: list[str], params: dict \| None = None) -> Awaitable[list[dict]] |
run_rule_sync |
Evaluate a named rule. |
abduce |
abduce(program: str, params: dict \| None = None) -> Awaitable[AbductionResult] |
abduce_sync |
Run an abductive program. |
assume |
assume(assume_block: str) -> AssumeBuilder |
— (sync) | Open a hypothetical-mutation builder. |
assume is synchronous and returns an AssumeBuilder; the awaitable lives on the
builder's terminal run/run_sync. See Reasoning with Locy.
Sessions & lifecycle¶
| Method | Signature | Sync | Description |
|---|---|---|---|
session |
session(session_id: str) -> Session |
— (sync) | Open a conversation thread. |
delete_session |
delete_session(session_id: str) -> Awaitable[DeletionReport] |
delete_session_sync |
Hard-delete a session and cascade. |
forget_participant |
forget_participant(participant_id: str) -> Awaitable[DeletionReport] |
forget_participant_sync |
Forget a participant. |
Session¶
A Session is a conversation thread you write through. observe takes a mutable, serialized
borrow — feed turns in order, one at a time. Each observe is durable and committed before it
returns, so reads see the write immediately.
session = agent.session("conversation-1")
result = await session.observe(uniko.Turn("alice", "I love hiking in the mountains."))
assert result.message_node_id > 0
Properties¶
| Property | Type |
|---|---|
session_id |
str |
Write & ingest¶
| Method | Signature | Sync | Description |
|---|---|---|---|
observe |
observe(turn: Turn) -> Awaitable[ObserveResult] |
observe_sync |
Durable, committed-before-return turn; read-after-write. |
ingest |
ingest(source: IngestSource) -> Awaitable[IngestOutcome] |
ingest_sync |
Standalone doc/blob, not a turn. |
submit |
submit(turn: Turn) -> Awaitable[None] |
submit_sync |
Fire-and-forget streaming turn. |
submit_source |
submit_source(source: IngestSource) -> Awaitable[None] |
submit_source_sync |
Fire-and-forget streaming doc/blob. |
flush |
flush() -> Awaitable[None] |
flush_sync |
Wait for the streaming pipeline to drain. |
summarize |
summarize() -> Awaitable[int \| None] |
summarize_sync |
New summary node id, or None if nothing new. |
Streaming lane requires streaming(True)
submit / submit_source are fire-and-forget — they return immediately and process in the
background. They require the instance was built with .streaming(True). Use flush() to wait
for the backlog to clear.
Forget & delete¶
| Method | Signature | Sync | Description |
|---|---|---|---|
forget_turn |
forget_turn(message_id: str) -> Awaitable[DeletionReport] |
forget_turn_sync |
Soft-forget: redact, keep the chain intact. |
delete_turn |
delete_turn(message_id: str) -> Awaitable[DeletionReport] |
delete_turn_sync |
Hard-delete a turn + cascade. |
delete_document |
delete_document(artifact_id: str) -> Awaitable[DeletionReport] |
delete_document_sync |
Hard-delete an artifact + cascade. |
Data¶
By-id retrieval, reached via agent.data. An unknown id returns None — these methods never raise
for a missing id.
| Method | Signature | Sync | Description |
|---|---|---|---|
message |
message(message_id: str) -> Awaitable[MessageView \| None] |
message_sync |
Fetch a stored message. |
artifact |
artifact(artifact_id: str) -> Awaitable[ArtifactView \| None] |
artifact_sync |
Fetch an artifact's metadata + text. |
artifact_bytes |
artifact_bytes(artifact_id: str) -> Awaitable[bytes \| None] |
artifact_bytes_sync |
Fetch the raw binary payload. |
Binary payloads: use artifact_bytes
Raw bytes cannot be read back through a Cypher RETURN. artifact_bytes is the supported path
for binary payloads — reach for it instead of query/query_in when you need the bytes.
Goals¶
Goals & tasks, reached via agent.goals. Reads are scoped to the goals the agent owns and the tasks
assigned to it. Transition methods return bool — False when the id does not resolve. Creation
methods return the new node id (int).
The Participant must exist first
Create the agent's Participant by sending at least one turn before creating goals:
await agent.session("setup").observe(uniko.Turn("assistant", "kicking off"))
goals = agent.goals
gid = await goals.create("Ship the Python SDK", goal_id="g1",
description="PyO3 bindings", metrics={"target_phase": 3})
await goals.start("g1")
tid = await goals.create_task("write tests", goal_id="g1", task_id="t1", priority=0.8)
ctx = await goals.context("g1")
await goals.complete("g1", {"done": True})
Reads¶
Each read has a _sync twin (all_sync, get_sync, …).
| Method | Signature | Returns |
|---|---|---|
all |
all() |
list[GoalView] |
in_phase |
in_phase(phase: str) |
list[GoalView] |
active |
active() |
list[GoalView] |
planned |
planned() |
list[GoalView] |
completed |
completed() |
list[GoalView] |
get |
get(goal_id: str) |
GoalView \| None |
tasks |
tasks() |
list[TaskView] |
tasks_in |
tasks_in(phase: str) |
list[TaskView] |
tasks_of |
tasks_of(goal_id: str) |
list[TaskView] |
context |
context(goal_id: str) |
GoalContext \| None |
Transitions¶
Each returns Awaitable[bool] (False when the id doesn't resolve) and has a _sync twin.
| Method | Signature |
|---|---|
start |
start(goal_id: str) |
abandon |
abandon(goal_id: str) |
set_status |
set_status(goal_id: str, status: str) |
complete |
complete(goal_id: str, result: Any \| None = None) |
start_task |
start_task(task_id: str) |
block_task |
block_task(task_id: str) |
complete_task |
complete_task(task_id: str) |
set_task_status |
set_task_status(task_id: str, status: str) |
Creation¶
Both return Awaitable[int] (the new node id) and have a _sync twin. All arguments after title
are keyword-only.
create(title, *, goal_id=None, description=None, status=None, metrics=None,
guardrails=None, deadline: datetime | None = None, parent_goal_id=None)
create_task(title, *, task_id=None, description=None, status=None,
priority: float | None = None, goal_id=None,
depends_on_task_id=None, subtask_of_task_id=None)
Valid phase strings
Phases are plain lowercase strings, not enums.
- Goal phases:
planned,active,completed,abandoned - Task phases:
planned,active,completed,blocked
Passing an invalid phase to in_phase / tasks_in raises ValueError. To leave a blocked
task, use start_task, complete_task, or set_task_status.
AssumeBuilder¶
Returned by Agent.assume. Lets you run a hypothetical mutation plus a query against it; the
mutation rolls back and never touches the real graph.
| Method | Signature | Description |
|---|---|---|
then_query |
then_query(query: str) -> AssumeBuilder |
Cypher to run against the hypothetical state. |
param |
param(key: str, value: Any) -> AssumeBuilder |
Bind a query parameter. |
run |
run() -> Awaitable[list[dict]] |
Execute; returns rows. Has run_sync. |
rows = await (agent.assume(
"ASSUME { CREATE (:Fact {fact_id: 'srv-port', subject: 'srv', predicate: 'port', object: '9090'}) }")
.then_query("MATCH (f:Fact {subject: 'srv'}) RETURN f")
.run())
assert len(rows) == 1
after = await agent.query("MATCH (f:Fact {subject: 'srv'}) RETURN f")
assert after == [] # hypothetical mutation rolled back
Input & value-object builders¶
All input builders are synchronous constructors with chainable setters.
Turn¶
A Turn is reusable — feeding it to observe/submit does not consume it.
| Setter | Signature | Default / Notes |
|---|---|---|
.id |
.id(message_id: str) |
Auto-generated if unset. |
.content_type |
.content_type(content_type: str) |
"text" |
.at |
.at(timestamp: datetime) |
now |
.addressed_to |
.addressed_to(recipients: list[str]) |
— |
.metadata |
.metadata(key: str, value: Any) |
Add one metadata entry. |
.attach |
.attach(source: IngestSource) |
Attach one source. |
.attachments |
.attachments(sources: list[IngestSource]) |
Attach many. |
turn = (uniko.Turn("alice", "see the attached report")
.addressed_to(["bob"])
.metadata("channel", "email")
.attach(uniko.IngestSource.from_path("report.pdf")))
IngestSource¶
Factories build a source; chainable setters refine it.
| Member | Signature | Notes |
|---|---|---|
from_text |
@staticmethod from_text(content: str) |
Text payload. |
from_bytes |
@staticmethod from_bytes(data: bytes) |
Bytes payload (one arg — set MIME via .with_mime). |
from_path |
@staticmethod from_path(path: str) |
Load from a file path. |
.with_mime |
.with_mime(mime: str) |
Skip sniffing; invalid MIME raises ValueError. |
.with_id |
.with_id(id: str) |
Set the artifact id. |
.with_path |
.with_path(path: str) |
Set the logical path. |
Scope¶
Confines reads to a subset of the graph. Chainable.
| Setter | Signature | Notes |
|---|---|---|
.sessions |
.sessions(sessions: list[str]) |
Restrict to these sessions. |
.participants |
.participants(participants: list[str]) |
Restrict to these participants. |
.since |
.since(since: datetime) |
Inclusive lower bound. |
.until |
.until(until: datetime) |
Exclusive upper bound. |
scope = uniko.Scope().sessions(["s1"]).since(cutoff)
bundle = await agent.recall_in("budget", scope)
Output value types¶
All output types are frozen — read-only getters, no setters — and implement __repr__.
RecallSource¶
The provenance of a recalled item.
| Field | Type |
|---|---|
kind |
str |
message_id |
str \| None |
artifact_id |
str \| None |
chunk_id |
str \| None |
RecallItem¶
One ranked piece of context.
| Field | Type |
|---|---|
node_id |
int |
kind |
str |
score |
float |
content |
str |
sources |
list[RecallSource] |
ContextBundle¶
The result of recall / recall_in. Implements __len__.
| Field | Type | Description |
|---|---|---|
items |
list[RecallItem] |
Ranked context items. |
total_tokens |
int |
Token count of the bundle. |
phase1_only |
bool |
Only phase-1 retrieval ran. |
phase2_only |
bool |
Only phase-2 retrieval ran. |
coverage |
float |
Retrieval coverage estimate. |
Answer¶
The result of answer / answer_in.
| Field | Type |
|---|---|
text |
str |
model |
str \| None |
input_tokens |
int \| None |
output_tokens |
int \| None |
recorded_episode |
int \| None |
context |
ContextBundle |
| Method | Signature | Returns |
|---|---|---|
citations |
citations() |
list[RecallSource] |
ObserveResult¶
The result of observe.
| Field | Type |
|---|---|
message_node_id |
int |
chunk_node_ids |
list[int] |
session_node_id |
int |
sender_node_id |
int \| None |
sender_id |
str \| None |
extracted_entities |
list[tuple[int, str]] |
extracted_observations |
list[int] |
attachment_count |
int |
DeletionReport¶
Returned by every delete/forget/purge verb.
| Field | Type |
|---|---|
nodes_deleted |
int |
edges_deleted |
int |
facts_invalidated |
int |
chains_repaired |
int |
nodes_redacted |
int |
root_existed |
bool |
MessageView¶
Returned by Data.message.
| Field | Type |
|---|---|
message_id |
str |
sender_id |
str |
content |
str |
timestamp |
datetime (tz-aware) |
session_id |
str |
addressed_to |
list[str] |
attachments |
list[str] |
ArtifactView¶
Returned by Data.artifact.
| Field | Type |
|---|---|
artifact_id |
str |
kind |
str |
mime |
str \| None |
path |
str \| None |
text |
str |
attached_to_message |
str \| None |
GoalView¶
| Field | Type |
|---|---|
goal_id |
str |
title |
str |
description |
str \| None |
status |
str |
phase |
str |
created_at |
datetime \| None |
deadline |
datetime \| None |
completed_at |
datetime \| None |
metrics |
Any \| None |
TaskView¶
| Field | Type |
|---|---|
task_id |
str |
title |
str |
description |
str \| None |
status |
str |
phase |
str |
priority |
float \| None |
goal_id |
str \| None |
created_at |
datetime \| None |
completed_at |
datetime \| None |
GoalContext¶
Returned by Goals.context.
| Field | Type |
|---|---|
goal |
GoalView |
tasks |
list[TaskView] |
sessions |
list[str] |
recent_messages |
list[str] |
facts |
list[str] |
entities |
list[str] |
IngestOutcome¶
Returned by Session.ingest.
| Field | Type |
|---|---|
kind |
str |
artifact_id |
str |
artifact_node_id |
int |
chunk_node_ids |
list[int] |
was_deduplicated |
bool |
page_count |
int \| None |
extraction_failed |
bool \| None |
AbductionResult¶
Returned by Agent.abduce. Implements __len__.
| Field | Type |
|---|---|
modifications |
list[dict[str, Any]] |
Each modification dict has keys modification, validated: bool, and cost: float.
result = await agent.abduce("ABDUCE reachable WHERE a.kind = 'query'")
for mod in result.modifications:
print(mod["modification"], mod["validated"], mod["cost"])
Value ↔ Python type mapping¶
Cypher rows (query, query_in, run_rule, AssumeBuilder.run) come back as plain
list[dict[str, Any]]. Engine values map to native Python types as follows.
Engine Value |
Python type |
|---|---|
| Null | None |
| Bool | bool |
| Int | int |
| Float | float |
| String | str |
| Bytes | bytes — not readable via Cypher RETURN; use Data.artifact_bytes |
| List | list |
| Map / record | dict |
| DateTime | datetime (tz-aware) |
Exceptions¶
The base is UnikoError(Exception). Every instance carries a .kind: str attribute, so you can
branch on the kind even when the concrete class is the base.
try:
await agent.observe(turn)
except uniko.ConflictError:
... # transient — retry
except uniko.UnikoError as e:
if e.kind == "storage":
... # a storage-domain error folded into the base
Dedicated subclasses¶
Each is an issubclass of UnikoError and carries a fixed .kind.
| Class | .kind |
When |
|---|---|---|
ConfigError |
"config" |
answer() without an LLM; using a handle after shutdown; bad configuration. |
LlmError |
"llm" |
The generation model failed. |
TimeoutError |
"timeout" |
An operation timed out. |
ConflictError |
"conflict" |
Transient SSI / antidependency abort — retriable. |
UnsupportedError |
"unsupported" |
No extractor for the modality. |
uniko.TimeoutError
uniko.TimeoutError is a UnikoError subclass, distinct from the built-in
builtins.TimeoutError. Catch uniko.TimeoutError (or the base) for engine timeouts.
Storage-domain errors¶
These fold into the base UnikoError (they are not dedicated subclasses) but still carry a
distinct .kind — branch on it:
"storage" · "search" · "schema" · "pipeline" · "locy" · "embedding" · "internal"
with pytest.raises(uniko.ConfigError) as info:
await agent.answer("anything") # no LLM configured
assert info.value.kind == "config"
Next steps¶
Quick Start¶
The 15-minute taste — install, observe a turn, recall it back.
Working with the Facade¶
The full mental model behind these verbs, in Rust and Python.
Recall & Retrieval¶
How recall ranks context, and how Scope narrows it.
Reasoning with Locy¶
Rules, assume, and abduce — the logic surface in depth.
Configuration¶
Embedders, models, recall budgets, and the defaults behind config().
Data Model¶
The nodes and edges your turns and goals become.