mnemo-engine 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,365 @@
1
+ Metadata-Version: 2.4
2
+ Name: mnemo-engine
3
+ Version: 0.4.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: License :: OSI Approved :: Apache Software License
7
+ Summary: Encrypted, portable agent-memory engine — Python bindings
8
+ Home-Page: https://toarchkumar.github.io/mnemo/
9
+ License: Apache-2.0
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
+ Project-URL: Homepage, https://github.com/toarchkumar/mnemo
13
+ Project-URL: Repository, https://github.com/toarchkumar/mnemo
14
+
15
+ # Memory Nemo (MNemo) — Python bindings
16
+
17
+ Repository overview: [root README](../README.md) ·
18
+ landing page: [index.html](../index.html).
19
+
20
+ Python bindings for **Memory Nemo (MNemo)**, the encrypted single-file
21
+ agent-memory engine. The package is a thin [PyO3](https://pyo3.rs) wrapper
22
+ over the Rust core in the
23
+ sibling `mnemo/` crate — the storage engine, AES-256-GCM encryption, the
24
+ write-ahead log, the IVF+PQ index, snapshots, and the agent-memory model all
25
+ run as compiled Rust; Python only sees a small, ergonomic surface.
26
+
27
+ > Distribution name on PyPI: `mnemo-engine` (both `mnemo` and `mnemo-db`
28
+ > were already taken by unrelated projects). The import name stays
29
+ > `mnemo`, so `pip install mnemo-engine` then `import mnemo` is the
30
+ > full setup.
31
+
32
+ ## For AI agents
33
+
34
+ An agent that's been handed a `.mnemo` file and its passphrase can become
35
+ productive in two calls — no external documentation required:
36
+
37
+ ```python
38
+ import mnemo, os
39
+
40
+ db = mnemo.open("agent.mnemo", os.environ["MNEMO_PASSPHRASE"])
41
+
42
+ # The file introduces itself: returns memories tagged metadata.area="onboarding",
43
+ # manifest first. Each entry tells you the embedder, agent_id convention,
44
+ # project metadata, and any other context the file's author recorded.
45
+ for entry in db.about():
46
+ print(entry["content"])
47
+ ```
48
+
49
+ Creating a new database? It's self-describing from creation:
50
+
51
+ ```python
52
+ db = mnemo.open("new.mnemo", "passphrase", dimensions=384)
53
+ db.insert_default_manifest() # same scaffold that `mnemo init` adds
54
+ db.flush()
55
+ ```
56
+
57
+ The scaffold tells the next agent what to do: replace it with one that
58
+ records your real embedder and conventions. See the [main README](../mnemo/README.md#self-describing-databases)
59
+ for the full pattern.
60
+
61
+ ## Build & install
62
+
63
+ The bindings build with [maturin](https://www.maturin.rs):
64
+
65
+ ```bash
66
+ pip install maturin
67
+ cd mnemo-python
68
+ maturin build --release # produces a wheel in target/wheels/
69
+ pip install target/wheels/mnemo-*.whl
70
+ ```
71
+
72
+ `maturin develop` installs straight into the active virtualenv during
73
+ development. The extension is built against the stable ABI (`abi3-py38`), so a
74
+ single wheel works on CPython 3.8 and newer.
75
+
76
+ ## Usage
77
+
78
+ ```python
79
+ import mnemo
80
+
81
+ # Open an existing database, or create one (dimensions required to create).
82
+ db = mnemo.open("agent.mnemo", "passphrase", dimensions=4)
83
+
84
+ # Store typed memories. memory_type is one of:
85
+ # "episodic", "semantic", "procedural", "working".
86
+ db.remember(
87
+ "the user prefers concise answers",
88
+ "procedural",
89
+ [0.1, 0.2, 0.3, 0.4],
90
+ importance=0.8,
91
+ agent_id="assistant",
92
+ metadata={"source": "onboarding"},
93
+ )
94
+
95
+ # Multi-signal recall — similarity blended with recency, importance, frequency.
96
+ for hit in db.recall([0.1, 0.2, 0.3, 0.4], top_k=5):
97
+ print(hit["score"], hit["content"])
98
+
99
+ db.flush()
100
+ db.close()
101
+ ```
102
+
103
+ `mnemo.open` returns a `Mnemo` object that is also a context manager —
104
+ `with mnemo.open(...) as db:` flushes automatically on exit.
105
+
106
+ ### Sessions
107
+
108
+ A `Session` wraps the database for one conversation: it records each turn as
109
+ `working` memory and, when closed, consolidates those turns into durable
110
+ `episodic` memory.
111
+
112
+ ```python
113
+ db = mnemo.open("agent.mnemo", "passphrase", dimensions=4)
114
+
115
+ with db.session("assistant") as chat:
116
+ chat.add_turn(mnemo.Turn.user("my flight is Friday", [1.0, 0.0, 0.0, 0.0]))
117
+ chat.add_turn(mnemo.Turn.assistant("noted", [0.9, 0.1, 0.0, 0.0]))
118
+ context = chat.recall([1.0, 0.0, 0.0, 0.0], top_k=5)
119
+ # leaving the block consolidates the turns into episodic memory
120
+
121
+ # or, explicitly:
122
+ chat = db.session("assistant")
123
+ chat.add_turn(mnemo.Turn("system", "be concise", [0.0, 0.0, 0.0, 1.0]))
124
+ chat.close() # consolidate working -> episodic
125
+ # chat.discard() # alternative: throw the turns away
126
+ ```
127
+
128
+ `mnemo.Turn` has `Turn.user(...)`, `Turn.assistant(...)`, `Turn.system(...)`,
129
+ and `Turn(role, content, vector)`. A `Session`'s `recall` is always scoped to
130
+ its own agent.
131
+
132
+ ## API
133
+
134
+ `mnemo.open(path, passphrase, dimensions=None) -> Mnemo`
135
+
136
+ `Mnemo` methods:
137
+
138
+ | Method | Purpose |
139
+ |---|---|
140
+ | `remember(content, memory_type, vector, *, agent_id, importance, session_id, ttl_secs, shared, metadata)` | Store a memory; returns its id |
141
+ | `recall(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Multi-signal ranked retrieval. `track_access=False` skips access-stat updates (fully read-only recall) |
142
+ | `search(query, top_k=10)` | Exact nearest-neighbour search |
143
+ | `get(id)` / `delete(id)` | Fetch / soft-delete by id |
144
+ | `about()` | Self-describing onboarding briefing — memories tagged `metadata.area="onboarding"`, manifest first |
145
+ | `insert_default_manifest()` | Insert the canonical scaffold manifest (same one `mnemo init` adds); returns its id |
146
+ | `session(agent_id)` | Begin a conversation `Session` |
147
+ | `flush()` / `close()` | Persist pending changes |
148
+ | `verify()` | Decrypt and re-validate every record |
149
+ | `build_index()` / `drop_index()` / `has_index()` | Approximate index control |
150
+ | `snapshots()` / `restore_to(txn_id)` / `restore_to_time(unix_secs)` | Point-in-time recovery |
151
+ | `set_cache_capacity(pages)` / `page_cache_stats()` | Page-cache tuning (renamed from `cache_stats` in v0.4.0 — that name now belongs to the result cache below) |
152
+ | `cache_put(namespace, key, value, content_type="text", ttl_secs=None)` | Exact-key result cache put (Phase 10.1). `value` accepts `str` or `bytes` |
153
+ | `cache_get(namespace, key)` | Exact-key cache get — returns `dict` on hit (`value` is `bytes`) or `None` |
154
+ | `cache_delete(namespace, key)` / `cache_purge(namespace=None, expired_only=False)` | Tombstone by key or in bulk |
155
+ | `cache_stats(namespace=None)` | Result-cache stats: `{entries, bytes, hits, misses, hit_rate, evictions}` |
156
+ | `cache_put_semantic(namespace, key, vector, value, model, content_type="text", ttl_secs=None)` | Semantic cache put (Phase 10.2) — vector must match db dimensions |
157
+ | `cache_get_semantic(namespace, query, model, threshold=0.97)` | Top-1 cosine over the namespace's vectored entries whose `model` matches |
158
+ | `set_max_snapshots(max)` | Override the snapshot-manifest retention cap (default 256; `0` disables) |
159
+ | `stats()` | Summary statistics |
160
+ | `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
161
+ | `len(db)` | Live memory count |
162
+
163
+ `Session` methods: `add_turn(turn)`, `recall(query, top_k=10, memory_types=None)`,
164
+ `close()`, `discard()`, `id()`, `agent()`, `turn_ids()`, `turn_count()`; also a
165
+ context manager (exiting consolidates).
166
+
167
+ Memories and results are returned as plain dicts; `metadata` round-trips as a
168
+ nested dict.
169
+
170
+ ## Result caching
171
+
172
+ MNemo doubles as a durable result cache — memoize LLM tool-call outputs,
173
+ prompt→completion pairs, HTTP responses, anything reconstructible on miss —
174
+ in the same encrypted single file that holds your agent memory. See the
175
+ [core README's Result caching section](../mnemo/README.md) for the
176
+ concepts (namespaces, budgets, TTL, Strict vs Batched flush policy).
177
+
178
+ ### Recipe: `@db.cached(...)` decorator (~90 lines, pure Python)
179
+
180
+ Copy this into your project as `mnemo_cached.py` or paste inline. It
181
+ wraps any function whose arguments serialize to JSON, keying the cache
182
+ on `f"{namespace}:{fn_name}:{json_args}"`. When `embed` is supplied it
183
+ switches to semantic mode; otherwise it's an exact-key cache.
184
+
185
+ ```python
186
+ """@db.cached — pure-Python helper on top of mnemo.Mnemo's cache API.
187
+
188
+ Usage:
189
+ import mnemo, json, os
190
+ from mnemo_cached import cached
191
+
192
+ db = mnemo.open("agent.mnemo", os.environ["MNEMO_PASSPHRASE"])
193
+
194
+ @cached(db, namespace="llm", ttl=3600)
195
+ def call_llm(prompt: str) -> str:
196
+ return openai.chat.completions.create(...).choices[0].message.content
197
+
198
+ # Second call with the same prompt is a cache hit.
199
+ answer = call_llm("summarize this doc")
200
+ """
201
+ from __future__ import annotations
202
+
203
+ import functools
204
+ import hashlib
205
+ import json
206
+ from typing import Any, Callable, Optional
207
+
208
+
209
+ def cached(
210
+ db,
211
+ *,
212
+ namespace: str,
213
+ ttl: Optional[int] = None,
214
+ embed: Optional[Callable[[str], list[float]]] = None,
215
+ model: Optional[str] = None,
216
+ threshold: float = 0.97,
217
+ key_fn: Optional[Callable[..., str]] = None,
218
+ ):
219
+ """Memoize a function's results into a mnemo.Mnemo cache.
220
+
221
+ - Exact-key mode (default): key is JSON(args, kwargs) plus fn name.
222
+ - Semantic mode: pass `embed=my_embedder` and `model="..."`; the key
223
+ is embedded and semantic recall is tried before falling back.
224
+ - Custom `key_fn(*args, **kwargs) -> str` overrides the default key.
225
+ """
226
+ if embed is not None and model is None:
227
+ raise ValueError("semantic mode requires `model=`")
228
+
229
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
230
+ @functools.wraps(fn)
231
+ def wrapper(*args, **kwargs):
232
+ key = key_fn(*args, **kwargs) if key_fn else _default_key(fn, args, kwargs)
233
+
234
+ # Try semantic hit first (if configured); otherwise exact-key.
235
+ if embed is not None:
236
+ vec = embed(key)
237
+ hit = db.cache_get_semantic(namespace, vec, model, threshold)
238
+ if hit is not None:
239
+ return _decode(hit["value"])
240
+ # Miss — compute the real result.
241
+ result = fn(*args, **kwargs)
242
+ encoded = _encode(result)
243
+ db.cache_put_semantic(namespace, key, vec, encoded, model, ttl_secs=ttl)
244
+ return result
245
+
246
+ hit = db.cache_get(namespace, key)
247
+ if hit is not None:
248
+ return _decode(hit["value"])
249
+ result = fn(*args, **kwargs)
250
+ encoded = _encode(result)
251
+ db.cache_put(namespace, key, encoded, ttl_secs=ttl)
252
+ return result
253
+
254
+ return wrapper
255
+
256
+ return decorator
257
+
258
+
259
+ def _default_key(fn, args, kwargs) -> str:
260
+ payload = json.dumps(
261
+ {"fn": fn.__qualname__, "args": args, "kwargs": kwargs},
262
+ default=str, sort_keys=True,
263
+ )
264
+ return hashlib.sha256(payload.encode()).hexdigest()
265
+
266
+
267
+ def _encode(v: Any) -> bytes:
268
+ if isinstance(v, (bytes, bytearray)):
269
+ return bytes(v)
270
+ if isinstance(v, str):
271
+ return v.encode()
272
+ return json.dumps(v, default=str).encode()
273
+
274
+
275
+ def _decode(b: bytes) -> Any:
276
+ # Best-effort round-trip: try JSON first, fall back to str, then bytes.
277
+ try:
278
+ return json.loads(b)
279
+ except (json.JSONDecodeError, UnicodeDecodeError):
280
+ try:
281
+ return b.decode()
282
+ except UnicodeDecodeError:
283
+ return b
284
+ ```
285
+
286
+ ### Recipe: OpenAI/Anthropic call wrapped end-to-end
287
+
288
+ ```python
289
+ import os, mnemo
290
+ from openai import OpenAI
291
+ from mnemo_cached import cached
292
+
293
+ db = mnemo.open("agent.mnemo", os.environ["MNEMO_PASSPHRASE"])
294
+ client = OpenAI()
295
+
296
+ @cached(db, namespace="openai-gpt-4o-mini", ttl=86_400)
297
+ def chat(prompt: str) -> str:
298
+ r = client.chat.completions.create(
299
+ model="gpt-4o-mini",
300
+ messages=[{"role": "user", "content": prompt}],
301
+ )
302
+ return r.choices[0].message.content
303
+
304
+ # First call → hits the model, caches the response.
305
+ # Second identical call → hits the cache, no OpenAI request.
306
+ print(chat("Give me a haiku about SQLite."))
307
+ print(chat("Give me a haiku about SQLite."))
308
+
309
+ print("cache stats:", db.cache_stats("openai-gpt-4o-mini"))
310
+ ```
311
+
312
+ Anthropic swap-in — same shape, different client:
313
+
314
+ ```python
315
+ import anthropic
316
+ from mnemo_cached import cached
317
+
318
+ client = anthropic.Anthropic()
319
+
320
+ @cached(db, namespace="claude-3-5-sonnet", ttl=86_400)
321
+ def chat(prompt: str) -> str:
322
+ r = client.messages.create(
323
+ model="claude-3-5-sonnet-latest",
324
+ max_tokens=1024,
325
+ messages=[{"role": "user", "content": prompt}],
326
+ )
327
+ return r.content[0].text
328
+ ```
329
+
330
+ ### Recipe: semantic cache with an embedder
331
+
332
+ ```python
333
+ from openai import OpenAI
334
+ from mnemo_cached import cached
335
+
336
+ client = OpenAI()
337
+ def embed(text: str) -> list[float]:
338
+ return client.embeddings.create(
339
+ model="text-embedding-3-small", input=text,
340
+ ).data[0].embedding
341
+
342
+ # Semantic mode: prompts that differ in wording but mean the same
343
+ # thing hit the same cache entry.
344
+ @cached(db, namespace="llm-semantic", ttl=86_400,
345
+ embed=embed, model="text-embedding-3-small", threshold=0.97)
346
+ def chat(prompt: str) -> str:
347
+ return client.chat.completions.create(
348
+ model="gpt-4o-mini",
349
+ messages=[{"role": "user", "content": prompt}],
350
+ ).choices[0].message.content
351
+ ```
352
+
353
+ **Note:** the database must have been created with `dimensions` matching
354
+ your embedder's output — `text-embedding-3-small` is 1536-dim,
355
+ `bge-large-en-v1.5` is 1024-dim, etc. Set at
356
+ `mnemo.open(path, pw, dimensions=1536)` on first creation.
357
+
358
+ **Don't use this for:** multi-node fleets where the cache must be
359
+ consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
360
+ optimized for a single-host agent that owns its cache file.
361
+
362
+ ## License
363
+
364
+ Apache-2.0.
365
+