memwal 0.1.1.dev0__tar.gz → 0.1.2.dev1__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.dev1}/PKG-INFO +13 -10
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/README.md +12 -9
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/.env.example +4 -2
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/async_remember_demo.py +3 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/interactive_demo.py +3 -3
- memwal-0.1.2.dev1/examples/verify_credentials.py +67 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/__init__.py +1 -1
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/client.py +21 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/pyproject.toml +1 -1
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/run_tests.py +3 -3
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_client.py +41 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_integration.py +5 -5
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/.gitignore +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/.gitignore +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/compatibility.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/middleware.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/types.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/utils.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/__init__.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_env_presets.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_middleware.py +0 -0
- {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/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.dev1
|
|
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,14 @@ 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_PRIVATE_KEY` is the delegate private key from the MemWal 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 |
|
|
@@ -23,11 +23,14 @@ 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_PRIVATE_KEY` is the delegate private key from the MemWal 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 |
|
|
@@ -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("
|
|
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("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("
|
|
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
|
|
|
@@ -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("
|
|
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 ───────────────────────────────────────────────────────────────────
|
|
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
|