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.
Files changed (27) hide show
  1. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/PKG-INFO +1 -1
  2. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/__init__.py +1 -1
  3. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/middleware.py +9 -13
  4. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/pyproject.toml +1 -1
  5. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_middleware.py +78 -0
  6. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/.gitignore +0 -0
  7. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/CHANGELOG.md +0 -0
  8. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/README.md +0 -0
  9. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/.env.example +0 -0
  10. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/.gitignore +0 -0
  11. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/async_remember_demo.py +0 -0
  12. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/interactive_demo.py +0 -0
  13. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/examples/verify_credentials.py +0 -0
  14. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/client.py +0 -0
  15. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/compatibility.py +0 -0
  16. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/mock.py +0 -0
  17. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/types.py +0 -0
  18. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/memwal/utils.py +0 -0
  19. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  20. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/run_tests.py +0 -0
  21. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/__init__.py +0 -0
  22. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_auth_rejected_message.py +0 -0
  23. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_client.py +0 -0
  24. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_env_presets.py +0 -0
  25. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_integration.py +0 -0
  26. {memwal-0.1.7.dev3 → memwal-0.1.7.dev4}/tests/test_mock.py +0 -0
  27. {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.dev3
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
@@ -122,4 +122,4 @@ __all__ = [
122
122
  "RecallManualResult",
123
123
  ]
124
124
 
125
- __version__ = "0.1.7.dev3"
125
+ __version__ = "0.1.7.dev4"
@@ -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
- # For sync generate, we inject memories synchronously via asyncio.run
410
- import asyncio
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
- try:
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.7.dev3"
7
+ version = "0.1.7.dev4"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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