memwal 0.2.2.dev6__tar.gz → 0.2.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.
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/CHANGELOG.md +7 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/PKG-INFO +1 -1
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/__init__.py +3 -1
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/client.py +70 -4
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/middleware.py +9 -3
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/mock.py +14 -1
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/types.py +3 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/pyproject.toml +1 -1
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_client.py +53 -4
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_mock.py +16 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/.gitignore +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/README.md +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/examples/.env.example +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/examples/.gitignore +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/examples/async_remember_demo.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/examples/interactive_demo.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/examples/verify_credentials.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/compatibility.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/memwal/utils.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/run_tests.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/__init__.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_ask_validation.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_auth_rejected_message.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_blank_text.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_destroy_and_cancel_shield.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_empty_namespace.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_env_presets.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_forget.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_integration.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_json_body.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_list_namespaces.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_middleware.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_missing_response_fields.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_nonfinite_validation.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_normalize_server_url.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_polling_delay.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_recall_validation.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_recall_weights.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_remember_derived_key.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_retry_after_http_date.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_signing.py +0 -0
- {memwal-0.2.2.dev6 → memwal-0.2.3.dev0}/tests/test_transport_retry.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# memwal
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `RecallResult` gains `degraded: bool = False`, set to `True` whenever one or more vector matches were dropped, and `recall()` returns the degraded result (`results=[]`, `degraded=True`, `dropped_count`) by default when all matches fail to download or decrypt. (#1036, #1070)
|
|
8
|
+
- `recall()` and `RecallParams` accept `on_dropped: str = "ignore"` (`"ignore"` or `"throw"`) across `MemWal`, `MemWalSync`, `MemWalMock`, and `MemWalMockSync`. Passing `on_dropped="throw"` raises `MemWalRecallAllDropped` (`code = "RECALL_ALL_DROPPED"`, exposing `total`, `dropped_count`, and `namespace`) when all vector search matches are dropped during Walrus blob download or SEAL decryption. (#1036, #1070)
|
|
9
|
+
|
|
3
10
|
## 0.2.2
|
|
4
11
|
|
|
5
12
|
Unreleased.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
@@ -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.
|
|
137
|
+
__version__ = "0.2.3.dev0"
|
|
@@ -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":
|
|
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
|
-
|
|
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(
|
|
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`."""
|
|
@@ -659,7 +659,9 @@ def with_memwal_langchain(
|
|
|
659
659
|
return messages
|
|
660
660
|
|
|
661
661
|
try:
|
|
662
|
-
recall_result = await memwal.recall(
|
|
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(
|
|
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(
|
|
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
|
|
@@ -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(
|
|
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(
|
|
@@ -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
|
|
@@ -25,6 +25,7 @@ from memwal.client import (
|
|
|
25
25
|
MemWalCompatibilityError,
|
|
26
26
|
MemWalError,
|
|
27
27
|
MemWalRateLimited,
|
|
28
|
+
MemWalRecallAllDropped,
|
|
28
29
|
MemWalRememberJobNotFound,
|
|
29
30
|
MemWalRememberJobTimeout,
|
|
30
31
|
MemWalSync,
|
|
@@ -830,7 +831,9 @@ class TestRecall:
|
|
|
830
831
|
assert result.results[1].blob_id == "b2"
|
|
831
832
|
|
|
832
833
|
@respx.mock
|
|
833
|
-
async def
|
|
834
|
+
async def test_recall_returns_degraded_result_by_default_and_raises_on_throw(
|
|
835
|
+
self, memwal_client: MemWal
|
|
836
|
+
) -> None:
|
|
834
837
|
mock_seal_session_prereqs()
|
|
835
838
|
respx.post(f"{_TEST_SERVER}/api/recall").mock(
|
|
836
839
|
return_value=httpx.Response(
|
|
@@ -838,9 +841,54 @@ class TestRecall:
|
|
|
838
841
|
json={"results": [], "total": 0, "dropped_count": 3},
|
|
839
842
|
)
|
|
840
843
|
)
|
|
841
|
-
|
|
842
|
-
assert
|
|
843
|
-
assert
|
|
844
|
+
omitted = await memwal_client.recall("coffee")
|
|
845
|
+
assert omitted.results == []
|
|
846
|
+
assert omitted.dropped_count == 3
|
|
847
|
+
assert omitted.total == 0
|
|
848
|
+
assert omitted.degraded is True
|
|
849
|
+
|
|
850
|
+
omitted_params = await memwal_client.recall(RecallParams(query="coffee"))
|
|
851
|
+
assert omitted_params.results == []
|
|
852
|
+
assert omitted_params.dropped_count == 3
|
|
853
|
+
assert omitted_params.total == 0
|
|
854
|
+
assert omitted_params.degraded is True
|
|
855
|
+
|
|
856
|
+
explicit_ignore = await memwal_client.recall(
|
|
857
|
+
RecallParams(query="coffee", on_dropped="ignore")
|
|
858
|
+
)
|
|
859
|
+
assert explicit_ignore.results == []
|
|
860
|
+
assert explicit_ignore.dropped_count == 3
|
|
861
|
+
assert explicit_ignore.total == 0
|
|
862
|
+
assert explicit_ignore.degraded is True
|
|
863
|
+
|
|
864
|
+
with pytest.raises(MemWalRecallAllDropped) as exc_info:
|
|
865
|
+
await memwal_client.recall("coffee", on_dropped="throw")
|
|
866
|
+
assert exc_info.value.code == "RECALL_ALL_DROPPED"
|
|
867
|
+
assert exc_info.value.dropped_count == 3
|
|
868
|
+
assert exc_info.value.total == 3
|
|
869
|
+
assert exc_info.value.namespace == "default"
|
|
870
|
+
|
|
871
|
+
with pytest.raises(MemWalRecallAllDropped) as params_exc:
|
|
872
|
+
await memwal_client.recall(
|
|
873
|
+
RecallParams(query="coffee", namespace="profile", on_dropped="throw")
|
|
874
|
+
)
|
|
875
|
+
assert params_exc.value.code == "RECALL_ALL_DROPPED"
|
|
876
|
+
assert params_exc.value.dropped_count == 3
|
|
877
|
+
assert params_exc.value.total == 3
|
|
878
|
+
assert params_exc.value.namespace == "profile"
|
|
879
|
+
|
|
880
|
+
sync_client = MemWalSync(memwal_client)
|
|
881
|
+
sync_default = sync_client.recall("coffee")
|
|
882
|
+
assert sync_default.results == []
|
|
883
|
+
assert sync_default.dropped_count == 3
|
|
884
|
+
assert sync_default.total == 0
|
|
885
|
+
assert sync_default.degraded is True
|
|
886
|
+
|
|
887
|
+
with pytest.raises(MemWalRecallAllDropped):
|
|
888
|
+
sync_client.recall("coffee", on_dropped="throw")
|
|
889
|
+
|
|
890
|
+
with pytest.raises(ValueError, match="on_dropped"):
|
|
891
|
+
await memwal_client.recall("coffee", on_dropped="warn")
|
|
844
892
|
|
|
845
893
|
@respx.mock
|
|
846
894
|
async def test_recall_preserves_dropped_count_with_max_distance(
|
|
@@ -866,6 +914,7 @@ class TestRecall:
|
|
|
866
914
|
assert [m.blob_id for m in result.results] == ["b1"]
|
|
867
915
|
assert result.total == 2
|
|
868
916
|
assert result.dropped_count == 25
|
|
917
|
+
assert result.degraded is True
|
|
869
918
|
|
|
870
919
|
@respx.mock
|
|
871
920
|
async def test_recall_preserves_score_and_created_at(
|
|
@@ -8,6 +8,7 @@ import re
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
10
|
from memwal import (
|
|
11
|
+
MemWal,
|
|
11
12
|
MemWalMock,
|
|
12
13
|
MemWalMockSeed,
|
|
13
14
|
MemWalMockSync,
|
|
@@ -202,6 +203,7 @@ def test_sync_mock_polling_and_analyze_signatures_match_production():
|
|
|
202
203
|
"remember_and_wait",
|
|
203
204
|
"wait_for_remember_jobs",
|
|
204
205
|
"remember_bulk_and_wait",
|
|
206
|
+
"recall",
|
|
205
207
|
"analyze",
|
|
206
208
|
"analyze_and_wait",
|
|
207
209
|
"forget_namespace",
|
|
@@ -217,6 +219,20 @@ def test_sync_mock_polling_and_analyze_signatures_match_production():
|
|
|
217
219
|
assert parameter_contract(getattr(MemWalMockSync, method_name)) == parameter_contract(
|
|
218
220
|
getattr(MemWalSync, method_name)
|
|
219
221
|
)
|
|
222
|
+
assert parameter_contract(MemWalMock.recall) == parameter_contract(MemWal.recall)
|
|
223
|
+
assert inspect.signature(MemWalMock.recall).parameters["on_dropped"].default == "ignore"
|
|
224
|
+
assert RecallParams(query="x").on_dropped == "ignore"
|
|
225
|
+
|
|
226
|
+
async_mock = MemWalMock.create(
|
|
227
|
+
namespace="sync",
|
|
228
|
+
initial_memories=[MemWalMockSeed(text="coffee preference", namespace="sync")],
|
|
229
|
+
)
|
|
230
|
+
sync_mock = MemWalMockSync(async_mock)
|
|
231
|
+
assert sync_mock.recall("coffee").total == 1
|
|
232
|
+
assert sync_mock.recall("coffee", on_dropped="ignore").total == 1
|
|
233
|
+
assert sync_mock.recall("coffee", on_dropped="throw").total == 1
|
|
234
|
+
with pytest.raises(ValueError, match="on_dropped"):
|
|
235
|
+
sync_mock.recall("coffee", on_dropped="warn")
|
|
220
236
|
|
|
221
237
|
|
|
222
238
|
@pytest.mark.asyncio
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|