memwal 0.1.0__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.0/.gitignore +65 -0
- memwal-0.1.0/PKG-INFO +237 -0
- memwal-0.1.0/README.md +198 -0
- memwal-0.1.0/examples/.env.example +8 -0
- memwal-0.1.0/examples/.gitignore +2 -0
- memwal-0.1.0/examples/async_remember_demo.py +179 -0
- memwal-0.1.0/examples/interactive_demo.py +215 -0
- memwal-0.1.0/memwal/__init__.py +113 -0
- memwal-0.1.0/memwal/client.py +1158 -0
- memwal-0.1.0/memwal/middleware.py +484 -0
- memwal-0.1.0/memwal/types.py +374 -0
- memwal-0.1.0/memwal/utils.py +290 -0
- memwal-0.1.0/pyproject.toml +63 -0
- memwal-0.1.0/run_tests.py +320 -0
- memwal-0.1.0/tests/__init__.py +0 -0
- memwal-0.1.0/tests/test_client.py +572 -0
- memwal-0.1.0/tests/test_env_presets.py +71 -0
- memwal-0.1.0/tests/test_integration.py +371 -0
- memwal-0.1.0/tests/test_middleware.py +582 -0
- memwal-0.1.0/tests/test_signing.py +274 -0
memwal-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
wheels/
|
|
12
|
+
*.whl
|
|
13
|
+
.eggs/
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
ENV/
|
|
20
|
+
|
|
21
|
+
# Test / coverage
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
coverage.xml
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
|
|
29
|
+
# Type checking
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.dmypy.json
|
|
32
|
+
dmypy.json
|
|
33
|
+
.pyright/
|
|
34
|
+
|
|
35
|
+
# Linting
|
|
36
|
+
.ruff_cache/
|
|
37
|
+
|
|
38
|
+
# Distribution / packaging
|
|
39
|
+
MANIFEST
|
|
40
|
+
pip-wheel-metadata/
|
|
41
|
+
share/python-wheels/
|
|
42
|
+
|
|
43
|
+
# IDE
|
|
44
|
+
.vscode/
|
|
45
|
+
.idea/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
*~
|
|
49
|
+
|
|
50
|
+
# macOS
|
|
51
|
+
.DS_Store
|
|
52
|
+
.AppleDouble
|
|
53
|
+
.LSOverride
|
|
54
|
+
|
|
55
|
+
# Env files
|
|
56
|
+
.env
|
|
57
|
+
.env.*
|
|
58
|
+
!.env.example
|
|
59
|
+
|
|
60
|
+
# Jupyter
|
|
61
|
+
.ipynb_checkpoints/
|
|
62
|
+
*.ipynb
|
|
63
|
+
|
|
64
|
+
# Docs build
|
|
65
|
+
site/
|
memwal-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memwal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
|
|
5
|
+
Project-URL: Homepage, https://memwal.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.memwal.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/MystenLabs/MemWal
|
|
8
|
+
Author: MemWal Team
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai,ed25519,memory,memwal,privacy,sui,walrus
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: pynacl>=1.5.0
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'all'
|
|
27
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
34
|
+
Provides-Extra: langchain
|
|
35
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
|
|
36
|
+
Provides-Extra: openai
|
|
37
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# memwal
|
|
41
|
+
|
|
42
|
+
Python SDK for [MemWal](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
|
|
43
|
+
|
|
44
|
+
All data processing (encryption, embedding, Walrus storage) happens server-side in a TEE. The SDK signs requests with your Ed25519 delegate key and sends text over HTTPS.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install memwal
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With optional integrations:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install memwal[langchain] # LangChain support
|
|
56
|
+
pip install memwal[openai] # OpenAI SDK support
|
|
57
|
+
pip install memwal[all] # Everything
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
Set your environment variables first:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export MEMWAL_KEY="your-ed25519-delegate-key-hex"
|
|
66
|
+
export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
|
|
67
|
+
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Async (recommended)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
import os
|
|
75
|
+
from memwal import MemWal
|
|
76
|
+
|
|
77
|
+
async def main():
|
|
78
|
+
memwal = MemWal.create(
|
|
79
|
+
key=os.environ["MEMWAL_KEY"],
|
|
80
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
81
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Store a memory
|
|
85
|
+
result = await memwal.remember("I'm allergic to peanuts")
|
|
86
|
+
print(result.blob_id)
|
|
87
|
+
|
|
88
|
+
# Recall memories
|
|
89
|
+
matches = await memwal.recall("food allergies")
|
|
90
|
+
for memory in matches.results:
|
|
91
|
+
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
92
|
+
|
|
93
|
+
# Analyze conversation for facts
|
|
94
|
+
analysis = await memwal.analyze("I love coffee and live in Tokyo")
|
|
95
|
+
for fact in analysis.facts:
|
|
96
|
+
print(fact.text)
|
|
97
|
+
|
|
98
|
+
await memwal.close()
|
|
99
|
+
|
|
100
|
+
asyncio.run(main())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Sync
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import os
|
|
107
|
+
from memwal import MemWalSync
|
|
108
|
+
|
|
109
|
+
client = MemWalSync.create(
|
|
110
|
+
key=os.environ["MEMWAL_KEY"],
|
|
111
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
112
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
result = client.remember("I'm allergic to peanuts")
|
|
116
|
+
matches = client.recall("food allergies")
|
|
117
|
+
client.close()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Context Manager
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import os
|
|
124
|
+
from memwal import MemWal
|
|
125
|
+
|
|
126
|
+
async with MemWal.create(
|
|
127
|
+
key=os.environ["MEMWAL_KEY"],
|
|
128
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
129
|
+
) as memwal:
|
|
130
|
+
await memwal.remember("I prefer dark mode")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Environment Presets
|
|
134
|
+
|
|
135
|
+
Instead of hardcoding a relayer URL, pass `env` to target a hosted relayer.
|
|
136
|
+
Same shorthand as the TypeScript SDK and MCP package.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from memwal import MemWal
|
|
140
|
+
|
|
141
|
+
memwal = MemWal.create(
|
|
142
|
+
key=os.environ["MEMWAL_KEY"],
|
|
143
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
144
|
+
env="prod", # prod | dev | staging | local
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| `env` | Relayer URL |
|
|
149
|
+
|-------|-------------|
|
|
150
|
+
| `prod` | `https://relayer.memwal.ai` |
|
|
151
|
+
| `dev` | `https://relayer.dev.memwal.ai` |
|
|
152
|
+
| `staging` | `https://relayer.staging.memwal.ai` |
|
|
153
|
+
| `local` | `http://127.0.0.1:8000` |
|
|
154
|
+
|
|
155
|
+
Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
|
|
156
|
+
over the default. An unknown preset raises `ValueError`. `env` is also accepted
|
|
157
|
+
by `MemWalSync.create`, `with_memwal_langchain`, and `with_memwal_openai`.
|
|
158
|
+
|
|
159
|
+
## AI Middleware
|
|
160
|
+
|
|
161
|
+
### LangChain
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import os
|
|
165
|
+
from langchain_openai import ChatOpenAI
|
|
166
|
+
from langchain_core.messages import HumanMessage
|
|
167
|
+
from memwal import with_memwal_langchain
|
|
168
|
+
|
|
169
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
170
|
+
smart_llm = with_memwal_langchain(
|
|
171
|
+
llm,
|
|
172
|
+
key=os.environ["MEMWAL_KEY"],
|
|
173
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
174
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
175
|
+
max_memories=5,
|
|
176
|
+
min_relevance=0.3,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Memories are automatically recalled and injected
|
|
180
|
+
response = await smart_llm.ainvoke([HumanMessage("What are my food allergies?")])
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### OpenAI SDK
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
import os
|
|
187
|
+
from openai import AsyncOpenAI
|
|
188
|
+
from memwal import with_memwal_openai
|
|
189
|
+
|
|
190
|
+
client = AsyncOpenAI()
|
|
191
|
+
smart_client = with_memwal_openai(
|
|
192
|
+
client,
|
|
193
|
+
key=os.environ["MEMWAL_KEY"],
|
|
194
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
195
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Memories are automatically recalled and injected
|
|
199
|
+
response = await smart_client.chat.completions.create(
|
|
200
|
+
model="gpt-4o",
|
|
201
|
+
messages=[{"role": "user", "content": "What are my food allergies?"}],
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### `MemWal.create(key, account_id, server_url?, namespace?)`
|
|
208
|
+
|
|
209
|
+
Create a new async client.
|
|
210
|
+
|
|
211
|
+
### Methods
|
|
212
|
+
|
|
213
|
+
| Method | Description |
|
|
214
|
+
|--------|-------------|
|
|
215
|
+
| `await remember(text, namespace?)` | Store a memory |
|
|
216
|
+
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
217
|
+
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
218
|
+
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
219
|
+
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
220
|
+
| `await health()` | Check server health |
|
|
221
|
+
| `await remember_manual(opts)` | Store with pre-computed vector |
|
|
222
|
+
| `await recall_manual(opts)` | Search with pre-computed vector |
|
|
223
|
+
| `await get_public_key_hex()` | Get Ed25519 public key |
|
|
224
|
+
|
|
225
|
+
## Authentication
|
|
226
|
+
|
|
227
|
+
Every request is signed with Ed25519:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT
|
memwal-0.1.0/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# memwal
|
|
2
|
+
|
|
3
|
+
Python SDK for [MemWal](https://memwal.ai) — Privacy-first AI memory with Ed25519 signing.
|
|
4
|
+
|
|
5
|
+
All data processing (encryption, embedding, Walrus storage) happens server-side in a TEE. The SDK signs requests with your Ed25519 delegate key and sends text over HTTPS.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install memwal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
With optional integrations:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install memwal[langchain] # LangChain support
|
|
17
|
+
pip install memwal[openai] # OpenAI SDK support
|
|
18
|
+
pip install memwal[all] # Everything
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
Set your environment variables first:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export MEMWAL_KEY="your-ed25519-delegate-key-hex"
|
|
27
|
+
export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
|
|
28
|
+
export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Async (recommended)
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import asyncio
|
|
35
|
+
import os
|
|
36
|
+
from memwal import MemWal
|
|
37
|
+
|
|
38
|
+
async def main():
|
|
39
|
+
memwal = MemWal.create(
|
|
40
|
+
key=os.environ["MEMWAL_KEY"],
|
|
41
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
42
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Store a memory
|
|
46
|
+
result = await memwal.remember("I'm allergic to peanuts")
|
|
47
|
+
print(result.blob_id)
|
|
48
|
+
|
|
49
|
+
# Recall memories
|
|
50
|
+
matches = await memwal.recall("food allergies")
|
|
51
|
+
for memory in matches.results:
|
|
52
|
+
print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
|
|
53
|
+
|
|
54
|
+
# Analyze conversation for facts
|
|
55
|
+
analysis = await memwal.analyze("I love coffee and live in Tokyo")
|
|
56
|
+
for fact in analysis.facts:
|
|
57
|
+
print(fact.text)
|
|
58
|
+
|
|
59
|
+
await memwal.close()
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Sync
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import os
|
|
68
|
+
from memwal import MemWalSync
|
|
69
|
+
|
|
70
|
+
client = MemWalSync.create(
|
|
71
|
+
key=os.environ["MEMWAL_KEY"],
|
|
72
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
73
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
result = client.remember("I'm allergic to peanuts")
|
|
77
|
+
matches = client.recall("food allergies")
|
|
78
|
+
client.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Context Manager
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import os
|
|
85
|
+
from memwal import MemWal
|
|
86
|
+
|
|
87
|
+
async with MemWal.create(
|
|
88
|
+
key=os.environ["MEMWAL_KEY"],
|
|
89
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
90
|
+
) as memwal:
|
|
91
|
+
await memwal.remember("I prefer dark mode")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Environment Presets
|
|
95
|
+
|
|
96
|
+
Instead of hardcoding a relayer URL, pass `env` to target a hosted relayer.
|
|
97
|
+
Same shorthand as the TypeScript SDK and MCP package.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from memwal import MemWal
|
|
101
|
+
|
|
102
|
+
memwal = MemWal.create(
|
|
103
|
+
key=os.environ["MEMWAL_KEY"],
|
|
104
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
105
|
+
env="prod", # prod | dev | staging | local
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| `env` | Relayer URL |
|
|
110
|
+
|-------|-------------|
|
|
111
|
+
| `prod` | `https://relayer.memwal.ai` |
|
|
112
|
+
| `dev` | `https://relayer.dev.memwal.ai` |
|
|
113
|
+
| `staging` | `https://relayer.staging.memwal.ai` |
|
|
114
|
+
| `local` | `http://127.0.0.1:8000` |
|
|
115
|
+
|
|
116
|
+
Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
|
|
117
|
+
over the default. An unknown preset raises `ValueError`. `env` is also accepted
|
|
118
|
+
by `MemWalSync.create`, `with_memwal_langchain`, and `with_memwal_openai`.
|
|
119
|
+
|
|
120
|
+
## AI Middleware
|
|
121
|
+
|
|
122
|
+
### LangChain
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import os
|
|
126
|
+
from langchain_openai import ChatOpenAI
|
|
127
|
+
from langchain_core.messages import HumanMessage
|
|
128
|
+
from memwal import with_memwal_langchain
|
|
129
|
+
|
|
130
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
131
|
+
smart_llm = with_memwal_langchain(
|
|
132
|
+
llm,
|
|
133
|
+
key=os.environ["MEMWAL_KEY"],
|
|
134
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
135
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
136
|
+
max_memories=5,
|
|
137
|
+
min_relevance=0.3,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Memories are automatically recalled and injected
|
|
141
|
+
response = await smart_llm.ainvoke([HumanMessage("What are my food allergies?")])
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### OpenAI SDK
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import os
|
|
148
|
+
from openai import AsyncOpenAI
|
|
149
|
+
from memwal import with_memwal_openai
|
|
150
|
+
|
|
151
|
+
client = AsyncOpenAI()
|
|
152
|
+
smart_client = with_memwal_openai(
|
|
153
|
+
client,
|
|
154
|
+
key=os.environ["MEMWAL_KEY"],
|
|
155
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
156
|
+
server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Memories are automatically recalled and injected
|
|
160
|
+
response = await smart_client.chat.completions.create(
|
|
161
|
+
model="gpt-4o",
|
|
162
|
+
messages=[{"role": "user", "content": "What are my food allergies?"}],
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## API Reference
|
|
167
|
+
|
|
168
|
+
### `MemWal.create(key, account_id, server_url?, namespace?)`
|
|
169
|
+
|
|
170
|
+
Create a new async client.
|
|
171
|
+
|
|
172
|
+
### Methods
|
|
173
|
+
|
|
174
|
+
| Method | Description |
|
|
175
|
+
|--------|-------------|
|
|
176
|
+
| `await remember(text, namespace?)` | Store a memory |
|
|
177
|
+
| `await recall(query, limit?, namespace?)` | Search memories |
|
|
178
|
+
| `await analyze(text, namespace?)` | Extract and store facts |
|
|
179
|
+
| `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
|
|
180
|
+
| `await restore(namespace, limit?)` | Restore a namespace |
|
|
181
|
+
| `await health()` | Check server health |
|
|
182
|
+
| `await remember_manual(opts)` | Store with pre-computed vector |
|
|
183
|
+
| `await recall_manual(opts)` | Search with pre-computed vector |
|
|
184
|
+
| `await get_public_key_hex()` | Get Ed25519 public key |
|
|
185
|
+
|
|
186
|
+
## Authentication
|
|
187
|
+
|
|
188
|
+
Every request is signed with Ed25519:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Local server (default) or remote relayer
|
|
2
|
+
MEMWAL_SERVER_URL=http://localhost:3001
|
|
3
|
+
# Ed25519 delegate private key (64-hex). Get from MemWal dashboard.
|
|
4
|
+
MEMWAL_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
|
|
5
|
+
# MemWalAccount object ID on Sui (the wallet's account)
|
|
6
|
+
MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
|
|
7
|
+
# Namespace for these test memories
|
|
8
|
+
MEMWAL_NAMESPACE=python-sdk-example
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""End-to-end smoke test for the async remember family (PR #121 / ENG-1406+1408).
|
|
2
|
+
|
|
3
|
+
Loads credentials from ./.env (see ``.env.example``), then walks every new
|
|
4
|
+
method that was added to mirror the TypeScript SDK:
|
|
5
|
+
|
|
6
|
+
1. health() sanity check
|
|
7
|
+
2. remember() returns RememberAcceptedResult (~500ms)
|
|
8
|
+
3. wait_for_remember_job() polls until ``done`` (~upload time)
|
|
9
|
+
4. remember_and_wait() single call doing both
|
|
10
|
+
5. recall() verify the new memory came back
|
|
11
|
+
6. remember_bulk_and_wait() 3 facts in one call
|
|
12
|
+
7. analyze_and_wait() LLM extracts facts + waits for them all
|
|
13
|
+
8. embed() raw embedding vector
|
|
14
|
+
|
|
15
|
+
Run::
|
|
16
|
+
|
|
17
|
+
cd packages/python-sdk-memwal
|
|
18
|
+
cp examples/.env.example examples/.env # then fill in real values
|
|
19
|
+
python3 examples/async_remember_demo.py
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_env() -> None:
|
|
32
|
+
"""Tiny .env loader so the demo doesn't need python-dotenv."""
|
|
33
|
+
|
|
34
|
+
env_path = Path(__file__).parent / ".env"
|
|
35
|
+
if not env_path.exists():
|
|
36
|
+
return
|
|
37
|
+
for line in env_path.read_text().splitlines():
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
40
|
+
continue
|
|
41
|
+
key, _, value = line.partition("=")
|
|
42
|
+
os.environ.setdefault(key.strip(), value.strip())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_load_env()
|
|
46
|
+
|
|
47
|
+
# Make ``import memwal`` work when running directly from the repo without
|
|
48
|
+
# `pip install -e .`.
|
|
49
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
50
|
+
|
|
51
|
+
from memwal import ( # noqa: E402
|
|
52
|
+
MemWal,
|
|
53
|
+
MemWalRememberJobFailed,
|
|
54
|
+
RememberBulkItem,
|
|
55
|
+
RememberBulkOptions,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _section(title: str) -> None:
|
|
60
|
+
print(f"\n──────── {title} ────────")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ms(start: float) -> int:
|
|
64
|
+
return int((time.monotonic() - start) * 1000)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def main() -> None:
|
|
68
|
+
server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:3001")
|
|
69
|
+
key = os.environ.get("MEMWAL_KEY")
|
|
70
|
+
account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
|
|
71
|
+
namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
|
|
72
|
+
|
|
73
|
+
if not key or not account_id:
|
|
74
|
+
print("ERROR: set MEMWAL_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
|
|
75
|
+
sys.exit(2)
|
|
76
|
+
|
|
77
|
+
print(f"server : {server_url}")
|
|
78
|
+
print(f"account_id : {account_id[:14]}...")
|
|
79
|
+
print(f"namespace : {namespace}")
|
|
80
|
+
|
|
81
|
+
async with MemWal.create(
|
|
82
|
+
key=key,
|
|
83
|
+
account_id=account_id,
|
|
84
|
+
server_url=server_url,
|
|
85
|
+
namespace=namespace,
|
|
86
|
+
) as memwal:
|
|
87
|
+
# 1. health
|
|
88
|
+
_section("1. health()")
|
|
89
|
+
h = await memwal.health()
|
|
90
|
+
print(f" status={h.status} version={h.version}")
|
|
91
|
+
|
|
92
|
+
# 2. remember() → 202 + job_id (PR #121: should return in ~500ms)
|
|
93
|
+
_section("2. remember() — returns 202 + job_id")
|
|
94
|
+
t0 = time.monotonic()
|
|
95
|
+
accepted = await memwal.remember(
|
|
96
|
+
"Python SDK demo: I prefer FastAPI over Flask for async HTTP services."
|
|
97
|
+
)
|
|
98
|
+
accept_ms = _ms(t0)
|
|
99
|
+
print(f" accept_ms={accept_ms} job_id={accepted.job_id} status={accepted.status}")
|
|
100
|
+
assert accepted.job_id, "expected job_id"
|
|
101
|
+
assert accept_ms < 5000, f"expected <5s response, got {accept_ms}ms"
|
|
102
|
+
|
|
103
|
+
# 3. wait_for_remember_job → polls until "done"
|
|
104
|
+
_section("3. wait_for_remember_job(job_id)")
|
|
105
|
+
t1 = time.monotonic()
|
|
106
|
+
result = await memwal.wait_for_remember_job(accepted.job_id, timeout_ms=60_000)
|
|
107
|
+
wait_ms = _ms(t1)
|
|
108
|
+
print(f" wait_ms={wait_ms} blob_id={result.blob_id} ns={result.namespace}")
|
|
109
|
+
|
|
110
|
+
# 4. remember_and_wait — convenience
|
|
111
|
+
_section("4. remember_and_wait()")
|
|
112
|
+
t2 = time.monotonic()
|
|
113
|
+
full = await memwal.remember_and_wait(
|
|
114
|
+
"Python SDK demo: I drink black coffee in the morning.",
|
|
115
|
+
timeout_ms=60_000,
|
|
116
|
+
)
|
|
117
|
+
total_ms = _ms(t2)
|
|
118
|
+
print(f" total_ms={total_ms} blob_id={full.blob_id}")
|
|
119
|
+
|
|
120
|
+
# 5. recall — confirm the memories are searchable
|
|
121
|
+
_section("5. recall('coffee')")
|
|
122
|
+
rc = await memwal.recall("coffee", limit=3)
|
|
123
|
+
print(f" found {len(rc.results)} / total={rc.total}")
|
|
124
|
+
for m in rc.results[:3]:
|
|
125
|
+
print(f" [{m.distance:.3f}] {m.text[:80]}")
|
|
126
|
+
|
|
127
|
+
# 6. remember_bulk_and_wait — 3 items in one call
|
|
128
|
+
_section("6. remember_bulk_and_wait() — 3 items")
|
|
129
|
+
t3 = time.monotonic()
|
|
130
|
+
bulk = await memwal.remember_bulk_and_wait(
|
|
131
|
+
[
|
|
132
|
+
RememberBulkItem(text="Bulk demo 1: Trees clean the air."),
|
|
133
|
+
RememberBulkItem(text="Bulk demo 2: Coffee comes from beans."),
|
|
134
|
+
RememberBulkItem(text="Bulk demo 3: Mountains are usually cold."),
|
|
135
|
+
],
|
|
136
|
+
opts=RememberBulkOptions(poll_interval_ms=2000, timeout_ms=120_000),
|
|
137
|
+
)
|
|
138
|
+
bulk_ms = _ms(t3)
|
|
139
|
+
print(
|
|
140
|
+
f" bulk_ms={bulk_ms} total={bulk.total} ok={bulk.succeeded} "
|
|
141
|
+
f"failed={bulk.failed} timed_out={bulk.timed_out}"
|
|
142
|
+
)
|
|
143
|
+
for r in bulk.results:
|
|
144
|
+
print(f" [{r.status}] id={r.id[:8]}... blob={r.blob_id[:20]}...")
|
|
145
|
+
|
|
146
|
+
# 7. analyze_and_wait — LLM splits text into facts + persists each
|
|
147
|
+
_section("7. analyze_and_wait()")
|
|
148
|
+
t4 = time.monotonic()
|
|
149
|
+
try:
|
|
150
|
+
an = await memwal.analyze_and_wait(
|
|
151
|
+
"Today I learned that the Pacific is the largest ocean and "
|
|
152
|
+
"that octopuses have three hearts.",
|
|
153
|
+
opts=RememberBulkOptions(timeout_ms=120_000),
|
|
154
|
+
)
|
|
155
|
+
ana_ms = _ms(t4)
|
|
156
|
+
print(
|
|
157
|
+
f" analyze_ms={ana_ms} facts={len(an.facts)} "
|
|
158
|
+
f"ok={an.succeeded} failed={an.failed} timed_out={an.timed_out}"
|
|
159
|
+
)
|
|
160
|
+
for f, r in zip(an.facts, an.results):
|
|
161
|
+
print(f" fact='{f.text[:60]}' status={r.status} blob={r.blob_id[:20]}...")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f" skipped (analyze requires server LLM key): {e}")
|
|
164
|
+
|
|
165
|
+
# 8. embed — raw embedding vector
|
|
166
|
+
_section("8. embed('hello world')")
|
|
167
|
+
try:
|
|
168
|
+
emb = await memwal.embed("hello world")
|
|
169
|
+
print(f" vector dims={len(emb.vector)} first 5={emb.vector[:5]}")
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f" skipped (server may not expose /api/embed yet): {e}")
|
|
172
|
+
|
|
173
|
+
print("\n✅ all sections completed")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
import asyncio
|
|
178
|
+
|
|
179
|
+
asyncio.run(main())
|