memwal 0.2.2.dev6__py3-none-any.whl → 0.2.3.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
memwal/__init__.py CHANGED
@@ -29,6 +29,7 @@ from .client import (
29
29
  MemWalError,
30
30
  MemWalJsonBodyError,
31
31
  MemWalRateLimited,
32
+ MemWalRecallAllDropped,
32
33
  MemWalRememberJobFailed,
33
34
  MemWalRememberJobNotFound,
34
35
  MemWalRememberJobTimeout,
@@ -87,6 +88,7 @@ __all__ = [
87
88
  "MemWalClockDriftError",
88
89
  "MemWalJsonBodyError",
89
90
  "MemWalRateLimited",
91
+ "MemWalRecallAllDropped",
90
92
  "MemWalRememberJobFailed",
91
93
  "MemWalRememberJobNotFound",
92
94
  "MemWalRememberJobTimeout",
@@ -132,4 +134,4 @@ __all__ = [
132
134
  "RecallManualResult",
133
135
  ]
134
136
 
135
- __version__ = "0.2.2.dev6"
137
+ __version__ = "0.2.3.dev0"
memwal/client.py CHANGED
@@ -453,6 +453,15 @@ def _check_limit(limit: Any) -> None:
453
453
  raise ValueError(f"limit must be a positive integer, got {limit!r}")
454
454
 
455
455
 
456
+ def _check_on_dropped(on_dropped: Any) -> str:
457
+ """Validate the recall all-dropped handling policy before signing."""
458
+ if on_dropped not in ("throw", "ignore"):
459
+ raise ValueError(
460
+ f"on_dropped must be 'throw' or 'ignore', got {on_dropped!r}"
461
+ )
462
+ return str(on_dropped)
463
+
464
+
456
465
  def _check_vector(vector: Any) -> None:
457
466
  """Reject a vector that is not a non-empty list of finite numbers (#1218).
458
467
 
@@ -1175,6 +1184,7 @@ class MemWal:
1175
1184
  limit: int = 10,
1176
1185
  namespace: Optional[str] = None,
1177
1186
  max_distance: Optional[float] = None,
1187
+ on_dropped: str = "ignore",
1178
1188
  ) -> RecallResult:
1179
1189
  """Recall memories similar to a query.
1180
1190
 
@@ -1200,6 +1210,11 @@ class MemWal:
1200
1210
  max_distance: Optional client-side relevance threshold. Memories
1201
1211
  with ``distance >= max_distance`` are dropped. Ignored when
1202
1212
  ``query`` is a :class:`RecallParams`.
1213
+ on_dropped: Policy when every matched memory failed download or
1214
+ decrypt (``"ignore"`` by default returns the degraded
1215
+ :class:`RecallResult` with ``degraded=True`` and
1216
+ ``dropped_count``; ``"throw"`` raises
1217
+ :class:`MemWalRecallAllDropped`).
1203
1218
 
1204
1219
  Returns:
1205
1220
  :class:`RecallResult` with decrypted text results.
@@ -1214,15 +1229,18 @@ class MemWal:
1214
1229
  max_distance = params.max_distance
1215
1230
  scoring_weights = params.scoring_weights
1216
1231
  sort = params.sort
1232
+ on_dropped = params.on_dropped
1217
1233
  else:
1218
1234
  query_text = query
1219
1235
  _reject_blank_text(query_text, "query cannot be empty")
1220
1236
  _check_limit(limit)
1221
1237
  validate_max_distance(max_distance)
1238
+ policy = _check_on_dropped(on_dropped)
1239
+ resolved_namespace = self._resolve_namespace(namespace)
1222
1240
  body = {
1223
1241
  "query": query_text,
1224
1242
  "limit": limit,
1225
- "namespace": self._resolve_namespace(namespace),
1243
+ "namespace": resolved_namespace,
1226
1244
  }
1227
1245
  if scoring_weights is not None:
1228
1246
  body["scoring_weights"] = scoring_weights.to_wire()
@@ -1241,16 +1259,34 @@ class MemWal:
1241
1259
  ]
1242
1260
  raw_dropped = data.get("dropped_count") or 0
1243
1261
  try:
1244
- dropped = int(raw_dropped)
1262
+ dropped = max(0, int(raw_dropped))
1245
1263
  except (TypeError, ValueError):
1246
1264
  dropped = 0
1247
- total_count = data.get("total", len(memories))
1265
+ degraded = bool(data.get("degraded")) or dropped > 0
1266
+ raw_total = data.get("total", len(memories))
1267
+ try:
1268
+ total_count = int(raw_total)
1269
+ except (TypeError, ValueError):
1270
+ total_count = len(memories)
1271
+
1272
+ if len(memories) == 0 and degraded:
1273
+ effective_dropped = dropped if dropped > 0 else max(1, total_count)
1274
+ effective_total = max(total_count, effective_dropped)
1275
+ if policy == "throw":
1276
+ raise MemWalRecallAllDropped(
1277
+ dropped_count=effective_dropped,
1278
+ total=effective_total,
1279
+ namespace=resolved_namespace,
1280
+ )
1281
+ dropped = effective_dropped
1282
+
1248
1283
  if max_distance is not None:
1249
1284
  memories = [m for m in memories if m.distance < max_distance]
1250
1285
  return RecallResult(
1251
1286
  results=memories,
1252
1287
  total=total_count,
1253
1288
  dropped_count=dropped,
1289
+ degraded=degraded,
1254
1290
  )
1255
1291
 
1256
1292
  async def forget(self, namespace: Optional[str] = None) -> ForgetResult:
@@ -2311,6 +2347,27 @@ class MemWalRememberJobTimeout(MemWalError):
2311
2347
  self.last_refusal = last_refusal
2312
2348
 
2313
2349
 
2350
+ class MemWalRecallAllDropped(MemWalError):
2351
+ """Raised by :meth:`MemWal.recall` when ``on_dropped="throw"`` is set and
2352
+ every matched memory failed to download or decrypt (``results == []`` and
2353
+ ``dropped_count > 0``).
2354
+
2355
+ By default (``on_dropped="ignore"``), :meth:`MemWal.recall` returns the
2356
+ degraded :class:`RecallResult` without raising.
2357
+ """
2358
+
2359
+ def __init__(self, dropped_count: int, total: int, namespace: str) -> None:
2360
+ noun = "match" if total == 1 else "matches"
2361
+ super().__init__(
2362
+ f'MemWal recall matched {total} {noun} in namespace "{namespace}", '
2363
+ f"but all {dropped_count} failed to download or decrypt"
2364
+ )
2365
+ self.code = "RECALL_ALL_DROPPED"
2366
+ self.dropped_count = dropped_count
2367
+ self.total = total
2368
+ self.namespace = namespace
2369
+
2370
+
2314
2371
  async def _discard_http_client(memwal: MemWal) -> None:
2315
2372
  """Close the cached httpx client, and drop it either way.
2316
2373
 
@@ -2502,10 +2559,19 @@ class MemWalSync:
2502
2559
  limit: int = 10,
2503
2560
  namespace: Optional[str] = None,
2504
2561
  max_distance: Optional[float] = None,
2562
+ on_dropped: str = "ignore",
2505
2563
  ) -> RecallResult:
2506
2564
  """Synchronous version of :meth:`MemWal.recall` (accepts
2507
2565
  :class:`RecallParams` for the recommended object-style call)."""
2508
- return self._run(self._inner.recall(query, limit, namespace, max_distance))
2566
+ return self._run(
2567
+ self._inner.recall(
2568
+ query,
2569
+ limit,
2570
+ namespace,
2571
+ max_distance,
2572
+ on_dropped=on_dropped,
2573
+ )
2574
+ )
2509
2575
 
2510
2576
  def forget(self, namespace: Optional[str] = None) -> ForgetResult:
2511
2577
  """Synchronous version of :meth:`MemWal.forget`."""
memwal/middleware.py CHANGED
@@ -659,7 +659,9 @@ def with_memwal_langchain(
659
659
  return messages
660
660
 
661
661
  try:
662
- recall_result = await memwal.recall(user_text, max_memories, namespace)
662
+ recall_result = await memwal.recall(
663
+ user_text, max_memories, namespace, on_dropped="throw"
664
+ )
663
665
  relevant = [
664
666
  m for m in recall_result.results
665
667
  if (1 - m.distance) >= min_relevance
@@ -837,7 +839,9 @@ def _wrap_async_openai(
837
839
  user_text = _find_last_user_message(messages)
838
840
  if user_text:
839
841
  try:
840
- recall_result = await memwal.recall(user_text, max_memories, namespace)
842
+ recall_result = await memwal.recall(
843
+ user_text, max_memories, namespace, on_dropped="throw"
844
+ )
841
845
  relevant = [
842
846
  m for m in recall_result.results
843
847
  if (1 - m.distance) >= min_relevance
@@ -915,7 +919,9 @@ def _wrap_sync_openai(
915
919
  if user_text:
916
920
  try:
917
921
  recall_result = _run_memwal(
918
- lambda: memwal.recall(user_text, max_memories, namespace)
922
+ lambda: memwal.recall(
923
+ user_text, max_memories, namespace, on_dropped="throw"
924
+ )
919
925
  )
920
926
  relevant = [
921
927
  m for m in recall_result.results
memwal/mock.py CHANGED
@@ -267,6 +267,7 @@ class MemWalMock:
267
267
  limit: int = 10,
268
268
  namespace: Optional[str] = None,
269
269
  max_distance: Optional[float] = None,
270
+ on_dropped: str = "ignore",
270
271
  ) -> RecallResult:
271
272
  """Token-overlap recall over the in-memory store.
272
273
 
@@ -281,6 +282,7 @@ class MemWalMock:
281
282
  namespace = query.namespace
282
283
  max_distance = query.max_distance
283
284
  sort = query.sort
285
+ on_dropped = query.on_dropped
284
286
  query_text = query.query
285
287
  else:
286
288
  sort = None
@@ -288,6 +290,10 @@ class MemWalMock:
288
290
  _validate_text(query_text, "query")
289
291
  _check_limit(limit)
290
292
  validate_max_distance(max_distance)
293
+ if on_dropped not in ("throw", "ignore"):
294
+ raise ValueError(
295
+ f"on_dropped must be 'throw' or 'ignore', got {on_dropped!r}"
296
+ )
291
297
  if sort not in (None, "relevance", "recent"):
292
298
  raise ValueError("sort must be 'relevance' or 'recent'")
293
299
  resolved_namespace = self._namespace if namespace is None else namespace
@@ -670,9 +676,16 @@ class MemWalMockSync:
670
676
  limit: int = 10,
671
677
  namespace: Optional[str] = None,
672
678
  max_distance: Optional[float] = None,
679
+ on_dropped: str = "ignore",
673
680
  ) -> RecallResult:
