memwal 0.1.0.dev0__py3-none-any.whl
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.
- memwal/__init__.py +111 -0
- memwal/client.py +1014 -0
- memwal/middleware.py +472 -0
- memwal/types.py +340 -0
- memwal/utils.py +172 -0
- memwal-0.1.0.dev0.dist-info/METADATA +211 -0
- memwal-0.1.0.dev0.dist-info/RECORD +8 -0
- memwal-0.1.0.dev0.dist-info/WHEEL +4 -0
memwal/types.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memwal — Core Types
|
|
3
|
+
|
|
4
|
+
Dataclasses for all API request options and response types.
|
|
5
|
+
Ed25519 delegate key based SDK that communicates with
|
|
6
|
+
the MemWal Rust server (TEE).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
# ============================================================
|
|
15
|
+
# Config
|
|
16
|
+
# ============================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class MemWalConfig:
|
|
21
|
+
"""Configuration for creating a MemWal client.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
key: Ed25519 private key (hex string). This is the delegate key from app.memwal.com.
|
|
25
|
+
account_id: MemWalAccount object ID on Sui.
|
|
26
|
+
server_url: Server URL (default: https://relayer.memwal.ai).
|
|
27
|
+
namespace: Default namespace for memory isolation (default: "default").
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
key: str
|
|
31
|
+
account_id: str
|
|
32
|
+
server_url: str = "https://relayer.memwal.ai"
|
|
33
|
+
namespace: str = "default"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ============================================================
|
|
37
|
+
# API Response Types
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RememberResult:
|
|
43
|
+
"""Result from remember()."""
|
|
44
|
+
|
|
45
|
+
id: str
|
|
46
|
+
blob_id: str
|
|
47
|
+
owner: str
|
|
48
|
+
namespace: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class RecallMemory:
|
|
53
|
+
"""A single recalled memory."""
|
|
54
|
+
|
|
55
|
+
blob_id: str
|
|
56
|
+
text: str
|
|
57
|
+
distance: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class RecallResult:
|
|
62
|
+
"""Result from recall()."""
|
|
63
|
+
|
|
64
|
+
results: List[RecallMemory]
|
|
65
|
+
total: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class AnalyzedFact:
|
|
70
|
+
"""A single extracted fact."""
|
|
71
|
+
|
|
72
|
+
text: str
|
|
73
|
+
id: str
|
|
74
|
+
blob_id: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class AnalyzeResult:
|
|
79
|
+
"""Result from analyze().
|
|
80
|
+
|
|
81
|
+
Per PR #121: each extracted fact is enqueued as a background remember
|
|
82
|
+
job. ``job_ids`` aligns with ``facts`` positionally; poll those
|
|
83
|
+
job_ids (e.g. via :meth:`MemWal.wait_for_remember_jobs`) to know when
|
|
84
|
+
the corresponding memory is fully persisted on Walrus.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
facts: List[AnalyzedFact]
|
|
88
|
+
fact_count: int
|
|
89
|
+
job_ids: List[str]
|
|
90
|
+
status: str
|
|
91
|
+
owner: str
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def total(self) -> int:
|
|
95
|
+
"""Backward-compat alias for ``fact_count`` (v0.1 callers)."""
|
|
96
|
+
return self.fact_count
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class HealthResult:
|
|
101
|
+
"""Server health response."""
|
|
102
|
+
|
|
103
|
+
status: str
|
|
104
|
+
version: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class RestoreResult:
|
|
109
|
+
"""Result from restore()."""
|
|
110
|
+
|
|
111
|
+
restored: int
|
|
112
|
+
skipped: int
|
|
113
|
+
total: int
|
|
114
|
+
namespace: str
|
|
115
|
+
owner: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class AskMemory:
|
|
120
|
+
"""A memory used to answer a question."""
|
|
121
|
+
|
|
122
|
+
blob_id: str
|
|
123
|
+
text: str
|
|
124
|
+
distance: float
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class AskResult:
|
|
129
|
+
"""Result from ask()."""
|
|
130
|
+
|
|
131
|
+
answer: str
|
|
132
|
+
memories_used: int
|
|
133
|
+
memories: List[AskMemory]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ============================================================
|
|
137
|
+
# Manual Flow Types
|
|
138
|
+
# ============================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class RememberManualOptions:
|
|
143
|
+
"""Options for remember_manual().
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
blob_id: Walrus blob ID (user already uploaded encrypted data).
|
|
147
|
+
vector: Embedding vector (user already generated).
|
|
148
|
+
namespace: Namespace (default: config namespace or "default").
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
blob_id: str
|
|
152
|
+
vector: List[float]
|
|
153
|
+
namespace: Optional[str] = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class RememberManualResult:
|
|
158
|
+
"""Result from remember_manual()."""
|
|
159
|
+
|
|
160
|
+
id: str
|
|
161
|
+
blob_id: str
|
|
162
|
+
owner: str
|
|
163
|
+
namespace: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class RecallManualOptions:
|
|
168
|
+
"""Options for recall_manual().
|
|
169
|
+
|
|
170
|
+
Attributes:
|
|
171
|
+
vector: Pre-computed query embedding vector.
|
|
172
|
+
limit: Max number of results (default: 10).
|
|
173
|
+
namespace: Namespace (default: config namespace or "default").
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
vector: List[float]
|
|
177
|
+
limit: int = 10
|
|
178
|
+
namespace: Optional[str] = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class RecallManualHit:
|
|
183
|
+
"""A single search hit -- raw blob_id + distance (no decrypted text)."""
|
|
184
|
+
|
|
185
|
+
blob_id: str
|
|
186
|
+
distance: float
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class RecallManualResult:
|
|
191
|
+
"""Result from recall_manual()."""
|
|
192
|
+
|
|
193
|
+
results: List[RecallManualHit]
|
|
194
|
+
total: int
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ============================================================
|
|
198
|
+
# Async remember (PR #121: ENG-1406 / ENG-1408)
|
|
199
|
+
# ============================================================
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class RememberAcceptedResult:
|
|
204
|
+
"""Result from remember() / remember_async() — server returns 202 immediately.
|
|
205
|
+
|
|
206
|
+
The actual upload + on-chain commit happen in a background worker.
|
|
207
|
+
Poll ``GET /api/remember/{job_id}`` (via wait_for_remember_job) to follow
|
|
208
|
+
progress until status reaches "done" or "failed".
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
job_id: str
|
|
212
|
+
status: str
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class RememberJobStatus:
|
|
217
|
+
"""One snapshot of an async remember job, returned by the status endpoint.
|
|
218
|
+
|
|
219
|
+
``status`` transitions: pending → running → uploaded → done, or → failed.
|
|
220
|
+
``not_found`` is returned when the job_id is unknown or not the caller's.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
job_id: str
|
|
224
|
+
status: str
|
|
225
|
+
owner: Optional[str] = None
|
|
226
|
+
namespace: Optional[str] = None
|
|
227
|
+
blob_id: Optional[str] = None
|
|
228
|
+
error: Optional[str] = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ============================================================
|
|
232
|
+
# Bulk remember (ENG-1408)
|
|
233
|
+
# ============================================================
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class RememberBulkItem:
|
|
238
|
+
"""One item in a bulk remember request.
|
|
239
|
+
|
|
240
|
+
``namespace`` overrides the client default for this item only.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
text: str
|
|
244
|
+
namespace: Optional[str] = None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass
|
|
248
|
+
class RememberBulkAcceptedResult:
|
|
249
|
+
"""Result from remember_bulk() / remember_bulk_async() — 202 with job_ids.
|
|
250
|
+
|
|
251
|
+
``job_ids`` aligns positionally with the input ``items`` list.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
job_ids: List[str]
|
|
255
|
+
total: int
|
|
256
|
+
status: str
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class RememberBulkStatusItem:
|
|
261
|
+
"""Per-item status returned by the bulk status endpoint."""
|
|
262
|
+
|
|
263
|
+
job_id: str
|
|
264
|
+
status: str
|
|
265
|
+
blob_id: Optional[str] = None
|
|
266
|
+
error: Optional[str] = None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass
|
|
270
|
+
class RememberBulkStatusResult:
|
|
271
|
+
"""Result from get_remember_bulk_status()."""
|
|
272
|
+
|
|
273
|
+
results: List[RememberBulkStatusItem]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class RememberBulkOptions:
|
|
278
|
+
"""Polling options for remember_bulk_and_wait() / wait_for_remember_jobs().
|
|
279
|
+
|
|
280
|
+
``poll_interval_ms`` is the base poll cadence (default 1500ms).
|
|
281
|
+
``timeout_ms`` is the total wait budget before raising TimeoutError
|
|
282
|
+
(default 120_000ms).
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
poll_interval_ms: int = 1500
|
|
286
|
+
timeout_ms: int = 120_000
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass
|
|
290
|
+
class RememberBulkItemResult:
|
|
291
|
+
"""One settled item in a bulk-and-wait result."""
|
|
292
|
+
|
|
293
|
+
id: str
|
|
294
|
+
blob_id: str
|
|
295
|
+
status: str # "done" | "failed" | "timeout"
|
|
296
|
+
error: Optional[str] = None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass
|
|
300
|
+
class RememberBulkResult:
|
|
301
|
+
"""Aggregate result from remember_bulk_and_wait() / wait_for_remember_jobs().
|
|
302
|
+
|
|
303
|
+
``succeeded + failed + timed_out == total``. ``results`` preserves input order.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
results: List[RememberBulkItemResult]
|
|
307
|
+
total: int
|
|
308
|
+
succeeded: int
|
|
309
|
+
failed: int
|
|
310
|
+
timed_out: int
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ============================================================
|
|
314
|
+
# Embed + analyze
|
|
315
|
+
# ============================================================
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class EmbedResult:
|
|
320
|
+
"""Result from embed() — raw embedding vector for a piece of text."""
|
|
321
|
+
|
|
322
|
+
vector: List[float]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass
|
|
326
|
+
class AnalyzeWaitResult:
|
|
327
|
+
"""Result from analyze_and_wait().
|
|
328
|
+
|
|
329
|
+
Combines the analyze fact-extraction output with the bulk-style settled
|
|
330
|
+
results for each enqueued background remember job. Mirrors the TS SDK's
|
|
331
|
+
``AnalyzeWaitResult extends RememberBulkResult``.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
results: List[RememberBulkItemResult]
|
|
335
|
+
total: int
|
|
336
|
+
succeeded: int
|
|
337
|
+
failed: int
|
|
338
|
+
timed_out: int
|
|
339
|
+
facts: List[AnalyzedFact]
|
|
340
|
+
owner: str
|
memwal/utils.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memwal — Shared Utilities
|
|
3
|
+
|
|
4
|
+
Crypto and encoding helpers for Ed25519 signing and SHA-256 hashing.
|
|
5
|
+
Uses PyNaCl (nacl.signing) as the primary Ed25519 implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
import nacl.signing
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def hex_to_bytes(hex_str: str) -> bytes:
|
|
17
|
+
"""Convert a hex string to bytes.
|
|
18
|
+
|
|
19
|
+
Handles optional ``0x`` prefix.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
hex_str: Hex-encoded string, optionally prefixed with ``0x``.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Raw bytes.
|
|
26
|
+
"""
|
|
27
|
+
clean = hex_str[2:] if hex_str.startswith("0x") else hex_str
|
|
28
|
+
return bytes.fromhex(clean)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bytes_to_hex(b: bytes) -> str:
|
|
32
|
+
"""Convert bytes to a lowercase hex string (no ``0x`` prefix).
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
b: Raw bytes.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Hex-encoded string.
|
|
39
|
+
"""
|
|
40
|
+
return b.hex()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sha256_hex(data: str) -> str:
|
|
44
|
+
"""Compute the SHA-256 hex digest of a UTF-8 string.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data: Input string.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Lowercase hex SHA-256 digest.
|
|
51
|
+
"""
|
|
52
|
+
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_signing_key(private_key_hex: str) -> nacl.signing.SigningKey:
|
|
56
|
+
"""Build a PyNaCl ``SigningKey`` from an Ed25519 private key hex string.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
private_key_hex: 32-byte Ed25519 seed as hex (64 hex chars), optionally ``0x``-prefixed.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A ``nacl.signing.SigningKey`` instance.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the decoded seed is not exactly 32 bytes.
|
|
66
|
+
"""
|
|
67
|
+
seed_bytes = hex_to_bytes(private_key_hex)
|
|
68
|
+
if len(seed_bytes) != 32:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Ed25519 seed must be exactly 32 bytes, got {len(seed_bytes)}"
|
|
71
|
+
)
|
|
72
|
+
return nacl.signing.SigningKey(seed_bytes)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def sign_message(message: str, signing_key: nacl.signing.SigningKey) -> Tuple[str, str]:
|
|
76
|
+
"""Sign a UTF-8 message with an Ed25519 signing key.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
message: The message string to sign.
|
|
80
|
+
signing_key: A PyNaCl ``SigningKey``.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A tuple of ``(signature_hex, public_key_hex)``.
|
|
84
|
+
"""
|
|
85
|
+
signed = signing_key.sign(message.encode("utf-8"))
|
|
86
|
+
signature_bytes: bytes = signed.signature
|
|
87
|
+
public_key_bytes: bytes = bytes(signing_key.verify_key)
|
|
88
|
+
return bytes_to_hex(signature_bytes), bytes_to_hex(public_key_bytes)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_signature_message(
|
|
92
|
+
timestamp: str,
|
|
93
|
+
method: str,
|
|
94
|
+
path: str,
|
|
95
|
+
body_sha256: str,
|
|
96
|
+
nonce: str = "",
|
|
97
|
+
account_id: str = "",
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Build the canonical signing message.
|
|
100
|
+
|
|
101
|
+
Current format (matches Rust server ``services/server/src/auth.rs``)::
|
|
102
|
+
|
|
103
|
+
"{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}"
|
|
104
|
+
|
|
105
|
+
The trailing ``nonce`` was added in MED-1 (replay protection); the
|
|
106
|
+
``account_id`` was added in LOW-23 so an intermediary can't swap the
|
|
107
|
+
account hint without invalidating the signature. Both fields are
|
|
108
|
+
REQUIRED — passing empty strings will fail signature verification on
|
|
109
|
+
the server.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
timestamp: Unix seconds as string.
|
|
113
|
+
method: Uppercase HTTP method (e.g. ``"POST"``).
|
|
114
|
+
path: URL path with query (e.g. ``"/api/remember"``).
|
|
115
|
+
body_sha256: SHA-256 hex digest of the JSON body string.
|
|
116
|
+
nonce: UUID v4 sent as the ``x-nonce`` header (required).
|
|
117
|
+
account_id: MemWalAccount object ID sent as ``x-account-id``
|
|
118
|
+
(required; empty string here will mismatch on server).
|
|
119
|
+
"""
|
|
120
|
+
return f"{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def delegate_key_to_sui_address(private_key_hex: str) -> str:
|
|
124
|
+
"""Derive the Sui address from an Ed25519 delegate key.
|
|
125
|
+
|
|
126
|
+
Sui Ed25519 address derivation:
|
|
127
|
+
blake2b-256(0x00 || public_key)[0:32]
|
|
128
|
+
|
|
129
|
+
where ``0x00`` is the Ed25519 scheme flag byte.
|
|
130
|
+
|
|
131
|
+
This matches the TypeScript SDK's ``delegateKeyToSuiAddress()`` exactly,
|
|
132
|
+
and is the same derivation used by the Sui wallet.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
private_key_hex: Ed25519 private key as hex (64 hex chars / 32 bytes),
|
|
136
|
+
optionally ``0x``-prefixed.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Sui address as a ``0x``-prefixed lowercase hex string (32 bytes / 64 hex chars).
|
|
140
|
+
|
|
141
|
+
Example::
|
|
142
|
+
|
|
143
|
+
address = delegate_key_to_sui_address("944aa24c09d8b6d6...")
|
|
144
|
+
# "0x1a2b3c..."
|
|
145
|
+
"""
|
|
146
|
+
signing_key = build_signing_key(private_key_hex)
|
|
147
|
+
public_key_bytes = bytes(signing_key.verify_key) # 32 bytes
|
|
148
|
+
|
|
149
|
+
# Sui scheme flag for Ed25519 = 0x00, then the 32-byte public key
|
|
150
|
+
scheme_input = bytes([0x00]) + public_key_bytes # 33 bytes total
|
|
151
|
+
|
|
152
|
+
# blake2b with 32-byte (256-bit) digest
|
|
153
|
+
address_bytes = hashlib.blake2b(scheme_input, digest_size=32).digest()
|
|
154
|
+
return "0x" + bytes_to_hex(address_bytes)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def delegate_key_to_public_key(private_key_hex: str) -> bytes:
|
|
158
|
+
"""Get the Ed25519 public key bytes from a delegate key.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
private_key_hex: Ed25519 private key as hex, optionally ``0x``-prefixed.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
32-byte Ed25519 public key.
|
|
165
|
+
|
|
166
|
+
Example::
|
|
167
|
+
|
|
168
|
+
pub = delegate_key_to_public_key("944aa24c...")
|
|
169
|
+
print(pub.hex()) # "d5b76c57..."
|
|
170
|
+
"""
|
|
171
|
+
signing_key = build_signing_key(private_key_hex)
|
|
172
|
+
return bytes(signing_key.verify_key)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memwal
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
|
|
5
|
+
Project-URL: Homepage, https://memwal.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.memwal.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/MystenLabs/MemWal
|
|
8
|
+
Author: MemWal Team
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai,ed25519,memory,memwal,privacy,sui,walrus
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: pynacl>=1.5.0
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'all'
|
|
27
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
34
|
+
Provides-Extra: langchain
|
|
35
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
|
|
36
|
+
Provides-Extra: openai
|
|
37
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# memwal
|
|
41
|
+
|
|
42
|
+
Python SDK for [MemWal](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
|
|
43
|
+
|
|
44
|
+
All data processing (encryption, embedding, Walrus storage) happens server-side in a TEE. The SDK signs requests with your Ed25519 delegate key and sends text over HTTPS.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install memwal
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With optional integrations:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install memwal[langchain] # LangChain support
|
|
56
|
+
pip install memwal[openai] # OpenAI SDK support
|
|
57
|
+
pip install memwal[all] # Everything
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
Set your environment variables first:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export MEMWAL_KEY="your-ed25519-delegate-key-hex"
|
|
66
|
+
export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
|
|
67
|
+
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Async (recommended)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
import os
|
|
75
|
+
from memwal import MemWal
|
|
76
|
+
|
|
77
|
+
async def main():
|
|
78
|
+
memwal = MemWal.create(
|
|
79
|
+
key=os.environ["MEMWAL_KEY"],
|
|
80
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
81
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Store a memory
|
|
85
|
+
result = await memwal.remember("I'm allergic to peanuts")
|
|
86
|
+
print(result.blob_id)
|
|
87
|
+
|
|
88
|
+
# Recall memories
|
|
89
|
+
matches = await memwal.recall("food allergies")
|
|
90
|
+
for memory in matches.results:
|
|
91
|
+
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
92
|
+
|
|
93
|
+
# Analyze conversation for facts
|
|
94
|
+
analysis = await memwal.analyze("I love coffee and live in Tokyo")
|
|
95
|
+
for fact in analysis.facts:
|
|
96
|
+
print(fact.text)
|
|
97
|
+
|
|
98
|
+
await memwal.close()
|
|
99
|
+
|
|
100
|
+
asyncio.run(main())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Sync
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import os
|
|
107
|
+
from memwal import MemWalSync
|
|
108
|
+
|
|
109
|
+
client = MemWalSync.create(
|
|
110
|
+
key=os.environ["MEMWAL_KEY"],
|
|
111
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
112
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
result = client.remember("I'm allergic to peanuts")
|
|
116
|
+
matches = client.recall("food allergies")
|
|
117
|
+
client.close()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Context Manager
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import os
|
|
124
|
+
from memwal import MemWal
|
|
125
|
+
|
|
126
|
+
async with MemWal.create(
|
|
127
|
+
key=os.environ["MEMWAL_KEY"],
|
|
128
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
129
|
+
) as memwal:
|
|
130
|
+
await memwal.remember("I prefer dark mode")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## AI Middleware
|
|
134
|
+
|
|
135
|
+
### LangChain
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
import os
|
|
139
|
+
from langchain_openai import ChatOpenAI
|
|
140
|
+
from langchain_core.messages import HumanMessage
|
|
141
|
+
from memwal import with_memwal_langchain
|
|
142
|
+
|
|
143
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
144
|
+
smart_llm = with_memwal_langchain(
|
|
145
|
+
llm,
|
|
146
|
+
key=os.environ["MEMWAL_KEY"],
|
|
147
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
148
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
149
|
+
max_memories=5,
|
|
150
|
+
min_relevance=0.3,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Memories are automatically recalled and injected
|
|
154
|
+
response = await smart_llm.ainvoke([HumanMessage("What are my food allergies?")])
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### OpenAI SDK
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import os
|
|
161
|
+
from openai import AsyncOpenAI
|
|
162
|
+
from memwal import with_memwal_openai
|
|
163
|
+
|
|
164
|
+
client = AsyncOpenAI()
|
|
165
|
+
smart_client = with_memwal_openai(
|
|
166
|
+
client,
|
|
167
|
+
key=os.environ["MEMWAL_KEY"],
|
|
168
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
169
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Memories are automatically recalled and injected
|
|
173
|
+
response = await smart_client.chat.completions.create(
|
|
174
|
+
model="gpt-4o",
|
|
175
|
+
messages=[{"role": "user", "content": "What are my food allergies?"}],
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## API Reference
|
|
180
|
+
|
|
181
|
+
### `MemWal.create(key, account_id, server_url?, namespace?)`
|
|
182
|
+
|
|
183
|
+
Create a new async client.
|
|
184
|
+
|
|
185
|
+
### Methods
|
|
186
|
+
|
|
187
|
+
| Method | Description |
|
|
188
|
+
|--------|-------------|
|
|
189
|
+
| `await remember(text, namespace?)` | Store a memory |
|
|
190
|
+
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
191
|
+
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
192
|
+
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
193
|
+
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
194
|
+
| `await health()` | Check server health |
|
|
195
|
+
| `await remember_manual(opts)` | Store with pre-computed vector |
|
|
196
|
+
| `await recall_manual(opts)` | Search with pre-computed vector |
|
|
197
|
+
| `await get_public_key_hex()` | Get Ed25519 public key |
|
|
198
|
+
|
|
199
|
+
## Authentication
|
|
200
|
+
|
|
201
|
+
Every request is signed with Ed25519:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}.{nonce}.{account_id}"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, `x-delegate-key`, `x-account-id`.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|