memwal 0.1.1.dev0__tar.gz → 0.1.2.dev0__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.1.dev0 → memwal-0.1.2.dev0}/PKG-INFO +14 -10
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/README.md +13 -9
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/.env.example +4 -2
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/async_remember_demo.py +3 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/interactive_demo.py +3 -3
- memwal-0.1.2.dev0/examples/verify_credentials.py +67 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/__init__.py +1 -1
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/client.py +21 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/pyproject.toml +1 -1
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/run_tests.py +3 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_client.py +41 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_integration.py +5 -5
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/.gitignore +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/.gitignore +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/compatibility.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/middleware.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/types.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/utils.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/__init__.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_env_presets.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_middleware.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2.dev0
|
|
4
4
|
Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
|
|
5
5
|
Project-URL: Homepage, https://memwal.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.memwal.ai
|
|
@@ -62,11 +62,15 @@ pip install memwal[all] # Everything
|
|
|
62
62
|
Set your environment variables first:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
export
|
|
65
|
+
export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
|
|
66
66
|
export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
|
|
67
67
|
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
`MEMWAL_KEY` is still accepted as a backwards-compatibility alias, but new apps
|
|
71
|
+
should use `MEMWAL_PRIVATE_KEY` so it is clear that the delegate private key is
|
|
72
|
+
the server-side secret.
|
|
73
|
+
|
|
70
74
|
### Async (recommended)
|
|
71
75
|
|
|
72
76
|
```python
|
|
@@ -76,7 +80,7 @@ from memwal import MemWal
|
|
|
76
80
|
|
|
77
81
|
async def main():
|
|
78
82
|
memwal = MemWal.create(
|
|
79
|
-
key=os.environ["
|
|
83
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
80
84
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
81
85
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
82
86
|
)
|
|
@@ -86,7 +90,7 @@ async def main():
|
|
|
86
90
|
print(result.blob_id)
|
|
87
91
|
|
|
88
92
|
# Recall memories
|
|
89
|
-
matches = await memwal.recall("food allergies")
|
|
93
|
+
matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
|
|
90
94
|
for memory in matches.results:
|
|
91
95
|
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
92
96
|
|
|
@@ -107,7 +111,7 @@ import os
|
|
|
107
111
|
from memwal import MemWalSync
|
|
108
112
|
|
|
109
113
|
client = MemWalSync.create(
|
|
110
|
-
key=os.environ["
|
|
114
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
111
115
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
112
116
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
113
117
|
)
|
|
@@ -124,7 +128,7 @@ import os
|
|
|
124
128
|
from memwal import MemWal
|
|
125
129
|
|
|
126
130
|
async with MemWal.create(
|
|
127
|
-
key=os.environ["
|
|
131
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
128
132
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
129
133
|
) as memwal:
|
|
130
134
|
await memwal.remember("I prefer dark mode")
|
|
@@ -139,7 +143,7 @@ Same shorthand as the TypeScript SDK and MCP package.
|
|
|
139
143
|
from memwal import MemWal
|
|
140
144
|
|
|
141
145
|
memwal = MemWal.create(
|
|
142
|
-
key=os.environ["
|
|
146
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
143
147
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
144
148
|
env="prod", # prod | dev | staging | local
|
|
145
149
|
)
|
|
@@ -169,7 +173,7 @@ from memwal import with_memwal_langchain
|
|
|
169
173
|
llm = ChatOpenAI(model="gpt-4o")
|
|
170
174
|
smart_llm = with_memwal_langchain(
|
|
171
175
|
llm,
|
|
172
|
-
key=os.environ["
|
|
176
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
173
177
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
174
178
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
175
179
|
max_memories=5,
|
|
@@ -190,7 +194,7 @@ from memwal import with_memwal_openai
|
|
|
190
194
|
client = AsyncOpenAI()
|
|
191
195
|
smart_client = with_memwal_openai(
|
|
192
196
|
client,
|
|
193
|
-
key=os.environ["
|
|
197
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
194
198
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
195
199
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
196
200
|
)
|
|
@@ -213,7 +217,7 @@ Create a new async client.
|
|
|
213
217
|
| Method | Description |
|
|
214
218
|
|--------|-------------|
|
|
215
219
|
| `await remember(text, namespace?)` | Store a memory |
|
|
216
|
-
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
220
|
+
| `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
|
|
217
221
|
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
218
222
|
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
219
223
|
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
@@ -23,11 +23,15 @@ pip install memwal[all] # Everything
|
|
|
23
23
|
Set your environment variables first:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
export
|
|
26
|
+
export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
|
|
27
27
|
export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
|
|
28
28
|
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`MEMWAL_KEY` is still accepted as a backwards-compatibility alias, but new apps
|
|
32
|
+
should use `MEMWAL_PRIVATE_KEY` so it is clear that the delegate private key is
|
|
33
|
+
the server-side secret.
|
|
34
|
+
|
|
31
35
|
### Async (recommended)
|
|
32
36
|
|
|
33
37
|
```python
|
|
@@ -37,7 +41,7 @@ from memwal import MemWal
|
|
|
37
41
|
|
|
38
42
|
async def main():
|
|
39
43
|
memwal = MemWal.create(
|
|
40
|
-
key=os.environ["
|
|
44
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
41
45
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
42
46
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
43
47
|
)
|
|
@@ -47,7 +51,7 @@ async def main():
|
|
|
47
51
|
print(result.blob_id)
|
|
48
52
|
|
|
49
53
|
# Recall memories
|
|
50
|
-
matches = await memwal.recall("food allergies")
|
|
54
|
+
matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
|
|
51
55
|
for memory in matches.results:
|
|
52
56
|
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
53
57
|
|
|
@@ -68,7 +72,7 @@ import os
|
|
|
68
72
|
from memwal import MemWalSync
|
|
69
73
|
|
|
70
74
|
client = MemWalSync.create(
|
|
71
|
-
key=os.environ["
|
|
75
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
72
76
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
73
77
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
74
78
|
)
|
|
@@ -85,7 +89,7 @@ import os
|
|
|
85
89
|
from memwal import MemWal
|
|
86
90
|
|
|
87
91
|
async with MemWal.create(
|
|
88
|
-
key=os.environ["
|
|
92
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
89
93
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
90
94
|
) as memwal:
|
|
91
95
|
await memwal.remember("I prefer dark mode")
|
|
@@ -100,7 +104,7 @@ Same shorthand as the TypeScript SDK and MCP package.
|
|
|
100
104
|
from memwal import MemWal
|
|
101
105
|
|
|
102
106
|
memwal = MemWal.create(
|
|
103
|
-
key=os.environ["
|
|
107
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
104
108
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
105
109
|
env="prod", # prod | dev | staging | local
|
|
106
110
|
)
|
|
@@ -130,7 +134,7 @@ from memwal import with_memwal_langchain
|
|
|
130
134
|
llm = ChatOpenAI(model="gpt-4o")
|
|
131
135
|
smart_llm = with_memwal_langchain(
|
|
132
136
|
llm,
|
|
133
|
-
key=os.environ["
|
|
137
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
134
138
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
135
139
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
136
140
|
max_memories=5,
|
|
@@ -151,7 +155,7 @@ from memwal import with_memwal_openai
|
|
|
151
155
|
client = AsyncOpenAI()
|
|
152
156
|
smart_client = with_memwal_openai(
|
|
153
157
|
client,
|
|
154
|
-
key=os.environ["
|
|
158
|
+
key=os.environ["MEMWAL_PRIVATE_KEY"],
|
|
155
159
|
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
156
160
|
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
157
161
|
)
|
|
@@ -174,7 +178,7 @@ Create a new async client.
|
|
|
174
178
|
| Method | Description |
|
|
175
179
|
|--------|-------------|
|
|
176
180
|
| `await remember(text, namespace?)` | Store a memory |
|
|
177
|
-
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
181
|
+
| `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
|
|
178
182
|
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
179
183
|
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
180
184
|
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Local server (default) or remote relayer
|
|
2
|
-
MEMWAL_SERVER_URL=http://localhost:
|
|
2
|
+
MEMWAL_SERVER_URL=http://localhost:8000
|
|
3
3
|
# Ed25519 delegate private key (64-hex). Get from MemWal dashboard.
|
|
4
|
-
|
|
4
|
+
MEMWAL_PRIVATE_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
|
|
5
|
+
# Optional: paste the dashboard delegate public key so verification can catch mismatches.
|
|
6
|
+
MEMWAL_DELEGATE_PUBLIC_KEY=
|
|
5
7
|
# MemWalAccount object ID on Sui (the wallet's account)
|
|
6
8
|
MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
|
|
7
9
|
# Namespace for these test memories
|
|
@@ -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("MEMWAL_KEY")
|
|
68
|
+
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
|
|
69
|
+
key = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_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("MEMWAL_KEY")
|
|
97
|
+
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
|
|
98
|
+
key = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_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 os.environ.get("MEMWAL_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("MemWal 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
|
|
@@ -79,6 +79,11 @@ from .utils import (
|
|
|
79
79
|
T = TypeVar("T")
|
|
80
80
|
SEAL_SESSION_TTL_MIN = 5
|
|
81
81
|
SEAL_SESSION_SAFETY_MARGIN_MS = 30_000
|
|
82
|
+
AUTH_REJECTED_MESSAGE = (
|
|
83
|
+
"401 from relayer: typically wrong private key, key not registered on this "
|
|
84
|
+
"account, account ID mismatch, or staging/mainnet mismatch. Check .env.local "
|
|
85
|
+
"and dashboard credentials."
|
|
86
|
+
)
|
|
82
87
|
|
|
83
88
|
|
|
84
89
|
# ============================================================
|
|
@@ -472,6 +477,7 @@ class MemWal:
|
|
|
472
477
|
query: str,
|
|
473
478
|
limit: int = 10,
|
|
474
479
|
namespace: Optional[str] = None,
|
|
480
|
+
max_distance: Optional[float] = None,
|
|
475
481
|
) -> RecallResult:
|
|
476
482
|
"""Recall memories similar to a query.
|
|
477
483
|
|
|
@@ -481,6 +487,8 @@ class MemWal:
|
|
|
481
487
|
query: Search query.
|
|
482
488
|
limit: Max number of results (default: 10).
|
|
483
489
|
namespace: Override the default namespace.
|
|
490
|
+
max_distance: Optional client-side relevance threshold. Memories with
|
|
491
|
+
``distance >= max_distance`` are dropped.
|
|
484
492
|
|
|
485
493
|
Returns:
|
|
486
494
|
:class:`RecallResult` with decrypted text results.
|
|
@@ -498,6 +506,9 @@ class MemWal:
|
|
|
498
506
|
)
|
|
499
507
|
for m in data.get("results", [])
|
|
500
508
|
]
|
|
509
|
+
if max_distance is not None:
|
|
510
|
+
memories = [m for m in memories if m.distance < max_distance]
|
|
511
|
+
return RecallResult(results=memories, total=len(memories))
|
|
501
512
|
return RecallResult(results=memories, total=data.get("total", len(memories)))
|
|
502
513
|
|
|
503
514
|
async def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
|
|
@@ -991,7 +1002,10 @@ class _HttpStatusError(MemWalError):
|
|
|
991
1002
|
"""
|
|
992
1003
|
|
|
993
1004
|
def __init__(self, status: int, body: str) -> None:
|
|
994
|
-
|
|
1005
|
+
if status == 401:
|
|
1006
|
+
super().__init__(AUTH_REJECTED_MESSAGE)
|
|
1007
|
+
else:
|
|
1008
|
+
super().__init__(f"MemWal API error ({status}): {body}")
|
|
995
1009
|
self.status = status
|
|
996
1010
|
self.body = body
|
|
997
1011
|
|
|
@@ -1170,10 +1184,14 @@ class MemWalSync:
|
|
|
1170
1184
|
return self._run(self._inner.remember_bulk_and_wait(items, opts))
|
|
1171
1185
|
|
|
1172
1186
|
def recall(
|
|
1173
|
-
self,
|
|
1187
|
+
self,
|
|
1188
|
+
query: str,
|
|
1189
|
+
limit: int = 10,
|
|
1190
|
+
namespace: Optional[str] = None,
|
|
1191
|
+
max_distance: Optional[float] = None,
|
|
1174
1192
|
) -> RecallResult:
|
|
1175
1193
|
"""Synchronous version of :meth:`MemWal.recall`."""
|
|
1176
|
-
return self._run(self._inner.recall(query, limit, namespace))
|
|
1194
|
+
return self._run(self._inner.recall(query, limit, namespace, max_distance))
|
|
1177
1195
|
|
|
1178
1196
|
def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
|
|
1179
1197
|
"""Synchronous version of :meth:`MemWal.analyze`."""
|
|
@@ -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("MEMWAL_KEY", "")
|
|
30
|
+
PRIVATE_KEY = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_KEY", "")
|
|
31
31
|
ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
|
|
32
32
|
HAS_KEY = bool(PRIVATE_KEY and ACCOUNT_ID)
|
|
33
33
|
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -341,6 +366,22 @@ class TestErrorHandling:
|
|
|
341
366
|
with pytest.raises(MemWalError, match="401"):
|
|
342
367
|
await memwal_client.remember("test")
|
|
343
368
|
|
|
369
|
+
@respx.mock
|
|
370
|
+
async def test_empty_401_uses_workshop_friendly_message(
|
|
371
|
+
self, memwal_client: MemWal
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Empty-body auth failures should still give actionable guidance."""
|
|
374
|
+
mock_seal_session_prereqs()
|
|
375
|
+
respx.post(f"{_TEST_SERVER}/api/recall").mock(
|
|
376
|
+
return_value=httpx.Response(401, text="")
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
with pytest.raises(
|
|
380
|
+
MemWalError,
|
|
381
|
+
match="wrong private key.*account ID mismatch.*staging/mainnet mismatch",
|
|
382
|
+
):
|
|
383
|
+
await memwal_client.recall("test")
|
|
384
|
+
|
|
344
385
|
@respx.mock
|
|
345
386
|
async def test_500_raises_memwal_error(self, memwal_client: MemWal) -> None:
|
|
346
387
|
"""Server errors should raise MemWalError."""
|
|
@@ -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("MEMWAL_KEY", "")
|
|
56
|
+
PRIVATE_KEY_HEX = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_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 ───────────────────────────────────────────────────────────────────
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|