674
681
  return self._run(
675
- self._inner.recall(query, limit, namespace, max_distance)
682
+ self._inner.recall(
683
+ query,
684
+ limit,
685
+ namespace,
686
+ max_distance,
687
+ on_dropped=on_dropped,
688
+ )
676
689
  )
677
690
 
678
691
  def analyze(
memwal/types.py CHANGED
@@ -121,6 +121,7 @@ class RecallParams:
121
121
  max_distance: Optional[float] = None
122
122
  scoring_weights: Optional["ScoringWeights"] = None
123
123
  sort: Optional[str] = None
124
+ on_dropped: str = "ignore"
124
125
 
125
126
 
126
127
  @dataclass
@@ -132,6 +133,8 @@ class RecallResult:
132
133
  # Matches omitted because download or decrypt failed. 0 also means the
133
134
  # relayer omitted the field (older servers).
134
135
  dropped_count: int = 0
136
+ # True whenever one or more matched blobs were dropped (dropped_count > 0).
137
+ degraded: bool = False
135
138
 
136
139
 
137
140
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.2.2.dev6
3
+ Version: 0.2.3.dev0
4
4
  Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memory.walrus.xyz
6
6
  Project-URL: Documentation, https://memory.walrus.xyz
@@ -0,0 +1,10 @@
1
+ memwal/__init__.py,sha256=qms4CbnYe-3LFoMW15Af_aByF0pWXs0yp0vsz2cV8Ts,3290
2
+ memwal/client.py,sha256=R6Btr_iUaChkoTHo0zJaqQiPM-Anz__5MRBfRS_ABpQ,104586
3
+ memwal/compatibility.py,sha256=R3Gt1gVRjYSLNa78leNrTpGMZI1UATYo7N8Ie_44CGI,3054
4
+ memwal/middleware.py,sha256=n_kbXDO4kLqtR8ctPInAzdsorl7jTGg9rnOwhRA4aH4,38603
5
+ memwal/mock.py,sha256=0YBmxeixQ6lTjtiYRRTev3ZEw-x5-2lyWCeI6LYK6dM,25282
6
+ memwal/types.py,sha256=03e91D9rFKQesa7q7oYRIBZJ_Ru96yxGzhLaOkY742Q,14282
7
+ memwal/utils.py,sha256=RYlELBpKUHN9RuvWQu0cSGMoyXpAdTWlNKawfDpDbQ0,13361
8
+ memwal-0.2.3.dev0.dist-info/METADATA,sha256=PKU67DhcdIRBfVqGyMg149fJIfcG_cGyzeiJw2o_To8,12151
9
+ memwal-0.2.3.dev0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ memwal-0.2.3.dev0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- memwal/__init__.py,sha256=dSoujKelLWb8QaOZtr9vDVQSpxy6OZ8vXRY4z7gK1Rc,3232
2
- memwal/client.py,sha256=eCp7wnvWryITN_Z3fw_NwBV1u1_mlU_mVrdBhLUoltM,102076
3
- memwal/compatibility.py,sha256=R3Gt1gVRjYSLNa78leNrTpGMZI1UATYo7N8Ie_44CGI,3054
4
- memwal/middleware.py,sha256=z2xdbvaobBg4byJXnrX7amWdVYE4jfeO4t31DoDBv_o,38413
5
- memwal/mock.py,sha256=oG0znsO-gekoCkQ0mNgT1kjQwP1ku8TjQEkaNkbRDlQ,24878
6
- memwal/types.py,sha256=jekUFnpGhVezU_VavY4DEzty6EWb0GxAwspmszk5Cdc,14144
7
- memwal/utils.py,sha256=RYlELBpKUHN9RuvWQu0cSGMoyXpAdTWlNKawfDpDbQ0,13361
8
- memwal-0.2.2.dev6.dist-info/METADATA,sha256=lqyLcVerCRkvXJXvEYG24Zh7ZiOE0ugIbV9rTXingHM,12151
9
- memwal-0.2.2.dev6.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
- memwal-0.2.2.dev6.dist-info/RECORD,,