memwal 0.1.0.dev1__tar.gz → 0.1.0.dev3__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.dev1 → memwal-0.1.0.dev3}/PKG-INFO +29 -3
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/README.md +28 -2
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/examples/async_remember_demo.py +2 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/memwal/__init__.py +3 -1
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/memwal/client.py +166 -22
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/memwal/middleware.py +14 -2
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/memwal/types.py +36 -2
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/memwal/utils.py +118 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/pyproject.toml +1 -1
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/tests/test_client.py +119 -3
- memwal-0.1.0.dev3/tests/test_env_presets.py +71 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/tests/test_integration.py +34 -11
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/tests/test_middleware.py +47 -1
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/tests/test_signing.py +42 -17
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/.gitignore +0 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/examples/.env.example +0 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/examples/.gitignore +0 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/examples/interactive_demo.py +0 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/run_tests.py +0 -0
- {memwal-0.1.0.dev1 → memwal-0.1.0.dev3}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
|
|
5
5
|
Project-URL: Homepage, https://memwal.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.memwal.ai
|
|
@@ -130,6 +130,32 @@ async with MemWal.create(
|
|
|
130
130
|
await memwal.remember("I prefer dark mode")
|
|
131
131
|
```
|
|
132
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
|
+
|
|
133
159
|
## AI Middleware
|
|
134
160
|
|
|
135
161
|
### LangChain
|
|
@@ -201,10 +227,10 @@ Create a new async client.
|
|
|
201
227
|
Every request is signed with Ed25519:
|
|
202
228
|
|
|
203
229
|
```
|
|
204
|
-
message = f"{timestamp}.{method}.{path}.{sha256(body)}
|
|
230
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}"
|
|
205
231
|
```
|
|
206
232
|
|
|
207
|
-
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-
|
|
233
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
|
|
208
234
|
|
|
209
235
|
## License
|
|
210
236
|
|
|
@@ -91,6 +91,32 @@ async with MemWal.create(
|
|
|
91
91
|
await memwal.remember("I prefer dark mode")
|
|
92
92
|
```
|
|
93
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
|
+
|
|
94
120
|
## AI Middleware
|
|
95
121
|
|
|
96
122
|
### LangChain
|
|
@@ -162,10 +188,10 @@ Create a new async client.
|
|
|
162
188
|
Every request is signed with Ed25519:
|
|
163
189
|
|
|
164
190
|
```
|
|
165
|
-
message = f"{timestamp}.{method}.{path}.{sha256(body)}
|
|
191
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}"
|
|
166
192
|
```
|
|
167
193
|
|
|
168
|
-
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-
|
|
194
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
|
|
169
195
|
|
|
170
196
|
## License
|
|
171
197
|
|
|
@@ -25,6 +25,7 @@ import os
|
|
|
25
25
|
import sys
|
|
26
26
|
import time
|
|
27
27
|
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def _load_env() -> None:
|
|
@@ -49,6 +50,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
49
50
|
|
|
50
51
|
from memwal import ( # noqa: E402
|
|
51
52
|
MemWal,
|
|
53
|
+
MemWalRememberJobFailed,
|
|
52
54
|
RememberBulkItem,
|
|
53
55
|
RememberBulkOptions,
|
|
54
56
|
)
|
|
@@ -33,6 +33,7 @@ from .client import (
|
|
|
33
33
|
from .middleware import with_memwal_langchain, with_memwal_openai
|
|
34
34
|
from .utils import delegate_key_to_sui_address, delegate_key_to_public_key
|
|
35
35
|
from .types import (
|
|
36
|
+
ENV_PRESETS,
|
|
36
37
|
AnalyzedFact,
|
|
37
38
|
AnalyzeResult,
|
|
38
39
|
AnalyzeWaitResult,
|
|
@@ -81,6 +82,7 @@ __all__ = [
|
|
|
81
82
|
"withMemWal",
|
|
82
83
|
# Types
|
|
83
84
|
"MemWalConfig",
|
|
85
|
+
"ENV_PRESETS",
|
|
84
86
|
"AskMemory",
|
|
85
87
|
"AskResult",
|
|
86
88
|
"RememberResult",
|
|
@@ -108,4 +110,4 @@ __all__ = [
|
|
|
108
110
|
"RecallManualResult",
|
|
109
111
|
]
|
|
110
112
|
|
|
111
|
-
__version__ = "0.1.0.
|
|
113
|
+
__version__ = "0.1.0.dev3"
|
|
@@ -26,12 +26,14 @@ Example::
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
28
|
import asyncio
|
|
29
|
+
import base64
|
|
29
30
|
import json
|
|
30
31
|
import random
|
|
31
32
|
import time
|
|
32
|
-
from typing import Any, Dict, List, Optional, Sequence, TypeVar
|
|
33
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar
|
|
33
34
|
|
|
34
35
|
import httpx
|
|
36
|
+
import nacl.signing
|
|
35
37
|
|
|
36
38
|
from .types import (
|
|
37
39
|
AnalyzedFact,
|
|
@@ -55,20 +57,27 @@ from .types import (
|
|
|
55
57
|
RememberBulkResult,
|
|
56
58
|
RememberBulkStatusItem,
|
|
57
59
|
RememberBulkStatusResult,
|
|
60
|
+
RememberJobStatus,
|
|
58
61
|
RememberManualOptions,
|
|
59
62
|
RememberManualResult,
|
|
60
63
|
RememberResult,
|
|
61
64
|
RestoreResult,
|
|
62
65
|
)
|
|
63
66
|
from .utils import (
|
|
67
|
+
build_seal_session_personal_message,
|
|
64
68
|
build_signature_message,
|
|
65
69
|
build_signing_key,
|
|
66
70
|
bytes_to_hex,
|
|
71
|
+
delegate_key_to_sui_address,
|
|
72
|
+
encode_sui_private_key,
|
|
67
73
|
sha256_hex,
|
|
68
74
|
sign_message,
|
|
75
|
+
sign_sui_personal_message,
|
|
69
76
|
)
|
|
70
77
|
|
|
71
78
|
T = TypeVar("T")
|
|
79
|
+
SEAL_SESSION_TTL_MIN = 5
|
|
80
|
+
SEAL_SESSION_SAFETY_MARGIN_MS = 30_000
|
|
72
81
|
|
|
73
82
|
|
|
74
83
|
# ============================================================
|
|
@@ -122,22 +131,30 @@ class MemWal:
|
|
|
122
131
|
self._server_url = config.server_url.rstrip("/")
|
|
123
132
|
self._namespace = config.namespace
|
|
124
133
|
self._client: Optional[httpx.AsyncClient] = None
|
|
134
|
+
self._server_config: Optional[Dict[str, str]] = None
|
|
135
|
+
self._session_cache: Optional[Tuple[str, int]] = None
|
|
136
|
+
self._session_build_task: Optional[asyncio.Task[str]] = None
|
|
125
137
|
|
|
126
138
|
@classmethod
|
|
127
139
|
def create(
|
|
128
140
|
cls,
|
|
129
141
|
key: str,
|
|
130
142
|
account_id: str,
|
|
131
|
-
server_url: str = "
|
|
143
|
+
server_url: str = "http://localhost:8000",
|
|
132
144
|
namespace: str = "default",
|
|
145
|
+
env: Optional[str] = None,
|
|
133
146
|
) -> "MemWal":
|
|
134
147
|
"""Create a new MemWal client instance.
|
|
135
148
|
|
|
136
149
|
Args:
|
|
137
150
|
key: Ed25519 private key hex string (the delegate key).
|
|
138
151
|
account_id: MemWalAccount object ID on Sui.
|
|
139
|
-
server_url: Server URL (default: ``
|
|
152
|
+
server_url: Server URL (default: ``http://localhost:8000``).
|
|
140
153
|
namespace: Default namespace for memory isolation (default: ``"default"``).
|
|
154
|
+
env: Optional relayer preset — ``"prod"``, ``"dev"``, ``"staging"``,
|
|
155
|
+
or ``"local"``. Resolves ``server_url`` to the matching hosted
|
|
156
|
+
relayer unless an explicit non-default ``server_url`` is given.
|
|
157
|
+
Precedence: explicit ``server_url`` > ``env`` > default.
|
|
141
158
|
|
|
142
159
|
Returns:
|
|
143
160
|
A configured :class:`MemWal` instance.
|
|
@@ -147,6 +164,7 @@ class MemWal:
|
|
|
147
164
|
account_id=account_id,
|
|
148
165
|
server_url=server_url,
|
|
149
166
|
namespace=namespace,
|
|
167
|
+
env=env,
|
|
150
168
|
)
|
|
151
169
|
return cls(config)
|
|
152
170
|
|
|
@@ -652,11 +670,16 @@ class MemWal:
|
|
|
652
670
|
Returns:
|
|
653
671
|
:class:`RememberManualResult` with id, blob_id, owner, namespace.
|
|
654
672
|
"""
|
|
655
|
-
data = await self._signed_request(
|
|
656
|
-
"
|
|
657
|
-
"
|
|
658
|
-
|
|
659
|
-
|
|
673
|
+
data = await self._signed_request(
|
|
674
|
+
"POST",
|
|
675
|
+
"/api/remember/manual",
|
|
676
|
+
{
|
|
677
|
+
"blob_id": opts.blob_id,
|
|
678
|
+
"vector": opts.vector,
|
|
679
|
+
"namespace": opts.namespace or self._namespace,
|
|
680
|
+
},
|
|
681
|
+
include_seal_session=False,
|
|
682
|
+
)
|
|
660
683
|
return RememberManualResult(
|
|
661
684
|
id=data["id"],
|
|
662
685
|
blob_id=data["blob_id"],
|
|
@@ -676,11 +699,16 @@ class MemWal:
|
|
|
676
699
|
Returns:
|
|
677
700
|
:class:`RecallManualResult` with blob_id + distance pairs.
|
|
678
701
|
"""
|
|
679
|
-
data = await self._signed_request(
|
|
680
|
-
"
|
|
681
|
-
"
|
|
682
|
-
|
|
683
|
-
|
|
702
|
+
data = await self._signed_request(
|
|
703
|
+
"POST",
|
|
704
|
+
"/api/recall/manual",
|
|
705
|
+
{
|
|
706
|
+
"vector": opts.vector,
|
|
707
|
+
"limit": opts.limit,
|
|
708
|
+
"namespace": opts.namespace or self._namespace,
|
|
709
|
+
},
|
|
710
|
+
include_seal_session=False,
|
|
711
|
+
)
|
|
684
712
|
hits = [
|
|
685
713
|
RecallManualHit(blob_id=h["blob_id"], distance=h["distance"])
|
|
686
714
|
for h in data.get("results", [])
|
|
@@ -699,31 +727,141 @@ class MemWal:
|
|
|
699
727
|
# Internal: Signed HTTP Requests
|
|
700
728
|
# ============================================================
|
|
701
729
|
|
|
730
|
+
async def _fetch_server_config(self) -> Dict[str, str]:
|
|
731
|
+
if self._server_config is not None:
|
|
732
|
+
return self._server_config
|
|
733
|
+
|
|
734
|
+
response = await self._http.get(f"{self._server_url}/config")
|
|
735
|
+
if response.status_code != 200:
|
|
736
|
+
raise MemWalError(f"GET /config returned {response.status_code}")
|
|
737
|
+
|
|
738
|
+
data = response.json()
|
|
739
|
+
package_id = data.get("packageId")
|
|
740
|
+
network = data.get("network")
|
|
741
|
+
sui_rpc_url = data.get("suiRpcUrl")
|
|
742
|
+
if not package_id or not network or not sui_rpc_url:
|
|
743
|
+
raise MemWalError("GET /config response missing packageId / network / suiRpcUrl")
|
|
744
|
+
|
|
745
|
+
self._server_config = {
|
|
746
|
+
"packageId": package_id,
|
|
747
|
+
"network": network,
|
|
748
|
+
"suiRpcUrl": sui_rpc_url,
|
|
749
|
+
}
|
|
750
|
+
return self._server_config
|
|
751
|
+
|
|
752
|
+
async def _assert_first_package_version(self, sui_rpc_url: str, package_id: str) -> None:
|
|
753
|
+
response = await self._http.post(
|
|
754
|
+
sui_rpc_url,
|
|
755
|
+
json={
|
|
756
|
+
"jsonrpc": "2.0",
|
|
757
|
+
"id": 1,
|
|
758
|
+
"method": "sui_getObject",
|
|
759
|
+
"params": [package_id, {"showBcs": False, "showContent": False, "showType": False}],
|
|
760
|
+
},
|
|
761
|
+
)
|
|
762
|
+
if response.status_code != 200:
|
|
763
|
+
raise MemWalError(f"sui_getObject returned {response.status_code}")
|
|
764
|
+
|
|
765
|
+
body = response.json()
|
|
766
|
+
result = body.get("result", {})
|
|
767
|
+
version = None
|
|
768
|
+
if isinstance(result, dict):
|
|
769
|
+
data = result.get("data")
|
|
770
|
+
if isinstance(data, dict):
|
|
771
|
+
version = data.get("version")
|
|
772
|
+
if version is None:
|
|
773
|
+
obj = result.get("object")
|
|
774
|
+
if isinstance(obj, dict):
|
|
775
|
+
version = obj.get("version")
|
|
776
|
+
if str(version) != "1":
|
|
777
|
+
raise MemWalError(
|
|
778
|
+
f"SEAL package {package_id} must be at version 1 to build x-seal-session, got {version!r}"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
async def _build_seal_session_inner(self) -> str:
|
|
782
|
+
cfg = await self._fetch_server_config()
|
|
783
|
+
await self._assert_first_package_version(cfg["suiRpcUrl"], cfg["packageId"])
|
|
784
|
+
|
|
785
|
+
session_signing_key = nacl.signing.SigningKey.generate()
|
|
786
|
+
session_public_key = bytes(session_signing_key.verify_key)
|
|
787
|
+
creation_time_ms = int(time.time() * 1000)
|
|
788
|
+
personal_message = build_seal_session_personal_message(
|
|
789
|
+
package_id=cfg["packageId"],
|
|
790
|
+
ttl_min=SEAL_SESSION_TTL_MIN,
|
|
791
|
+
creation_time_ms=creation_time_ms,
|
|
792
|
+
session_public_key_bytes=session_public_key,
|
|
793
|
+
)
|
|
794
|
+
personal_message_signature = sign_sui_personal_message(
|
|
795
|
+
personal_message,
|
|
796
|
+
self._signing_key,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
json_str = json.dumps(
|
|
800
|
+
{
|
|
801
|
+
"address": delegate_key_to_sui_address(self._private_key_hex),
|
|
802
|
+
"packageId": cfg["packageId"],
|
|
803
|
+
"mvrName": None,
|
|
804
|
+
"creationTimeMs": creation_time_ms,
|
|
805
|
+
"ttlMin": SEAL_SESSION_TTL_MIN,
|
|
806
|
+
"personalMessageSignature": personal_message_signature,
|
|
807
|
+
"sessionKey": encode_sui_private_key(bytes(session_signing_key)),
|
|
808
|
+
},
|
|
809
|
+
separators=(",", ":"),
|
|
810
|
+
)
|
|
811
|
+
session_bytes = base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
|
|
812
|
+
self._session_cache = (
|
|
813
|
+
session_bytes,
|
|
814
|
+
int(time.time() * 1000) + SEAL_SESSION_TTL_MIN * 60_000 - SEAL_SESSION_SAFETY_MARGIN_MS,
|
|
815
|
+
)
|
|
816
|
+
return session_bytes
|
|
817
|
+
|
|
818
|
+
async def _build_seal_session(self) -> str:
|
|
819
|
+
now_ms = int(time.time() * 1000)
|
|
820
|
+
if self._session_cache is not None:
|
|
821
|
+
cached_bytes, expires_at_ms = self._session_cache
|
|
822
|
+
if now_ms < expires_at_ms:
|
|
823
|
+
return cached_bytes
|
|
824
|
+
|
|
825
|
+
if self._session_build_task is not None:
|
|
826
|
+
return await self._session_build_task
|
|
827
|
+
|
|
828
|
+
self._session_build_task = asyncio.create_task(self._build_seal_session_inner())
|
|
829
|
+
try:
|
|
830
|
+
return await self._session_build_task
|
|
831
|
+
finally:
|
|
832
|
+
self._session_build_task = None
|
|
833
|
+
|
|
702
834
|
async def _signed_request(
|
|
703
835
|
self,
|
|
704
836
|
method: str,
|
|
705
837
|
path: str,
|
|
706
838
|
body: Dict[str, Any],
|
|
707
839
|
accepted_statuses: tuple = (200,),
|
|
840
|
+
include_seal_session: bool = True,
|
|
708
841
|
) -> Dict[str, Any]:
|
|
709
842
|
"""Make a signed request to the server.
|
|
710
843
|
|
|
711
844
|
Signature format:
|
|
712
845
|
``{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}``
|
|
713
846
|
|
|
847
|
+
For ``GET`` requests the canonical body string is the empty string,
|
|
848
|
+
and no HTTP request body is sent. This keeps the signed payload hash
|
|
849
|
+
byte-compatible with the TypeScript SDK and with intermediaries that
|
|
850
|
+
strip ``GET`` bodies on the wire.
|
|
851
|
+
|
|
714
852
|
Headers sent:
|
|
715
853
|
- ``x-public-key``: Ed25519 public key hex
|
|
716
854
|
- ``x-signature``: Ed25519 signature hex
|
|
717
855
|
- ``x-timestamp``: Unix seconds string
|
|
718
|
-
- ``x-
|
|
719
|
-
- ``x-delegate-key``: Private key hex
|
|
856
|
+
- ``x-seal-session``: Base64-encoded exported session envelope
|
|
720
857
|
- ``x-account-id``: MemWalAccount object ID
|
|
721
858
|
- ``Content-Type``: application/json
|
|
722
859
|
"""
|
|
723
860
|
import uuid
|
|
724
861
|
|
|
862
|
+
method_upper = method.upper()
|
|
725
863
|
timestamp = str(int(time.time()))
|
|
726
|
-
body_str = json.dumps(body, separators=(",", ":"))
|
|
864
|
+
body_str = "" if method_upper == "GET" else json.dumps(body, separators=(",", ":"))
|
|
727
865
|
body_hash = sha256_hex(body_str)
|
|
728
866
|
# MED-1 / LOW-23: nonce + account_id are part of the canonical signed
|
|
729
867
|
# message. Server rejects the request as "unsupported legacy SDK"
|
|
@@ -732,7 +870,7 @@ class MemWal:
|
|
|
732
870
|
|
|
733
871
|
message = build_signature_message(
|
|
734
872
|
timestamp,
|
|
735
|
-
|
|
873
|
+
method_upper,
|
|
736
874
|
path,
|
|
737
875
|
body_hash,
|
|
738
876
|
nonce=nonce,
|
|
@@ -747,15 +885,16 @@ class MemWal:
|
|
|
747
885
|
"x-signature": signature_hex,
|
|
748
886
|
"x-timestamp": timestamp,
|
|
749
887
|
"x-nonce": nonce,
|
|
750
|
-
"x-delegate-key": self._private_key_hex,
|
|
751
888
|
"x-account-id": self._account_id,
|
|
752
889
|
}
|
|
890
|
+
if include_seal_session:
|
|
891
|
+
headers["x-seal-session"] = await self._build_seal_session()
|
|
753
892
|
|
|
754
893
|
response = await self._http.request(
|
|
755
|
-
method=
|
|
894
|
+
method=method_upper,
|
|
756
895
|
url=url,
|
|
757
896
|
headers=headers,
|
|
758
|
-
content=body_str,
|
|
897
|
+
content=None if method_upper == "GET" else body_str,
|
|
759
898
|
)
|
|
760
899
|
|
|
761
900
|
if response.status_code not in accepted_statuses:
|
|
@@ -842,22 +981,27 @@ class MemWalSync:
|
|
|
842
981
|
cls,
|
|
843
982
|
key: str,
|
|
844
983
|
account_id: str,
|
|
845
|
-
server_url: str = "
|
|
984
|
+
server_url: str = "http://localhost:8000",
|
|
846
985
|
namespace: str = "default",
|
|
986
|
+
env: Optional[str] = None,
|
|
847
987
|
) -> "MemWalSync":
|
|
848
988
|
"""Create a synchronous MemWal client.
|
|
849
989
|
|
|
850
|
-
Same parameters as :meth:`MemWal.create
|
|
990
|
+
Same parameters as :meth:`MemWal.create` (including the ``env``
|
|
991
|
+
relayer preset).
|
|
851
992
|
"""
|
|
852
993
|
inner = MemWal.create(
|
|
853
994
|
key=key,
|
|
854
995
|
account_id=account_id,
|
|
855
996
|
server_url=server_url,
|
|
856
997
|
namespace=namespace,
|
|
998
|
+
env=env,
|
|
857
999
|
)
|
|
858
1000
|
return cls(inner)
|
|
859
1001
|
|
|
860
1002
|
def _run(self, coro: Any) -> Any:
|
|
1003
|
+
import asyncio
|
|
1004
|
+
|
|
861
1005
|
try:
|
|
862
1006
|
loop = asyncio.get_running_loop()
|
|
863
1007
|
except RuntimeError:
|
|
@@ -137,12 +137,13 @@ def with_memwal_langchain(
|
|
|
137
137
|
llm: "BaseChatModel",
|
|
138
138
|
key: str,
|
|
139
139
|
account_id: str,
|
|
140
|
-
server_url: str = "
|
|
140
|
+
server_url: str = "http://localhost:8000",
|
|
141
141
|
namespace: str = "default",
|
|
142
142
|
max_memories: int = 5,
|
|
143
143
|
auto_save: bool = True,
|
|
144
144
|
min_relevance: float = 0.3,
|
|
145
145
|
debug: bool = False,
|
|
146
|
+
env: Optional[str] = None,
|
|
146
147
|
) -> "BaseChatModel":
|
|
147
148
|
"""Wrap a LangChain ``BaseChatModel`` with MemWal memory management.
|
|
148
149
|
|
|
@@ -163,6 +164,8 @@ def with_memwal_langchain(
|
|
|
163
164
|
auto_save: Auto-save new facts from conversation.
|
|
164
165
|
min_relevance: Minimum similarity score (0-1) to include a memory.
|
|
165
166
|
debug: Enable debug logging.
|
|
167
|
+
env: Optional relayer preset (``"prod"`` / ``"dev"`` / ``"staging"`` /
|
|
168
|
+
``"local"``). Same precedence as :meth:`MemWal.create`.
|
|
166
169
|
|
|
167
170
|
Returns:
|
|
168
171
|
A wrapped ``BaseChatModel`` that automatically uses MemWal memory.
|
|
@@ -181,6 +184,7 @@ def with_memwal_langchain(
|
|
|
181
184
|
account_id=account_id,
|
|
182
185
|
server_url=server_url,
|
|
183
186
|
namespace=namespace,
|
|
187
|
+
env=env,
|
|
184
188
|
)
|
|
185
189
|
|
|
186
190
|
log = logger.debug if not debug else logger.warning
|
|
@@ -254,6 +258,8 @@ def with_memwal_langchain(
|
|
|
254
258
|
messages: List[List[BaseMessage]], *args: Any, **kwargs: Any
|
|
255
259
|
) -> ChatResult:
|
|
256
260
|
# For sync generate, we inject memories synchronously via asyncio.run
|
|
261
|
+
import asyncio
|
|
262
|
+
|
|
257
263
|
enriched = []
|
|
258
264
|
for msg_list in messages:
|
|
259
265
|
try:
|
|
@@ -290,12 +296,13 @@ def with_memwal_openai(
|
|
|
290
296
|
client: Any,
|
|
291
297
|
key: str,
|
|
292
298
|
account_id: str,
|
|
293
|
-
server_url: str = "
|
|
299
|
+
server_url: str = "http://localhost:8000",
|
|
294
300
|
namespace: str = "default",
|
|
295
301
|
max_memories: int = 5,
|
|
296
302
|
auto_save: bool = True,
|
|
297
303
|
min_relevance: float = 0.3,
|
|
298
304
|
debug: bool = False,
|
|
305
|
+
env: Optional[str] = None,
|
|
299
306
|
) -> Any:
|
|
300
307
|
"""Wrap an OpenAI client with MemWal memory management.
|
|
301
308
|
|
|
@@ -318,6 +325,8 @@ def with_memwal_openai(
|
|
|
318
325
|
auto_save: Auto-save new facts from conversation.
|
|
319
326
|
min_relevance: Minimum similarity score (0-1) to include a memory.
|
|
320
327
|
debug: Enable debug logging.
|
|
328
|
+
env: Optional relayer preset (``"prod"`` / ``"dev"`` / ``"staging"`` /
|
|
329
|
+
``"local"``). Same precedence as :meth:`MemWal.create`.
|
|
321
330
|
|
|
322
331
|
Returns:
|
|
323
332
|
The same client, with ``chat.completions.create`` wrapped to use MemWal.
|
|
@@ -327,6 +336,7 @@ def with_memwal_openai(
|
|
|
327
336
|
account_id=account_id,
|
|
328
337
|
server_url=server_url,
|
|
329
338
|
namespace=namespace,
|
|
339
|
+
env=env,
|
|
330
340
|
)
|
|
331
341
|
|
|
332
342
|
log = logger.debug if not debug else logger.warning
|
|
@@ -408,6 +418,8 @@ def _wrap_sync_openai(
|
|
|
408
418
|
original_create = client.chat.completions.create
|
|
409
419
|
|
|
410
420
|
def patched_create(*args: Any, **kwargs: Any) -> Any:
|
|
421
|
+
import asyncio
|
|
422
|
+
|
|
411
423
|
messages = kwargs.get("messages") or (args[0] if args else None)
|
|
412
424
|
if messages is None:
|
|
413
425
|
return original_create(*args, **kwargs)
|
|
@@ -15,6 +15,21 @@ from typing import List, Optional
|
|
|
15
15
|
# Config
|
|
16
16
|
# ============================================================
|
|
17
17
|
|
|
18
|
+
#: Default ``server_url`` when neither an explicit URL nor an ``env`` preset
|
|
19
|
+
#: is supplied. Kept as a module constant so ``__post_init__`` can tell an
|
|
20
|
+
#: untouched default apart from an explicitly-passed custom URL.
|
|
21
|
+
DEFAULT_SERVER_URL = "http://localhost:8000"
|
|
22
|
+
|
|
23
|
+
#: Named relayer environments. Mirrors the TypeScript SDK / MCP package
|
|
24
|
+
#: ``--prod`` / ``--dev`` / ``--staging`` / ``--local`` presets so the same
|
|
25
|
+
#: shorthand works across every MemWal client.
|
|
26
|
+
ENV_PRESETS = {
|
|
27
|
+
"prod": "https://relayer.memwal.ai",
|
|
28
|
+
"dev": "https://relayer.dev.memwal.ai",
|
|
29
|
+
"staging": "https://relayer.staging.memwal.ai",
|
|
30
|
+
"local": "http://127.0.0.1:8000",
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
|
|
19
34
|
@dataclass
|
|
20
35
|
class MemWalConfig:
|
|
@@ -23,14 +38,33 @@ class MemWalConfig:
|
|
|
23
38
|
Attributes:
|
|
24
39
|
key: Ed25519 private key (hex string). This is the delegate key from app.memwal.com.
|
|
25
40
|
account_id: MemWalAccount object ID on Sui.
|
|
26
|
-
server_url: Server URL (default:
|
|
41
|
+
server_url: Server URL (default: http://localhost:8000). An explicit
|
|
42
|
+
non-default value always wins over ``env``.
|
|
27
43
|
namespace: Default namespace for memory isolation (default: "default").
|
|
44
|
+
env: Optional relayer preset — one of ``"prod"``, ``"dev"``,
|
|
45
|
+
``"staging"``, ``"local"``. Resolves ``server_url`` to the matching
|
|
46
|
+
hosted relayer when ``server_url`` is left at its default.
|
|
47
|
+
Precedence: explicit ``server_url`` > ``env`` > default.
|
|
28
48
|
"""
|
|
29
49
|
|
|
30
50
|
key: str
|
|
31
51
|
account_id: str
|
|
32
|
-
server_url: str =
|
|
52
|
+
server_url: str = DEFAULT_SERVER_URL
|
|
33
53
|
namespace: str = "default"
|
|
54
|
+
env: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if self.env is not None:
|
|
58
|
+
preset = ENV_PRESETS.get(self.env)
|
|
59
|
+
if preset is None:
|
|
60
|
+
valid = ", ".join(sorted(ENV_PRESETS))
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown env preset {self.env!r}. Valid presets: {valid}"
|
|
63
|
+
)
|
|
64
|
+
# Explicit, non-default server_url takes precedence over the
|
|
65
|
+
# preset; only fill from the preset when server_url is untouched.
|
|
66
|
+
if self.server_url == DEFAULT_SERVER_URL:
|
|
67
|
+
self.server_url = preset
|
|
34
68
|
|
|
35
69
|
|
|
36
70
|
# ============================================================
|