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.
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/.gitignore +2 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/CHANGELOG.md +10 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/PKG-INFO +18 -11
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/README.md +17 -10
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/.env.example +2 -2
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/__init__.py +3 -3
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/client.py +11 -7
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/middleware.py +27 -7
- memwal-0.1.4.dev1/notebooks/walrus_memory_python_sdk.ipynb +804 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/pyproject.toml +4 -1
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_client.py +25 -1
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_integration.py +8 -6
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_middleware.py +32 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_signing.py +3 -3
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/.gitignore +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/interactive_demo.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/examples/verify_credentials.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/compatibility.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/types.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/memwal/utils.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/run_tests.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/__init__.py +0 -0
- {memwal-0.1.4.dev0 → memwal-0.1.4.dev1}/tests/test_env_presets.py +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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="
|
|
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?)` |
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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="
|
|
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?)` |
|
|
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
|
|
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=
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
421
|
-
|
|
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 =
|
|
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
|
-
|
|
472
|
+
def _analyze() -> None:
|
|
454
473
|
try:
|
|
455
|
-
|
|
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
|
-
|
|
478
|
+
thread = threading.Thread(target=_analyze, daemon=True)
|
|
479
|
+
thread.start()
|
|
460
480
|
|
|
461
481
|
return result
|
|
462
482
|
|