memwal 0.1.11.dev11__tar.gz → 0.1.11.dev13__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.11.dev11 → memwal-0.1.11.dev13}/PKG-INFO +1 -1
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/__init__.py +1 -1
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/client.py +11 -6
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/mock.py +12 -5
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/types.py +4 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/utils.py +3 -1
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/pyproject.toml +1 -1
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_client.py +116 -1
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_mock.py +9 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_signing.py +9 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/.gitignore +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/CHANGELOG.md +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/README.md +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/.env.example +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/.gitignore +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/interactive_demo.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/examples/verify_credentials.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/compatibility.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/memwal/middleware.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/run_tests.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/__init__.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_auth_rejected_message.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_blank_text.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_env_presets.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_integration.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_list_namespaces.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_middleware.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_normalize_server_url.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_polling_delay.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_remember_derived_key.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_retry_after_http_date.py +0 -0
- {memwal-0.1.11.dev11 → memwal-0.1.11.dev13}/tests/test_transport_retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.11.
|
|
3
|
+
Version: 0.1.11.dev13
|
|
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
|
|
@@ -1055,6 +1055,8 @@ class MemWal:
|
|
|
1055
1055
|
blob_id=m["blob_id"],
|
|
1056
1056
|
text=m["text"],
|
|
1057
1057
|
distance=m["distance"],
|
|
1058
|
+
score=m.get("score"),
|
|
1059
|
+
created_at=m.get("created_at"),
|
|
1058
1060
|
)
|
|
1059
1061
|
for m in data.get("results", [])
|
|
1060
1062
|
]
|
|
@@ -1063,14 +1065,12 @@ class MemWal:
|
|
|
1063
1065
|
dropped = int(raw_dropped)
|
|
1064
1066
|
except (TypeError, ValueError):
|
|
1065
1067
|
dropped = 0
|
|
1068
|
+
total_count = data.get("total", len(memories))
|
|
1066
1069
|
if max_distance is not None:
|
|
1067
1070
|
memories = [m for m in memories if m.distance < max_distance]
|
|
1068
|
-
return RecallResult(
|
|
1069
|
-
results=memories, total=len(memories), dropped_count=dropped
|
|
1070
|
-
)
|
|
1071
1071
|
return RecallResult(
|
|
1072
1072
|
results=memories,
|
|
1073
|
-
total=
|
|
1073
|
+
total=total_count,
|
|
1074
1074
|
dropped_count=dropped,
|
|
1075
1075
|
)
|
|
1076
1076
|
|
|
@@ -1455,7 +1455,12 @@ class MemWal:
|
|
|
1455
1455
|
include_seal_session=False,
|
|
1456
1456
|
)
|
|
1457
1457
|
hits = [
|
|
1458
|
-
RecallManualHit(
|
|
1458
|
+
RecallManualHit(
|
|
1459
|
+
blob_id=h["blob_id"],
|
|
1460
|
+
distance=h["distance"],
|
|
1461
|
+
created_at=h.get("created_at"),
|
|
1462
|
+
importance=h.get("importance"),
|
|
1463
|
+
)
|
|
1459
1464
|
for h in data.get("results", [])
|
|
1460
1465
|
]
|
|
1461
1466
|
return RecallManualResult(results=hits, total=data.get("total", len(hits)))
|
|
@@ -1817,7 +1822,7 @@ class MemWalClockDriftError(MemWalError):
|
|
|
1817
1822
|
|
|
1818
1823
|
def _redact_internal_urls(text: str) -> str:
|
|
1819
1824
|
return re.sub(
|
|
1820
|
-
r"https?://(?:localhost|127\.0\.0\.
|
|
1825
|
+
r"https?://(?:(?:[\w-]+\.)*localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\]|0\.0\.0\.0)(?::\d+)?[^\s)\]>'\"]*",
|
|
1821
1826
|
"[internal]",
|
|
1822
1827
|
text,
|
|
1823
1828
|
flags=re.IGNORECASE,
|
|
@@ -278,14 +278,21 @@ class MemWalMock:
|
|
|
278
278
|
for memory in self._memories
|
|
279
279
|
if memory.namespace == resolved_namespace
|
|
280
280
|
]
|
|
281
|
-
if max_distance is not None:
|
|
282
|
-
ranked = [item for item in ranked if item[1] < max_distance]
|
|
283
281
|
ranked.sort(key=lambda item: (item[1], item[0].sequence))
|
|
282
|
+
page = ranked[:limit]
|
|
283
|
+
if max_distance is not None:
|
|
284
|
+
page = [item for item in page if item[1] < max_distance]
|
|
284
285
|
results = [
|
|
285
|
-
RecallMemory(
|
|
286
|
-
|
|
286
|
+
RecallMemory(
|
|
287
|
+
blob_id=memory.blob_id,
|
|
288
|
+
text=memory.text,
|
|
289
|
+
distance=score,
|
|
290
|
+
score=1.0 - score,
|
|
291
|
+
created_at=_mock_timestamp(memory.sequence),
|
|
292
|
+
)
|
|
293
|
+
for memory, score in page
|
|
287
294
|
]
|
|
288
|
-
return RecallResult(results=results, total=len(
|
|
295
|
+
return RecallResult(results=results, total=len(ranked[:limit]))
|
|
289
296
|
|
|
290
297
|
async def analyze(
|
|
291
298
|
self,
|
|
@@ -88,6 +88,8 @@ class RecallMemory:
|
|
|
88
88
|
blob_id: str
|
|
89
89
|
text: str
|
|
90
90
|
distance: float
|
|
91
|
+
score: Optional[float] = None
|
|
92
|
+
created_at: Optional[str] = None
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
@dataclass
|
|
@@ -328,6 +330,8 @@ class RecallManualHit:
|
|
|
328
330
|
|
|
329
331
|
blob_id: str
|
|
330
332
|
distance: float
|
|
333
|
+
created_at: Optional[str] = None
|
|
334
|
+
importance: Optional[float] = None
|
|
331
335
|
|
|
332
336
|
|
|
333
337
|
@dataclass
|
|
@@ -299,7 +299,9 @@ def normalize_private_key(key: str) -> str:
|
|
|
299
299
|
candidate = key.strip()
|
|
300
300
|
if candidate.lower().startswith("suiprivkey1"):
|
|
301
301
|
return bytes_to_hex(decode_sui_private_key(candidate))
|
|
302
|
-
|
|
302
|
+
if candidate.startswith(("0x", "0X")):
|
|
303
|
+
return candidate[2:]
|
|
304
|
+
return candidate
|
|
303
305
|
|
|
304
306
|
|
|
305
307
|
def encode_sui_private_key(seed_bytes: bytes) -> str:
|
|
@@ -841,6 +841,40 @@ class TestRecall:
|
|
|
841
841
|
assert result.results == []
|
|
842
842
|
assert result.dropped_count == 3
|
|
843
843
|
|
|
844
|
+
@respx.mock
|
|
845
|
+
async def test_recall_preserves_score_and_created_at(
|
|
846
|
+
self, memwal_client: MemWal
|
|
847
|
+
) -> None:
|
|
848
|
+
mock_seal_session_prereqs()
|
|
849
|
+
respx.post(f"{_TEST_SERVER}/api/recall").mock(
|
|
850
|
+
return_value=httpx.Response(
|
|
851
|
+
200,
|
|
852
|
+
json={
|
|
853
|
+
"results": [
|
|
854
|
+
{
|
|
855
|
+
"blob_id": "b1",
|
|
856
|
+
"text": "Fact with metadata",
|
|
857
|
+
"distance": 0.1,
|
|
858
|
+
"score": 0.95,
|
|
859
|
+
"created_at": "2026-03-30T12:00:00Z",
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
"blob_id": "b2",
|
|
863
|
+
"text": "Legacy fact",
|
|
864
|
+
"distance": 0.3,
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
"total": 2,
|
|
868
|
+
},
|
|
869
|
+
)
|
|
870
|
+
)
|
|
871
|
+
result = await memwal_client.recall("test")
|
|
872
|
+
assert len(result.results) == 2
|
|
873
|
+
assert result.results[0].score == 0.95
|
|
874
|
+
assert result.results[0].created_at == "2026-03-30T12:00:00Z"
|
|
875
|
+
assert result.results[1].score is None
|
|
876
|
+
assert result.results[1].created_at is None
|
|
877
|
+
|
|
844
878
|
@respx.mock
|
|
845
879
|
async def test_accepts_recall_params_object(
|
|
846
880
|
self, memwal_client: MemWal
|
|
@@ -935,9 +969,33 @@ class TestRecall:
|
|
|
935
969
|
body = json.loads(route.calls[0].request.content)
|
|
936
970
|
assert body["limit"] == 10
|
|
937
971
|
assert len(result.results) == 1
|
|
938
|
-
assert result.total ==
|
|
972
|
+
assert result.total == 2
|
|
939
973
|
assert result.results[0].blob_id == "b1"
|
|
940
974
|
|
|
975
|
+
@respx.mock
|
|
976
|
+
async def test_max_distance_filters_all_preserves_total(
|
|
977
|
+
self, memwal_client: MemWal
|
|
978
|
+
) -> None:
|
|
979
|
+
"""When all matches exceed max_distance, results is empty but total
|
|
980
|
+
preserves matches (#1066)."""
|
|
981
|
+
mock_seal_session_prereqs()
|
|
982
|
+
respx.post(f"{_TEST_SERVER}/api/recall").mock(
|
|
983
|
+
return_value=httpx.Response(
|
|
984
|
+
200,
|
|
985
|
+
json={
|
|
986
|
+
"results": [
|
|
987
|
+
{"blob_id": "b1", "text": "I love coffee", "distance": 0.8},
|
|
988
|
+
{"blob_id": "b2", "text": "I live in Tokyo", "distance": 0.9},
|
|
989
|
+
],
|
|
990
|
+
"total": 2,
|
|
991
|
+
},
|
|
992
|
+
)
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
result = await memwal_client.recall("coffee", limit=10, max_distance=0.5)
|
|
996
|
+
assert len(result.results) == 0
|
|
997
|
+
assert result.total == 2
|
|
998
|
+
|
|
941
999
|
@respx.mock
|
|
942
1000
|
async def test_get_signed_request_uses_empty_body_hash_and_no_wire_body(
|
|
943
1001
|
self, memwal_client: MemWal
|
|
@@ -1451,6 +1509,41 @@ class TestManualAPI:
|
|
|
1451
1509
|
assert len(result.results) == 1
|
|
1452
1510
|
assert result.results[0].blob_id == "b1"
|
|
1453
1511
|
|
|
1512
|
+
@respx.mock
|
|
1513
|
+
async def test_recall_manual_preserves_created_at_and_importance(
|
|
1514
|
+
self, memwal_client: MemWal
|
|
1515
|
+
) -> None:
|
|
1516
|
+
_mock_version()
|
|
1517
|
+
respx.post(f"{_TEST_SERVER}/api/recall/manual").mock(
|
|
1518
|
+
return_value=httpx.Response(
|
|
1519
|
+
200,
|
|
1520
|
+
json={
|
|
1521
|
+
"results": [
|
|
1522
|
+
{
|
|
1523
|
+
"blob_id": "b1",
|
|
1524
|
+
"distance": 0.15,
|
|
1525
|
+
"created_at": "2026-03-30T12:00:00Z",
|
|
1526
|
+
"importance": 0.85,
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
"blob_id": "b2",
|
|
1530
|
+
"distance": 0.4,
|
|
1531
|
+
},
|
|
1532
|
+
],
|
|
1533
|
+
"total": 2,
|
|
1534
|
+
},
|
|
1535
|
+
)
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
opts = RecallManualOptions(vector=[0.1, 0.2, 0.3], limit=5)
|
|
1539
|
+
result = await memwal_client.recall_manual(opts)
|
|
1540
|
+
|
|
1541
|
+
assert len(result.results) == 2
|
|
1542
|
+
assert result.results[0].created_at == "2026-03-30T12:00:00Z"
|
|
1543
|
+
assert result.results[0].importance == 0.85
|
|
1544
|
+
assert result.results[1].created_at is None
|
|
1545
|
+
assert result.results[1].importance is None
|
|
1546
|
+
|
|
1454
1547
|
@respx.mock
|
|
1455
1548
|
async def test_recall_manual_forwards_scoring_weights(self, memwal_client: MemWal) -> None:
|
|
1456
1549
|
_mock_version()
|
|
@@ -1583,3 +1676,25 @@ class TestSealSession:
|
|
|
1583
1676
|
assert exported["ttlMin"] == 5
|
|
1584
1677
|
assert exported["sessionKey"].startswith("suiprivkey")
|
|
1585
1678
|
assert exported["personalMessageSignature"]
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
class TestRedactInternalUrls:
|
|
1682
|
+
@pytest.mark.parametrize(
|
|
1683
|
+
"raw, expected",
|
|
1684
|
+
[
|
|
1685
|
+
("Error at http://localhost:8080/path", "Error at [internal]"),
|
|
1686
|
+
("Error at http://127.0.0.1:3000/api", "Error at [internal]"),
|
|
1687
|
+
("Error at http://127.0.0.2:8000/status", "Error at [internal]"),
|
|
1688
|
+
("Error at http://127.128.0.1:9000/info", "Error at [internal]"),
|
|
1689
|
+
("Error at http://[::1]:9000/seal/encrypt", "Error at [internal]"),
|
|
1690
|
+
("Error at http://0.0.0.0:8000/health", "Error at [internal]"),
|
|
1691
|
+
("Error at http://sidecar.localhost:8080/v1", "Error at [internal]"),
|
|
1692
|
+
("Error at http://sub.domain.localhost:3000/test", "Error at [internal]"),
|
|
1693
|
+
("Error at https://api.wal.app/v1/remember", "Error at https://api.wal.app/v1/remember"),
|
|
1694
|
+
],
|
|
1695
|
+
)
|
|
1696
|
+
def test_redact_internal_urls(self, raw: str, expected: str) -> None:
|
|
1697
|
+
from memwal.client import _redact_internal_urls
|
|
1698
|
+
|
|
1699
|
+
assert _redact_internal_urls(raw) == expected
|
|
1700
|
+
|
|
@@ -39,7 +39,11 @@ async def test_mock_remember_and_recall_are_deterministic_and_offline(monkeypatc
|
|
|
39
39
|
"I drink tea at night",
|
|
40
40
|
]
|
|
41
41
|
assert recalled.results[0].distance == 0
|
|
42
|
+
assert recalled.results[0].score == 1.0
|
|
43
|
+
assert recalled.results[0].created_at == "2026-01-01T00:00:01.000Z"
|
|
42
44
|
assert recalled.results[1].distance == 1
|
|
45
|
+
assert recalled.results[1].score == 0.0
|
|
46
|
+
assert recalled.results[1].created_at == "2026-01-01T00:00:02.000Z"
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
@pytest.mark.asyncio
|
|
@@ -57,6 +61,11 @@ async def test_mock_isolates_namespaces_and_honors_max_distance():
|
|
|
57
61
|
assert [memory.text for memory in alice.results] == ["Alice likes ramen"]
|
|
58
62
|
assert [memory.text for memory in bob.results] == ["Bob likes tacos"]
|
|
59
63
|
assert empty.total == 0
|
|
64
|
+
dropped = await mock.recall(
|
|
65
|
+
RecallParams(query="likes", namespace="user-a", max_distance=0)
|
|
66
|
+
)
|
|
67
|
+
assert dropped.results == []
|
|
68
|
+
assert dropped.total == 1
|
|
60
69
|
|
|
61
70
|
|
|
62
71
|
@pytest.mark.asyncio
|
|
@@ -324,11 +324,20 @@ class TestPrivateKeyFormats:
|
|
|
324
324
|
for form in (
|
|
325
325
|
self._SEED_HEX,
|
|
326
326
|
f"0x{self._SEED_HEX}",
|
|
327
|
+
f"0X{self._SEED_HEX}",
|
|
328
|
+
f" 0X{self._SEED_HEX} ",
|
|
327
329
|
encode_sui_private_key(seed),
|
|
328
330
|
f" {encode_sui_private_key(seed)} ",
|
|
329
331
|
):
|
|
330
332
|
assert normalize_private_key(form) == self._SEED_HEX
|
|
331
333
|
|
|
334
|
+
def test_uppercase_0x_prefix_key_produces_valid_client(self) -> None:
|
|
335
|
+
from memwal.client import MemWal
|
|
336
|
+
|
|
337
|
+
upper_0x = f"0X{self._SEED_HEX}"
|
|
338
|
+
client = MemWal.create(key=upper_0x, account_id="0xtest")
|
|
339
|
+
assert client._private_key_hex == self._SEED_HEX
|
|
340
|
+
|
|
332
341
|
def test_bech32_key_produces_the_same_client_identity(self) -> None:
|
|
333
342
|
from memwal.client import MemWal
|
|
334
343
|
from memwal.utils import encode_sui_private_key
|
|
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
|