memwal 0.1.2.dev3__tar.gz → 0.1.3.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.
Files changed (23) hide show
  1. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/CHANGELOG.md +11 -0
  2. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/PKG-INFO +6 -6
  3. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/README.md +5 -5
  4. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/examples/async_remember_demo.py +2 -1
  5. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/examples/interactive_demo.py +3 -3
  6. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/memwal/__init__.py +4 -2
  7. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/memwal/client.py +70 -21
  8. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/memwal/types.py +18 -0
  9. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/pyproject.toml +1 -1
  10. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/tests/test_client.py +58 -1
  11. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/.gitignore +0 -0
  12. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/examples/.env.example +0 -0
  13. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/examples/.gitignore +0 -0
  14. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/examples/verify_credentials.py +0 -0
  15. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/memwal/compatibility.py +0 -0
  16. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/memwal/middleware.py +0 -0
  17. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/memwal/utils.py +0 -0
  18. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/run_tests.py +0 -0
  19. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/tests/__init__.py +0 -0
  20. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/tests/test_env_presets.py +0 -0
  21. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/tests/test_integration.py +0 -0
  22. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/tests/test_middleware.py +0 -0
  23. {memwal-0.1.2.dev3 → memwal-0.1.3.dev0}/tests/test_signing.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # memwal
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Added
6
+
7
+ - Added `RecallParams` for object-style `recall(...)` calls.
8
+
9
+ ### Changed
10
+
11
+ - Changed the default `restore()` limit from `50` to `10` to match the relayer and TypeScript SDK.
12
+ - Documented `restore()` response fields, default limit, pagination behavior, and performance expectations.
13
+
3
14
  ## 0.1.2
4
15
 
5
16
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memwal
3
- Version: 0.1.2.dev3
3
+ Version: 0.1.3.dev0
4
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
@@ -75,7 +75,7 @@ must stay server-side.
75
75
  ```python
76
76
  import asyncio
77
77
  import os
78
- from memwal import MemWal
78
+ from memwal import MemWal, RecallParams
79
79
 
80
80
  async def main():
81
81
  memwal = MemWal.create(
@@ -89,7 +89,7 @@ async def main():
89
89
  print(result.blob_id)
90
90
 
91
91
  # Recall memories
92
- matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
92
+ matches = await memwal.recall(RecallParams(query="food allergies", limit=10, max_distance=0.7))
93
93
  for memory in matches.results:
94
94
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
95
95
 
@@ -107,7 +107,7 @@ asyncio.run(main())
107
107
 
108
108
  ```python
109
109
  import os
110
- from memwal import MemWalSync
110
+ from memwal import MemWalSync, RecallParams
111
111
 
