memwal 0.1.7.dev3__tar.gz → 0.1.7.dev4__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.7.dev3 → memwal-0.1.7.dev4}/PKG-INFO +1 -1
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/__init__.py +1 -1
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/middleware.py +9 -13
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/pyproject.toml +1 -1
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_middleware.py +78 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/.gitignore +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/CHANGELOG.md +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/README.md +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/.env.example +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/.gitignore +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/interactive_demo.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/verify_credentials.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/client.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/compatibility.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/mock.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/types.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/utils.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/run_tests.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/__init__.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_auth_rejected_message.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_client.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_env_presets.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_integration.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_mock.py +0 -0
- {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.7.
|
|
3
|
+
Version: 0.1.7.dev4
|
|
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
|
|
@@ -403,24 +403,20 @@ def with_memwal_langchain(
|
|
|
403
403
|
|
|
404
404
|
return result
|
|
405
405
|
|
|
406
|
+
def _run_memwal(coro_factory: Callable[[], Any]) -> Any:
|
|
407
|
+
# Keep httpx clients bound to the short-lived loop that uses them.
|
|
408
|
+
return _run_blocking(lambda: _with_fresh_http_client(memwal, coro_factory()))
|
|
409
|
+
|
|
406
410
|
def patched_generate(
|
|
407
411
|
messages: List[List[BaseMessage]], *args: Any, **kwargs: Any
|
|
408
412
|
) -> ChatResult:
|
|
409
|
-
#
|
|
410
|
-
|
|
411
|
-
|
|
413
|
+
# Inject via _run_blocking so recall still runs when a loop is already
|
|
414
|
+
# running (notebooks, async hosts) — the same helper the sync OpenAI
|
|
415
|
+
# wrapper uses. Passing the messages through untouched there made
|
|
416
|
+
# Walrus Memory look connected while recall silently never ran.
|
|
412
417
|
enriched = []
|
|
413
418
|
for msg_list in messages:
|
|
414
|
-
|
|
415
|
-
loop = asyncio.get_running_loop()
|
|
416
|
-
except RuntimeError:
|
|
417
|
-
loop = None
|
|
418
|
-
|
|
419
|
-
if loop is not None and loop.is_running():
|
|
420
|
-
# Already in async context -- cannot use asyncio.run
|
|
421
|
-
enriched.append(msg_list)
|
|
422
|
-
else:
|
|
423
|
-
enriched.append(asyncio.run(_inject_memories(msg_list)))
|
|
419
|
+
enriched.append(_run_memwal(lambda: _inject_memories(msg_list)))
|
|
424
420
|
|
|
425
421
|
result = original_generate(enriched, *args, **kwargs)
|
|
426
422
|
|
|
@@ -447,6 +447,84 @@ class TestWithMemWalLangChain:
|
|
|
447
447
|
await smart_llm._agenerate([[SystemMessage("only system")]])
|
|
448
448
|
assert not recall_route.called
|
|
449
449
|
|
|
450
|
+
def _capturing_generate(self, captured: list):
|
|
451
|
+
"""Replacement for llm._generate that records the batch it received."""
|
|
452
|
+
from langchain_core.messages import AIMessage
|
|
453
|
+
from langchain_core.outputs import ChatGeneration, ChatResult
|
|
454
|
+
|
|
455
|
+
def _generate(messages_batch, *a, **kw):
|
|
456
|
+
captured.extend(messages_batch)
|
|
457
|
+
return ChatResult(
|
|
458
|
+
generations=[ChatGeneration(message=AIMessage(content="ok"))]
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return _generate
|
|
462
|
+
|
|
463
|
+
@respx.mock
|
|
464
|
+
def test_sync_generate_injects_memories(self) -> None:
|
|
465
|
+
"""The plain sync path (no running loop) recalls and injects."""
|
|
466
|
+
_mock_seal_session_prereqs()
|
|
467
|
+
from langchain_core.messages import HumanMessage
|
|
468
|
+
|
|
469
|
+
llm = self._make_llm()
|
|
470
|
+
captured: list = []
|
|
471
|
+
llm._generate = self._capturing_generate(captured)
|
|
472
|
+
|
|
473
|
+
recall_route = respx.post(_RECALL_URL).mock(
|
|
474
|
+
return_value=_mock_recall([
|
|
475
|
+
{"blob_id": "b1", "text": "User loves coffee", "distance": 0.05}
|
|
476
|
+
])
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
smart_llm = with_memwal_langchain(
|
|
480
|
+
llm, key=_KEY_HEX, account_id=_ACCOUNT_ID, server_url=_SERVER, auto_save=False
|
|
481
|
+
)
|
|
482
|
+
smart_llm._generate([[HumanMessage("What do I drink?")]])
|
|
483
|
+
|
|
484
|
+
assert recall_route.called
|
|
485
|
+
assert any("User loves coffee" in m.content for m in captured[0])
|
|
486
|
+
|
|
487
|
+
@respx.mock
|
|
488
|
+
async def test_sync_generate_injects_memories_inside_running_loop(self) -> None:
|
|
489
|
+
"""GH #607: sync _generate must still recall when a loop is already
|
|
490
|
+
running (notebooks, async hosts).
|
|
491
|
+
|
|
492
|
+
It used to append the untouched msg_list in that branch, so callers got
|
|
493
|
+
a normal LLM answer with no memory context and no warning -- Walrus
|
|
494
|
+
Memory looked connected while recall never ran.
|
|
495
|
+
"""
|
|
496
|
+
_mock_seal_session_prereqs()
|
|
497
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
498
|
+
|
|
499
|
+
llm = self._make_llm()
|
|
500
|
+
captured: list = []
|
|
501
|
+
llm._generate = self._capturing_generate(captured)
|
|
502
|
+
|
|
503
|
+
recall_route = respx.post(_RECALL_URL).mock(
|
|
504
|
+
return_value=_mock_recall([
|
|
505
|
+
{"blob_id": "b1", "text": "User loves coffee", "distance": 0.05}
|
|
506
|
+
])
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
smart_llm = with_memwal_langchain(
|
|
510
|
+
llm, key=_KEY_HEX, account_id=_ACCOUNT_ID, server_url=_SERVER, auto_save=False
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# An async test body is itself a running loop -- the exact condition
|
|
514
|
+
# that used to disable injection.
|
|
515
|
+
assert asyncio.get_running_loop().is_running()
|
|
516
|
+
smart_llm._generate([[HumanMessage("What do I drink?")]])
|
|
517
|
+
|
|
518
|
+
assert recall_route.called, "recall was skipped inside a running loop"
|
|
519
|
+
assert len(captured) == 1
|
|
520
|
+
injected = captured[0]
|
|
521
|
+
assert len(injected) == 3, "guard + memory message were not injected"
|
|
522
|
+
assert any(
|
|
523
|
+
isinstance(m, HumanMessage) and "User loves coffee" in m.content
|
|
524
|
+
for m in injected
|
|
525
|
+
)
|
|
526
|
+
assert any(isinstance(m, SystemMessage) for m in injected)
|
|
527
|
+
|
|
450
528
|
def test_wraps_a_real_pydantic_backed_chat_model(self) -> None:
|
|
451
529
|
"""with_memwal_langchain must work on a real BaseChatModel, not just
|
|
452
530
|
a MagicMock. LangChain chat models are Pydantic models that reject
|
|
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
|