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
+ [![PyPI version](https://badge.fury.io/py/memlayer-py.svg)](https://badge.fury.io/py/memlayer-py)
46
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
47
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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