syntarus 0.2.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.
syntarus/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Official Syntarus project-scoped memory clients."""
2
+
3
+ from .client import AsyncMemoryClient, MemoryClient
4
+ from .exceptions import (
5
+ APIConnectionError,
6
+ APITimeoutError,
7
+ AuthenticationError,
8
+ EventFailedError,
9
+ PermissionDenied,
10
+ RateLimitError,
11
+ SyntarusError,
12
+ )
13
+
14
+ __all__ = [
15
+ "AsyncMemoryClient", "MemoryClient", "SyntarusError", "AuthenticationError",
16
+ "PermissionDenied", "RateLimitError", "APIConnectionError", "APITimeoutError",
17
+ "EventFailedError",
18
+ ]
syntarus/client.py ADDED
@@ -0,0 +1,219 @@
1
+ """Synchronous and asynchronous clients for the Syntarus v1 memory API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ import uuid
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from .exceptions import (
13
+ APIConnectionError,
14
+ APITimeoutError,
15
+ AuthenticationError,
16
+ EventFailedError,
17
+ PermissionDenied,
18
+ RateLimitError,
19
+ SyntarusError,
20
+ )
21
+
22
+ DEFAULT_BASE_URL = "https://ai.syntarus.com/v1"
23
+
24
+
25
+ def _raise_for_response(response: httpx.Response) -> None:
26
+ if response.is_success:
27
+ return
28
+ try:
29
+ detail = response.json().get("detail", "Request failed.")
30
+ except Exception:
31
+ detail = "Request failed."
32
+ if response.status_code == 401:
33
+ raise AuthenticationError("Project API key was rejected.")
34
+ if response.status_code == 403:
35
+ raise PermissionDenied(str(detail))
36
+ if response.status_code == 429:
37
+ raw_retry = response.headers.get("Retry-After")
38
+ try:
39
+ retry_after = float(raw_retry) if raw_retry is not None else None
40
+ except ValueError:
41
+ retry_after = None
42
+ raise RateLimitError("Project API rate limit exceeded.", retry_after=retry_after)
43
+ raise SyntarusError(f"Syntarus API error ({response.status_code}): {detail}")
44
+
45
+
46
+ class _BaseClient:
47
+ def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL, timeout: float = 20.0):
48
+ if not api_key.startswith("sk_mem_"):
49
+ raise ValueError("api_key must be a Syntarus project API key")
50
+ self._base_url = base_url.rstrip("/")
51
+ self._headers = {"Authorization": f"Bearer {api_key}", "User-Agent": "syntarus-python/0.2.0"}
52
+ self._timeout = timeout
53
+
54
+ @staticmethod
55
+ def _payload(user_id: str, messages: list[dict[str, str]], agent_id: str | None, run_id: str | None, metadata: dict[str, Any] | None) -> dict[str, Any]:
56
+ return {"user_id": user_id, "messages": messages, "agent_id": agent_id, "run_id": run_id, "metadata": metadata or {}}
57
+
58
+
59
+ class MemoryClient(_BaseClient):
60
+ """Blocking client. Use ``AsyncMemoryClient`` inside async applications."""
61
+
62
+ def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL, timeout: float = 20.0):
63
+ super().__init__(api_key, base_url=base_url, timeout=timeout)
64
+ self._client = httpx.Client(base_url=self._base_url, headers=self._headers, timeout=self._timeout)
65
+
66
+ def close(self) -> None:
67
+ self._client.close()
68
+
69
+ def __enter__(self) -> "MemoryClient":
70
+ return self
71
+
72
+ def __exit__(self, *_args: object) -> None:
73
+ self.close()
74
+
75
+ def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
76
+ try:
77
+ response = self._client.request(method, path, **kwargs)
78
+ except httpx.TimeoutException as exc:
79
+ raise APITimeoutError("Syntarus API request timed out.") from exc
80
+ except httpx.RequestError as exc:
81
+ raise APIConnectionError("Could not connect to the Syntarus API.") from exc
82
+ _raise_for_response(response)
83
+ return response
84
+
85
+ def add(self, *, user_id: str, messages: list[dict[str, str]], agent_id: str | None = None, run_id: str | None = None, metadata: dict[str, Any] | None = None, idempotency_key: str | None = None) -> dict[str, Any]:
86
+ response = self._request("POST", "/memories", json=self._payload(user_id, messages, agent_id, run_id, metadata), headers={"Idempotency-Key": idempotency_key or uuid.uuid4().hex})
87
+ return response.json()
88
+
89
+ def event(self, event_id: str) -> dict[str, Any]:
90
+ response = self._request("GET", f"/events/{event_id}")
91
+ return response.json()
92
+
93
+ def wait_for_event(self, event_id: str, *, timeout: float = 60.0, poll_interval: float = 0.5) -> dict[str, Any]:
94
+ if timeout <= 0 or poll_interval <= 0:
95
+ raise ValueError("timeout and poll_interval must be positive")
96
+ deadline = time.monotonic() + timeout
97
+ while True:
98
+ event = self.event(event_id)
99
+ if event.get("status") == "succeeded":
100
+ return event
101
+ if event.get("status") in {"dead_letter", "cancelled"}:
102
+ raise EventFailedError(event)
103
+ if time.monotonic() >= deadline:
104
+ raise APITimeoutError(f"Timed out waiting for event {event_id}.")
105
+ time.sleep(min(poll_interval, max(0.0, deadline - time.monotonic())))
106
+
107
+ def search(self, query: str, *, user_id: str, top_k: int = 10, agent_id: str | None = None) -> dict[str, Any]:
108
+ response = self._request("POST", "/memories/search", json={"user_id": user_id, "query": query, "top_k": top_k, "agent_id": agent_id})
109
+ return response.json()
110
+
111
+ def export(self, *, user_id: str, agent_id: str | None = None) -> dict[str, Any]:
112
+ params = {"user_id": user_id, **({"agent_id": agent_id} if agent_id else {})}
113
+ response = self._request("GET", "/memories/export", params=params)
114
+ return response.json()
115
+
116
+ def delete(self, memory_id: str) -> None:
117
+ self._request("DELETE", f"/memories/{memory_id}")
118
+
119
+ def delete_user(self, *, user_id: str, agent_id: str | None = None) -> None:
120
+ params = {"user_id": user_id, **({"agent_id": agent_id} if agent_id else {})}
121
+ self._request("DELETE", "/memories", params=params)
122
+
123
+ def create_webhook(self, url: str, *, event_types: list[str] | None = None) -> dict[str, Any]:
124
+ response = self._request("POST", "/webhooks", json={"url": url, "event_types": event_types or ["memory.ingest.succeeded"]})
125
+ return response.json()
126
+
127
+ def list_webhooks(self) -> dict[str, Any]:
128
+ return self._request("GET", "/webhooks").json()
129
+
130
+ def webhook_deliveries(self, *, webhook_id: str | None = None, limit: int = 100) -> dict[str, Any]:
131
+ params: dict[str, Any] = {"limit": limit}
132
+ if webhook_id:
133
+ params["webhook_id"] = webhook_id
134
+ return self._request("GET", "/webhooks/deliveries", params=params).json()
135
+
136
+ def delete_webhook(self, webhook_id: str) -> None:
137
+ self._request("DELETE", f"/webhooks/{webhook_id}")
138
+
139
+
140
+ class AsyncMemoryClient(_BaseClient):
141
+ """Async client for FastAPI, LangGraph and other async agent runtimes."""
142
+
143
+ def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE_URL, timeout: float = 20.0):
144
+ super().__init__(api_key, base_url=base_url, timeout=timeout)
145
+ self._client = httpx.AsyncClient(base_url=self._base_url, headers=self._headers, timeout=self._timeout)
146
+
147
+ async def aclose(self) -> None:
148
+ await self._client.aclose()
149
+
150
+ async def __aenter__(self) -> "AsyncMemoryClient":
151
+ return self
152
+
153
+ async def __aexit__(self, *_args: object) -> None:
154
+ await self.aclose()
155
+
156
+ async def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
157
+ try:
158
+ response = await self._client.request(method, path, **kwargs)
159
+ except httpx.TimeoutException as exc:
160
+ raise APITimeoutError("Syntarus API request timed out.") from exc
161
+ except httpx.RequestError as exc:
162
+ raise APIConnectionError("Could not connect to the Syntarus API.") from exc
163
+ _raise_for_response(response)
164
+ return response
165
+
166
+ async def add(self, *, user_id: str, messages: list[dict[str, str]], agent_id: str | None = None, run_id: str | None = None, metadata: dict[str, Any] | None = None, idempotency_key: str | None = None) -> dict[str, Any]:
167
+ response = await self._request("POST", "/memories", json=self._payload(user_id, messages, agent_id, run_id, metadata), headers={"Idempotency-Key": idempotency_key or uuid.uuid4().hex})
168
+ return response.json()
169
+
170
+ async def event(self, event_id: str) -> dict[str, Any]:
171
+ response = await self._request("GET", f"/events/{event_id}")
172
+ return response.json()
173
+
174
+ async def wait_for_event(self, event_id: str, *, timeout: float = 60.0, poll_interval: float = 0.5) -> dict[str, Any]:
175
+ if timeout <= 0 or poll_interval <= 0:
176
+ raise ValueError("timeout and poll_interval must be positive")
177
+ deadline = time.monotonic() + timeout
178
+ while True:
179
+ event = await self.event(event_id)
180
+ if event.get("status") == "succeeded":
181
+ return event
182
+ if event.get("status") in {"dead_letter", "cancelled"}:
183
+ raise EventFailedError(event)
184
+ if time.monotonic() >= deadline:
185
+ raise APITimeoutError(f"Timed out waiting for event {event_id}.")
186
+ await asyncio.sleep(min(poll_interval, max(0.0, deadline - time.monotonic())))
187
+
188
+ async def search(self, query: str, *, user_id: str, top_k: int = 10, agent_id: str | None = None) -> dict[str, Any]:
189
+ response = await self._request("POST", "/memories/search", json={"user_id": user_id, "query": query, "top_k": top_k, "agent_id": agent_id})
190
+ return response.json()
191
+
192
+ async def export(self, *, user_id: str, agent_id: str | None = None) -> dict[str, Any]:
193
+ params = {"user_id": user_id, **({"agent_id": agent_id} if agent_id else {})}
194
+ response = await self._request("GET", "/memories/export", params=params)
195
+ return response.json()
196
+
197
+ async def delete(self, memory_id: str) -> None:
198
+ await self._request("DELETE", f"/memories/{memory_id}")
199
+
200
+ async def delete_user(self, *, user_id: str, agent_id: str | None = None) -> None:
201
+ params = {"user_id": user_id, **({"agent_id": agent_id} if agent_id else {})}
202
+ await self._request("DELETE", "/memories", params=params)
203
+
204
+ async def create_webhook(self, url: str, *, event_types: list[str] | None = None) -> dict[str, Any]:
205
+ response = await self._request("POST", "/webhooks", json={"url": url, "event_types": event_types or ["memory.ingest.succeeded"]})
206
+ return response.json()
207
+
208
+ async def list_webhooks(self) -> dict[str, Any]:
209
+ return (await self._request("GET", "/webhooks")).json()
210
+
211
+ async def webhook_deliveries(self, *, webhook_id: str | None = None, limit: int = 100) -> dict[str, Any]:
212
+ params: dict[str, Any] = {"limit": limit}
213
+ if webhook_id:
214
+ params["webhook_id"] = webhook_id
215
+ response = await self._request("GET", "/webhooks/deliveries", params=params)
216
+ return response.json()
217
+
218
+ async def delete_webhook(self, webhook_id: str) -> None:
219
+ await self._request("DELETE", f"/webhooks/{webhook_id}")
syntarus/exceptions.py ADDED
@@ -0,0 +1,38 @@
1
+ """Typed API errors. Exception messages intentionally omit request secrets."""
2
+
3
+
4
+ class SyntarusError(Exception):
5
+ """Base exception raised for an unsuccessful Syntarus API response."""
6
+
7
+
8
+ class AuthenticationError(SyntarusError):
9
+ """The project API key is invalid, expired, or revoked."""
10
+
11
+
12
+ class PermissionDenied(SyntarusError):
13
+ """The project API key does not have the required scope."""
14
+
15
+
16
+ class RateLimitError(SyntarusError):
17
+ """The project API key has exceeded its request budget."""
18
+
19
+ def __init__(self, message: str, *, retry_after: float | None = None):
20
+ super().__init__(message)
21
+ self.retry_after = retry_after
22
+
23
+
24
+ class APIConnectionError(SyntarusError):
25
+ """The Syntarus API could not be reached."""
26
+
27
+
28
+ class APITimeoutError(APIConnectionError):
29
+ """The Syntarus API did not respond before the configured timeout."""
30
+
31
+
32
+ class EventFailedError(SyntarusError):
33
+ """An accepted asynchronous memory event reached a terminal failure."""
34
+
35
+ def __init__(self, event: dict):
36
+ self.event = event
37
+ detail = event.get("last_error") or f"event ended with status {event.get('status')}"
38
+ super().__init__(str(detail))
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: syntarus
3
+ Version: 0.2.0
4
+ Summary: Project-scoped memory client for Syntarus
5
+ Project-URL: Documentation, https://www.syntarus.com/pages/developers.html
6
+ Project-URL: Homepage, https://www.syntarus.com
7
+ Project-URL: Source, https://github.com/sujalkherawat25-stack/memoryos/tree/main/sdk
8
+ Author: Syntarus
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Syntarus
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: agents,ai,llm,memory
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Requires-Python: >=3.10
40
+ Requires-Dist: httpx<1,>=0.27
41
+ Provides-Extra: test
42
+ Requires-Dist: build<2,>=1.2; extra == 'test'
43
+ Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'test'
44
+ Requires-Dist: pytest<9,>=8; extra == 'test'
45
+ Requires-Dist: twine<7,>=5; extra == 'test'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # Syntarus Python SDK
49
+
50
+ The official Python client for the project-scoped Syntarus memory API.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install syntarus
56
+ ```
57
+
58
+ Python 3.10 or newer is required. Keep project API keys in a server-side
59
+ secret manager; never ship them in browser or mobile applications.
60
+
61
+ ## Add and search memory
62
+
63
+ ```python
64
+ from syntarus import MemoryClient
65
+
66
+ with MemoryClient(api_key="sk_mem_...") as memory:
67
+ accepted = memory.add(
68
+ user_id="customer_123",
69
+ agent_id="support_agent",
70
+ run_id="ticket_456",
71
+ messages=[
72
+ {"role": "user", "content": "I prefer vegetarian food."},
73
+ {"role": "assistant", "content": "I will remember that."},
74
+ ],
75
+ metadata={"channel": "voice"},
76
+ idempotency_key="call-456-final",
77
+ )
78
+ memory.wait_for_event(accepted["event_id"])
79
+ result = memory.search(
80
+ "What food does the customer prefer?",
81
+ user_id="customer_123",
82
+ agent_id="support_agent",
83
+ )
84
+ print(result["context"])
85
+ ```
86
+
87
+ `agent_id` creates an isolated memory namespace. Omit it for a user profile
88
+ shared by every agent in the project. `run_id` groups the conversation history
89
+ used during ingestion. `metadata` is event metadata and is returned by event
90
+ status and webhook payloads; it is not currently a memory-search filter.
91
+
92
+ ## Async client
93
+
94
+ ```python
95
+ from syntarus import AsyncMemoryClient
96
+
97
+ async with AsyncMemoryClient(api_key="sk_mem_...") as memory:
98
+ accepted = await memory.add(
99
+ user_id="customer_123",
100
+ messages=[{"role": "user", "content": "Call me after 5 PM."}],
101
+ )
102
+ await memory.wait_for_event(accepted["event_id"])
103
+ ```
104
+
105
+ ## Lifecycle operations
106
+
107
+ ```python
108
+ memory.export(user_id="customer_123", agent_id="support_agent")
109
+ memory.delete("memory-uuid")
110
+ memory.delete_user(user_id="customer_123", agent_id="support_agent")
111
+ ```
112
+
113
+ Deleting a user without `agent_id` deletes only the shared user namespace.
114
+ Delete each agent namespace separately when an application uses agent-scoped
115
+ memory.
116
+
117
+ ## Webhooks
118
+
119
+ ```python
120
+ created = memory.create_webhook("https://example.com/syntarus/events")
121
+ signing_secret = created["signing_secret"] # returned once
122
+ memory.list_webhooks()
123
+ memory.webhook_deliveries(webhook_id=created["webhook"]["id"])
124
+ memory.delete_webhook(created["webhook"]["id"])
125
+ ```
126
+
127
+ Deliveries include `X-Syntarus-Timestamp`, `X-Syntarus-Delivery`, and
128
+ `X-Syntarus-Signature-256`. Verify the signature as HMAC-SHA256 over
129
+ `<timestamp>.<raw_request_body>` and reject timestamps outside a five-minute
130
+ window. A non-2xx response is retried durably and eventually becomes a
131
+ dead-letter delivery visible through `webhook_deliveries`.
132
+
133
+ ## Errors
134
+
135
+ The SDK raises typed exceptions derived from `SyntarusError`:
136
+
137
+ - `AuthenticationError`
138
+ - `PermissionDenied`
139
+ - `RateLimitError` (`retry_after` is available when the API sends it)
140
+ - `APIConnectionError`
141
+ - `APITimeoutError`
142
+ - `EventFailedError` (`event` contains the terminal event record)
@@ -0,0 +1,7 @@
1
+ syntarus/__init__.py,sha256=e3EKF7l7_kp1NpQ4ODO30zi00EhdpgWgisF_zVvLbXc,492
2
+ syntarus/client.py,sha256=_Em0uDOZfhB_ex_cEOveq_NuIRebD1D4rOkfSTtMRzE,10380
3
+ syntarus/exceptions.py,sha256=MZd6DpODf8kTBZGwJTSviIWLo-ajWLZykPylw3ryHZU,1175
4
+ syntarus-0.2.0.dist-info/METADATA,sha256=jZB6YrXiCoNc4aIev9lHaMBGz4mpPGfbM-MbYIpRGq0,5231
5
+ syntarus-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
6
+ syntarus-0.2.0.dist-info/licenses/LICENSE,sha256=g-up2h6UU5Af0pIbAAu214vBiACCwqocsK3gvD6BbkA,1065
7
+ syntarus-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Syntarus
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.