memwal 0.1.4.dev0__tar.gz → 0.1.4.dev1__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 (24) hide show
  1. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/.gitignore +2 -0
  2. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/CHANGELOG.md +10 -0
  3. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/PKG-INFO +18 -11
  4. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/README.md +17 -10
  5. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/.env.example +2 -2
  6. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/__init__.py +3 -3
  7. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/client.py +11 -7
  8. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/middleware.py +27 -7
  9. memwal-0.1.4.dev1/notebooks/walrus_memory_python_sdk.ipynb +804 -0
  10. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/pyproject.toml +4 -1
  11. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_client.py +25 -1
  12. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_integration.py +8 -6
  13. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_middleware.py +32 -0
  14. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_signing.py +3 -3
  15. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/.gitignore +0 -0
  16. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/async_remember_demo.py +0 -0
  17. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/interactive_demo.py +0 -0
  18. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/verify_credentials.py +0 -0
  19. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/compatibility.py +0 -0
  20. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/types.py +0 -0
  21. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/utils.py +0 -0
  22. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/run_tests.py +0 -0
  23. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/__init__.py +0 -0
  24. {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_env_presets.py +0 -0
@@ -60,6 +60,8 @@ share/python-wheels/
60
60
  # Jupyter
61
61
  .ipynb_checkpoints/
62
62
  *.ipynb
63
+ !notebooks/
64
+ !notebooks/walrus_memory_python_sdk.ipynb
63
65
 
64
66
  # Docs build
65
67
  site/
@@ -1,5 +1,15 @@
1
1
  # memwal
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, `MemWalSync`, health/compatibility checks, delegate public-key/address derivation, `remember`, `remember_async`, async job waiting, `recall`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, optional `ask`, `analyze`, `analyze_and_wait`, `embed`, manual methods with scoring weights, `restore`, optional OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting.
8
+
9
+ ### Fixed
10
+
11
+ - Fixed `MemWalSync` reuse inside notebooks so repeated calls do not reuse an HTTP transport from a closed event loop.
12
+
3
13
  ## 0.1.4
4
14
 
5
15
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memwal
3
- Version: 0.1.4.dev0
3
+ Version: 0.1.4.dev1
4
4
  Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memwal.ai
6
6
  Project-URL: Documentation, https://docs.memwal.ai
@@ -57,6 +57,10 @@ pip install memwal[openai] # OpenAI SDK support
57
57
  pip install memwal[all] # Everything
58
58
  ```
59
59
 
60
+ ## Try It In Colab
61
+
62
+ Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure `staging` configuration, optional `prod`, health checks, `remember`, `remember_async`, async job waiting, `recall`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting.
63
+
60
64
  ## Quick Start
61
65
 
62
66
  Set your environment variables first:
@@ -84,8 +88,8 @@ async def main():
84
88
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
85
89
  )
86
90
 
87
- # Store a memory
88
- result = await memwal.remember("I'm allergic to peanuts")
91
+ # Store a memory and wait until the background job is searchable
92
+ result = await memwal.remember_and_wait("I'm allergic to peanuts")
89
93
  print(result.blob_id)
90
94
 
91
95
  # Recall memories
@@ -93,8 +97,8 @@ async def main():
93
97
  for memory in matches.results:
94
98
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
95
99
 
96
- # Analyze conversation for facts
97
- analysis = await memwal.analyze("I love coffee and live in Tokyo")
100
+ # Analyze conversation for facts and wait until extracted facts are searchable
101
+ analysis = await memwal.analyze_and_wait("I love coffee and live in Tokyo")
98
102
  for fact in analysis.facts:
99
103
  print(fact.text)
100
104
 
@@ -115,7 +119,7 @@ client = MemWalSync.create(
115
119
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
116
120
  )
117
121
 
118
- result = client.remember("I'm allergic to peanuts")
122
+ result = client.remember_and_wait("I'm allergic to peanuts")
119
123
  matches = client.recall(RecallParams(query="food allergies"))
120
124
  client.close()
121
125
  ```
@@ -130,7 +134,7 @@ async with MemWal.create(
130
134
  key=os.environ["MEMWAL_PRIVATE_KEY"],
131
135
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
132
136
  ) as memwal:
133
- await memwal.remember("I prefer dark mode")
137
+ await memwal.remember_and_wait("I prefer dark mode")
134
138
  ```
135
139
 
136
140
  ## Environment Presets
@@ -144,16 +148,14 @@ from memwal import MemWal
144
148
  memwal = MemWal.create(
145
149
  key=os.environ["MEMWAL_PRIVATE_KEY"],
146
150
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
147
- env="prod", # prod | dev | staging | local
151
+ env="staging", # staging for testing, prod for production
148
152
  )
149
153
  ```
150
154
 
151
155
  | `env` | Relayer URL |
152
156
  |-------|-------------|
153
157
  | `prod` | `https://relayer.memwal.ai` |
154
- | `dev` | `https://relayer.dev.memwal.ai` |
155
158
  | `staging` | `https://relayer.staging.memwal.ai` |
156
- | `local` | `http://127.0.0.1:8000` |
157
159
 
158
160
  Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
159
161
  over the default. An unknown preset raises `ValueError`. `env` is also accepted
@@ -215,7 +217,12 @@ Create a new async client.
215
217
 
216
218
  | Method | Description |
217
219
  |--------|-------------|
218
- | `await remember(text, namespace?)` | Store a memory |
220
+ | `await remember(text, namespace?)` | Accept a background remember job and return `job_id` |
221
+ | `await wait_for_remember_job(job_id, ...)` | Poll one remember job until it is searchable |
222
+ | `await remember_and_wait(text, namespace?, ...)` | Store a memory and wait until it is searchable |
223
+ | `await remember_bulk(items)` | Accept several background remember jobs |
224
+ | `await wait_for_remember_jobs(job_ids, opts?)` | Poll several remember jobs together |
225
+ | `await remember_bulk_and_wait(items, opts?)` | Store several memories and wait for completion |
219
226
  | `await recall(RecallParams(query, limit?, namespace?, max_distance?))` | Search memories, optionally filtering by distance |
220
227
  | `await analyze(text, namespace?)` | Extract and store facts |
221
228
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
@@ -18,6 +18,10 @@ pip install memwal[openai] # OpenAI SDK support
18
18
  pip install memwal[all] # Everything
19
19
  ```
20
20
 
21
+ ## Try It In Colab
22
+
23
+ Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure `staging` configuration, optional `prod`, health checks, `remember`, `remember_async`, async job waiting, `recall`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting.
24
+
21
25
  ## Quick Start
22
26
 
23
27
  Set your environment variables first:
@@ -45,8 +49,8 @@ async def main():
45
49
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
46
50
  )
47
51
 
48
- # Store a memory
49
- result = await memwal.remember("I'm allergic to peanuts")
52
+ # Store a memory and wait until the background job is searchable
53
+ result = await memwal.remember_and_wait("I'm allergic to peanuts")
50
54
  print(result.blob_id)
51
55
 
52
56
  # Recall memories
@@ -54,8 +58,8 @@ async def main():
54
58
  for memory in matches.results:
55
59
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
56
60
 
57
- # Analyze conversation for facts
58
- analysis = await memwal.analyze("I love coffee and live in Tokyo")
61
+ # Analyze conversation for facts and wait until extracted facts are searchable
62
+ analysis = await memwal.analyze_and_wait("I love coffee and live in Tokyo")
59
63
  for fact in analysis.facts:
60
64
  print(fact.text)
61
65
 
@@ -76,7 +80,7 @@ client = MemWalSync.create(
76
80
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
77
81
  )
78
82
 
79
- result = client.remember("I'm allergic to peanuts")
83
+ result = client.remember_and_wait("I'm allergic to peanuts")
80
84
  matches = client.recall(RecallParams(query="food allergies"))
81
85
  client.close()
82
86
  ```
@@ -91,7 +95,7 @@ async with MemWal.create(
91
95
  key=os.environ["MEMWAL_PRIVATE_KEY"],
92
96
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
93
97
  ) as memwal:
94
- await memwal.remember("I prefer dark mode")
98
+ await memwal.remember_and_wait("I prefer dark mode")
95
99
  ```
96
100
 
97
101
  ## Environment Presets
@@ -105,16 +109,14 @@ from memwal import MemWal
105
109
  memwal = MemWal.create(
106
110
  key=os.environ["MEMWAL_PRIVATE_KEY"],
107
111
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
108
- env="prod", # prod | dev | staging | local
112
+ env="staging", # staging for testing, prod for production
109
113
  )
110
114
  ```
111
115
 
112
116
  | `env` | Relayer URL |
113
117
  |-------|-------------|
114
118
  | `prod` | `https://relayer.memwal.ai` |
115
- | `dev` | `https://relayer.dev.memwal.ai` |
116
119
  | `staging` | `https://relayer.staging.memwal.ai` |
117
- | `local` | `http://127.0.0.1:8000` |
118
120
 
119
121
  Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
120
122
  over the default. An unknown preset raises `ValueError`. `env` is also accepted
@@ -176,7 +178,12 @@ Create a new async client.
176
178
 
177
179
  | Method | Description |
178
180
  |--------|-------------|
179
- | `await remember(text, namespace?)` | Store a memory |
181
+ | `await remember(text, namespace?)` | Accept a background remember job and return `job_id` |
182
+ | `await wait_for_remember_job(job_id, ...)` | Poll one remember job until it is searchable |
183
+ | `await remember_and_wait(text, namespace?, ...)` | Store a memory and wait until it is searchable |
184
+ | `await remember_bulk(items)` | Accept several background remember jobs |
185
+ | `await wait_for_remember_jobs(job_ids, opts?)` | Poll several remember jobs together |
186
+ | `await remember_bulk_and_wait(items, opts?)` | Store several memories and wait for completion |
180
187
  | `await recall(RecallParams(query, limit?, namespace?, max_distance?))` | Search memories, optionally filtering by distance |
181
188
  | `await analyze(text, namespace?)` | Extract and store facts |
182
189
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
@@ -1,10 +1,10 @@
1
1
  # Local server (default) or remote relayer
2
2
  MEMWAL_SERVER_URL=http://localhost:8000
3
3
  # Ed25519 delegate private key (64-hex). Get from Walrus Memory dashboard.
4
- MEMWAL_PRIVATE_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
4
+ MEMWAL_PRIVATE_KEY=<your-ed25519-delegate-private-key-hex>
5
5
  # Optional: paste the dashboard delegate public key so verification can catch mismatches.
6
6
  MEMWAL_DELEGATE_PUBLIC_KEY=
7
7
  # Walrus Memory account object ID on Sui (the wallet's account)
8
- MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
8
+ MEMWAL_ACCOUNT_ID=0x-your-walrus-memory-account-id
9
9
  # Namespace for these test memories
10
10
  MEMWAL_NAMESPACE=python-sdk-example
@@ -13,13 +13,13 @@ Quick start::
13
13
  )
