memlayer-py 0.1.0__py3-none-any.whl
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.
memlayer/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from sdk.memlayer.client import (
|
|
2
|
+
MemLayerClient,
|
|
3
|
+
AsyncMemLayerClient,
|
|
4
|
+
Memory,
|
|
5
|
+
StoreResult,
|
|
6
|
+
DeleteResult,
|
|
7
|
+
ContextResult,
|
|
8
|
+
MemLayerError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
PlanLimitError,
|
|
11
|
+
MemoryNotFoundError,
|
|
12
|
+
DuplicateMemoryError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MemLayerClient",
|
|
19
|
+
"AsyncMemLayerClient",
|
|
20
|
+
"Memory",
|
|
21
|
+
"StoreResult",
|
|
22
|
+
"DeleteResult",
|
|
23
|
+
"ContextResult",
|
|
24
|
+
"MemLayerError",
|
|
25
|
+
"AuthenticationError",
|
|
26
|
+
"PlanLimitError",
|
|
27
|
+
"MemoryNotFoundError",
|
|
28
|
+
"DuplicateMemoryError",
|
|
29
|
+
]
|
memlayer/client.py
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MemLayer Python SDK
|
|
3
|
+
===================
|
|
4
|
+
Persistent memory for AI agents.
|
|
5
|
+
|
|
6
|
+
Install:
|
|
7
|
+
pip install memlayer-py
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from memlayer import MemLayerClient
|
|
11
|
+
|
|
12
|
+
client = MemLayerClient(api_key="ml_live_xxx")
|
|
13
|
+
|
|
14
|
+
# Store a memory
|
|
15
|
+
mem_id = client.remember(
|
|
16
|
+
"User prefers dark mode",
|
|
17
|
+
user_id="user_123",
|
|
18
|
+
agent_id="support_bot",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Recall relevant memories
|
|
22
|
+
memories = client.recall(
|
|
23
|
+
"what UI preferences does this user have?",
|
|
24
|
+
user_id="user_123",
|
|
25
|
+
agent_id="support_bot",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
for m in memories:
|
|
29
|
+
print(m.content, m.score)
|
|
30
|
+
|
|
31
|
+
# Load context at session start
|
|
32
|
+
context = client.context(user_id="user_123", agent_id="support_bot")
|
|
33
|
+
|
|
34
|
+
# Forget everything for a user
|
|
35
|
+
client.forget_all(user_id="user_123", agent_id="support_bot")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import Optional, Any
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── Data classes ──────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Memory:
|
|
47
|
+
id: str
|
|
48
|
+
content: str
|
|
49
|
+
user_id: str
|
|
50
|
+
agent_id: str
|
|
51
|
+
memory_type: str
|
|
52
|
+
metadata: dict
|
|
53
|
+
importance: float
|
|
54
|
+
score: Optional[float]
|
|
55
|
+
score_detail: Optional[dict]
|
|
56
|
+
created_at: str
|
|
57
|
+
last_accessed: Optional[str]
|
|
58
|
+
expires_at: Optional[str]
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, d: dict) -> "Memory":
|
|
62
|
+
return cls(
|
|
63
|
+
id=d["id"],
|
|
64
|
+
content=d["content"],
|
|
65
|
+
user_id=d["user_id"],
|
|
66
|
+
agent_id=d["agent_id"],
|
|
67
|
+
memory_type=d["memory_type"],
|
|
68
|
+
metadata=d.get("metadata") or {},
|
|
69
|
+
importance=d.get("importance", 0.5),
|
|
70
|
+
score=d.get("score"),
|
|
71
|
+
score_detail=d.get("score_detail"),
|
|
72
|
+
created_at=d["created_at"],
|
|
73
|
+
last_accessed=d.get("last_accessed"),
|
|
74
|
+
expires_at=d.get("expires_at"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
score_str = f", score={self.score:.3f}" if self.score else ""
|
|
79
|
+
return f"Memory(id={self.id[:8]}..., content={self.content[:50]!r}{score_str})"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class StoreResult:
|
|
84
|
+
id: str
|
|
85
|
+
message: str = "Memory stored successfully"
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_duplicate(self) -> bool:
|
|
89
|
+
return self.id == "duplicate"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class DeleteResult:
|
|
94
|
+
deleted: int
|
|
95
|
+
message: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ContextResult:
|
|
100
|
+
memories: list[Memory]
|
|
101
|
+
total: int
|
|
102
|
+
limit: int
|
|
103
|
+
offset: int
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class SearchResult:
|
|
108
|
+
memories: list[Memory]
|
|
109
|
+
query: str
|
|
110
|
+
total: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Exceptions ────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
class MemLayerError(Exception):
|
|
116
|
+
"""Base exception for all MemLayer SDK errors."""
|
|
117
|
+
def __init__(self, status_code: int, detail: str):
|
|
118
|
+
self.status_code = status_code
|
|
119
|
+
self.detail = detail
|
|
120
|
+
super().__init__(f"MemLayer API error {status_code}: {detail}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class AuthenticationError(MemLayerError):
|
|
124
|
+
"""Raised when the API key is invalid or missing."""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PlanLimitError(MemLayerError):
|
|
129
|
+
"""Raised when the tenant has hit their plan memory limit."""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class MemoryNotFoundError(MemLayerError):
|
|
134
|
+
"""Raised when a memory ID does not exist."""
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DuplicateMemoryError(MemLayerError):
|
|
139
|
+
"""Raised when a duplicate memory is detected."""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ── Sync Client ───────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
class MemLayerClient:
|
|
146
|
+
"""
|
|
147
|
+
Synchronous MemLayer client.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
client = MemLayerClient(api_key="ml_live_xxx")
|
|
151
|
+
mem_id = client.remember("User is in Lagos", user_id="u1", agent_id="bot")
|
|
152
|
+
memories = client.recall("where is the user?", user_id="u1", agent_id="bot")
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
api_key: str,
|
|
158
|
+
base_url: str = "https://memlayer.online",
|
|
159
|
+
timeout: float = 30.0,
|
|
160
|
+
):
|
|
161
|
+
self.base_url = base_url.rstrip("/")
|
|
162
|
+
self._headers = {
|
|
163
|
+
"X-API-Key": api_key,
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
}
|
|
166
|
+
self._client = httpx.Client(
|
|
167
|
+
headers=self._headers,
|
|
168
|
+
timeout=timeout,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# ── Store ──────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def remember(
|
|
174
|
+
self,
|
|
175
|
+
content: str,
|
|
176
|
+
user_id: str,
|
|
177
|
+
agent_id: str,
|
|
178
|
+
memory_type: str = "episodic",
|
|
179
|
+
metadata: Optional[dict] = None,
|
|
180
|
+
importance: float = 0.5,
|
|
181
|
+
ttl_days: Optional[int] = None,
|
|
182
|
+
) -> StoreResult:
|
|
183
|
+
"""
|
|
184
|
+
Store a memory for a user+agent pair.
|
|
185
|
+
Returns a StoreResult with the memory ID.
|
|
186
|
+
Returns StoreResult with id='duplicate' if content already exists.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
content: The memory text to store.
|
|
190
|
+
user_id: Identifier for the end user.
|
|
191
|
+
agent_id: Identifier for the agent.
|
|
192
|
+
memory_type: 'episodic', 'semantic', or 'summary'.
|
|
193
|
+
metadata: Optional dict of extra data to store with the memory.
|
|
194
|
+
importance: Priority score 0.0 to 1.0 (default 0.5).
|
|
195
|
+
ttl_days: Days until memory expires. None = permanent.
|
|
196
|
+
"""
|
|
197
|
+
resp = self._client.post(
|
|
198
|
+
f"{self.base_url}/memories",
|
|
199
|
+
json={
|
|
200
|
+
"content": content,
|
|
201
|
+
"user_id": user_id,
|
|
202
|
+
"agent_id": agent_id,
|
|
203
|
+
"memory_type": memory_type,
|
|
204
|
+
"metadata": metadata or {},
|
|
205
|
+
"importance": importance,
|
|
206
|
+
"ttl_days": ttl_days,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
self._raise_for_status(resp)
|
|
210
|
+
data = resp.json()
|
|
211
|
+
return StoreResult(id=data["id"], message=data.get("message", ""))
|
|
212
|
+
|
|
213
|
+
# ── Search ─────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
def recall(
|
|
216
|
+
self,
|
|
217
|
+
query: str,
|
|
218
|
+
user_id: str,
|
|
219
|
+
agent_id: str,
|
|
220
|
+
top_k: int = 5,
|
|
221
|
+
min_score: float = 0.70,
|
|
222
|
+
memory_type: Optional[str] = None,
|
|
223
|
+
) -> list[Memory]:
|
|
224
|
+
"""
|
|
225
|
+
Semantic search — returns top-k memories ranked by hybrid score.
|
|
226
|
+
Hybrid score = 70% cosine similarity + 20% recency + 10% importance.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
query: Natural language query.
|
|
230
|
+
user_id: Identifier for the end user.
|
|
231
|
+
agent_id: Identifier for the agent.
|
|
232
|
+
top_k: Number of memories to return (1-20).
|
|
233
|
+
min_score: Minimum hybrid score threshold (0.0-1.0).
|
|
234
|
+
memory_type: Filter by 'episodic', 'semantic', or 'summary'.
|
|
235
|
+
"""
|
|
236
|
+
params: dict[str, Any] = {
|
|
237
|
+
"query": query,
|
|
238
|
+
"user_id": user_id,
|
|
239
|
+
"agent_id": agent_id,
|
|
240
|
+
"top_k": top_k,
|
|
241
|
+
"min_score": min_score,
|
|
242
|
+
}
|
|
243
|
+
if memory_type:
|
|
244
|
+
params["memory_type"] = memory_type
|
|
245
|
+
|
|
246
|
+
resp = self._client.get(
|
|
247
|
+
f"{self.base_url}/memories/search",
|
|
248
|
+
params=params,
|
|
249
|
+
)
|
|
250
|
+
self._raise_for_status(resp)
|
|
251
|
+
data = resp.json()
|
|
252
|
+
return [Memory.from_dict(m) for m in data["memories"]]
|
|
253
|
+
|
|
254
|
+
# ── Context ────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
def context(
|
|
257
|
+
self,
|
|
258
|
+
user_id: str,
|
|
259
|
+
agent_id: str,
|
|
260
|
+
top_k: int = 10,
|
|
261
|
+
current_message: Optional[str] = None,
|
|
262
|
+
) -> ContextResult:
|
|
263
|
+
"""
|
|
264
|
+
Load memories at session start — before any query is made.
|
|
265
|
+
If current_message is provided, uses semantic search.
|
|
266
|
+
Otherwise returns memories ranked by importance + recency.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
user_id: Identifier for the end user.
|
|
270
|
+
agent_id: Identifier for the agent.
|
|
271
|
+
top_k: Number of memories to return (1-50).
|
|
272
|
+
current_message: First message in the session (optional).
|
|
273
|
+
"""
|
|
274
|
+
params: dict[str, Any] = {
|
|
275
|
+
"user_id": user_id,
|
|
276
|
+
"agent_id": agent_id,
|
|
277
|
+
"top_k": top_k,
|
|
278
|
+
}
|
|
279
|
+
if current_message:
|
|
280
|
+
params["current_message"] = current_message
|
|
281
|
+
|
|
282
|
+
resp = self._client.get(
|
|
283
|
+
f"{self.base_url}/memories/context",
|
|
284
|
+
params=params,
|
|
285
|
+
)
|
|
286
|
+
self._raise_for_status(resp)
|
|
287
|
+
data = resp.json()
|
|
288
|
+
return ContextResult(
|
|
289
|
+
memories=[Memory.from_dict(m) for m in data["memories"]],
|
|
290
|
+
total=data["total"],
|
|
291
|
+
limit=data["limit"],
|
|
292
|
+
offset=data["offset"],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# ── List ───────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
def list(
|
|
298
|
+
self,
|
|
299
|
+
user_id: str,
|
|
300
|
+
agent_id: str,
|
|
301
|
+
memory_type: Optional[str] = None,
|
|
302
|
+
limit: int = 20,
|
|
303
|
+
offset: int = 0,
|
|
304
|
+
) -> ContextResult:
|
|
305
|
+
"""
|
|
306
|
+
List all memories for a user+agent pair (paginated).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
user_id: Identifier for the end user.
|
|
310
|
+
agent_id: Identifier for the agent.
|
|
311
|
+
memory_type: Filter by memory type.
|
|
312
|
+
limit: Results per page (1-100).
|
|
313
|
+
offset: Pagination offset.
|
|
314
|
+
"""
|
|
315
|
+
params: dict[str, Any] = {
|
|
316
|
+
"user_id": user_id,
|
|
317
|
+
"agent_id": agent_id,
|
|
318
|
+
"limit": limit,
|
|
319
|
+
"offset": offset,
|
|
320
|
+
}
|
|
321
|
+
if memory_type:
|
|
322
|
+
params["memory_type"] = memory_type
|
|
323
|
+
|
|
324
|
+
resp = self._client.get(
|
|
325
|
+
f"{self.base_url}/memories",
|
|
326
|
+
params=params,
|
|
327
|
+
)
|
|
328
|
+
self._raise_for_status(resp)
|
|
329
|
+
data = resp.json()
|
|
330
|
+
return ContextResult(
|
|
331
|
+
memories=[Memory.from_dict(m) for m in data["memories"]],
|
|
332
|
+
total=data["total"],
|
|
333
|
+
limit=data["limit"],
|
|
334
|
+
offset=data["offset"],
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# ── Update ─────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
def update(
|
|
340
|
+
self,
|
|
341
|
+
memory_id: str,
|
|
342
|
+
user_id: str,
|
|
343
|
+
agent_id: str,
|
|
344
|
+
new_content: str,
|
|
345
|
+
importance: Optional[float] = None,
|
|
346
|
+
metadata: Optional[dict] = None,
|
|
347
|
+
) -> Memory:
|
|
348
|
+
"""
|
|
349
|
+
Update an existing memory with new content.
|
|
350
|
+
Re-embeds the new content. Same memory ID — just new content.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
memory_id: UUID of the memory to update.
|
|
354
|
+
user_id: Identifier for the end user.
|
|
355
|
+
agent_id: Identifier for the agent.
|
|
356
|
+
new_content: Replacement content.
|
|
357
|
+
importance: New importance score (optional).
|
|
358
|
+
metadata: New metadata (optional).
|
|
359
|
+
"""
|
|
360
|
+
body: dict[str, Any] = {
|
|
361
|
+
"user_id": user_id,
|
|
362
|
+
"agent_id": agent_id,
|
|
363
|
+
"new_content": new_content,
|
|
364
|
+
}
|
|
365
|
+
if importance is not None:
|
|
366
|
+
body["importance"] = importance
|
|
367
|
+
if metadata is not None:
|
|
368
|
+
body["metadata"] = metadata
|
|
369
|
+
|
|
370
|
+
resp = self._client.patch(
|
|
371
|
+
f"{self.base_url}/memories/{memory_id}",
|
|
372
|
+
json=body,
|
|
373
|
+
)
|
|
374
|
+
self._raise_for_status(resp)
|
|
375
|
+
return Memory.from_dict(resp.json())
|
|
376
|
+
|
|
377
|
+
# ── Duplicate check ────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
def is_duplicate(
|
|
380
|
+
self,
|
|
381
|
+
content: str,
|
|
382
|
+
user_id: str,
|
|
383
|
+
agent_id: str,
|
|
384
|
+
threshold: float = 0.95,
|
|
385
|
+
) -> bool:
|
|
386
|
+
"""
|
|
387
|
+
Check if a memory already exists before storing.
|
|
388
|
+
Returns True if a near-identical memory is found.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
content: Memory text to check.
|
|
392
|
+
user_id: Identifier for the end user.
|
|
393
|
+
agent_id: Identifier for the agent.
|
|
394
|
+
threshold: Similarity threshold (0.0-1.0). Default 0.95.
|
|
395
|
+
"""
|
|
396
|
+
resp = self._client.post(
|
|
397
|
+
f"{self.base_url}/memories/duplicate-check",
|
|
398
|
+
params={
|
|
399
|
+
"content": content,
|
|
400
|
+
"user_id": user_id,
|
|
401
|
+
"agent_id": agent_id,
|
|
402
|
+
"threshold": threshold,
|
|
403
|
+
},
|
|
404
|
+
)
|
|
405
|
+
self._raise_for_status(resp)
|
|
406
|
+
return resp.json()["is_duplicate"]
|
|
407
|
+
|
|
408
|
+
# ── Delete one ─────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
def forget(
|
|
411
|
+
self,
|
|
412
|
+
memory_id: str,
|
|
413
|
+
user_id: str,
|
|
414
|
+
agent_id: str,
|
|
415
|
+
) -> DeleteResult:
|
|
416
|
+
"""
|
|
417
|
+
Delete a single memory by ID.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
memory_id: UUID of the memory to delete.
|
|
421
|
+
user_id: Identifier for the end user.
|
|
422
|
+
agent_id: Identifier for the agent.
|
|
423
|
+
"""
|
|
424
|
+
resp = self._client.delete(
|
|
425
|
+
f"{self.base_url}/memories/{memory_id}",
|
|
426
|
+
params={"user_id": user_id, "agent_id": agent_id},
|
|
427
|
+
)
|
|
428
|
+
self._raise_for_status(resp)
|
|
429
|
+
data = resp.json()
|
|
430
|
+
return DeleteResult(deleted=data["deleted"], message=data["message"])
|
|
431
|
+
|
|
432
|
+
# ── Wipe all ───────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
def forget_all(
|
|
435
|
+
self,
|
|
436
|
+
user_id: str,
|
|
437
|
+
agent_id: str,
|
|
438
|
+
) -> DeleteResult:
|
|
439
|
+
"""
|
|
440
|
+
Wipe all memories for a user+agent pair.
|
|
441
|
+
Use for GDPR requests or account resets.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
user_id: Identifier for the end user.
|
|
445
|
+
agent_id: Identifier for the agent.
|
|
446
|
+
"""
|
|
447
|
+
resp = self._client.delete(
|
|
448
|
+
f"{self.base_url}/memories",
|
|
449
|
+
params={
|
|
450
|
+
"user_id": user_id,
|
|
451
|
+
"agent_id": agent_id,
|
|
452
|
+
"confirm": True,
|
|
453
|
+
},
|
|
454
|
+
)
|
|
455
|
+
self._raise_for_status(resp)
|
|
456
|
+
data = resp.json()
|
|
457
|
+
return DeleteResult(deleted=data["deleted"], message=data["message"])
|
|
458
|
+
|
|
459
|
+
# ── Helpers ────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
def _raise_for_status(self, resp: httpx.Response) -> None:
|
|
462
|
+
if resp.status_code < 400:
|
|
463
|
+
return
|
|
464
|
+
try:
|
|
465
|
+
detail = resp.json().get("detail", resp.text)
|
|
466
|
+
except Exception:
|
|
467
|
+
detail = resp.text
|
|
468
|
+
|
|
469
|
+
if resp.status_code == 401:
|
|
470
|
+
raise AuthenticationError(resp.status_code, detail)
|
|
471
|
+
if resp.status_code == 402:
|
|
472
|
+
raise PlanLimitError(resp.status_code, detail)
|
|
473
|
+
if resp.status_code == 404:
|
|
474
|
+
raise MemoryNotFoundError(resp.status_code, detail)
|
|
475
|
+
if resp.status_code == 409:
|
|
476
|
+
raise DuplicateMemoryError(resp.status_code, detail)
|
|
477
|
+
raise MemLayerError(resp.status_code, detail)
|
|
478
|
+
|
|
479
|
+
def close(self):
|
|
480
|
+
self._client.close()
|
|
481
|
+
|
|
482
|
+
def __enter__(self):
|
|
483
|
+
return self
|
|
484
|
+
|
|
485
|
+
def __exit__(self, *args):
|
|
486
|
+
self.close()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ── Async Client ──────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
class AsyncMemLayerClient:
|
|
492
|
+
"""
|
|
493
|
+
Async MemLayer client for use with asyncio, LangGraph, and FastAPI.
|
|
494
|
+
|
|
495
|
+
Example:
|
|
496
|
+
async with AsyncMemLayerClient(api_key="ml_live_xxx") as client:
|
|
497
|
+
mem_id = await client.remember("user likes dark mode", user_id="u1", agent_id="bot")
|
|
498
|
+
memories = await client.recall("UI preferences", user_id="u1", agent_id="bot")
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
def __init__(
|
|
502
|
+
self,
|
|
503
|
+
api_key: str,
|
|
504
|
+
base_url: str = "https://memlayer.online",
|
|
505
|
+
timeout: float = 30.0,
|
|
506
|
+
):
|
|
507
|
+
self.base_url = base_url.rstrip("/")
|
|
508
|
+
self._headers = {
|
|
509
|
+
"X-API-Key": api_key,
|
|
510
|
+
"Content-Type": "application/json",
|
|
511
|
+
}
|
|
512
|
+
self._timeout = timeout
|
|
513
|
+
|
|
514
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
515
|
+
return httpx.AsyncClient(headers=self._headers, timeout=self._timeout)
|
|
516
|
+
|
|
517
|
+
async def remember(
|
|
518
|
+
self,
|
|
519
|
+
content: str,
|
|
520
|
+
user_id: str,
|
|
521
|
+
agent_id: str,
|
|
522
|
+
memory_type: str = "episodic",
|
|
523
|
+
metadata: Optional[dict] = None,
|
|
524
|
+
importance: float = 0.5,
|
|
525
|
+
ttl_days: Optional[int] = None,
|
|
526
|
+
) -> StoreResult:
|
|
527
|
+
"""Store a memory. Returns StoreResult with memory ID."""
|
|
528
|
+
async with self._get_client() as client:
|
|
529
|
+
resp = await client.post(
|
|
530
|
+
f"{self.base_url}/memories",
|
|
531
|
+
json={
|
|
532
|
+
"content": content,
|
|
533
|
+
"user_id": user_id,
|
|
534
|
+
"agent_id": agent_id,
|
|
535
|
+
"memory_type": memory_type,
|
|
536
|
+
"metadata": metadata or {},
|
|
537
|
+
"importance": importance,
|
|
538
|
+
"ttl_days": ttl_days,
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
self._raise_for_status(resp)
|
|
542
|
+
data = resp.json()
|
|
543
|
+
return StoreResult(id=data["id"], message=data.get("message", ""))
|
|
544
|
+
|
|
545
|
+
async def recall(
|
|
546
|
+
self,
|
|
547
|
+
query: str,
|
|
548
|
+
user_id: str,
|
|
549
|
+
agent_id: str,
|
|
550
|
+
top_k: int = 5,
|
|
551
|
+
min_score: float = 0.70,
|
|
552
|
+
memory_type: Optional[str] = None,
|
|
553
|
+
) -> list[Memory]:
|
|
554
|
+
"""Semantic search — returns top-k memories by hybrid score."""
|
|
555
|
+
params: dict[str, Any] = {
|
|
556
|
+
"query": query,
|
|
557
|
+
"user_id": user_id,
|
|
558
|
+
"agent_id": agent_id,
|
|
559
|
+
"top_k": top_k,
|
|
560
|
+
"min_score": min_score,
|
|
561
|
+
}
|
|
562
|
+
if memory_type:
|
|
563
|
+
params["memory_type"] = memory_type
|
|
564
|
+
|
|
565
|
+
async with self._get_client() as client:
|
|
566
|
+
resp = await client.get(
|
|
567
|
+
f"{self.base_url}/memories/search",
|
|
568
|
+
params=params,
|
|
569
|
+
)
|
|
570
|
+
self._raise_for_status(resp)
|
|
571
|
+
return [Memory.from_dict(m) for m in resp.json()["memories"]]
|
|
572
|
+
|
|
573
|
+
async def context(
|
|
574
|
+
self,
|
|
575
|
+
user_id: str,
|
|
576
|
+
agent_id: str,
|
|
577
|
+
top_k: int = 10,
|
|
578
|
+
current_message: Optional[str] = None,
|
|
579
|
+
) -> ContextResult:
|
|
580
|
+
"""Load memories at session start."""
|
|
581
|
+
params: dict[str, Any] = {
|
|
582
|
+
"user_id": user_id,
|
|
583
|
+
"agent_id": agent_id,
|
|
584
|
+
"top_k": top_k,
|
|
585
|
+
}
|
|
586
|
+
if current_message:
|
|
587
|
+
params["current_message"] = current_message
|
|
588
|
+
|
|
589
|
+
async with self._get_client() as client:
|
|
590
|
+
resp = await client.get(
|
|
591
|
+
f"{self.base_url}/memories/context",
|
|
592
|
+
params=params,
|
|
593
|
+
)
|
|
594
|
+
self._raise_for_status(resp)
|
|
595
|
+
data = resp.json()
|
|
596
|
+
return ContextResult(
|
|
597
|
+
memories=[Memory.from_dict(m) for m in data["memories"]],
|
|
598
|
+
total=data["total"],
|
|
599
|
+
limit=data["limit"],
|
|
600
|
+
offset=data["offset"],
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
async def update(
|
|
604
|
+
self,
|
|
605
|
+
memory_id: str,
|
|
606
|
+
user_id: str,
|
|
607
|
+
agent_id: str,
|
|
608
|
+
new_content: str,
|
|
609
|
+
importance: Optional[float] = None,
|
|
610
|
+
metadata: Optional[dict] = None,
|
|
611
|
+
) -> Memory:
|
|
612
|
+
"""Update an existing memory with new content."""
|
|
613
|
+
body: dict[str, Any] = {
|
|
614
|
+
"user_id": user_id,
|
|
615
|
+
"agent_id": agent_id,
|
|
616
|
+
"new_content": new_content,
|
|
617
|
+
}
|
|
618
|
+
if importance is not None:
|
|
619
|
+
body["importance"] = importance
|
|
620
|
+
if metadata is not None:
|
|
621
|
+
body["metadata"] = metadata
|
|
622
|
+
|
|
623
|
+
async with self._get_client() as client:
|
|
624
|
+
resp = await client.patch(
|
|
625
|
+
f"{self.base_url}/memories/{memory_id}",
|
|
626
|
+
json=body,
|
|
627
|
+
)
|
|
628
|
+
self._raise_for_status(resp)
|
|
629
|
+
return Memory.from_dict(resp.json())
|
|
630
|
+
|
|
631
|
+
async def forget(
|
|
632
|
+
self,
|
|
633
|
+
memory_id: str,
|
|
634
|
+
user_id: str,
|
|
635
|
+
agent_id: str,
|
|
636
|
+
) -> DeleteResult:
|
|
637
|
+
"""Delete a single memory by ID."""
|
|
638
|
+
async with self._get_client() as client:
|
|
639
|
+
resp = await client.delete(
|
|
640
|
+
f"{self.base_url}/memories/{memory_id}",
|
|
641
|
+
params={"user_id": user_id, "agent_id": agent_id},
|
|
642
|
+
)
|
|
643
|
+
self._raise_for_status(resp)
|
|
644
|
+
data = resp.json()
|
|
645
|
+
return DeleteResult(deleted=data["deleted"], message=data["message"])
|
|
646
|
+
|
|
647
|
+
async def forget_all(
|
|
648
|
+
self,
|
|
649
|
+
user_id: str,
|
|
650
|
+
agent_id: str,
|
|
651
|
+
) -> DeleteResult:
|
|
652
|
+
"""Wipe all memories for a user+agent pair."""
|
|
653
|
+
async with self._get_client() as client:
|
|
654
|
+
resp = await client.delete(
|
|
655
|
+
f"{self.base_url}/memories",
|
|
656
|
+
params={
|
|
657
|
+
"user_id": user_id,
|
|
658
|
+
"agent_id": agent_id,
|
|
659
|
+
"confirm": True,
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
self._raise_for_status(resp)
|
|
663
|
+
data = resp.json()
|
|
664
|
+
return DeleteResult(deleted=data["deleted"], message=data["message"])
|
|
665
|
+
|
|
666
|
+
async def is_duplicate(
|
|
667
|
+
self,
|
|
668
|
+
content: str,
|
|
669
|
+
user_id: str,
|
|
670
|
+
agent_id: str,
|
|
671
|
+
threshold: float = 0.95,
|
|
672
|
+
) -> bool:
|
|
673
|
+
"""Check if a memory already exists before storing."""
|
|
674
|
+
async with self._get_client() as client:
|
|
675
|
+
resp = await client.post(
|
|
676
|
+
f"{self.base_url}/memories/duplicate-check",
|
|
677
|
+
params={
|
|
678
|
+
"content": content,
|
|
679
|
+
"user_id": user_id,
|
|
680
|
+
"agent_id": agent_id,
|
|
681
|
+
"threshold": threshold,
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
self._raise_for_status(resp)
|
|
685
|
+
return resp.json()["is_duplicate"]
|
|
686
|
+
|
|
687
|
+
def _raise_for_status(self, resp: httpx.Response) -> None:
|
|
688
|
+
if resp.status_code < 400:
|
|
689
|
+
return
|
|
690
|
+
try:
|
|
691
|
+
detail = resp.json().get("detail", resp.text)
|
|
692
|
+
except Exception:
|
|
693
|
+
detail = resp.text
|
|
694
|
+
|
|
695
|
+
if resp.status_code == 401:
|
|
696
|
+
raise AuthenticationError(resp.status_code, detail)
|
|
697
|
+
if resp.status_code == 402:
|
|
698
|
+
raise PlanLimitError(resp.status_code, detail)
|
|
699
|
+
if resp.status_code == 404:
|
|
700
|
+
raise MemoryNotFoundError(resp.status_code, detail)
|
|
701
|
+
if resp.status_code == 409:
|
|
702
|
+
raise DuplicateMemoryError(resp.status_code, detail)
|
|
703
|
+
raise MemLayerError(resp.status_code, detail)
|
|
704
|
+
|
|
705
|
+
async def __aenter__(self):
|
|
706
|
+
return self
|
|
707
|
+
|
|
708
|
+
async def __aexit__(self, *args):
|
|
709
|
+
pass
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memlayer-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Persistent memory for AI agents — one API key, your agent remembers everything.
|
|
5
|
+
Home-page: https://github.com/yourusername/memlayer
|
|
6
|
+
Author: Victor Sunday
|
|
7
|
+
Author-email: sunvictor567@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Documentation, https://memlayer.online/docs
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/memlayer/issues
|
|
11
|
+
Project-URL: Homepage, https://memlayer.online
|
|
12
|
+
Keywords: ai agent memory vector search langchain langgraph memlayer persistent memory
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Development Status :: 4 - Beta
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: keywords
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: project-url
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
40
|
+
|
|
41
|
+
# memlayer-py
|
|
42
|
+
|
|
43
|
+
> Persistent memory for AI agents. One API key. Your agent remembers everything.
|
|
44
|
+
|
|
45
|
+
[](https://badge.fury.io/py/memlayer-py)
|
|
46
|
+
[](https://www.python.org/downloads/)
|
|
47
|
+
[](https://opensource.org/licenses/MIT)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## The Problem
|
|
52
|
+
|
|
53
|
+
Every time a user starts a new conversation with your AI agent, it starts from zero. It doesn't remember their name, their preferences, what they complained about last week, or anything they've ever told it. You're either stuffing conversation history into the system prompt and hitting token limits, or your agent is asking the same questions over and over.
|
|
54
|
+
|
|
55
|
+
**MemLayer fixes that.**
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install memlayer-py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from memlayer import MemLayerClient
|
|
71
|
+
|
|
72
|
+
client = MemLayerClient(
|
|
73
|
+
api_key="ml_live_xxx",
|
|
74
|
+
base_url="https://memlayer.online",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Store what your agent learns
|
|
78
|
+
client.remember(
|
|
79
|
+
"User prefers concise bullet points over long paragraphs",
|
|
80
|
+
user_id="user_123",
|
|
81
|
+
agent_id="support_bot",
|
|
82
|
+
memory_type="semantic",
|
|
83
|
+
importance=0.8,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Load context at the start of every session
|
|
87
|
+
context = client.context(user_id="user_123", agent_id="support_bot")
|
|
88
|
+
for m in context.memories:
|
|
89
|
+
print(m.content)
|
|
90
|
+
|
|
91
|
+
# Search when the user asks something
|
|
92
|
+
memories = client.recall(
|
|
93
|
+
"what are this user's communication preferences?",
|
|
94
|
+
user_id="user_123",
|
|
95
|
+
agent_id="support_bot",
|
|
96
|
+
)
|
|
97
|
+
for m in memories:
|
|
98
|
+
print(f"[{m.score:.3f}] {m.content}")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Get an API Key
|
|
104
|
+
|
|
105
|
+
1. Visit [memlayer.online](https://memlayer.online)
|
|
106
|
+
2. Sign up for a free account
|
|
107
|
+
3. Copy your API key (`ml_live_xxx`)
|
|
108
|
+
|
|
109
|
+
Free plan includes 500 memories and 100 requests per day — enough to build and test your agent fully.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Core Methods
|
|
114
|
+
|
|
115
|
+
### `remember()` — Store a memory
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
result = client.remember(
|
|
119
|
+
content="User is based in Lagos, Nigeria",
|
|
120
|
+
user_id="user_123",
|
|
121
|
+
agent_id="support_bot",
|
|
122
|
+
memory_type="semantic", # episodic | semantic | summary
|
|
123
|
+
importance=0.8, # 0.0 (low) to 1.0 (high)
|
|
124
|
+
ttl_days=30, # auto-expire after 30 days. None = permanent
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
print(result.id) # memory UUID
|
|
128
|
+
print(result.is_duplicate) # True if already stored
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `recall()` — Semantic search
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
memories = client.recall(
|
|
135
|
+
query="what does this user prefer?",
|
|
136
|
+
user_id="user_123",
|
|
137
|
+
agent_id="support_bot",
|
|
138
|
+
top_k=5, # return top 5 results
|
|
139
|
+
min_score=0.70, # minimum relevance threshold
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for m in memories:
|
|
143
|
+
print(m.content) # memory text
|
|
144
|
+
print(m.score) # hybrid relevance score
|
|
145
|
+
print(m.importance) # importance weight
|
|
146
|
+
print(m.memory_type) # episodic | semantic | summary
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Retrieval uses hybrid scoring — **70% semantic similarity + 20% recency + 10% importance** — so the most relevant and recent memories always rank first.
|
|
150
|
+
|
|
151
|
+
### `context()` — Load session context
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Call this at the start of every conversation
|
|
155
|
+
context = client.context(
|
|
156
|
+
user_id="user_123",
|
|
157
|
+
agent_id="support_bot",
|
|
158
|
+
top_k=10,
|
|
159
|
+
|
|
160
|
+
# Optional: pass the user's first message for smarter retrieval
|
|
161
|
+
current_message="I need help with my order",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Inject into your system prompt
|
|
165
|
+
system_prompt = "You are a helpful assistant.\n\nWhat you know about this user:\n"
|
|
166
|
+
for m in context.memories:
|
|
167
|
+
system_prompt += f"- {m.content}\n"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `update()` — Update a memory
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# User moved cities — update the old memory in place
|
|
174
|
+
updated = client.update(
|
|
175
|
+
memory_id="775263ee-73af-4416-9804-1f274048ae08",
|
|
176
|
+
user_id="user_123",
|
|
177
|
+
agent_id="support_bot",
|
|
178
|
+
new_content="User moved from Lagos to Abuja, Nigeria",
|
|
179
|
+
importance=0.95,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
print(updated.content) # "User moved from Lagos to Abuja, Nigeria"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Same memory ID — just new content and a fresh embedding. No duplicate memories.
|
|
186
|
+
|
|
187
|
+
### `forget()` — Delete one memory
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
result = client.forget(
|
|
191
|
+
memory_id="775263ee-73af-4416-9804-1f274048ae08",
|
|
192
|
+
user_id="user_123",
|
|
193
|
+
agent_id="support_bot",
|
|
194
|
+
)
|
|
195
|
+
print(result.deleted) # 1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `forget_all()` — Wipe all memories
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
# Use for GDPR requests or account resets
|
|
202
|
+
result = client.forget_all(user_id="user_123", agent_id="support_bot")
|
|
203
|
+
print(result.deleted) # number of memories deleted
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `is_duplicate()` — Check before storing
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Optional — remember() already checks internally
|
|
210
|
+
# Use this when you want to check without committing to store
|
|
211
|
+
already_known = client.is_duplicate(
|
|
212
|
+
content="User is based in Lagos",
|
|
213
|
+
user_id="user_123",
|
|
214
|
+
agent_id="support_bot",
|
|
215
|
+
threshold=0.95,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if not already_known:
|
|
219
|
+
client.remember("User is based in Lagos", user_id="user_123", agent_id="support_bot")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `list()` — Browse all memories
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# Paginate through all stored memories
|
|
226
|
+
page = client.list(
|
|
227
|
+
user_id="user_123",
|
|
228
|
+
agent_id="support_bot",
|
|
229
|
+
limit=20,
|
|
230
|
+
offset=0,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
print(f"Total: {page.total}")
|
|
234
|
+
for m in page.memories:
|
|
235
|
+
print(m.content, m.created_at)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Memory Types
|
|
241
|
+
|
|
242
|
+
| Type | Use For | Example |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `episodic` | Things that happened | "User complained about slow delivery" |
|
|
245
|
+
| `semantic` | Facts about the user | "User is based in Lagos, Nigeria" |
|
|
246
|
+
| `summary` | Compressed older memories | Auto-generated by MemLayer |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Async Support
|
|
251
|
+
|
|
252
|
+
For LangGraph, FastAPI, and any async application:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from memlayer import AsyncMemLayerClient
|
|
256
|
+
|
|
257
|
+
async def main():
|
|
258
|
+
async with AsyncMemLayerClient(api_key="ml_live_xxx") as client:
|
|
259
|
+
|
|
260
|
+
# Store
|
|
261
|
+
result = await client.remember(
|
|
262
|
+
"User prefers dark mode",
|
|
263
|
+
user_id="user_123",
|
|
264
|
+
agent_id="support_bot",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Search
|
|
268
|
+
memories = await client.recall(
|
|
269
|
+
"UI preferences",
|
|
270
|
+
user_id="user_123",
|
|
271
|
+
agent_id="support_bot",
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## LangGraph Integration
|
|
278
|
+
|
|
279
|
+
Drop MemLayer into any LangGraph graph as store and retrieve nodes:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from memlayer import AsyncMemLayerClient
|
|
283
|
+
from langgraph.graph import StateGraph, MessagesState
|
|
284
|
+
|
|
285
|
+
maas = AsyncMemLayerClient(api_key="ml_live_xxx")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def load_memory(state: MessagesState):
|
|
289
|
+
"""Load relevant memories before the agent responds."""
|
|
290
|
+
last_message = state["messages"][-1].content
|
|
291
|
+
|
|
292
|
+
memories = await maas.recall(
|
|
293
|
+
query=last_message,
|
|
294
|
+
user_id=state["user_id"],
|
|
295
|
+
agent_id="my_agent",
|
|
296
|
+
top_k=5,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
memory_context = "\n".join(f"- {m.content}" for m in memories)
|
|
300
|
+
state["memory_context"] = memory_context
|
|
301
|
+
return state
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def save_memory(state: MessagesState):
|
|
305
|
+
"""Save what the agent learned after responding."""
|
|
306
|
+
last_message = state["messages"][-1].content
|
|
307
|
+
|
|
308
|
+
await maas.remember(
|
|
309
|
+
content=last_message,
|
|
310
|
+
user_id=state["user_id"],
|
|
311
|
+
agent_id="my_agent",
|
|
312
|
+
memory_type="episodic",
|
|
313
|
+
)
|
|
314
|
+
return state
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# Wire into your graph
|
|
318
|
+
builder = StateGraph(MessagesState)
|
|
319
|
+
builder.add_node("load_memory", load_memory)
|
|
320
|
+
builder.add_node("agent", your_agent_node)
|
|
321
|
+
builder.add_node("save_memory", save_memory)
|
|
322
|
+
|
|
323
|
+
builder.add_edge("load_memory", "agent")
|
|
324
|
+
builder.add_edge("agent", "save_memory")
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Error Handling
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from memlayer import (
|
|
333
|
+
MemLayerClient,
|
|
334
|
+
AuthenticationError,
|
|
335
|
+
PlanLimitError,
|
|
336
|
+
MemoryNotFoundError,
|
|
337
|
+
DuplicateMemoryError,
|
|
338
|
+
MemLayerError,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
client = MemLayerClient(api_key="ml_live_xxx")
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
result = client.remember(
|
|
345
|
+
"User prefers dark mode",
|
|
346
|
+
user_id="user_123",
|
|
347
|
+
agent_id="support_bot",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
except AuthenticationError:
|
|
351
|
+
print("Invalid API key — check your ml_live_xxx key")
|
|
352
|
+
|
|
353
|
+
except PlanLimitError:
|
|
354
|
+
print("Memory limit reached — upgrade your plan at memlayer.online")
|
|
355
|
+
|
|
356
|
+
except DuplicateMemoryError:
|
|
357
|
+
print("This memory already exists — skipped")
|
|
358
|
+
|
|
359
|
+
except MemoryNotFoundError:
|
|
360
|
+
print("Memory ID not found")
|
|
361
|
+
|
|
362
|
+
except MemLayerError as e:
|
|
363
|
+
print(f"API error {e.status_code}: {e.detail}")
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Context Manager
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
# Sync
|
|
372
|
+
with MemLayerClient(api_key="ml_live_xxx") as client:
|
|
373
|
+
client.remember("something", user_id="u1", agent_id="bot")
|
|
374
|
+
|
|
375
|
+
# Async
|
|
376
|
+
async with AsyncMemLayerClient(api_key="ml_live_xxx") as client:
|
|
377
|
+
await client.remember("something", user_id="u1", agent_id="bot")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Pricing
|
|
383
|
+
|
|
384
|
+
| Plan | Memories | Requests/day | Price |
|
|
385
|
+
|---|---|---|---|
|
|
386
|
+
| Free | 500 | 100 | $0/mo |
|
|
387
|
+
| Pro | 50,000 | 10,000 | $19/mo |
|
|
388
|
+
| Enterprise | Unlimited | Unlimited | $99+/mo |
|
|
389
|
+
|
|
390
|
+
Enterprise includes BYOD (Bring Your Own Database) — your data never leaves your Supabase instance.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## REST API
|
|
395
|
+
|
|
396
|
+
You don't need the SDK — every method maps to a REST endpoint:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
# Store
|
|
400
|
+
curl -X POST https://memlayer.online/memories \
|
|
401
|
+
-H "X-API-Key: ml_live_xxx" \
|
|
402
|
+
-H "Content-Type: application/json" \
|
|
403
|
+
-d '{"content": "User prefers dark mode", "user_id": "u1", "agent_id": "bot"}'
|
|
404
|
+
|
|
405
|
+
# Search
|
|
406
|
+
curl "https://memlayer.online/memories/search?query=preferences&user_id=u1&agent_id=bot" \
|
|
407
|
+
-H "X-API-Key: ml_live_xxx"
|
|
408
|
+
|
|
409
|
+
# Context
|
|
410
|
+
curl "https://memlayer.online/memories/context?user_id=u1&agent_id=bot" \
|
|
411
|
+
-H "X-API-Key: ml_live_xxx"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Full API reference at [memlayer.online/docs](https://memlayer.online/docs).
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Links
|
|
419
|
+
|
|
420
|
+
- [Website](https://memlayer.online)
|
|
421
|
+
- [API Docs](https://memlayer.online/docs)
|
|
422
|
+
- [GitHub](https://github.com/yourusername/memlayer)
|
|
423
|
+
- [Report a Bug](https://github.com/yourusername/memlayer/issues)
|
|
424
|
+
- [Email](mailto:support@memlayer.online)
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
memlayer/__init__.py,sha256=tyPl6AimPv8i3mf1Ma9HnFflmIqM6f1FP7SVoQBvE3Y,575
|
|
2
|
+
memlayer/client.py,sha256=WW-f7Yk8OT_tXfbXYijpI4ZEzf4TaqNRcjZ4CdRleMw,23943
|
|
3
|
+
memlayer_py-0.1.0.dist-info/licenses/LICENSE,sha256=B_XNTK2p3yc-FWqtdXjStFoudGNhLxXWrsW9CCiOKTU,1089
|
|
4
|
+
memlayer_py-0.1.0.dist-info/METADATA,sha256=obQTKiimaxEQ67ilcXasLxHwSpfU-TjS08dP25k2Wqk,11535
|
|
5
|
+
memlayer_py-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
memlayer_py-0.1.0.dist-info/top_level.txt,sha256=ET9kj1uGmnujtUVF0pEqmE4SgJgK25s3o7e8czqmPOU,9
|
|
7
|
+
memlayer_py-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Victor Sunday
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
memlayer
|