112
112
  client = MemWalSync.create(
113
113
  key=os.environ["MEMWAL_PRIVATE_KEY"],
@@ -116,7 +116,7 @@ client = MemWalSync.create(
116
116
  )
117
117
 
118
118
  result = client.remember("I'm allergic to peanuts")
119
- matches = client.recall("food allergies")
119
+ matches = client.recall(RecallParams(query="food allergies"))
120
120
  client.close()
121
121
  ```
122
122
 
@@ -216,7 +216,7 @@ Create a new async client.
216
216
  | Method | Description |
217
217
  |--------|-------------|
218
218
  | `await remember(text, namespace?)` | Store a memory |
219
- | `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
219
+ | `await recall(RecallParams(query, limit?, namespace?, max_distance?))` | Search memories, optionally filtering by distance |
220
220
  | `await analyze(text, namespace?)` | Extract and store facts |
221
221
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
222
222
  | `await restore(namespace, limit?)` | Restore a namespace |
@@ -36,7 +36,7 @@ must stay server-side.
36
36
  ```python
37
37
  import asyncio
38
38
  import os
39
- from memwal import MemWal
39
+ from memwal import MemWal, RecallParams
40
40
 
41
41
  async def main():
42
42
  memwal = MemWal.create(
@@ -50,7 +50,7 @@ async def main():
50
50
  print(result.blob_id)
51
51
 
52
52
  # Recall memories
53
- matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
53
+ matches = await memwal.recall(RecallParams(query="food allergies", limit=10, max_distance=0.7))
54
54
  for memory in matches.results:
55
55
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
56
56
 
@@ -68,7 +68,7 @@ asyncio.run(main())
68
68
 
69
69
  ```python
70
70
  import os
71
- from memwal import MemWalSync
71
+ from memwal import MemWalSync, RecallParams
72
72
 
73
73
  client = MemWalSync.create(
74
74
  key=os.environ["MEMWAL_PRIVATE_KEY"],
@@ -77,7 +77,7 @@ client = MemWalSync.create(
77
77
  )
78
78
 
79
79
  result = client.remember("I'm allergic to peanuts")
80
- matches = client.recall("food allergies")
80
+ matches = client.recall(RecallParams(query="food allergies"))
81
81
  client.close()
82
82
  ```
83
83
 
@@ -177,7 +177,7 @@ Create a new async client.
177
177
  | Method | Description |
178
178
  |--------|-------------|
179
179
  | `await remember(text, namespace?)` | Store a memory |
180
- | `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
180
+ | `await recall(RecallParams(query, limit?, namespace?, max_distance?))` | Search memories, optionally filtering by distance |
181
181
  | `await analyze(text, namespace?)` | Extract and store facts |
182
182
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
183
183
  | `await restore(namespace, limit?)` | Restore a namespace |
@@ -51,6 +51,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
51
51
  from memwal import ( # noqa: E402
52
52
  MemWal,
53
53
  MemWalRememberJobFailed,
54
+ RecallParams,
54
55
  RememberBulkItem,
55
56
  RememberBulkOptions,
56
57
  )
@@ -119,7 +120,7 @@ async def main() -> None:
119
120
 
120
121
  # 5. recall — confirm the memories are searchable
121
122
  _section("5. recall('coffee')")
122
- rc = await memwal.recall("coffee", limit=3)
123
+ rc = await memwal.recall(RecallParams(query="coffee", limit=3))
123
124
  print(f" found {len(rc.results)} / total={rc.total}")
124
125
  for m in rc.results[:3]:
125
126
  print(f" [{m.distance:.3f}] {m.text[:80]}")
@@ -39,7 +39,7 @@ def _load_env() -> None:
39
39
  _load_env()
40
40
  sys.path.insert(0, str(Path(__file__).parent.parent))
41
41
 
42
- from memwal import MemWal, RememberBulkItem, RememberBulkOptions # noqa: E402
42
+ from memwal import MemWal, RecallParams, RememberBulkItem, RememberBulkOptions # noqa: E402
43
43
 
44
44
  # Path to the running server's log so we can show what happens on the
45
45
  # server side as each SDK call is made.
@@ -164,7 +164,7 @@ async def main() -> None:
164
164
  print(f" → query='{query}'")
165
165
  off = _log_offset()
166
166
  t = _now_ms()
167
- rc = await memwal.recall(query, limit=3)
167
+ rc = await memwal.recall(RecallParams(query=query, limit=3))
168
168
  rec_ms = _now_ms() - t
169
169
  print(f" ← {rec_ms} ms {len(rc.results)} results")
170
170
  for m in rc.results:
@@ -199,7 +199,7 @@ async def main() -> None:
199
199
  # ───────────────────────────────────────────────────────────
200
200
  _hr("STEP 6 POST /api/recall query='japan'")
201
201
  off = _log_offset()
202
- rc2 = await memwal.recall("japan", limit=2)
202
+ rc2 = await memwal.recall(RecallParams(query="japan", limit=2))
203
203
  print(f" ← {len(rc2.results)} results")
204
204
  for m in rc2.results:
205
205
  print(f" [{m.distance:.3f}] {m.text}")
@@ -14,7 +14,7 @@ Quick start::
14
14
 
15
15
  # Async
16
16
  result = await memwal.remember("I love coffee")
17
- matches = await memwal.recall("beverage preferences")
17
+ matches = await memwal.recall(RecallParams(query="beverage preferences"))
18
18
 
19
19
  # Sync wrapper
20
20
  from memwal import MemWalSync
@@ -46,6 +46,7 @@ from .types import (
46
46
  RecallManualOptions,
47
47
  RecallManualResult,
48
48
  RecallMemory,
49
+ RecallParams,
49
50
  RecallResult,
50
51
  RememberAcceptedResult,
51
52
  RememberBulkAcceptedResult,
@@ -98,6 +99,7 @@ __all__ = [
98
99
  "RememberBulkStatusResult",
99
100
  "RememberBulkItemResult",
100
101
  "RememberBulkResult",
102
+ "RecallParams",
101
103
  "RecallResult",
102
104
  "RecallMemory",
103
105
  "EmbedResult",
@@ -114,4 +116,4 @@ __all__ = [
114
116
  "RecallManualResult",
115
117
  ]
116
118
 
117
- __version__ = "0.1.2.dev3"
119
+ __version__ = "0.1.3.dev0"
@@ -20,7 +20,7 @@ Example::
20
20
 
21
21
  # Async usage
22
22
  result = await memwal.remember("I'm allergic to peanuts")
23
- matches = await memwal.recall("food allergies")
23
+ matches = await memwal.recall(RecallParams(query="food allergies"))
24
24
  """
25
25
 
26
26
  from __future__ import annotations
@@ -49,6 +49,7 @@ from .types import (
49
49
  RecallManualOptions,
50
50
  RecallManualResult,
51
51
  RecallMemory,
52
+ RecallParams,
52
53
  RecallResult,
53
54
  RememberAcceptedResult,
54
55
  RememberBulkAcceptedResult,
@@ -473,7 +474,7 @@ class MemWal:
473
474
 
474
475
  async def recall(
475
476
  self,
476
- query: str,
477
+ query: "str | RecallParams",
477
478
  limit: int = 10,
478
479
  namespace: Optional[str] = None,
479
480
  max_distance: Optional[float] = None,
@@ -482,18 +483,40 @@ class MemWal:
482
483
 
483
484
  Server handles: verify -> embed query -> search -> Walrus download -> decrypt.
484
485
 
486
+ **Preferred call style** is to pass a :class:`RecallParams`
487
+ object so the call site reads self-describingly:
488
+
489
+ await client.recall(RecallParams(
490
+ query="food allergies", limit=5, namespace="profile"))
491
+
492
+ The legacy positional call ``client.recall(query, limit, namespace)``
493
+ is still supported but is easy to mis-read as
494
+ ``recall(query, namespace)``. Prefer kwargs at minimum.
495
+
485
496
  Args:
486
- query: Search query.
487
- limit: Max number of results (default: 10).
488
- namespace: Override the default namespace.
489
- max_distance: Optional client-side relevance threshold. Memories with
490
- ``distance >= max_distance`` are dropped.
497
+ query: Search query, or a :class:`RecallParams` carrying query +
498
+ limit + namespace + max_distance in one object.
499
+ limit: Max number of results (default: 10). Ignored when ``query``
500
+ is a :class:`RecallParams`.
501
+ namespace: Override the default namespace. Ignored when ``query``
502
+ is a :class:`RecallParams`.
503
+ max_distance: Optional client-side relevance threshold. Memories
504
+ with ``distance >= max_distance`` are dropped. Ignored when
505
+ ``query`` is a :class:`RecallParams`.
491
506
 
492
507
  Returns:
493
508
  :class:`RecallResult` with decrypted text results.
494
509
  """
510
+ if isinstance(query, RecallParams):
511
+ params = query
512
+ query_text = params.query
513
+ limit = params.limit
514
+ namespace = params.namespace
515
+ max_distance = params.max_distance
516
+ else:
517
+ query_text = query
495
518
  data = await self._signed_request("POST", "/api/recall", {
496
- "query": query,
519
+ "query": query_text,
497
520
  "limit": limit,
498
521
  "namespace": namespace or self._namespace,
499
522
  })
@@ -630,18 +653,42 @@ class MemWal:
630
653
  memories=memories,
631
654
  )
632
655
 
633
- async def restore(self, namespace: str, limit: int = 50) -> RestoreResult:
634
- """Restore a namespace.
656
+ async def restore(self, namespace: str, limit: int = 10) -> RestoreResult:
657
+ """Rebuild missing local index entries for ``namespace`` from Walrus.
658
+
659
+ The relayer queries Walrus for blobs the caller owns in ``namespace``,
660
+ ignores blobs already indexed locally, downloads the missing ones,
661
+ SEAL-decrypts them with the delegate key, re-embeds the plaintext, and
662
+ inserts a fresh vector row per blob.
663
+
664
+ Response semantics:
635
665
 
636
- Server downloads all blobs from Walrus, decrypts with delegate key,
637
- re-embeds, and re-indexes.
666
+ * ``restored`` blobs that completed the full
667
+ download decrypt → embed → DB insert pipeline this call.
668
+ * ``skipped`` — on-chain blobs already present in the local index
669
+ (no work needed). Decrypt / embed failures are dropped silently and
670
+ do **not** count as either restored or skipped.
671
+ * ``total`` — count of on-chain blobs the relayer saw for
672
+ ``(owner, namespace)`` before the limit was applied.
638
673
 
639
674
  Args:
640
- namespace: Namespace to restore.
641
- limit: Max entries to restore (default: 50).
675
+ namespace: Namespace to restore. Exact match — no prefix or
676
+ hierarchy semantics (see SKILL.md "Namespace Semantics").
677
+ limit: Max blobs the relayer will *inspect* this call (default:
678
+ 10, matches the server-side default and the TypeScript SDK).
679
+ The relayer fetches blobs newest-first and caps the work at
680
+ this number; it does **not** cap ``restored`` separately.
642
681
 
643
682
  Returns:
644
- :class:`RestoreResult` with count of restored entries.
683
+ :class:`RestoreResult`.
684
+
685
+ Notes:
686
+ * **No pagination cursor** — restore is single-shot. To rebuild a
687
+ large namespace, call repeatedly with a growing ``limit`` or
688
+ prune the local index first.
689
+ * **Performance** scales linearly in ``limit``: up to 10 Walrus
690
+ downloads in parallel, then 3 SEAL decrypts in parallel, then
691
+ embeddings. Expect seconds-per-blob on cold caches.
645
692
  """
646
693
  data = await self._signed_request("POST", "/api/restore", {
647
694
  "namespace": namespace,
@@ -1053,11 +1100,11 @@ class MemWalSync:
1053
1100
 
1054
1101
  Example::
1055
1102
 
1056
- from memwal import MemWalSync
1103
+ from memwal import MemWalSync, RecallParams
1057
1104
 
1058
1105
  client = MemWalSync.create(key="...", account_id="0x...")
1059
1106
  result = client.remember("I love coffee")
1060
- matches = client.recall("coffee preferences")
1107
+ matches = client.recall(RecallParams(query="coffee preferences"))
1061
1108
  """
1062
1109
 
1063
1110
  def __init__(self, inner: MemWal) -> None:
@@ -1189,12 +1236,13 @@ class MemWalSync:
1189
1236
 
1190
1237
  def recall(
1191
1238
  self,
1192
- query: str,
1239
+ query: "str | RecallParams",
1193
1240
  limit: int = 10,
1194
1241
  namespace: Optional[str] = None,
1195
1242
  max_distance: Optional[float] = None,
1196
1243
  ) -> RecallResult:
1197
- """Synchronous version of :meth:`MemWal.recall`."""
1244
+ """Synchronous version of :meth:`MemWal.recall` (accepts
1245
+ :class:`RecallParams` for the recommended object-style call)."""
1198
1246
  return self._run(self._inner.recall(query, limit, namespace, max_distance))
1199
1247
 
1200
1248
  def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
@@ -1218,8 +1266,9 @@ class MemWalSync:
1218
1266
  """Synchronous version of :meth:`MemWal.ask`."""
1219
1267
  return self._run(self._inner.ask(question, limit, namespace))
1220
1268
 
1221
- def restore(self, namespace: str, limit: int = 50) -> RestoreResult:
1222
- """Synchronous version of :meth:`MemWal.restore`."""
1269
+ def restore(self, namespace: str, limit: int = 10) -> RestoreResult:
1270
+ """Synchronous version of :meth:`MemWal.restore`. Default limit is 10
1271
+ (matches server + TypeScript SDK)."""
1223
1272
  return self._run(self._inner.restore(namespace, limit))
1224
1273
 
1225
1274
  def health(self) -> HealthResult:
@@ -92,6 +92,24 @@ class RecallMemory:
92
92
  distance: float
93
93
 
94
94
 
95
+ @dataclass
96
+ class RecallParams:
97
+ """Object-style input for :meth:`MemWal.recall`.
98
+
99
+ Preferred over positional args because positional ``recall(query, limit,
100
+ namespace)`` is easy to mis-read as ``recall(query, namespace)`` at call
101
+ sites. Construct this dataclass and pass it as the sole argument:
102
+
103
+ client.recall(RecallParams(query="food allergies", limit=5,
104
+ namespace="profile"))
105
+ """
106
+
107
+ query: str
108
+ limit: int = 10
109
+ namespace: Optional[str] = None
110
+ max_distance: Optional[float] = None
111
+
112
+
95
113
  @dataclass
96
114
  class RecallResult:
97
115
  """Result from recall()."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.2.dev3"
7
+ version = "0.1.3.dev0"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -17,7 +17,12 @@ import pytest
17
17
  import respx
18
18
 
19
19
  from memwal.client import MemWal, MemWalCompatibilityError, MemWalError
20
- from memwal.types import RecallManualOptions, RememberManualOptions, ScoringWeights
20
+ from memwal.types import (
21
+ RecallManualOptions,
22
+ RecallParams,
23
+ RememberManualOptions,
24
+ ScoringWeights,
25
+ )
21
26
  from memwal.utils import build_signature_message, bytes_to_hex, sha256_hex
22
27
 
23
28
  # ============================================================
@@ -270,6 +275,58 @@ class TestRecall:
270
275
  assert result.results[0].distance == 0.1
271
276
  assert result.results[1].blob_id == "b2"
272
277
 
278
+ @respx.mock
279
+ async def test_accepts_recall_params_object(
280
+ self, memwal_client: MemWal
281
+ ) -> None:
282
+ """recall(RecallParams(...)) sends the same request body as
283
+ the positional form, with query/limit/namespace pulled from the
284
+ dataclass instead of positional args."""
285
+ mock_seal_session_prereqs()
286
+ route = respx.post(f"{_TEST_SERVER}/api/recall").mock(
287
+ return_value=httpx.Response(
288
+ 200,
289
+ json={"results": [], "total": 0},
290
+ )
291
+ )
292
+
293
+ await memwal_client.recall(
294
+ RecallParams(query="coffee", limit=7, namespace="profile"),
295
+ )
296
+
297
+ assert route.called
298
+ body = json.loads(route.calls[0].request.content)
299
+ assert body["query"] == "coffee"
300
+ assert body["limit"] == 7
301
+ assert body["namespace"] == "profile"
302
+
303
+ @respx.mock
304
+ async def test_recall_params_object_applies_max_distance_filter(
305
+ self, memwal_client: MemWal
306
+ ) -> None:
307
+ """max_distance on the RecallParams dataclass drops weak matches
308
+ client-side, matching the positional max_distance kwarg behavior."""
309
+ mock_seal_session_prereqs()
310
+ respx.post(f"{_TEST_SERVER}/api/recall").mock(
311
+ return_value=httpx.Response(
312
+ 200,
313
+ json={
314
+ "results": [
315
+ {"blob_id": "b1", "text": "tight", "distance": 0.1},
316
+ {"blob_id": "b2", "text": "noisy", "distance": 0.9},
317
+ ],
318
+ "total": 2,
319
+ },
320
+ )
321
+ )
322
+
323
+ result = await memwal_client.recall(
324
+ RecallParams(query="x", limit=10, max_distance=0.5),
325
+ )
326
+
327
+ assert len(result.results) == 1
328
+ assert result.results[0].blob_id == "b1"
329
+
273
330
  @respx.mock
274
331
  async def test_sends_correct_headers(self, memwal_client: MemWal) -> None:
275
332
  """recall() should include all required auth headers."""
File without changes
File without changes
File without changes