memorylayer-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.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: memorylayer-py
3
+ Version: 0.1.0
4
+ Summary: Privacy-first memory API for LLMs
5
+ Author-email: "rec0.ai" <hello@rec0.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://rec0.ai
8
+ Project-URL: Documentation, https://docs.rec0.ai
9
+ Project-URL: Repository, https://github.com/rec0ai/rec0-python
10
+ Project-URL: Bug Tracker, https://github.com/rec0ai/rec0-python/issues
11
+ Keywords: llm,memory,ai,privacy,rag
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: requests>=2.28.0
24
+ Requires-Dist: httpx>=0.24.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: responses>=0.25.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
29
+
30
+ # rec0 — memory for any LLM
31
+
32
+ > Give your AI a permanent memory in 3 lines of code.
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/rec0.svg)](https://pypi.org/project/rec0/)
35
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install rec0
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ from rec0 import Memory
48
+
49
+ mem = Memory(api_key="r0_xxx", user_id="user_123")
50
+ mem.store("User prefers Python and dark mode")
51
+ context = mem.context("user preferences")
52
+ # inject context into your LLM prompt — done
53
+ ```
54
+
55
+ That's it. `context` returns a bullet-list string ready to prepend to any system prompt.
56
+
57
+ ---
58
+
59
+ ## Why rec0
60
+
61
+ | | rec0 | Mem0 |
62
+ |---|---|---|
63
+ | **Privacy** | Data never leaves your servers | Processed externally |
64
+ | **Cost** | $0.002 / 1K ops | ~$0.10 / 1K ops |
65
+ | **Setup** | 3 lines | OAuth + config |
66
+ | **LLM support** | Any model | OpenAI-first |
67
+ | **GDPR** | 1 API call | Manual |
68
+
69
+ - **Privacy-first:** embeddings and summaries run on YOUR infrastructure — no user data touches third-party APIs
70
+ - **LLM-agnostic:** works with OpenAI, Anthropic, Gemini, Llama, Mistral — anything that takes a string
71
+ - **Memory lifecycle:** automatic importance scoring, recall-count boosting, and time-based decay
72
+ - **GDPR compliant:** right-to-erasure in one call (`mem.delete_user()`)
73
+
74
+ ---
75
+
76
+ ## Full API reference
77
+
78
+ ### `Memory(user_id, api_key, app_id, base_url)`
79
+
80
+ | Parameter | Type | Default | Description |
81
+ |---|---|---|---|
82
+ | `user_id` | `str` | required | Your end-user identifier |
83
+ | `api_key` | `str` | `$REC0_API_KEY` | Your rec0 API key |
84
+ | `app_id` | `str` | `"default"` | Namespace for multi-app isolation |
85
+ | `base_url` | `str` | prod URL | Override for self-hosting |
86
+
87
+ ### Methods
88
+
89
+ #### `mem.store(content)` → `MemoryObject`
90
+ Store a new memory. Auto-generates embedding and summary server-side.
91
+
92
+ ```python
93
+ m = mem.store("User is building a SaaS product in Python")
94
+ print(m.id) # UUID
95
+ print(m.importance) # starts at 1.0, increases with each recall
96
+ ```
97
+
98
+ #### `mem.context(query, limit=5)` → `str`
99
+ **The most-used method.** Returns a bullet-list string to inject into your LLM prompt.
100
+
101
+ ```python
102
+ context = mem.context("what does the user like", limit=5)
103
+ # "- User prefers Python and dark mode\n- User is building a SaaS product"
104
+
105
+ # Typical usage with OpenAI:
106
+ messages = [
107
+ {"role": "system", "content": f"User context:\n{context}"},
108
+ {"role": "user", "content": user_message},
109
+ ]
110
+ ```
111
+
112
+ #### `mem.recall(query, limit=5)` → `List[MemoryObject]`
113
+ Returns memories ranked by semantic similarity. Use when you need scores or metadata.
114
+
115
+ ```python
116
+ memories = mem.recall("programming preferences", limit=3)
117
+ for m in memories:
118
+ print(f"{m.content} (score: {m.relevance_score})")
119
+ ```
120
+
121
+ #### `mem.list()` → `List[MemoryObject]`
122
+ All active memories for this user, ordered by creation time.
123
+
124
+ #### `mem.delete(memory_id)` → `None`
125
+ Soft-delete a specific memory (retained for audit trail).
126
+
127
+ #### `mem.delete_user()` → `dict`
128
+ GDPR right-to-erasure. Removes all memories for this user.
129
+
130
+ #### `mem.export()` → `dict`
131
+ GDPR data export. Returns all memory data as a dictionary.
132
+
133
+ #### `mem.ping()` → `bool`
134
+ Connectivity check. Returns `True` if the API is reachable.
135
+
136
+ ```python
137
+ if not mem.ping():
138
+ print("rec0 API unreachable — check your key")
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Error handling
144
+
145
+ ```python
146
+ from rec0 import Memory, Rec0Error, AuthError, RateLimitError, NotFoundError
147
+
148
+ mem = Memory(api_key="r0_xxx", user_id="user_123")
149
+
150
+ try:
151
+ mem.store("User loves rec0")
152
+ except AuthError:
153
+ print("Invalid API key — check REC0_API_KEY")
154
+ except RateLimitError as e:
155
+ print(f"Rate limited — retry in {e.retry_after}s")
156
+ except NotFoundError:
157
+ print("Memory not found")
158
+ except Rec0Error as e:
159
+ print(f"Unexpected error: {e}")
160
+ ```
161
+
162
+ Rate limits are handled automatically: rec0 will wait `retry_after` seconds and retry once before raising.
163
+
164
+ ---
165
+
166
+ ## Async usage
167
+
168
+ Every method has an async equivalent via `AsyncMemory`:
169
+
170
+ ```python
171
+ import asyncio
172
+ from rec0 import AsyncMemory
173
+
174
+ async def main():
175
+ mem = AsyncMemory(api_key="r0_xxx", user_id="user_123")
176
+ await mem.store("User is a night-owl developer")
177
+ context = await mem.context("when does the user work")
178
+ print(context)
179
+
180
+ asyncio.run(main())
181
+ ```
182
+
183
+ `AsyncMemory` uses `httpx` under the hood and is safe to use in FastAPI, Django async views, and any `asyncio` application.
184
+
185
+ ---
186
+
187
+ ## Environment variables
188
+
189
+ | Variable | Description |
190
+ |---|---|
191
+ | `REC0_API_KEY` | Your rec0 API key (used automatically if `api_key=` not passed) |
192
+ | `REC0_BASE_URL` | Override the API base URL (optional, for self-hosting) |
193
+
194
+ ```bash
195
+ export REC0_API_KEY=r0_your_key_here
196
+ ```
197
+
198
+ ```python
199
+ # api_key is now auto-loaded — no need to hardcode it
200
+ mem = Memory(user_id="user_123")
201
+ ```
202
+
203
+ ---
204
+
205
+ ## MemoryObject fields
206
+
207
+ | Field | Type | Description |
208
+ |---|---|---|
209
+ | `id` | `str` | UUID |
210
+ | `content` | `str` | The original memory text |
211
+ | `summary` | `str \| None` | Auto-generated summary |
212
+ | `importance` | `float` | 1.0–10.0; increases with recall |
213
+ | `recall_count` | `int` | Times this memory was recalled |
214
+ | `relevance_score` | `float \| None` | Similarity score (recall only) |
215
+ | `created_at` | `datetime` | When stored |
216
+ | `is_active` | `bool` | False if deleted |
217
+
218
+ ---
219
+
220
+ ## Self-hosting
221
+
222
+ rec0 is open-source. Deploy your own instance on Railway, Fly, or any server:
223
+
224
+ ```bash
225
+ git clone https://github.com/patelyash2511/memorylayer
226
+ # See README for Railway deployment instructions
227
+ ```
228
+
229
+ Then point the SDK at your instance:
230
+
231
+ ```python
232
+ mem = Memory(
233
+ api_key="your_key",
234
+ user_id="user_123",
235
+ base_url="https://your-instance.up.railway.app",
236
+ )
237
+ ```
238
+
239
+ ---
240
+
241
+ [rec0.ai](https://rec0.ai) · [docs](https://docs.rec0.ai) · [discord](https://discord.gg/rec0) · [twitter](https://twitter.com/rec0ai)
@@ -0,0 +1,10 @@
1
+ rec0/__init__.py,sha256=NReh7VjGkvbR76XKFFvymatPGHFWThhlLxGAJT3lCgA,742
2
+ rec0/async_client.py,sha256=B-GpMT0BpxscOT_kXwxf1LiGktQXyHBPJFL5ykuTHk4,7445
3
+ rec0/client.py,sha256=C_fGgd503bJTDD6Gs1qDJrGLoc0BZ6ztKk2mCyHxjoY,7970
4
+ rec0/exceptions.py,sha256=u7b3DgO0A7Ecs2pOOWTX1i-ihoKpQqBLw8LQvwgoqBw,740
5
+ rec0/models.py,sha256=nh4jDfOeenZMdu2daDbvVd9XDd3sOmr6F15K0JXJWvs,1826
6
+ rec0/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
7
+ memorylayer_py-0.1.0.dist-info/METADATA,sha256=PdtGY0LaEXCkI5IvPKPrQrt2qLMDiBwtv3E7fmP2A44,7038
8
+ memorylayer_py-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ memorylayer_py-0.1.0.dist-info/top_level.txt,sha256=8N2dtJTBzcUZEXxkphDTo3LB6vKzMcbiag7DWe4iQQU,5
10
+ memorylayer_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 @@
1
+ rec0
rec0/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """rec0 — privacy-first memory API for LLMs.
2
+
3
+ Quick start::
4
+
5
+ from rec0 import Memory
6
+
7
+ mem = Memory(api_key="r0_xxx", user_id="user_123")
8
+ mem.store("User prefers dark mode and uses VSCode")
9
+ context = mem.context("user preferences")
10
+ # inject context into your LLM prompt — done
11
+ """
12
+
13
+ from .async_client import AsyncMemory
14
+ from .client import Memory
15
+ from .exceptions import AuthError, NotFoundError, RateLimitError, Rec0Error, ServerError
16
+ from .models import MemoryObject, RecallResult
17
+ from .version import __version__
18
+
19
+ __all__ = [
20
+ "Memory",
21
+ "AsyncMemory",
22
+ "MemoryObject",
23
+ "RecallResult",
24
+ "Rec0Error",
25
+ "AuthError",
26
+ "RateLimitError",
27
+ "NotFoundError",
28
+ "ServerError",
29
+ "__version__",
30
+ ]
rec0/async_client.py ADDED
@@ -0,0 +1,201 @@
1
+ """rec0 async client — AsyncMemory class backed by httpx."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import asyncio
7
+ import os
8
+ from typing import List, Optional
9
+
10
+ import httpx
11
+
12
+ from .exceptions import AuthError, NotFoundError, RateLimitError, Rec0Error, ServerError
13
+ from .models import MemoryObject, RecallResult
14
+ from .version import __version__
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _DEFAULT_BASE_URL = "https://memorylayer-production.up.railway.app"
19
+ _DEFAULT_TIMEOUT = 10 # seconds
20
+
21
+
22
+ def _raise_for_response(resp: httpx.Response) -> None:
23
+ """Map HTTP error codes to typed exceptions."""
24
+ if resp.status_code < 400:
25
+ return
26
+ try:
27
+ body = resp.json()
28
+ detail = body.get("detail", body)
29
+ if isinstance(detail, dict):
30
+ message = detail.get("message", str(detail))
31
+ else:
32
+ message = str(detail)
33
+ except Exception:
34
+ message = resp.text or f"HTTP {resp.status_code}"
35
+
36
+ if resp.status_code == 401:
37
+ raise AuthError(message)
38
+ if resp.status_code == 404:
39
+ raise NotFoundError(message)
40
+ if resp.status_code == 429:
41
+ retry_after = 60
42
+ try:
43
+ detail = resp.json().get("detail", {})
44
+ if isinstance(detail, dict):
45
+ retry_after = int(detail.get("retry_after_seconds", 60))
46
+ except Exception:
47
+ pass
48
+ raise RateLimitError(message, retry_after=retry_after)
49
+ if resp.status_code >= 500:
50
+ raise ServerError(message)
51
+ raise Rec0Error(f"HTTP {resp.status_code}: {message}")
52
+
53
+
54
+ class AsyncMemory:
55
+ """Asynchronous rec0 client.
56
+
57
+ Example::
58
+
59
+ from rec0 import AsyncMemory
60
+
61
+ async def main():
62
+ mem = AsyncMemory(api_key="r0_xxx", user_id="user_123")
63
+ await mem.store("User is a Python developer")
64
+ context = await mem.context("user preferences")
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ user_id: str,
70
+ api_key: Optional[str] = None,
71
+ app_id: str = "default",
72
+ base_url: str = _DEFAULT_BASE_URL,
73
+ timeout: int = _DEFAULT_TIMEOUT,
74
+ ) -> None:
75
+ resolved_key = api_key or os.environ.get("REC0_API_KEY", "")
76
+ if not resolved_key:
77
+ raise AuthError(
78
+ "No API key provided. Pass api_key= or set REC0_API_KEY env var."
79
+ )
80
+ self._api_key = resolved_key
81
+ self.user_id = user_id
82
+ self.app_id = app_id
83
+ self._base_url = base_url.rstrip("/")
84
+ self._timeout = timeout
85
+ self._headers = {
86
+ "X-API-Key": self._api_key,
87
+ "Content-Type": "application/json",
88
+ "User-Agent": f"rec0-python/{__version__}",
89
+ }
90
+
91
+ # ── Internal helpers ───────────────────────────────────────────────────────
92
+
93
+ async def _post(self, path: str, json: dict) -> httpx.Response:
94
+ try:
95
+ async with httpx.AsyncClient(
96
+ headers=self._headers, timeout=self._timeout
97
+ ) as client:
98
+ resp = await client.post(f"{self._base_url}{path}", json=json)
99
+ except httpx.TimeoutException:
100
+ raise Rec0Error("Request timed out")
101
+ except httpx.ConnectError as exc:
102
+ raise Rec0Error(f"Connection error: {exc}")
103
+ _raise_for_response(resp)
104
+ return resp
105
+
106
+ async def _get(self, path: str, params: Optional[dict] = None) -> httpx.Response:
107
+ try:
108
+ async with httpx.AsyncClient(
109
+ headers=self._headers, timeout=self._timeout
110
+ ) as client:
111
+ resp = await client.get(f"{self._base_url}{path}", params=params)
112
+ except httpx.TimeoutException:
113
+ raise Rec0Error("Request timed out")
114
+ except httpx.ConnectError as exc:
115
+ raise Rec0Error(f"Connection error: {exc}")
116
+ _raise_for_response(resp)
117
+ return resp
118
+
119
+ async def _delete(self, path: str) -> httpx.Response:
120
+ try:
121
+ async with httpx.AsyncClient(
122
+ headers=self._headers, timeout=self._timeout
123
+ ) as client:
124
+ resp = await client.delete(f"{self._base_url}{path}")
125
+ except httpx.TimeoutException:
126
+ raise Rec0Error("Request timed out")
127
+ except httpx.ConnectError as exc:
128
+ raise Rec0Error(f"Connection error: {exc}")
129
+ _raise_for_response(resp)
130
+ return resp
131
+
132
+ async def _post_with_retry(self, path: str, json: dict) -> httpx.Response:
133
+ """POST with one automatic retry on RateLimitError."""
134
+ try:
135
+ return await self._post(path, json)
136
+ except RateLimitError as exc:
137
+ logger.warning("rec0: rate limited, retrying in %ds...", exc.retry_after)
138
+ await asyncio.sleep(exc.retry_after)
139
+ return await self._post(path, json)
140
+
141
+ # ── Public API ─────────────────────────────────────────────────────────────
142
+
143
+ async def store(self, content: str) -> MemoryObject:
144
+ """Store a new memory."""
145
+ resp = await self._post_with_retry(
146
+ "/v1/memory/store",
147
+ {"user_id": self.user_id, "app_id": self.app_id, "content": content},
148
+ )
149
+ return MemoryObject._from_dict(resp.json())
150
+
151
+ async def recall(self, query: str, limit: int = 5) -> List[MemoryObject]:
152
+ """Recall memories most relevant to *query*."""
153
+ resp = await self._post_with_retry(
154
+ "/v1/memory/recall",
155
+ {
156
+ "user_id": self.user_id,
157
+ "app_id": self.app_id,
158
+ "query": query,
159
+ "limit": limit,
160
+ },
161
+ )
162
+ data = resp.json()
163
+ return [MemoryObject._from_dict(m) for m in data.get("memories", [])]
164
+
165
+ async def context(self, query: str, limit: int = 5) -> str:
166
+ """Build a context string ready to inject into any LLM prompt."""
167
+ memories = await self.recall(query, limit=limit)
168
+ return "\n".join(f"- {m.content}" for m in memories)
169
+
170
+ async def list(self) -> List[MemoryObject]:
171
+ """List all active memories for this user."""
172
+ resp = await self._get(
173
+ "/v1/memory/list",
174
+ params={"user_id": self.user_id, "app_id": self.app_id},
175
+ )
176
+ data = resp.json()
177
+ return [MemoryObject._from_dict(m) for m in data.get("memories", [])]
178
+
179
+ async def delete(self, memory_id: str) -> None:
180
+ """Soft-delete a specific memory by ID."""
181
+ await self._delete(f"/v1/memory/{memory_id}")
182
+
183
+ async def delete_user(self) -> dict:
184
+ """GDPR right-to-erasure: delete all memories for this user."""
185
+ resp = await self._delete(f"/v1/users/{self.user_id}")
186
+ if resp.status_code == 204 or not resp.content:
187
+ return {"deleted": True, "memories_removed": 0}
188
+ return resp.json()
189
+
190
+ async def export(self) -> dict:
191
+ """GDPR data export: return all memories for this user as a dict."""
192
+ resp = await self._get(f"/v1/users/{self.user_id}/export")
193
+ return resp.json()
194
+
195
+ async def ping(self) -> bool:
196
+ """Check connectivity. Returns True if server is reachable."""
197
+ try:
198
+ await self._get("/health")
199
+ return True
200
+ except Exception:
201
+ return False
rec0/client.py ADDED
@@ -0,0 +1,232 @@
1
+ """rec0 sync client — Memory class backed by requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import time
8
+ from typing import List, Optional
9
+
10
+ import requests
11
+
12
+ from .exceptions import AuthError, NotFoundError, RateLimitError, Rec0Error, ServerError
13
+ from .models import MemoryObject, RecallResult
14
+ from .version import __version__
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _DEFAULT_BASE_URL = "https://memorylayer-production.up.railway.app"
19
+ _DEFAULT_TIMEOUT = 10 # seconds
20
+
21
+
22
+ def _raise_for_response(resp: requests.Response) -> None:
23
+ """Map HTTP error codes to typed exceptions."""
24
+ if resp.status_code < 400:
25
+ return
26
+ try:
27
+ body = resp.json()
28
+ detail = body.get("detail", body)
29
+ if isinstance(detail, dict):
30
+ message = detail.get("message", str(detail))
31
+ else:
32
+ message = str(detail)
33
+ except Exception:
34
+ message = resp.text or f"HTTP {resp.status_code}"
35
+
36
+ if resp.status_code == 401:
37
+ raise AuthError(message)
38
+ if resp.status_code == 404:
39
+ raise NotFoundError(message)
40
+ if resp.status_code == 429:
41
+ retry_after = 60
42
+ try:
43
+ detail = resp.json().get("detail", {})
44
+ if isinstance(detail, dict):
45
+ retry_after = int(detail.get("retry_after_seconds", 60))
46
+ except Exception:
47
+ pass
48
+ raise RateLimitError(message, retry_after=retry_after)
49
+ if resp.status_code >= 500:
50
+ raise ServerError(message)
51
+ raise Rec0Error(f"HTTP {resp.status_code}: {message}")
52
+
53
+
54
+ class Memory:
55
+ """Synchronous rec0 client.
56
+
57
+ Example::
58
+
59
+ from rec0 import Memory
60
+
61
+ mem = Memory(api_key="r0_xxx", user_id="user_123")
62
+ mem.store("User prefers dark mode")
63
+ context = mem.context("user preferences")
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ user_id: str,
69
+ api_key: Optional[str] = None,
70
+ app_id: str = "default",
71
+ base_url: str = _DEFAULT_BASE_URL,
72
+ timeout: int = _DEFAULT_TIMEOUT,
73
+ ) -> None:
74
+ resolved_key = api_key or os.environ.get("REC0_API_KEY", "")
75
+ if not resolved_key:
76
+ raise AuthError(
77
+ "No API key provided. Pass api_key= or set REC0_API_KEY env var."
78
+ )
79
+ self._api_key = resolved_key
80
+ self.user_id = user_id
81
+ self.app_id = app_id
82
+ self._base_url = base_url.rstrip("/")
83
+ self._timeout = timeout
84
+ self._session = requests.Session()
85
+ self._session.headers.update(
86
+ {
87
+ "X-API-Key": self._api_key,
88
+ "Content-Type": "application/json",
89
+ "User-Agent": f"rec0-python/{__version__}",
90
+ }
91
+ )
92
+
93
+ # ── Internal helpers ───────────────────────────────────────────────────────
94
+
95
+ def _post(self, path: str, json: dict) -> requests.Response:
96
+ try:
97
+ resp = self._session.post(
98
+ f"{self._base_url}{path}", json=json, timeout=self._timeout
99
+ )
100
+ except requests.Timeout:
101
+ raise Rec0Error("Request timed out")
102
+ except requests.ConnectionError as exc:
103
+ raise Rec0Error(f"Connection error: {exc}")
104
+ _raise_for_response(resp)
105
+ return resp
106
+
107
+ def _get(self, path: str, params: Optional[dict] = None) -> requests.Response:
108
+ try:
109
+ resp = self._session.get(
110
+ f"{self._base_url}{path}", params=params, timeout=self._timeout
111
+ )
112
+ except requests.Timeout:
113
+ raise Rec0Error("Request timed out")
114
+ except requests.ConnectionError as exc:
115
+ raise Rec0Error(f"Connection error: {exc}")
116
+ _raise_for_response(resp)
117
+ return resp
118
+
119
+ def _delete(self, path: str) -> requests.Response:
120
+ try:
121
+ resp = self._session.delete(
122
+ f"{self._base_url}{path}", timeout=self._timeout
123
+ )
124
+ except requests.Timeout:
125
+ raise Rec0Error("Request timed out")
126
+ except requests.ConnectionError as exc:
127
+ raise Rec0Error(f"Connection error: {exc}")
128
+ _raise_for_response(resp)
129
+ return resp
130
+
131
+ def _post_with_retry(self, path: str, json: dict) -> requests.Response:
132
+ """POST with one automatic retry on RateLimitError."""
133
+ try:
134
+ return self._post(path, json)
135
+ except RateLimitError as exc:
136
+ logger.warning("rec0: rate limited, retrying in %ds...", exc.retry_after)
137
+ time.sleep(exc.retry_after)
138
+ return self._post(path, json)
139
+
140
+ # ── Public API ─────────────────────────────────────────────────────────────
141
+
142
+ def store(self, content: str) -> MemoryObject:
143
+ """Store a new memory.
144
+
145
+ Args:
146
+ content: The text to remember.
147
+
148
+ Returns:
149
+ The stored :class:`MemoryObject`.
150
+ """
151
+ resp = self._post_with_retry(
152
+ "/v1/memory/store",
153
+ {"user_id": self.user_id, "app_id": self.app_id, "content": content},
154
+ )
155
+ return MemoryObject._from_dict(resp.json())
156
+
157
+ def recall(self, query: str, limit: int = 5) -> List[MemoryObject]:
158
+ """Recall memories most relevant to *query*.
159
+
160
+ Args:
161
+ query: Natural language query.
162
+ limit: Maximum number of results to return (default 5).
163
+
164
+ Returns:
165
+ List of :class:`MemoryObject` sorted by relevance (highest first).
166
+ """
167
+ resp = self._post_with_retry(
168
+ "/v1/memory/recall",
169
+ {
170
+ "user_id": self.user_id,
171
+ "app_id": self.app_id,
172
+ "query": query,
173
+ "limit": limit,
174
+ },
175
+ )
176
+ data = resp.json()
177
+ return [MemoryObject._from_dict(m) for m in data.get("memories", [])]
178
+
179
+ def context(self, query: str, limit: int = 5) -> str:
180
+ """Build a context string ready to inject into any LLM prompt.
181
+
182
+ Args:
183
+ query: What you want to remember about the user.
184
+ limit: Maximum memories to include (default 5).
185
+
186
+ Returns:
187
+ Newline-separated bullet list: ``"- memory1\\n- memory2\\n..."``
188
+ Returns an empty string when no memories exist.
189
+ """
190
+ memories = self.recall(query, limit=limit)
191
+ return "\n".join(f"- {m.content}" for m in memories)
192
+
193
+ def list(self) -> List[MemoryObject]:
194
+ """List all active memories for this user, ordered by creation time."""
195
+ resp = self._get(
196
+ "/v1/memory/list",
197
+ params={"user_id": self.user_id, "app_id": self.app_id},
198
+ )
199
+ data = resp.json()
200
+ return [MemoryObject._from_dict(m) for m in data.get("memories", [])]
201
+
202
+ def delete(self, memory_id: str) -> None:
203
+ """Soft-delete a specific memory by ID."""
204
+ self._delete(f"/v1/memory/{memory_id}")
205
+
206
+ def delete_user(self) -> dict:
207
+ """GDPR right-to-erasure: delete all memories for this user.
208
+
209
+ Returns:
210
+ ``{"deleted": True, "memories_removed": N}``
211
+ """
212
+ resp = self._delete(f"/v1/users/{self.user_id}")
213
+ if resp.status_code == 204 or not resp.content:
214
+ return {"deleted": True, "memories_removed": 0}
215
+ return resp.json()
216
+
217
+ def export(self) -> dict:
218
+ """GDPR data export: return all memories for this user as a dict."""
219
+ resp = self._get(f"/v1/users/{self.user_id}/export")
220
+ return resp.json()
221
+
222
+ def ping(self) -> bool:
223
+ """Check connectivity and confirm the API key works.
224
+
225
+ Returns:
226
+ ``True`` if the server is reachable, ``False`` otherwise.
227
+ """
228
+ try:
229
+ self._get("/health")
230
+ return True
231
+ except Exception:
232
+ return False
rec0/exceptions.py ADDED
@@ -0,0 +1,31 @@
1
+ """rec0 exceptions — every error maps to a meaningful Python type."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Rec0Error(Exception):
7
+ """Base exception for all rec0 errors."""
8
+
9
+
10
+ class AuthError(Rec0Error):
11
+ """Raised on 401 — invalid or missing API key."""
12
+
13
+
14
+ class RateLimitError(Rec0Error):
15
+ """Raised on 429 — rate limit exceeded.
16
+
17
+ Attributes:
18
+ retry_after: seconds to wait before retrying.
19
+ """
20
+
21
+ def __init__(self, message: str, retry_after: int = 60) -> None:
22
+ super().__init__(message)
23
+ self.retry_after = retry_after
24
+
25
+
26
+ class NotFoundError(Rec0Error):
27
+ """Raised on 404 — resource not found."""
28
+
29
+
30
+ class ServerError(Rec0Error):
31
+ """Raised on 5xx — unexpected server error."""
rec0/models.py ADDED
@@ -0,0 +1,61 @@
1
+ """rec0 data models — clean dataclasses for API responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import List, Optional
8
+
9
+
10
+ @dataclass
11
+ class MemoryObject:
12
+ id: str
13
+ user_id: str
14
+ app_id: str
15
+ content: str
16
+ summary: Optional[str]
17
+ importance: float
18
+ recall_count: int
19
+ created_at: datetime
20
+ updated_at: datetime
21
+ is_active: bool
22
+ rec0_version: str
23
+ relevance_score: Optional[float] = None # only present on recall results
24
+
25
+ @classmethod
26
+ def _from_dict(cls, data: dict) -> "MemoryObject":
27
+ return cls(
28
+ id=data["id"],
29
+ user_id=data["user_id"],
30
+ app_id=data["app_id"],
31
+ content=data["content"],
32
+ summary=data.get("summary"),
33
+ importance=float(data.get("importance", 1.0)),
34
+ recall_count=int(data.get("recall_count", 0)),
35
+ created_at=_parse_dt(data["created_at"]),
36
+ updated_at=_parse_dt(data["updated_at"]),
37
+ is_active=bool(data.get("is_active", True)),
38
+ rec0_version=data.get("rec0_version", "1.0.0"),
39
+ relevance_score=float(data["relevance_score"]) if "relevance_score" in data else None,
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class RecallResult:
45
+ memories: List[MemoryObject]
46
+ total_memories: int
47
+ recall_time_ms: int
48
+ rec0_version: str
49
+
50
+
51
+ def _parse_dt(value: str | datetime) -> datetime:
52
+ if isinstance(value, datetime):
53
+ return value
54
+ # Handle ISO strings with or without timezone suffix
55
+ value = value.rstrip("Z")
56
+ for fmt in ("%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"):
57
+ try:
58
+ return datetime.strptime(value, fmt)
59
+ except ValueError:
60
+ continue
61
+ raise ValueError(f"Cannot parse datetime: {value!r}")
rec0/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"