14
14
 
15
15
  # Async
16
- result = await memwal.remember("I love coffee")
16
+ result = await memwal.remember_and_wait("I love coffee")
17
17
  matches = await memwal.recall(RecallParams(query="beverage preferences"))
18
18
 
19
19
  # Sync wrapper
20
20
  from memwal import MemWalSync
21
21
  client = MemWalSync.create(key="...", account_id="0x...")
22
- result = client.remember("I love coffee")
22
+ result = client.remember_and_wait("I love coffee")
23
23
  """
24
24
 
25
25
  from .client import (
@@ -116,4 +116,4 @@ __all__ = [
116
116
  "RecallManualResult",
117
117
  ]
118
118
 
119
- __version__ = "0.1.4.dev0"
119
+ __version__ = "0.1.4.dev1"
@@ -177,7 +177,11 @@ def _occurred_at_to_wire(
177
177
  # shorthand; `fromisoformat` only accepts it on Python 3.11+,
178
178
  # so we normalise to "+00:00" before parsing for 3.9/3.10
179
179
  # compatibility.
180
- normalised = occurred_at.replace("Z", "+00:00", 1) if occurred_at.endswith("Z") else occurred_at
180
+ normalised = (
181
+ occurred_at.replace("Z", "+00:00", 1)
182
+ if occurred_at.endswith("Z")
183
+ else occurred_at
184
+ )
181
185
  try:
182
186
  parsed = datetime.fromisoformat(normalised)
183
187
  except ValueError as exc:
@@ -1251,6 +1255,12 @@ class MemWalSync:
1251
1255
  except RuntimeError:
1252
1256
  loop = None
1253
1257
 
1258
+ # Reset the httpx client before every asyncio.run() path so it is
1259
+ # recreated inside the loop that will use it. This matters in
1260
+ # notebooks/Jupyter where the sync wrapper runs coroutines in worker
1261
+ # threads with short-lived event loops.
1262
+ self._inner._client = None
1263
+
1254
1264
  if loop is not None and loop.is_running():
1255
1265
  # Already inside an event loop (e.g. Jupyter).
1256
1266
  # Create a new loop in a thread.
@@ -1259,12 +1269,6 @@ class MemWalSync:
1259
1269
  with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
1260
1270
  return pool.submit(asyncio.run, coro).result()
1261
1271
  else:
1262
- # Reset the httpx client before each asyncio.run() so it is
1263
- # recreated fresh inside the new event loop. Without this,
1264
- # reusing a MemWalSync instance across multiple calls raises
1265
- # "RuntimeError: Event loop is closed" because the client's
1266
- # transport is still bound to the previous (now-closed) loop.
1267
- self._inner._client = None
1268
1272
  return asyncio.run(coro)
1269
1273
 
1270
1274
  def remember(
@@ -128,6 +128,22 @@ def _fire_and_forget(coro: Any) -> None:
128
128
  thread.start()
129
129
 
130
130
 
131
+ def _run_blocking(coro_factory: Callable[[], Any]) -> Any:
132
+ """Run a coroutine factory from sync code, including notebooks."""
133
+ try:
134
+ loop = asyncio.get_running_loop()
135
+ except RuntimeError:
136
+ loop = None
137
+
138
+ if loop is not None and loop.is_running():
139
+ import concurrent.futures
140
+
141
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
142
+ return pool.submit(lambda: asyncio.run(coro_factory())).result()
143
+
144
+ return asyncio.run(coro_factory())
145
+
146
+
131
147
  # ============================================================
132
148
  # LangChain Integration
133
149
  # ============================================================
@@ -417,9 +433,12 @@ def _wrap_sync_openai(
417
433
  """Wrap a sync OpenAI client's chat.completions.create."""
