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.
- mnemo_engine-0.4.0/PKG-INFO +365 -0
- mnemo_engine-0.4.0/README.md +350 -0
- mnemo_engine-0.4.0/mnemo/.gitignore +1 -0
- mnemo_engine-0.4.0/mnemo/Cargo.lock +1145 -0
- mnemo_engine-0.4.0/mnemo/Cargo.toml +83 -0
- mnemo_engine-0.4.0/mnemo/README.md +779 -0
- mnemo_engine-0.4.0/mnemo/examples/quickstart.rs +70 -0
- mnemo_engine-0.4.0/mnemo/examples/session.rs +59 -0
- mnemo_engine-0.4.0/mnemo/src/bin/mnemo.rs +1449 -0
- mnemo_engine-0.4.0/mnemo/src/cache.rs +299 -0
- mnemo_engine-0.4.0/mnemo/src/crypto.rs +212 -0
- mnemo_engine-0.4.0/mnemo/src/error.rs +98 -0
- mnemo_engine-0.4.0/mnemo/src/format.rs +436 -0
- mnemo_engine-0.4.0/mnemo/src/index.rs +449 -0
- mnemo_engine-0.4.0/mnemo/src/lib.rs +80 -0
- mnemo_engine-0.4.0/mnemo/src/mcp.rs +844 -0
- mnemo_engine-0.4.0/mnemo/src/memory.rs +307 -0
- mnemo_engine-0.4.0/mnemo/src/pager.rs +254 -0
- mnemo_engine-0.4.0/mnemo/src/result_cache.rs +551 -0
- mnemo_engine-0.4.0/mnemo/src/session.rs +198 -0
- mnemo_engine-0.4.0/mnemo/src/store.rs +2547 -0
- mnemo_engine-0.4.0/mnemo/src/wal.rs +224 -0
- mnemo_engine-0.4.0/mnemo/tests/cli_smoke.rs +553 -0
- mnemo_engine-0.4.0/mnemo/tests/integration.rs +2313 -0
- mnemo_engine-0.4.0/mnemo-python/.gitignore +4 -0
- mnemo_engine-0.4.0/mnemo-python/Cargo.lock +1094 -0
- mnemo_engine-0.4.0/mnemo-python/Cargo.toml +38 -0
- mnemo_engine-0.4.0/mnemo-python/README.md +350 -0
- mnemo_engine-0.4.0/mnemo-python/src/lib.rs +958 -0
- mnemo_engine-0.4.0/mnemo-python/test_mnemo.py +240 -0
- mnemo_engine-0.4.0/pyproject.toml +32 -0
|
@@ -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
|
+
|