mnemo-engine 0.4.1__tar.gz → 0.5.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.1 → mnemo_engine-0.5.0}/PKG-INFO +60 -1
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/README.md +59 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/Cargo.lock +2 -2
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/Cargo.toml +1 -1
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/README.md +76 -5
- mnemo_engine-0.5.0/mnemo/examples/embedder_server.py +51 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/bin/mnemo.rs +273 -15
- mnemo_engine-0.5.0/mnemo/src/embed.rs +280 -0
- mnemo_engine-0.5.0/mnemo/src/embed_cmd.rs +572 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/lib.rs +4 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/mcp.rs +273 -82
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/memory.rs +13 -1
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/store.rs +159 -0
- mnemo_engine-0.5.0/mnemo/tests/cli_smoke.rs +1336 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/tests/integration.rs +370 -10
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/Cargo.lock +2 -2
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/Cargo.toml +1 -1
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/README.md +59 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/src/lib.rs +253 -7
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/test_mnemo.py +164 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/pyproject.toml +1 -1
- mnemo_engine-0.4.1/mnemo/tests/cli_smoke.rs +0 -553
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/.gitignore +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/deny.toml +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/examples/quickstart.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/examples/session.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/cache.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/crypto.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/error.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/format.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/index.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/pager.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/result_cache.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/session.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/wal.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/tests/proptest_store.proptest-regressions +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/tests/proptest_store.rs +0 -0
- {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/.gitignore +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mnemo-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -161,6 +161,10 @@ its own agent.
|
|
|
161
161
|
| `memories()` | Enumerate every live (non-deleted, non-expired) memory as a list of dicts |
|
|
162
162
|
| `dimensions()` | Embedding dimensionality this database was created with (also on `stats()["dimensions"]`) |
|
|
163
163
|
| `rekey(new_passphrase, fast=False)` | Change the passphrase. Re-derives the KEK and re-wraps the DEK; encrypted pages are not rewritten (O(1) in db size). `fast=True` uses the cheap-Argon2 test params — omit for production |
|
|
164
|
+
| `set_embedder(fn, *, name, dimensions, normalize=True)` | Register a Python callable `embed(texts: list[str]) -> list[list[float]]` as this handle's text embedder (Phase 3). Dimension-checks against the database's width; identity-checks against (and on first use upserts) the scaffold manifest's `embedder_name` / `embedder_dimensions` / `embedder_normalize` keys, so swapping embedders between opens fails loudly |
|
|
165
|
+
| `remember_text(content, memory_type, **kwargs)` | Embed `content` via the registered embedder and `remember` the result. Accepts the same per-memory kwargs as `remember` (`agent_id`, `importance`, `session_id`, `ttl_secs`, `shared`, `metadata`) |
|
|
166
|
+
| `recall_text(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Embed `query` via the registered embedder and run `recall`. Same return shape as `recall` |
|
|
167
|
+
| `cache_get_semantic_text(namespace, text, threshold=0.97)` | Text-side semantic cache lookup; uses the embedder's `name` as the semantic cache's `model` component |
|
|
164
168
|
| `stats()` | Summary statistics |
|
|
165
169
|
| `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
|
|
166
170
|
| `len(db)` | Live memory count |
|
|
@@ -370,6 +374,61 @@ your embedder's output — `text-embedding-3-small` is 1536-dim,
|
|
|
370
374
|
consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
|
|
371
375
|
optimized for a single-host agent that owns its cache file.
|
|
372
376
|
|
|
377
|
+
### Recipe: `set_embedder` + text-only `remember` / `recall`
|
|
378
|
+
|
|
379
|
+
Phase 3 ships a per-handle embedder hook so callers never have to
|
|
380
|
+
pass vectors by hand. Register once at open time; every subsequent
|
|
381
|
+
`remember_text` / `recall_text` / `cache_get_semantic_text` call on
|
|
382
|
+
the handle uses it.
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
import mnemo
|
|
386
|
+
from sentence_transformers import SentenceTransformer
|
|
387
|
+
|
|
388
|
+
model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim, normalized
|
|
389
|
+
|
|
390
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
391
|
+
return model.encode(texts, normalize_embeddings=True).tolist()
|
|
392
|
+
|
|
393
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=384)
|
|
394
|
+
db.set_embedder(embed, name="all-MiniLM-L6-v2",
|
|
395
|
+
dimensions=384, normalize=True)
|
|
396
|
+
|
|
397
|
+
db.remember_text("the user prefers tea", "semantic",
|
|
398
|
+
importance=0.8, agent_id="assistant")
|
|
399
|
+
hits = db.recall_text("how does the user like their drinks?", top_k=5)
|
|
400
|
+
for h in hits:
|
|
401
|
+
print(h["score"], h["content"])
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Or with OpenAI's hosted embeddings:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from openai import OpenAI
|
|
408
|
+
client = OpenAI()
|
|
409
|
+
|
|
410
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
411
|
+
r = client.embeddings.create(model="text-embedding-3-small", input=texts)
|
|
412
|
+
return [item.embedding for item in r.data]
|
|
413
|
+
|
|
414
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=1536)
|
|
415
|
+
db.set_embedder(embed, name="text-embedding-3-small",
|
|
416
|
+
dimensions=1536, normalize=False)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The identity (`name`, `dimensions`, `normalize`) is persisted on the
|
|
420
|
+
scaffold manifest the first time you `set_embedder`. Reopening with a
|
|
421
|
+
*different* embedder — say you swap MiniLM for BGE on the same file —
|
|
422
|
+
raises `RuntimeError: embedder identity mismatch: database manifest
|
|
423
|
+
records embedder (name="all-MiniLM-L6-v2", …), but this handle was
|
|
424
|
+
passed embedder (name="bge-large-en-v1.5", …). Mixing embedders
|
|
425
|
+
silently corrupts recall quality — open the file with the original
|
|
426
|
+
embedder, or create a fresh database for the new one.`
|
|
427
|
+
|
|
428
|
+
Mnemo also now refuses any `remember` call with an all-zero vector,
|
|
429
|
+
so a mis-wired embedder (returning `[0.0] * dims`) fails loudly on
|
|
430
|
+
the first write instead of polluting recall silently.
|
|
431
|
+
|
|
373
432
|
## License
|
|
374
433
|
|
|
375
434
|
Apache-2.0.
|
|
@@ -147,6 +147,10 @@ its own agent.
|
|
|
147
147
|
| `memories()` | Enumerate every live (non-deleted, non-expired) memory as a list of dicts |
|
|
148
148
|
| `dimensions()` | Embedding dimensionality this database was created with (also on `stats()["dimensions"]`) |
|
|
149
149
|
| `rekey(new_passphrase, fast=False)` | Change the passphrase. Re-derives the KEK and re-wraps the DEK; encrypted pages are not rewritten (O(1) in db size). `fast=True` uses the cheap-Argon2 test params — omit for production |
|
|
150
|
+
| `set_embedder(fn, *, name, dimensions, normalize=True)` | Register a Python callable `embed(texts: list[str]) -> list[list[float]]` as this handle's text embedder (Phase 3). Dimension-checks against the database's width; identity-checks against (and on first use upserts) the scaffold manifest's `embedder_name` / `embedder_dimensions` / `embedder_normalize` keys, so swapping embedders between opens fails loudly |
|
|
151
|
+
| `remember_text(content, memory_type, **kwargs)` | Embed `content` via the registered embedder and `remember` the result. Accepts the same per-memory kwargs as `remember` (`agent_id`, `importance`, `session_id`, `ttl_secs`, `shared`, `metadata`) |
|
|
152
|
+
| `recall_text(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Embed `query` via the registered embedder and run `recall`. Same return shape as `recall` |
|
|
153
|
+
| `cache_get_semantic_text(namespace, text, threshold=0.97)` | Text-side semantic cache lookup; uses the embedder's `name` as the semantic cache's `model` component |
|
|
150
154
|
| `stats()` | Summary statistics |
|
|
151
155
|
| `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
|
|
152
156
|
| `len(db)` | Live memory count |
|
|
@@ -356,6 +360,61 @@ your embedder's output — `text-embedding-3-small` is 1536-dim,
|
|
|
356
360
|
consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
|
|
357
361
|
optimized for a single-host agent that owns its cache file.
|
|
358
362
|
|
|
363
|
+
### Recipe: `set_embedder` + text-only `remember` / `recall`
|
|
364
|
+
|
|
365
|
+
Phase 3 ships a per-handle embedder hook so callers never have to
|
|
366
|
+
pass vectors by hand. Register once at open time; every subsequent
|
|
367
|
+
`remember_text` / `recall_text` / `cache_get_semantic_text` call on
|
|
368
|
+
the handle uses it.
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
import mnemo
|
|
372
|
+
from sentence_transformers import SentenceTransformer
|
|
373
|
+
|
|
374
|
+
model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim, normalized
|
|
375
|
+
|
|
376
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
377
|
+
return model.encode(texts, normalize_embeddings=True).tolist()
|
|
378
|
+
|
|
379
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=384)
|
|
380
|
+
db.set_embedder(embed, name="all-MiniLM-L6-v2",
|
|
381
|
+
dimensions=384, normalize=True)
|
|
382
|
+
|
|
383
|
+
db.remember_text("the user prefers tea", "semantic",
|
|
384
|
+
importance=0.8, agent_id="assistant")
|
|
385
|
+
hits = db.recall_text("how does the user like their drinks?", top_k=5)
|
|
386
|
+
for h in hits:
|
|
387
|
+
print(h["score"], h["content"])
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Or with OpenAI's hosted embeddings:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from openai import OpenAI
|
|
394
|
+
client = OpenAI()
|
|
395
|
+
|
|
396
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
397
|
+
r = client.embeddings.create(model="text-embedding-3-small", input=texts)
|
|
398
|
+
return [item.embedding for item in r.data]
|
|
399
|
+
|
|
400
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=1536)
|
|
401
|
+
db.set_embedder(embed, name="text-embedding-3-small",
|
|
402
|
+
dimensions=1536, normalize=False)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The identity (`name`, `dimensions`, `normalize`) is persisted on the
|
|
406
|
+
scaffold manifest the first time you `set_embedder`. Reopening with a
|
|
407
|
+
*different* embedder — say you swap MiniLM for BGE on the same file —
|
|
408
|
+
raises `RuntimeError: embedder identity mismatch: database manifest
|
|
409
|
+
records embedder (name="all-MiniLM-L6-v2", …), but this handle was
|
|
410
|
+
passed embedder (name="bge-large-en-v1.5", …). Mixing embedders
|
|
411
|
+
silently corrupts recall quality — open the file with the original
|
|
412
|
+
embedder, or create a fresh database for the new one.`
|
|
413
|
+
|
|
414
|
+
Mnemo also now refuses any `remember` call with an all-zero vector,
|
|
415
|
+
so a mis-wired embedder (returning `[0.0] * dims`) fails loudly on
|
|
416
|
+
the first write instead of polluting recall silently.
|
|
417
|
+
|
|
359
418
|
## License
|
|
360
419
|
|
|
361
420
|
Apache-2.0.
|
|
@@ -462,7 +462,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
462
462
|
|
|
463
463
|
[[package]]
|
|
464
464
|
name = "mnemo-engine"
|
|
465
|
-
version = "0.
|
|
465
|
+
version = "0.5.0"
|
|
466
466
|
dependencies = [
|
|
467
467
|
"aes-gcm",
|
|
468
468
|
"argon2",
|
|
@@ -485,7 +485,7 @@ dependencies = [
|
|
|
485
485
|
|
|
486
486
|
[[package]]
|
|
487
487
|
name = "mnemo-node"
|
|
488
|
-
version = "0.
|
|
488
|
+
version = "0.5.0"
|
|
489
489
|
dependencies = [
|
|
490
490
|
"mnemo-engine",
|
|
491
491
|
"napi",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# below stay `mnemo`, so consumers still write `use mnemo::...` and invoke
|
|
7
7
|
# `mnemo` on the command line after `cargo add mnemo-engine`.
|
|
8
8
|
name = "mnemo-engine"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.5.0"
|
|
10
10
|
edition = "2021"
|
|
11
11
|
rust-version = "1.75"
|
|
12
12
|
description = "Encrypted, single-file, portable agent memory engine with multi-signal recall, sessions, snapshots, and an IVF+PQ index"
|
|
@@ -41,10 +41,55 @@ mnemo serve --mcp path/to/agent.mnemo
|
|
|
41
41
|
|
|
42
42
|
The passphrase is read from `MNEMO_PASSPHRASE` (never a CLI flag — that
|
|
43
43
|
would leak into shell history and process listings). Tools exposed:
|
|
44
|
-
`about`, `remember`, `recall`, `forget`, `list`, `snapshot_list`, `stats
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
`about`, `remember`, `recall`, `forget`, `list`, `snapshot_list`, `stats`,
|
|
45
|
+
`cache_put`, `cache_get`, `cache_stats`, `cache_put_semantic`,
|
|
46
|
+
`cache_get_semantic`. Every mutation flushes before returning.
|
|
47
|
+
|
|
48
|
+
#### Text-in via `--embed-cmd` (Phase 3)
|
|
49
|
+
|
|
50
|
+
`remember`, `recall`, `cache_put_semantic`, and `cache_get_semantic`
|
|
51
|
+
accept **either** a caller-supplied `vector`/`query` **or** a `text`
|
|
52
|
+
string that the server embeds for them. Enable the text path by passing
|
|
53
|
+
an embedder command at serve start:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
MNEMO_PASSPHRASE=... mnemo serve --mcp agent.mnemo \
|
|
57
|
+
--embed-cmd "python3 mnemo/examples/embedder_server.py" \
|
|
58
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The embedder is a **persistent child process** spawned once at serve
|
|
62
|
+
start. Protocol: it reads one JSON-encoded string per line from stdin
|
|
63
|
+
and writes one JSON array of floats per line to stdout, flushed after
|
|
64
|
+
every reply. See `mnemo/examples/embedder_server.py` for a 10-line
|
|
65
|
+
reference implementation (sentence-transformers + MiniLM, 384-dim).
|
|
66
|
+
|
|
67
|
+
> **Stdout is for replies only.** Any line the embedder prints to
|
|
68
|
+
> stdout outside a reply — startup banners, model-load progress,
|
|
69
|
+
> warnings, "model loaded" messages — is a protocol violation and
|
|
70
|
+
> would shift every subsequent reply by one line. EmbedCmd detects
|
|
71
|
+
> it, kills the child, and surfaces a clear error ("stdout is
|
|
72
|
+
> reserved for replies only; log to stderr instead"); the next call
|
|
73
|
+
> respawns the child. **Log to stderr.** Python: `print("...",
|
|
74
|
+
> file=sys.stderr, flush=True)`, or configure `logging` with
|
|
75
|
+
> `stream=sys.stderr`. Stderr is inherited to the parent and is
|
|
76
|
+
> safe for anything you want to see while the server runs.
|
|
77
|
+
|
|
78
|
+
Per-request timeout defaults to 60 seconds; override with
|
|
79
|
+
`--embed-timeout <secs>` on `serve`, `remember`, and `recall`. Set
|
|
80
|
+
it longer than your slowest cold-start reply (sentence-transformers'
|
|
81
|
+
first `encode()` on CPU can be 3–5s). On timeout — or any other
|
|
82
|
+
protocol error (malformed JSON, wrong dimensionality, zero vector,
|
|
83
|
+
unexpected EOF) — the child is killed and the next call respawns a
|
|
84
|
+
fresh child with the same argv/env/timeout. The guarantee is
|
|
85
|
+
asymmetric and deliberate: **an error is allowed; a mismatched
|
|
86
|
+
vector never is.**
|
|
87
|
+
|
|
88
|
+
The `--embed-name` string is matched against (and persisted on) the
|
|
89
|
+
scaffold manifest's `metadata.embedder = { name, dimensions, normalize }`
|
|
90
|
+
object. Starting a server with a different `--embed-name` on an existing
|
|
91
|
+
file fails loudly — mixing embedders in one file silently corrupts
|
|
92
|
+
recall quality.
|
|
48
93
|
|
|
49
94
|
Claude Desktop config snippet (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
50
95
|
|
|
@@ -53,13 +98,39 @@ Claude Desktop config snippet (`~/Library/Application Support/Claude/claude_desk
|
|
|
53
98
|
"mcpServers": {
|
|
54
99
|
"mnemo": {
|
|
55
100
|
"command": "mnemo",
|
|
56
|
-
"args": [
|
|
101
|
+
"args": [
|
|
102
|
+
"serve", "--mcp", "/absolute/path/to/agent.mnemo",
|
|
103
|
+
"--embed-cmd", "python3 /absolute/path/to/embedder_server.py",
|
|
104
|
+
"--embed-name", "sentence-transformers/all-MiniLM-L6-v2"
|
|
105
|
+
],
|
|
57
106
|
"env": { "MNEMO_PASSPHRASE": "your-passphrase-here" }
|
|
58
107
|
}
|
|
59
108
|
}
|
|
60
109
|
}
|
|
61
110
|
```
|
|
62
111
|
|
|
112
|
+
Omit `--embed-cmd`/`--embed-name` to keep the pre-Phase-3
|
|
113
|
+
embedder-agnostic behavior (`vector`/`query` only).
|
|
114
|
+
|
|
115
|
+
#### CLI text path
|
|
116
|
+
|
|
117
|
+
The same `--embed-cmd` + `--embed-name` pair works on the one-shot
|
|
118
|
+
CLI commands:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
mnemo remember agent.mnemo --text "the user prefers tea" \
|
|
122
|
+
--type semantic --importance 0.8 --agent assistant \
|
|
123
|
+
--embed-cmd "python3 embedder_server.py" \
|
|
124
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2"
|
|
125
|
+
|
|
126
|
+
mnemo recall agent.mnemo --text "what drink?" --top-k 5 \
|
|
127
|
+
--embed-cmd "python3 embedder_server.py" \
|
|
128
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2" \
|
|
129
|
+
--no-track
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`--text` is mutually exclusive with `--query`/`--query-file`.
|
|
133
|
+
|
|
63
134
|
The server holds an exclusive write lock on the file for its lifetime
|
|
64
135
|
(see [Concurrency](#concurrency--read-only-opens)), so two MCP servers
|
|
65
136
|
on the same file will conflict — one will fail to start with
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Reference embedder for `mnemo` --embed-cmd / MCP serve (Phase 3 PR 10b).
|
|
2
|
+
|
|
3
|
+
Protocol:
|
|
4
|
+
* Read one JSON-encoded string per line from stdin.
|
|
5
|
+
* Write one JSON array of floats per line to stdout.
|
|
6
|
+
* Flush stdout after every reply, or mnemo will time out waiting.
|
|
7
|
+
* Close on EOF of stdin (which mnemo sends when the handle drops).
|
|
8
|
+
|
|
9
|
+
The script runs persistently — one spawn per `Mnemo` handle, reused for
|
|
10
|
+
every subsequent remember/recall/cache call. Keep heavy imports (model
|
|
11
|
+
load) at module scope so the cost is paid once.
|
|
12
|
+
|
|
13
|
+
**Stdout is reserved for replies only.** Any `print(...)` without
|
|
14
|
+
`file=sys.stderr` — a startup banner, a "model loaded" line, a
|
|
15
|
+
progress bar, a warning — shifts every subsequent reply by one line
|
|
16
|
+
and mnemo's EmbedCmd will kill the child with a clear error.
|
|
17
|
+
Log to stderr instead: `print("...", file=sys.stderr, flush=True)`.
|
|
18
|
+
`sentence-transformers`/`transformers`/`torch` already log to stderr
|
|
19
|
+
by default, so this is only a hazard for your own debug prints.
|
|
20
|
+
|
|
21
|
+
Usage::
|
|
22
|
+
|
|
23
|
+
# CLI, one-shot:
|
|
24
|
+
mnemo remember notes.mnemo \\
|
|
25
|
+
--text "the user prefers tea" \\
|
|
26
|
+
--embed-cmd "python3 embedder_server.py" \\
|
|
27
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2"
|
|
28
|
+
|
|
29
|
+
# MCP, persistent server:
|
|
30
|
+
MNEMO_PASSPHRASE=... mnemo serve --mcp notes.mnemo \\
|
|
31
|
+
--embed-cmd "python3 embedder_server.py" \\
|
|
32
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2"
|
|
33
|
+
|
|
34
|
+
Requires `sentence-transformers` + `torch`. Swap the model slug (and
|
|
35
|
+
pass the matching --embed-name) to use something else; just keep the
|
|
36
|
+
database's `dimensions` consistent with the embedder's output size.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
from sentence_transformers import SentenceTransformer
|
|
43
|
+
|
|
44
|
+
MODEL = "sentence-transformers/all-MiniLM-L6-v2" # 384-dim, normalized
|
|
45
|
+
|
|
46
|
+
model = SentenceTransformer(MODEL)
|
|
47
|
+
|
|
48
|
+
for line in sys.stdin:
|
|
49
|
+
text = json.loads(line)
|
|
50
|
+
vector = model.encode([text], normalize_embeddings=True)[0].tolist()
|
|
51
|
+
print(json.dumps(vector), flush=True)
|