418
434
  original_create = client.chat.completions.create
419
435
 
420
- def patched_create(*args: Any, **kwargs: Any) -> Any:
421
- import asyncio
436
+ def _run_memwal(coro_factory: Callable[[], Any]) -> Any:
437
+ # Keep httpx clients bound to the short-lived loop that uses them.
438
+ memwal._client = None
439
+ return _run_blocking(coro_factory)
422
440
 
441
+ def patched_create(*args: Any, **kwargs: Any) -> Any:
423
442
  messages = kwargs.get("messages") or (args[0] if args else None)
424
443
  if messages is None:
425
444
  return original_create(*args, **kwargs)
@@ -428,8 +447,8 @@ def _wrap_sync_openai(
428
447
  user_text = _find_last_user_message(messages)
429
448
  if user_text:
430
449
  try:
431
- recall_result = asyncio.run(
432
- memwal.recall(user_text, max_memories, namespace)
450
+ recall_result = _run_memwal(
451
+ lambda: memwal.recall(user_text, max_memories, namespace)
433
452
  )
434
453
  relevant = [
435
454
  m for m in recall_result.results
@@ -450,13 +469,14 @@ def _wrap_sync_openai(
450
469
 
451
470
  # Fire-and-forget analyze
452
471
  if auto_save and user_text:
453
- async def _analyze() -> None:
472
+ def _analyze() -> None:
454
473
  try:
455
- await memwal.analyze(user_text, namespace)
474
+ _run_memwal(lambda: memwal.analyze(user_text, namespace))
456
475
  except Exception as e:
457
476
  log(f"[Walrus Memory] Auto-save failed: {e}")
458
477
 
459
- _fire_and_forget(_analyze())
478
+ thread = threading.Thread(target=_analyze, daemon=True)
479
+ thread.start()
460
480
 
461
481
  return result
462
482