memwal 0.1.1.dev0__tar.gz → 0.1.2__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.
- memwal-0.1.2/CHANGELOG.md +34 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/PKG-INFO +19 -16
- {memwal-0.1.1.dev0 → memwal-0.1.2}/README.md +15 -12
- memwal-0.1.2/examples/.env.example +10 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/examples/async_remember_demo.py +3 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2}/examples/interactive_demo.py +3 -3
- memwal-0.1.2/examples/verify_credentials.py +67 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/memwal/__init__.py +5 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2}/memwal/client.py +43 -21
- {memwal-0.1.1.dev0 → memwal-0.1.2}/memwal/compatibility.py +6 -6
- {memwal-0.1.1.dev0 → memwal-0.1.2}/memwal/middleware.py +18 -18
- {memwal-0.1.1.dev0 → memwal-0.1.2}/memwal/types.py +40 -6
- {memwal-0.1.1.dev0 → memwal-0.1.2}/memwal/utils.py +2 -2
- {memwal-0.1.1.dev0 → memwal-0.1.2}/pyproject.toml +4 -4
- {memwal-0.1.1.dev0 → memwal-0.1.2}/run_tests.py +5 -5
- {memwal-0.1.1.dev0 → memwal-0.1.2}/tests/test_client.py +75 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2}/tests/test_integration.py +6 -6
- {memwal-0.1.1.dev0 → memwal-0.1.2}/tests/test_middleware.py +1 -1
- memwal-0.1.1.dev0/examples/.env.example +0 -8
- {memwal-0.1.1.dev0 → memwal-0.1.2}/.gitignore +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/examples/.gitignore +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/tests/__init__.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/tests/test_env_presets.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2}/tests/test_signing.py +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# memwal
|
|
2
|
+
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added `max_distance` to async and sync `recall()`.
|
|
8
|
+
- Added credential verification helper.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Updated docs/examples to use `MEMWAL_PRIVATE_KEY`.
|
|
13
|
+
- Rebranded package metadata and documentation from MemWal to Walrus Memory.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Made `401` relayer errors more actionable.
|
|
18
|
+
|
|
19
|
+
## 0.1.1
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added relayer `env` presets.
|
|
24
|
+
- Added compatibility checks and `compatibility()` helpers.
|
|
25
|
+
|
|
26
|
+
## 0.1.0
|
|
27
|
+
|
|
28
|
+
### Initial Release
|
|
29
|
+
|
|
30
|
+
- `MemWal` async client and `MemWalSync` sync wrapper
|
|
31
|
+
- Memory APIs: `remember`, `recall`, `analyze`, `ask`, `restore`, `health`
|
|
32
|
+
- Async job helpers for remember, bulk remember, and analyze
|
|
33
|
+
- LangChain/OpenAI middleware and delegate-key utilities
|
|
34
|
+
- Ed25519 delegate-key auth with namespace-scoped memory isolation
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Python SDK for
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
|
|
5
5
|
Project-URL: Homepage, https://memwal.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.memwal.ai
|
|
7
7
|
Project-URL: Repository, https://github.com/MystenLabs/MemWal
|
|
8
|
-
Author:
|
|
8
|
+
Author: Walrus Memory Team
|
|
9
9
|
License-Expression: MIT
|
|
10
|
-
Keywords: ai,ed25519,memory,memwal,privacy,sui,walrus
|
|
10
|
+
Keywords: ai,ed25519,memory,memwal,privacy,sui,walrus,walrus-memory
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -37,9 +37,9 @@ Provides-Extra: openai
|
|
|
37
37
|
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
38
38
|
Description-Content-Type: text/markdown
|
|
39
39
|
|
|
40
|
-
#
|
|
40
|
+
# Walrus Memory Python SDK
|
|
41
41
|
|
|
42
|
-
Python SDK for [
|
|
42
|
+
Python SDK for [Walrus Memory](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
|
|
43
43
|
|
|
44
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
45
|
|
|
@@ -62,11 +62,14 @@ pip install memwal[all] # Everything
|
|
|
62
62
|
Set your environment variables first:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
export
|
|
66
|
-
export MEMWAL_ACCOUNT_ID="0x-your-
|
|
65
|
+
export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
|
|
66
|
+
export MEMWAL_ACCOUNT_ID="0x-your-walrus-memory-account-id"
|
|
67
67
|
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
`MEMWAL_PRIVATE_KEY` is the delegate private key from the Walrus Memory dashboard and
|
|
71
|
+
must stay server-side.
|
|
72
|
+
|
|
70
73
|
### Async (recommended)
|
|
71
74
|
|
|
72
75
|
```python
|
|
@@ -76,7 +79,7 @@ from memwal import MemWal
|
|
|
76
79
|
|
|
77
80
|
async def main():
|
|
78
81
|
memwal = MemWal.create(
|
|
79
|
-
key=os.environ["
|
|
82
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
80
83
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
81
84
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
82
85
|
)
|
|
@@ -86,7 +89,7 @@ async def main():
|
|
|
86
89
|
print(result.blob_id)
|
|
87
90
|
|
|
88
91
|
# Recall memories
|
|
89
|
-
matches = await memwal.recall("food allergies")
|
|
92
|
+
matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
|
|
90
93
|
for memory in matches.results:
|
|
91
94
|
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
92
95
|
|
|
@@ -107,7 +110,7 @@ import os
|
|
|
107
110
|
from memwal import MemWalSync
|
|
108
111
|
|
|
109
112
|
client = MemWalSync.create(
|
|
110
|
-
key=os.environ["
|
|
113
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
111
114
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
112
115
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
113
116
|
)
|
|
@@ -124,7 +127,7 @@ import os
|
|
|
124
127
|
from memwal import MemWal
|
|
125
128
|
|
|
126
129
|
async with MemWal.create(
|
|
127
|
-
key=os.environ["
|
|
130
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
128
131
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
129
132
|
) as memwal:
|
|
130
133
|
await memwal.remember("I prefer dark mode")
|
|
@@ -139,7 +142,7 @@ Same shorthand as the TypeScript SDK and MCP package.
|
|
|
139
142
|
from memwal import MemWal
|
|
140
143
|
|
|
141
144
|
memwal = MemWal.create(
|
|
142
|
-
key=os.environ["
|
|
145
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
143
146
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
144
147
|
env="prod", # prod | dev | staging | local
|
|
145
148
|
)
|
|
@@ -169,7 +172,7 @@ from memwal import with_memwal_langchain
|
|
|
169
172
|
llm = ChatOpenAI(model="gpt-4o")
|
|
170
173
|
smart_llm = with_memwal_langchain(
|
|
171
174
|
llm,
|
|
172
|
-
key=os.environ["
|
|
175
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
173
176
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
174
177
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
175
178
|
max_memories=5,
|
|
@@ -190,7 +193,7 @@ from memwal import with_memwal_openai
|
|
|
190
193
|
client = AsyncOpenAI()
|
|
191
194
|
smart_client = with_memwal_openai(
|
|
192
195
|
client,
|
|
193
|
-
key=os.environ["
|
|
196
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
194
197
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
195
198
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
196
199
|
)
|
|
@@ -213,7 +216,7 @@ Create a new async client.
|
|
|
213
216
|
| Method | Description |
|
|
214
217
|
|--------|-------------|
|
|
215
218
|
| `await remember(text, namespace?)` | Store a memory |
|
|
216
|
-
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
219
|
+
| `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
|
|
217
220
|
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
218
221
|
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
219
222
|
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Walrus Memory Python SDK
|
|
2
2
|
|
|
3
|
-
Python SDK for [
|
|
3
|
+
Python SDK for [Walrus Memory](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
|
|
4
4
|
|
|
5
5
|
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.
|
|
6
6
|
|
|
@@ -23,11 +23,14 @@ pip install memwal[all] # Everything
|
|
|
23
23
|
Set your environment variables first:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
export
|
|
27
|
-
export MEMWAL_ACCOUNT_ID="0x-your-
|
|
26
|
+
export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
|
|
27
|
+
export MEMWAL_ACCOUNT_ID="0x-your-walrus-memory-account-id"
|
|
28
28
|
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`MEMWAL_PRIVATE_KEY` is the delegate private key from the Walrus Memory dashboard and
|
|
32
|
+
must stay server-side.
|
|
33
|
+
|
|
31
34
|
### Async (recommended)
|
|
32
35
|
|
|
33
36
|
```python
|
|
@@ -37,7 +40,7 @@ from memwal import MemWal
|
|
|
37
40
|
|
|
38
41
|
async def main():
|
|
39
42
|
memwal = MemWal.create(
|
|
40
|
-
key=os.environ["
|
|
43
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
41
44
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
42
45
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
43
46
|
)
|
|
@@ -47,7 +50,7 @@ async def main():
|
|
|
47
50
|
print(result.blob_id)
|
|
48
51
|
|
|
49
52
|
# Recall memories
|
|
50
|
-
matches = await memwal.recall("food allergies")
|
|
53
|
+
matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
|
|
51
54
|
for memory in matches.results:
|
|
52
55
|
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
53
56
|
|
|
@@ -68,7 +71,7 @@ import os
|
|
|
68
71
|
from memwal import MemWalSync
|
|
69
72
|
|
|
70
73
|
client = MemWalSync.create(
|
|
71
|
-
key=os.environ["
|
|
74
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
72
75
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
73
76
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
74
77
|
)
|
|
@@ -85,7 +88,7 @@ import os
|
|
|
85
88
|
from memwal import MemWal
|
|
86
89
|
|
|
87
90
|
async with MemWal.create(
|
|
88
|
-
key=os.environ["
|
|
91
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
89
92
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
90
93
|
) as memwal:
|
|
91
94
|
await memwal.remember("I prefer dark mode")
|
|
@@ -100,7 +103,7 @@ Same shorthand as the TypeScript SDK and MCP package.
|
|
|
100
103
|
from memwal import MemWal
|
|
101
104
|
|
|
102
105
|
memwal = MemWal.create(
|
|
103
|
-
key=os.environ["
|
|
106
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
104
107
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
105
108
|
env="prod", # prod | dev | staging | local
|
|
106
109
|
)
|
|
@@ -130,7 +133,7 @@ from memwal import with_memwal_langchain
|
|
|
130
133
|
llm = ChatOpenAI(model="gpt-4o")
|
|
131
134
|
smart_llm = with_memwal_langchain(
|
|
132
135
|
llm,
|
|
133
|
-
key=os.environ["
|
|
136
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
134
137
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
135
138
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
136
139
|
max_memories=5,
|
|
@@ -151,7 +154,7 @@ from memwal import with_memwal_openai
|
|
|
151
154
|
client = AsyncOpenAI()
|
|
152
155
|
smart_client = with_memwal_openai(
|
|
153
156
|
client,
|
|
154
|
-
key=os.environ["
|
|
157
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
155
158
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
156
159
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
157
160
|
)
|
|
@@ -174,7 +177,7 @@ Create a new async client.
|
|
|
174
177
|
| Method | Description |
|
|
175
178
|
|--------|-------------|
|
|
176
179
|
| `await remember(text, namespace?)` | Store a memory |
|
|
177
|
-
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
180
|
+
| `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
|
|
178
181
|
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
179
182
|
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
180
183
|
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Local server (default) or remote relayer
|
|
2
|
+
MEMWAL_SERVER_URL=http://localhost:8000
|
|
3
|
+
# Ed25519 delegate private key (64-hex). Get from Walrus Memory dashboard.
|
|
4
|
+
MEMWAL_PRIVATE_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
|
|
5
|
+
# Optional: paste the dashboard delegate public key so verification can catch mismatches.
|
|
6
|
+
MEMWAL_DELEGATE_PUBLIC_KEY=
|
|
7
|
+
# Walrus Memory account object ID on Sui (the wallet's account)
|
|
8
|
+
MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
|
|
9
|
+
# Namespace for these test memories
|
|
10
|
+
MEMWAL_NAMESPACE=python-sdk-example
|
|
@@ -65,13 +65,13 @@ def _ms(start: float) -> int:
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
async def main() -> None:
|
|
68
|
-
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:
|
|
69
|
-
key = os.environ.get("
|
|
68
|
+
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
|
|
69
|
+
key = os.environ.get("MEMWAL_PRIVATE_KEY")
|
|
70
70
|
account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
|
|
71
71
|
namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
|
|
72
72
|
|
|
73
73
|
if not key or not account_id:
|
|
74
|
-
print("ERROR: set
|
|
74
|
+
print("ERROR: set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
|
|
75
75
|
sys.exit(2)
|
|
76
76
|
|
|
77
77
|
print(f"server : {server_url}")
|
|
@@ -94,13 +94,13 @@ def _log_offset() -> int:
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
async def main() -> None:
|
|
97
|
-
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:
|
|
98
|
-
key = os.environ.get("
|
|
97
|
+
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
|
|
98
|
+
key = os.environ.get("MEMWAL_PRIVATE_KEY")
|
|
99
99
|
account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
|
|
100
100
|
namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
|
|
101
101
|
|
|
102
102
|
if not key or not account_id:
|
|
103
|
-
print("ERROR: set
|
|
103
|
+
print("ERROR: set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
|
|
104
104
|
sys.exit(2)
|
|
105
105
|
|
|
106
106
|
print(f"server : {server_url}")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Verify MemWal example credentials before calling the relayer.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python examples/verify_credentials.py
|
|
5
|
+
|
|
6
|
+
Optional:
|
|
7
|
+
Set MEMWAL_DELEGATE_PUBLIC_KEY to the dashboard public key to fail on
|
|
8
|
+
public/private key mismatch.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import nacl.signing
|
|
18
|
+
|
|
19
|
+
HEX_32_BYTES = re.compile(r"^(0x)?[0-9a-fA-F]{64}$")
|
|
20
|
+
ACCOUNT_ID = re.compile(r"^0x[0-9a-fA-F]{64}$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_hex(value: str) -> str:
|
|
24
|
+
value = value.strip()
|
|
25
|
+
return value[2:] if value.lower().startswith("0x") else value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
private_key = os.environ.get("MEMWAL_PRIVATE_KEY") or ""
|
|
30
|
+
account_id = os.environ.get("MEMWAL_ACCOUNT_ID") or ""
|
|
31
|
+
expected_public_key = os.environ.get("MEMWAL_DELEGATE_PUBLIC_KEY") or ""
|
|
32
|
+
server_url = os.environ.get("MEMWAL_SERVER_URL") or ""
|
|
33
|
+
|
|
34
|
+
if not private_key:
|
|
35
|
+
raise SystemExit("MEMWAL_PRIVATE_KEY is required")
|
|
36
|
+
if not HEX_32_BYTES.match(private_key):
|
|
37
|
+
raise SystemExit("MEMWAL_PRIVATE_KEY must be a 64-character Ed25519 private key hex string")
|
|
38
|
+
if account_id and not ACCOUNT_ID.match(account_id):
|
|
39
|
+
raise SystemExit("MEMWAL_ACCOUNT_ID must be a 0x-prefixed 32-byte Sui object ID")
|
|
40
|
+
|
|
41
|
+
signing_key = nacl.signing.SigningKey(bytes.fromhex(normalize_hex(private_key)))
|
|
42
|
+
derived_public_key = signing_key.verify_key.encode().hex()
|
|
43
|
+
|
|
44
|
+
if expected_public_key and derived_public_key != normalize_hex(expected_public_key).lower():
|
|
45
|
+
raise SystemExit(
|
|
46
|
+
"MEMWAL_PRIVATE_KEY does not derive MEMWAL_DELEGATE_PUBLIC_KEY. "
|
|
47
|
+
"You may have pasted a public key or a key from another account."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
print("Walrus Memory credentials look parseable.")
|
|
51
|
+
print(f"Derived delegate public key: {derived_public_key}")
|
|
52
|
+
if account_id:
|
|
53
|
+
print(f"Account ID: {account_id}")
|
|
54
|
+
if server_url:
|
|
55
|
+
print(f"Relayer URL: {server_url}")
|
|
56
|
+
if not expected_public_key:
|
|
57
|
+
print("Set MEMWAL_DELEGATE_PUBLIC_KEY to fail on public/private key mismatch.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
try:
|
|
62
|
+
main()
|
|
63
|
+
except SystemExit:
|
|
64
|
+
raise
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
print(exc, file=sys.stderr)
|
|
67
|
+
raise SystemExit(1) from exc
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Walrus Memory — Privacy-first AI memory SDK
|
|
3
3
|
|
|
4
4
|
Ed25519 delegate key auth + server-side TEE processing.
|
|
5
5
|
|
|
@@ -32,7 +32,6 @@ from .client import (
|
|
|
32
32
|
MemWalSync,
|
|
33
33
|
)
|
|
34
34
|
from .middleware import with_memwal_langchain, with_memwal_openai
|
|
35
|
-
from .utils import delegate_key_to_sui_address, delegate_key_to_public_key
|
|
36
35
|
from .types import (
|
|
37
36
|
ENV_PRESETS,
|
|
38
37
|
AnalyzedFact,
|
|
@@ -61,7 +60,9 @@ from .types import (
|
|
|
61
60
|
RememberManualResult,
|
|
62
61
|
RememberResult,
|
|
63
62
|
RestoreResult,
|
|
63
|
+
ScoringWeights,
|
|
64
64
|
)
|
|
65
|
+
from .utils import delegate_key_to_public_key, delegate_key_to_sui_address
|
|
65
66
|
|
|
66
67
|
# JS-style alias for developers coming from the TypeScript SDK
|
|
67
68
|
withMemWal = with_memwal_langchain
|
|
@@ -105,6 +106,7 @@ __all__ = [
|
|
|
105
106
|
"AnalyzedFact",
|
|
106
107
|
"HealthResult",
|
|
107
108
|
"RestoreResult",
|
|
109
|
+
"ScoringWeights",
|
|
108
110
|
"RememberManualOptions",
|
|
109
111
|
"RememberManualResult",
|
|
110
112
|
"RecallManualOptions",
|
|
@@ -112,4 +114,4 @@ __all__ = [
|
|
|
112
114
|
"RecallManualResult",
|
|
113
115
|
]
|
|
114
116
|
|
|
115
|
-
__version__ = "0.1.
|
|
117
|
+
__version__ = "0.1.2"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Walrus Memory — SDK Client
|
|
3
3
|
|
|
4
|
-
Ed25519 delegate key based client that communicates with the
|
|
4
|
+
Ed25519 delegate key based client that communicates with the Walrus Memory
|
|
5
5
|
Rust server (TEE). All data processing (encryption, embedding, Walrus)
|
|
6
6
|
happens server-side -- the SDK just signs requests and sends text.
|
|
7
7
|
|
|
@@ -58,7 +58,6 @@ from .types import (
|
|
|
58
58
|
RememberBulkResult,
|
|
59
59
|
RememberBulkStatusItem,
|
|
60
60
|
RememberBulkStatusResult,
|
|
61
|
-
RememberJobStatus,
|
|
62
61
|
RememberManualOptions,
|
|
63
62
|
RememberManualResult,
|
|
64
63
|
RememberResult,
|
|
@@ -79,6 +78,11 @@ from .utils import (
|
|
|
79
78
|
T = TypeVar("T")
|
|
80
79
|
SEAL_SESSION_TTL_MIN = 5
|
|
81
80
|
SEAL_SESSION_SAFETY_MARGIN_MS = 30_000
|
|
81
|
+
AUTH_REJECTED_MESSAGE = (
|
|
82
|
+
"401 from relayer: typically wrong private key, key not registered on this "
|
|
83
|
+
"account, account ID mismatch, or staging/mainnet mismatch. Check .env.local "
|
|
84
|
+
"and dashboard credentials."
|
|
85
|
+
)
|
|
82
86
|
|
|
83
87
|
|
|
84
88
|
# ============================================================
|
|
@@ -119,7 +123,7 @@ def _is_transient_polling_status(status: int) -> bool:
|
|
|
119
123
|
|
|
120
124
|
|
|
121
125
|
class MemWal:
|
|
122
|
-
"""Async-native
|
|
126
|
+
"""Async-native Walrus Memory client.
|
|
123
127
|
|
|
124
128
|
All API methods are ``async``. For synchronous usage, wrap calls with
|
|
125
129
|
``asyncio.run()`` or use the :class:`MemWalSync` convenience wrapper.
|
|
@@ -147,11 +151,11 @@ class MemWal:
|
|
|
147
151
|
namespace: str = "default",
|
|
148
152
|
env: Optional[str] = None,
|
|
149
153
|
) -> "MemWal":
|
|
150
|
-
"""Create a new
|
|
154
|
+
"""Create a new Walrus Memory client instance.
|
|
151
155
|
|
|
152
156
|
Args:
|
|
153
157
|
key: Ed25519 private key hex string (the delegate key).
|
|
154
|
-
account_id:
|
|
158
|
+
account_id: Walrus Memory account object ID on Sui.
|
|
155
159
|
server_url: Server URL (default: ``http://localhost:8000``).
|
|
156
160
|
namespace: Default namespace for memory isolation (default: ``"default"``).
|
|
157
161
|
env: Optional relayer preset — ``"prod"``, ``"dev"``, ``"staging"``,
|
|
@@ -472,6 +476,7 @@ class MemWal:
|
|
|
472
476
|
query: str,
|
|
473
477
|
limit: int = 10,
|
|
474
478
|
namespace: Optional[str] = None,
|
|
479
|
+
max_distance: Optional[float] = None,
|
|
475
480
|
) -> RecallResult:
|
|
476
481
|
"""Recall memories similar to a query.
|
|
477
482
|
|
|
@@ -481,6 +486,8 @@ class MemWal:
|
|
|
481
486
|
query: Search query.
|
|
482
487
|
limit: Max number of results (default: 10).
|
|
483
488
|
namespace: Override the default namespace.
|
|
489
|
+
max_distance: Optional client-side relevance threshold. Memories with
|
|
490
|
+
``distance >= max_distance`` are dropped.
|
|
484
491
|
|
|
485
492
|
Returns:
|
|
486
493
|
:class:`RecallResult` with decrypted text results.
|
|
@@ -498,6 +505,9 @@ class MemWal:
|
|
|
498
505
|
)
|
|
499
506
|
for m in data.get("results", [])
|
|
500
507
|
]
|
|
508
|
+
if max_distance is not None:
|
|
509
|
+
memories = [m for m in memories if m.distance < max_distance]
|
|
510
|
+
return RecallResult(results=memories, total=len(memories))
|
|
501
511
|
return RecallResult(results=memories, total=data.get("total", len(memories)))
|
|
502
512
|
|
|
503
513
|
async def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
|
|
@@ -717,14 +727,18 @@ class MemWal:
|
|
|
717
727
|
Returns:
|
|
718
728
|
:class:`RecallManualResult` with blob_id + distance pairs.
|
|
719
729
|
"""
|
|
730
|
+
body: Dict[str, Any] = {
|
|
731
|
+
"vector": opts.vector,
|
|
732
|
+
"limit": opts.limit,
|
|
733
|
+
"namespace": opts.namespace or self._namespace,
|
|
734
|
+
}
|
|
735
|
+
if opts.scoring_weights is not None:
|
|
736
|
+
body["scoring_weights"] = opts.scoring_weights.to_wire()
|
|
737
|
+
|
|
720
738
|
data = await self._signed_request(
|
|
721
739
|
"POST",
|
|
722
740
|
"/api/recall/manual",
|
|
723
|
-
|
|
724
|
-
"vector": opts.vector,
|
|
725
|
-
"limit": opts.limit,
|
|
726
|
-
"namespace": opts.namespace or self._namespace,
|
|
727
|
-
},
|
|
741
|
+
body,
|
|
728
742
|
include_seal_session=False,
|
|
729
743
|
)
|
|
730
744
|
hits = [
|
|
@@ -763,14 +777,14 @@ class MemWal:
|
|
|
763
777
|
health_response = await self._http.get(f"{self._server_url}/health")
|
|
764
778
|
if health_response.status_code != 200:
|
|
765
779
|
raise MemWalError(
|
|
766
|
-
"
|
|
780
|
+
"Walrus Memory compatibility check failed: "
|
|
767
781
|
f"GET /version returned {version_response.status_code}, "
|
|
768
782
|
f"and GET /health returned {health_response.status_code}"
|
|
769
783
|
)
|
|
770
784
|
metadata = health_response.json()
|
|
771
785
|
else:
|
|
772
786
|
raise MemWalError(
|
|
773
|
-
"
|
|
787
|
+
"Walrus Memory compatibility check failed: "
|
|
774
788
|
f"GET /version returned {version_response.status_code}"
|
|
775
789
|
)
|
|
776
790
|
|
|
@@ -829,7 +843,8 @@ class MemWal:
|
|
|
829
843
|
version = obj.get("version")
|
|
830
844
|
if str(version) != "1":
|
|
831
845
|
raise MemWalError(
|
|
832
|
-
f"SEAL package {package_id} must be at version 1 to build
|
|
846
|
+
f"SEAL package {package_id} must be at version 1 to build "
|
|
847
|
+
f"x-seal-session, got {version!r}"
|
|
833
848
|
)
|
|
834
849
|
|
|
835
850
|
async def _build_seal_session_inner(self) -> str:
|
|
@@ -909,7 +924,7 @@ class MemWal:
|
|
|
909
924
|
- ``x-timestamp``: Unix seconds string
|
|
910
925
|
- ``x-nonce``: UUID v4 replay-protection nonce
|
|
911
926
|
- ``x-seal-session``: Base64-encoded exported session envelope
|
|
912
|
-
- ``x-account-id``:
|
|
927
|
+
- ``x-account-id``: Walrus Memory account object ID
|
|
913
928
|
- ``Content-Type``: application/json
|
|
914
929
|
"""
|
|
915
930
|
import uuid
|
|
@@ -958,7 +973,7 @@ class MemWal:
|
|
|
958
973
|
err_text = response.text
|
|
959
974
|
if response.status_code == 426:
|
|
960
975
|
raise MemWalCompatibilityError(
|
|
961
|
-
"
|
|
976
|
+
"Walrus Memory relayer rejected this SDK as unsupported "
|
|
962
977
|
f"(HTTP 426 Upgrade Required). Relayer response: "
|
|
963
978
|
f"{err_text[:300] or 'upgrade required'}"
|
|
964
979
|
)
|
|
@@ -971,7 +986,7 @@ class MemWal:
|
|
|
971
986
|
|
|
972
987
|
|
|
973
988
|
class MemWalError(Exception):
|
|
974
|
-
"""Exception raised for
|
|
989
|
+
"""Exception raised for Walrus Memory API errors."""
|
|
975
990
|
|
|
976
991
|
pass
|
|
977
992
|
|
|
@@ -991,7 +1006,10 @@ class _HttpStatusError(MemWalError):
|
|
|
991
1006
|
"""
|
|
992
1007
|
|
|
993
1008
|
def __init__(self, status: int, body: str) -> None:
|
|
994
|
-
|
|
1009
|
+
if status == 401:
|
|
1010
|
+
super().__init__(AUTH_REJECTED_MESSAGE)
|
|
1011
|
+
else:
|
|
1012
|
+
super().__init__(f"Walrus Memory API error ({status}): {body}")
|
|
995
1013
|
self.status = status
|
|
996
1014
|
self.body = body
|
|
997
1015
|
|
|
@@ -1054,7 +1072,7 @@ class MemWalSync:
|
|
|
1054
1072
|
namespace: str = "default",
|
|
1055
1073
|
env: Optional[str] = None,
|
|
1056
1074
|
) -> "MemWalSync":
|
|
1057
|
-
"""Create a synchronous
|
|
1075
|
+
"""Create a synchronous Walrus Memory client.
|
|
1058
1076
|
|
|
1059
1077
|
Same parameters as :meth:`MemWal.create` (including the ``env``
|
|
1060
1078
|
relayer preset).
|
|
@@ -1170,10 +1188,14 @@ class MemWalSync:
|
|
|
1170
1188
|
return self._run(self._inner.remember_bulk_and_wait(items, opts))
|
|
1171
1189
|
|
|
1172
1190
|
def recall(
|
|
1173
|
-
self,
|
|
1191
|
+
self,
|
|
1192
|
+
query: str,
|
|
1193
|
+
limit: int = 10,
|
|
1194
|
+
namespace: Optional[str] = None,
|
|
1195
|
+
max_distance: Optional[float] = None,
|
|
1174
1196
|
) -> RecallResult:
|
|
1175
1197
|
"""Synchronous version of :meth:`MemWal.recall`."""
|
|
1176
|
-
return self._run(self._inner.recall(query, limit, namespace))
|
|
1198
|
+
return self._run(self._inner.recall(query, limit, namespace, max_distance))
|
|
1177
1199
|
|
|
1178
1200
|
def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
|
|
1179
1201
|
"""Synchronous version of :meth:`MemWal.analyze`."""
|
|
@@ -18,33 +18,33 @@ def compatibility_error(metadata: Dict[str, Any], server_url: str) -> Optional[s
|
|
|
18
18
|
|
|
19
19
|
if not api_version or not relayer_version or not isinstance(min_supported, dict):
|
|
20
20
|
return (
|
|
21
|
-
f"
|
|
21
|
+
f"Walrus Memory relayer at {server_url} does not expose compatibility metadata. "
|
|
22
22
|
"Upgrade the relayer to a version that serves GET /version, or use an older SDK."
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
api_major = _semver_major(str(api_version))
|
|
26
26
|
if api_major is None:
|
|
27
|
-
return f'
|
|
27
|
+
return f'Walrus Memory relayer at {server_url} returned invalid apiVersion "{api_version}".'
|
|
28
28
|
|
|
29
29
|
if api_major != SUPPORTED_RELAYER_API_MAJOR:
|
|
30
30
|
return (
|
|
31
|
-
"This
|
|
31
|
+
"This Walrus Memory Python SDK supports relayer API "
|
|
32
32
|
f"{SUPPORTED_RELAYER_API_MAJOR}.x, but {server_url} reports apiVersion "
|
|
33
33
|
f"{api_version}. Upgrade or downgrade the SDK/relayer pair."
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
min_python = min_supported.get("python")
|
|
37
37
|
if not isinstance(min_python, str):
|
|
38
|
-
return f"
|
|
38
|
+
return f"Walrus Memory relayer at {server_url} did not report minSupportedSdk.python."
|
|
39
39
|
if _parse_semver(min_python) is None:
|
|
40
40
|
return (
|
|
41
|
-
f'
|
|
41
|
+
f'Walrus Memory relayer at {server_url} returned invalid '
|
|
42
42
|
f'minSupportedSdk.python "{min_python}".'
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
if _compare_semver(MEMWAL_PYTHON_COMPATIBILITY_VERSION, min_python) < 0:
|
|
46
46
|
return (
|
|
47
|
-
f"
|
|
47
|
+
f"Walrus Memory relayer at {server_url} requires Python SDK >= {min_python}, "
|
|
48
48
|
f"but this package supports the {MEMWAL_PYTHON_COMPATIBILITY_VERSION} "
|
|
49
49
|
"compatibility baseline. Upgrade memwal or use an older compatible relayer."
|
|
50
50
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Walrus Memory — AI Middleware
|
|
3
3
|
|
|
4
4
|
Wraps LangChain and OpenAI SDK clients with automatic memory management.
|
|
5
5
|
Before each LLM call, relevant memories are recalled and injected.
|
|
@@ -145,7 +145,7 @@ def with_memwal_langchain(
|
|
|
145
145
|
debug: bool = False,
|
|
146
146
|
env: Optional[str] = None,
|
|
147
147
|
) -> "BaseChatModel":
|
|
148
|
-
"""Wrap a LangChain ``BaseChatModel`` with
|
|
148
|
+
"""Wrap a LangChain ``BaseChatModel`` with Walrus Memory management.
|
|
149
149
|
|
|
150
150
|
Before each call:
|
|
151
151
|
- Recall relevant memories for the last user message
|
|
@@ -157,8 +157,8 @@ def with_memwal_langchain(
|
|
|
157
157
|
Args:
|
|
158
158
|
llm: A LangChain ``BaseChatModel`` instance.
|
|
159
159
|
key: Ed25519 delegate key (hex).
|
|
160
|
-
account_id:
|
|
161
|
-
server_url:
|
|
160
|
+
account_id: Walrus Memory account object ID.
|
|
161
|
+
server_url: Walrus Memory server URL.
|
|
162
162
|
namespace: Default namespace.
|
|
163
163
|
max_memories: Max memories to inject per request.
|
|
164
164
|
auto_save: Auto-save new facts from conversation.
|
|
@@ -168,7 +168,7 @@ def with_memwal_langchain(
|
|
|
168
168
|
``"local"``). Same precedence as :meth:`MemWal.create`.
|
|
169
169
|
|
|
170
170
|
Returns:
|
|
171
|
-
A wrapped ``BaseChatModel`` that automatically uses
|
|
171
|
+
A wrapped ``BaseChatModel`` that automatically uses Walrus Memory.
|
|
172
172
|
"""
|
|
173
173
|
try:
|
|
174
174
|
from langchain_core.messages import HumanMessage, SystemMessage # noqa: F811
|
|
@@ -208,7 +208,7 @@ def with_memwal_langchain(
|
|
|
208
208
|
return messages
|
|
209
209
|
|
|
210
210
|
memory_context = _format_memories(relevant)
|
|
211
|
-
log(f"[
|
|
211
|
+
log(f"[Walrus Memory] Found {len(relevant)} relevant memories")
|
|
212
212
|
|
|
213
213
|
# Insert memory system message before the last user message
|
|
214
214
|
result = list(messages)
|
|
@@ -226,7 +226,7 @@ def with_memwal_langchain(
|
|
|
226
226
|
|
|
227
227
|
return result
|
|
228
228
|
except Exception as e:
|
|
229
|
-
log(f"[
|
|
229
|
+
log(f"[Walrus Memory] Memory search failed: {e}")
|
|
230
230
|
return messages
|
|
231
231
|
|
|
232
232
|
async def _post_analyze(messages: List[BaseMessage]) -> None:
|
|
@@ -238,7 +238,7 @@ def with_memwal_langchain(
|
|
|
238
238
|
try:
|
|
239
239
|
await memwal.analyze(user_text, namespace)
|
|
240
240
|
except Exception as e:
|
|
241
|
-
log(f"[
|
|
241
|
+
log(f"[Walrus Memory] Auto-save failed: {e}")
|
|
242
242
|
|
|
243
243
|
async def patched_agenerate(
|
|
244
244
|
messages: List[List[BaseMessage]], *args: Any, **kwargs: Any
|
|
@@ -304,7 +304,7 @@ def with_memwal_openai(
|
|
|
304
304
|
debug: bool = False,
|
|
305
305
|
env: Optional[str] = None,
|
|
306
306
|
) -> Any:
|
|
307
|
-
"""Wrap an OpenAI client with
|
|
307
|
+
"""Wrap an OpenAI client with Walrus Memory management.
|
|
308
308
|
|
|
309
309
|
Works with both ``openai.OpenAI`` (sync) and ``openai.AsyncOpenAI`` (async).
|
|
310
310
|
|
|
@@ -318,8 +318,8 @@ def with_memwal_openai(
|
|
|
318
318
|
Args:
|
|
319
319
|
client: An ``openai.OpenAI`` or ``openai.AsyncOpenAI`` instance.
|
|
320
320
|
key: Ed25519 delegate key (hex).
|
|
321
|
-
account_id:
|
|
322
|
-
server_url:
|
|
321
|
+
account_id: Walrus Memory account object ID.
|
|
322
|
+
server_url: Walrus Memory server URL.
|
|
323
323
|
namespace: Default namespace.
|
|
324
324
|
max_memories: Max memories to inject per request.
|
|
325
325
|
auto_save: Auto-save new facts from conversation.
|
|
@@ -329,7 +329,7 @@ def with_memwal_openai(
|
|
|
329
329
|
``"local"``). Same precedence as :meth:`MemWal.create`.
|
|
330
330
|
|
|
331
331
|
Returns:
|
|
332
|
-
The same client, with ``chat.completions.create`` wrapped to use
|
|
332
|
+
The same client, with ``chat.completions.create`` wrapped to use Walrus Memory.
|
|
333
333
|
"""
|
|
334
334
|
memwal = MemWal.create(
|
|
335
335
|
key=key,
|
|
@@ -379,14 +379,14 @@ def _wrap_async_openai(
|
|
|
379
379
|
]
|
|
380
380
|
if relevant:
|
|
381
381
|
memory_context = _format_memories(relevant)
|
|
382
|
-
log(f"[
|
|
382
|
+
log(f"[Walrus Memory] Found {len(relevant)} relevant memories")
|
|
383
383
|
messages = _inject_openai_memory(list(messages), memory_context)
|
|
384
384
|
if "messages" in kwargs:
|
|
385
385
|
kwargs["messages"] = messages
|
|
386
386
|
elif args:
|
|
387
387
|
args = (messages,) + args[1:]
|
|
388
388
|
except Exception as e:
|
|
389
|
-
log(f"[
|
|
389
|
+
log(f"[Walrus Memory] Memory search failed: {e}")
|
|
390
390
|
|
|
391
391
|
result = await original_create(*args, **kwargs)
|
|
392
392
|
|
|
@@ -396,7 +396,7 @@ def _wrap_async_openai(
|
|
|
396
396
|
try:
|
|
397
397
|
await memwal.analyze(user_text, namespace)
|
|
398
398
|
except Exception as e:
|
|
399
|
-
log(f"[
|
|
399
|
+
log(f"[Walrus Memory] Auto-save failed: {e}")
|
|
400
400
|
|
|
401
401
|
_fire_and_forget(_analyze())
|
|
402
402
|
|
|
@@ -437,14 +437,14 @@ def _wrap_sync_openai(
|
|
|
437
437
|
]
|
|
438
438
|
if relevant:
|
|
439
439
|
memory_context = _format_memories(relevant)
|
|
440
|
-
log(f"[
|
|
440
|
+
log(f"[Walrus Memory] Found {len(relevant)} relevant memories")
|
|
441
441
|
messages = _inject_openai_memory(list(messages), memory_context)
|
|
442
442
|
if "messages" in kwargs:
|
|
443
443
|
kwargs["messages"] = messages
|
|
444
444
|
elif args:
|
|
445
445
|
args = (messages,) + args[1:]
|
|
446
446
|
except Exception as e:
|
|
447
|
-
log(f"[
|
|
447
|
+
log(f"[Walrus Memory] Memory search failed: {e}")
|
|
448
448
|
|
|
449
449
|
result = original_create(*args, **kwargs)
|
|
450
450
|
|
|
@@ -454,7 +454,7 @@ def _wrap_sync_openai(
|
|
|
454
454
|
try:
|
|
455
455
|
await memwal.analyze(user_text, namespace)
|
|
456
456
|
except Exception as e:
|
|
457
|
-
log(f"[
|
|
457
|
+
log(f"[Walrus Memory] Auto-save failed: {e}")
|
|
458
458
|
|
|
459
459
|
_fire_and_forget(_analyze())
|
|
460
460
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Walrus Memory — Core Types
|
|
3
3
|
|
|
4
4
|
Dataclasses for all API request options and response types.
|
|
5
5
|
Ed25519 delegate key based SDK that communicates with
|
|
6
|
-
the
|
|
6
|
+
the Walrus Memory Rust server (TEE).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -22,7 +22,7 @@ DEFAULT_SERVER_URL = "http://localhost:8000"
|
|
|
22
22
|
|
|
23
23
|
#: Named relayer environments. Mirrors the TypeScript SDK / MCP package
|
|
24
24
|
#: ``--prod`` / ``--dev`` / ``--staging`` / ``--local`` presets so the same
|
|
25
|
-
#: shorthand works across every
|
|
25
|
+
#: shorthand works across every Walrus Memory client.
|
|
26
26
|
ENV_PRESETS = {
|
|
27
27
|
"prod": "https://relayer.memwal.ai",
|
|
28
28
|
"dev": "https://relayer.dev.memwal.ai",
|
|
@@ -33,11 +33,12 @@ ENV_PRESETS = {
|
|
|
33
33
|
|
|
34
34
|
@dataclass
|
|
35
35
|
class MemWalConfig:
|
|
36
|
-
"""Configuration for creating a
|
|
36
|
+
"""Configuration for creating a Walrus Memory client.
|
|
37
37
|
|
|
38
38
|
Attributes:
|
|
39
|
-
key: Ed25519 private key (hex string). This is the delegate key from
|
|
40
|
-
|
|
39
|
+
key: Ed25519 private key (hex string). This is the delegate key from the
|
|
40
|
+
Walrus Memory dashboard.
|
|
41
|
+
account_id: Walrus Memory account object ID on Sui.
|
|
41
42
|
server_url: Server URL (default: http://localhost:8000). An explicit
|
|
42
43
|
non-default value always wins over ``env``.
|
|
43
44
|
namespace: Default namespace for memory isolation (default: "default").
|
|
@@ -99,6 +100,37 @@ class RecallResult:
|
|
|
99
100
|
total: int
|
|
100
101
|
|
|
101
102
|
|
|
103
|
+
@dataclass
|
|
104
|
+
class ScoringWeights:
|
|
105
|
+
"""Optional composite-scoring weights for recall ranking.
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
semantic: Weight applied to semantic similarity (default: 1).
|
|
109
|
+
recency: Weight applied to recency decay (default: 0).
|
|
110
|
+
recency_half_life_days: Half-life for the recency term, in days (default: 30).
|
|
111
|
+
importance: Weight applied to memory importance (default: 0).
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
semantic: Optional[float] = None
|
|
115
|
+
recency: Optional[float] = None
|
|
116
|
+
recency_half_life_days: Optional[float] = None
|
|
117
|
+
importance: Optional[float] = None
|
|
118
|
+
|
|
119
|
+
def to_wire(self) -> Dict[str, float]:
|
|
120
|
+
"""Return the snake_case payload expected by the relayer."""
|
|
121
|
+
|
|
122
|
+
payload: Dict[str, float] = {}
|
|
123
|
+
if self.semantic is not None:
|
|
124
|
+
payload["semantic"] = self.semantic
|
|
125
|
+
if self.recency is not None:
|
|
126
|
+
payload["recency"] = self.recency
|
|
127
|
+
if self.recency_half_life_days is not None:
|
|
128
|
+
payload["recency_half_life_days"] = self.recency_half_life_days
|
|
129
|
+
if self.importance is not None:
|
|
130
|
+
payload["importance"] = self.importance
|
|
131
|
+
return payload
|
|
132
|
+
|
|
133
|
+
|
|
102
134
|
@dataclass
|
|
103
135
|
class AnalyzedFact:
|
|
104
136
|
"""A single extracted fact."""
|
|
@@ -212,11 +244,13 @@ class RecallManualOptions:
|
|
|
212
244
|
vector: Pre-computed query embedding vector.
|
|
213
245
|
limit: Max number of results (default: 10).
|
|
214
246
|
namespace: Namespace (default: config namespace or "default").
|
|
247
|
+
scoring_weights: Optional composite-scoring weights applied before returning hits.
|
|
215
248
|
"""
|
|
216
249
|
|
|
217
250
|
vector: List[float]
|
|
218
251
|
limit: int = 10
|
|
219
252
|
namespace: Optional[str] = None
|
|
253
|
+
scoring_weights: Optional[ScoringWeights] = None
|
|
220
254
|
|
|
221
255
|
|
|
222
256
|
@dataclass
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Walrus Memory — Shared Utilities
|
|
3
3
|
|
|
4
4
|
Crypto and encoding helpers for Ed25519 signing and SHA-256 hashing.
|
|
5
5
|
Uses PyNaCl (nacl.signing) as the primary Ed25519 implementation.
|
|
@@ -119,7 +119,7 @@ def build_signature_message(
|
|
|
119
119
|
path: URL path with query (e.g. ``"/api/remember"``).
|
|
120
120
|
body_sha256: SHA-256 hex digest of the JSON body string.
|
|
121
121
|
nonce: UUID v4 sent as the ``x-nonce`` header (required).
|
|
122
|
-
account_id:
|
|
122
|
+
account_id: Walrus Memory account object ID sent as ``x-account-id``
|
|
123
123
|
(required; empty string here will mismatch on server).
|
|
124
124
|
"""
|
|
125
125
|
return f"{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}"
|
|
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "memwal"
|
|
7
|
-
version = "0.1.
|
|
8
|
-
description = "Python SDK for
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [
|
|
13
|
-
{ name = "
|
|
13
|
+
{ name = "Walrus Memory Team" },
|
|
14
14
|
]
|
|
15
|
-
keywords = ["memwal", "ai", "memory", "ed25519", "privacy", "walrus", "sui"]
|
|
15
|
+
keywords = ["memwal", "walrus-memory", "ai", "memory", "ed25519", "privacy", "walrus", "sui"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Walrus Memory Python SDK — Test Runner
|
|
4
4
|
|
|
5
5
|
Runs all tests grouped by category and prints a clean summary.
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Usage:
|
|
|
8
8
|
python3 run_tests.py
|
|
9
9
|
|
|
10
10
|
With dev server (integration tests):
|
|
11
|
-
|
|
11
|
+
MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... MEMWAL_SERVER_URL=https://... python3 run_tests.py
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -27,7 +27,7 @@ DIM = "\033[2m"
|
|
|
27
27
|
RESET = "\033[0m"
|
|
28
28
|
|
|
29
29
|
SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai")
|
|
30
|
-
PRIVATE_KEY = os.environ.get("
|
|
30
|
+
PRIVATE_KEY = os.environ.get("MEMWAL_PRIVATE_KEY", "")
|
|
31
31
|
ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
|
|
32
32
|
HAS_KEY = bool(PRIVATE_KEY and ACCOUNT_ID)
|
|
33
33
|
|
|
@@ -77,7 +77,7 @@ def status(passed: int, failed: int) -> str:
|
|
|
77
77
|
|
|
78
78
|
def print_header() -> None:
|
|
79
79
|
print()
|
|
80
|
-
print(f"{BOLD}{CYAN}
|
|
80
|
+
print(f"{BOLD}{CYAN} Walrus Memory Python SDK — Test Suite{RESET}")
|
|
81
81
|
print(f" {DIM}Server: {SERVER_URL}{RESET}")
|
|
82
82
|
print(f" {DIM}Auth: {'✓ credentials set' if HAS_KEY else '✗ no key (integration tests skipped)'}{RESET}")
|
|
83
83
|
print()
|
|
@@ -192,7 +192,7 @@ def main() -> None:
|
|
|
192
192
|
print(f" {'─' * 78}")
|
|
193
193
|
|
|
194
194
|
if not HAS_KEY:
|
|
195
|
-
print(f"\n {YELLOW}Integration tests skipped — set
|
|
195
|
+
print(f"\n {YELLOW}Integration tests skipped — set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID to run them{RESET}")
|
|
196
196
|
print_totals(total_passed, total_failed, all_failures)
|
|
197
197
|
return
|
|
198
198
|
|
|
@@ -17,7 +17,7 @@ import pytest
|
|
|
17
17
|
import respx
|
|
18
18
|
|
|
19
19
|
from memwal.client import MemWal, MemWalCompatibilityError, MemWalError
|
|
20
|
-
from memwal.types import RecallManualOptions, RememberManualOptions
|
|
20
|
+
from memwal.types import RecallManualOptions, RememberManualOptions, ScoringWeights
|
|
21
21
|
from memwal.utils import build_signature_message, bytes_to_hex, sha256_hex
|
|
22
22
|
|
|
23
23
|
# ============================================================
|
|
@@ -95,7 +95,7 @@ def decode_seal_session_header(request: httpx.Request) -> dict[str, Any]:
|
|
|
95
95
|
|
|
96
96
|
@pytest.fixture
|
|
97
97
|
def memwal_client() -> MemWal:
|
|
98
|
-
"""Create a
|
|
98
|
+
"""Create a Walrus Memory client with a test key."""
|
|
99
99
|
return MemWal.create(
|
|
100
100
|
key=_TEST_KEY_HEX,
|
|
101
101
|
account_id=_TEST_ACCOUNT_ID,
|
|
@@ -290,6 +290,31 @@ class TestRecall:
|
|
|
290
290
|
assert "x-seal-session" in headers
|
|
291
291
|
assert "x-delegate-key" not in headers
|
|
292
292
|
|
|
293
|
+
@respx.mock
|
|
294
|
+
async def test_max_distance_filters_results(self, memwal_client: MemWal) -> None:
|
|
295
|
+
"""recall() should filter weak matches when max_distance is provided."""
|
|
296
|
+
mock_seal_session_prereqs()
|
|
297
|
+
route = respx.post(f"{_TEST_SERVER}/api/recall").mock(
|
|
298
|
+
return_value=httpx.Response(
|
|
299
|
+
200,
|
|
300
|
+
json={
|
|
301
|
+
"results": [
|
|
302
|
+
{"blob_id": "b1", "text": "I love coffee", "distance": 0.2},
|
|
303
|
+
{"blob_id": "b2", "text": "I live in Tokyo", "distance": 0.7},
|
|
304
|
+
],
|
|
305
|
+
"total": 2,
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
result = await memwal_client.recall("coffee", limit=10, max_distance=0.7)
|
|
311
|
+
|
|
312
|
+
body = json.loads(route.calls[0].request.content)
|
|
313
|
+
assert body["limit"] == 10
|
|
314
|
+
assert len(result.results) == 1
|
|
315
|
+
assert result.total == 1
|
|
316
|
+
assert result.results[0].blob_id == "b1"
|
|
317
|
+
|
|
293
318
|
@respx.mock
|
|
294
319
|
async def test_get_signed_request_uses_empty_body_hash_and_no_wire_body(
|
|
295
320
|
self, memwal_client: MemWal
|
|
@@ -302,7 +327,11 @@ class TestRecall:
|
|
|
302
327
|
)
|
|
303
328
|
)
|
|
304
329
|
|
|
305
|
-
result = await memwal_client.wait_for_remember_job(
|
|
330
|
+
result = await memwal_client.wait_for_remember_job(
|
|
331
|
+
"job-1",
|
|
332
|
+
poll_interval_ms=0,
|
|
333
|
+
timeout_ms=100,
|
|
334
|
+
)
|
|
306
335
|
|
|
307
336
|
request = route.calls[0].request
|
|
308
337
|
assert request.content == b""
|
|
@@ -341,6 +370,22 @@ class TestErrorHandling:
|
|
|
341
370
|
with pytest.raises(MemWalError, match="401"):
|
|
342
371
|
await memwal_client.remember("test")
|
|
343
372
|
|
|
373
|
+
@respx.mock
|
|
374
|
+
async def test_empty_401_uses_workshop_friendly_message(
|
|
375
|
+
self, memwal_client: MemWal
|
|
376
|
+
) -> None:
|
|
377
|
+
"""Empty-body auth failures should still give actionable guidance."""
|
|
378
|
+
mock_seal_session_prereqs()
|
|
379
|
+
respx.post(f"{_TEST_SERVER}/api/recall").mock(
|
|
380
|
+
return_value=httpx.Response(401, text="")
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
with pytest.raises(
|
|
384
|
+
MemWalError,
|
|
385
|
+
match="wrong private key.*account ID mismatch.*staging/mainnet mismatch",
|
|
386
|
+
):
|
|
387
|
+
await memwal_client.recall("test")
|
|
388
|
+
|
|
344
389
|
@respx.mock
|
|
345
390
|
async def test_500_raises_memwal_error(self, memwal_client: MemWal) -> None:
|
|
346
391
|
"""Server errors should raise MemWalError."""
|
|
@@ -542,6 +587,33 @@ class TestManualAPI:
|
|
|
542
587
|
assert len(result.results) == 1
|
|
543
588
|
assert result.results[0].blob_id == "b1"
|
|
544
589
|
|
|
590
|
+
@respx.mock
|
|
591
|
+
async def test_recall_manual_forwards_scoring_weights(self, memwal_client: MemWal) -> None:
|
|
592
|
+
_mock_version()
|
|
593
|
+
route = respx.post(f"{_TEST_SERVER}/api/recall/manual").mock(
|
|
594
|
+
return_value=httpx.Response(200, json={"results": [], "total": 0})
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
opts = RecallManualOptions(
|
|
598
|
+
vector=[0.1, 0.2, 0.3],
|
|
599
|
+
limit=5,
|
|
600
|
+
scoring_weights=ScoringWeights(
|
|
601
|
+
semantic=1.0,
|
|
602
|
+
recency=0.5,
|
|
603
|
+
recency_half_life_days=7.0,
|
|
604
|
+
importance=2.0,
|
|
605
|
+
),
|
|
606
|
+
)
|
|
607
|
+
await memwal_client.recall_manual(opts)
|
|
608
|
+
|
|
609
|
+
body = json.loads(route.calls[0].request.content)
|
|
610
|
+
assert body["scoring_weights"] == {
|
|
611
|
+
"semantic": 1.0,
|
|
612
|
+
"recency": 0.5,
|
|
613
|
+
"recency_half_life_days": 7.0,
|
|
614
|
+
"importance": 2.0,
|
|
615
|
+
}
|
|
616
|
+
|
|
545
617
|
|
|
546
618
|
class TestPublicKey:
|
|
547
619
|
async def test_get_public_key_hex(self, memwal_client: MemWal) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Integration tests for the
|
|
3
|
+
Integration tests for the Walrus Memory Python SDK against a live server.
|
|
4
4
|
|
|
5
5
|
Targets MEMWAL_SERVER_URL (default: https://relayer.dev.memwal.ai).
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ No-auth tests (always run, no env vars needed):
|
|
|
12
12
|
- Future timestamp → 401
|
|
13
13
|
- Unregistered key → SDK raises MemWalError
|
|
14
14
|
|
|
15
|
-
Authenticated tests (require
|
|
15
|
+
Authenticated tests (require MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID):
|
|
16
16
|
- remember() acceptance
|
|
17
17
|
- remember_and_wait()
|
|
18
18
|
- recall()
|
|
@@ -26,10 +26,10 @@ Usage:
|
|
|
26
26
|
python -m pytest tests/test_integration.py -v -m "not requires_key"
|
|
27
27
|
|
|
28
28
|
# Run full suite with real credentials
|
|
29
|
-
|
|
29
|
+
MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python -m pytest tests/test_integration.py -v
|
|
30
30
|
|
|
31
31
|
# Run against dev server using env vars
|
|
32
|
-
export
|
|
32
|
+
export MEMWAL_PRIVATE_KEY="944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261"
|
|
33
33
|
export MEMWAL_ACCOUNT_ID="0x70f9a6ff2df0ef6a9ecbfdc3f44c27c289ec3eb0cab5e10a5c07ca6165528565"
|
|
34
34
|
export MEMWAL_SERVER_URL="https://relayer.dev.memwal.ai"
|
|
35
35
|
python -m pytest tests/test_integration.py -v
|
|
@@ -53,14 +53,14 @@ from memwal.utils import build_signature_message, bytes_to_hex
|
|
|
53
53
|
# ── Config ───────────────────────────────────────────────────────────────────
|
|
54
54
|
|
|
55
55
|
SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai")
|
|
56
|
-
PRIVATE_KEY_HEX = os.environ.get("
|
|
56
|
+
PRIVATE_KEY_HEX = os.environ.get("MEMWAL_PRIVATE_KEY", "")
|
|
57
57
|
ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
|
|
58
58
|
|
|
59
59
|
HAS_KEY = bool(PRIVATE_KEY_HEX and ACCOUNT_ID)
|
|
60
60
|
|
|
61
61
|
requires_key = pytest.mark.skipif(
|
|
62
62
|
not HAS_KEY,
|
|
63
|
-
reason="
|
|
63
|
+
reason="MEMWAL_PRIVATE_KEY and MEMWAL_ACCOUNT_ID not set",
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -9,7 +9,7 @@ Covers:
|
|
|
9
9
|
- _format_memories: output format
|
|
10
10
|
- _inject_openai_memory: insertion position
|
|
11
11
|
|
|
12
|
-
No actual LLM or
|
|
12
|
+
No actual LLM or Walrus Memory server calls are made — all network traffic is mocked
|
|
13
13
|
with ``respx`` and all LLM responses are mocked with ``unittest.mock``.
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Local server (default) or remote relayer
|
|
2
|
-
MEMWAL_SERVER_URL=http://localhost:3001
|
|
3
|
-
# Ed25519 delegate private key (64-hex). Get from MemWal dashboard.
|
|
4
|
-
MEMWAL_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
|
|
5
|
-
# MemWalAccount object ID on Sui (the wallet's account)
|
|
6
|
-
MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
|
|
7
|
-
# Namespace for these test memories
|
|
8
|
-
MEMWAL_NAMESPACE=python-sdk-example
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|