zerolatency 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
5
+ *.pyc
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 0Latency
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,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: zerolatency
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the 0Latency memory API — persistent memory for AI agents.
5
+ Project-URL: Homepage, https://0latency.ai
6
+ Project-URL: Documentation, https://docs.0latency.ai
7
+ Project-URL: Repository, https://github.com/0latency/zerolatency-python
8
+ Author-email: 0Latency <dev@0latency.ai>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,llm,memory,rag
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.24
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == 'dev'
26
+ Requires-Dist: respx>=0.20; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # zerolatency
31
+
32
+ Python SDK for the [0Latency](https://0latency.ai) memory API — persistent memory for AI agents.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install zerolatency
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from zerolatency import Memory
44
+
45
+ memory = Memory("your-api-key")
46
+
47
+ # Store a memory
48
+ memory.add("User said they prefer dark mode and work in Python")
49
+
50
+ # Recall relevant memories
51
+ context = memory.recall("What are the user's preferences?")
52
+ print(context)
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Store memories
58
+
59
+ ```python
60
+ memory.add(
61
+ "User prefers concise answers",
62
+ agent_id="agent-123",
63
+ metadata={"source": "onboarding"},
64
+ )
65
+ ```
66
+
67
+ ### Recall memories
68
+
69
+ ```python
70
+ results = memory.recall("communication style", agent_id="agent-123", limit=5)
71
+ for m in results["memories"]:
72
+ print(m["content"])
73
+ ```
74
+
75
+ ### Extract memories from a conversation
76
+
77
+ ```python
78
+ conversation = [
79
+ {"role": "user", "content": "I'm a backend engineer who uses FastAPI."},
80
+ {"role": "assistant", "content": "Great! I'll keep that in mind."},
81
+ ]
82
+
83
+ job = memory.extract(conversation, agent_id="agent-123")
84
+ status = memory.extract_status(job["job_id"])
85
+ ```
86
+
87
+ ### Health check
88
+
89
+ ```python
90
+ print(memory.health())
91
+ ```
92
+
93
+ ### Context manager
94
+
95
+ ```python
96
+ with Memory("your-api-key") as memory:
97
+ memory.add("something to remember")
98
+ ```
99
+
100
+ ## Error handling
101
+
102
+ ```python
103
+ from zerolatency import Memory, AuthenticationError, RateLimitError, ZeroLatencyError
104
+
105
+ try:
106
+ memory = Memory("bad-key")
107
+ memory.add("test")
108
+ except AuthenticationError:
109
+ print("Check your API key")
110
+ except RateLimitError:
111
+ print("Slow down — retry after a backoff")
112
+ except ZeroLatencyError as e:
113
+ print(f"API error {e.status_code}: {e.message}")
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ | Parameter | Default | Description |
119
+ |------------|------------------------------|--------------------------|
120
+ | `api_key` | *required* | Your 0Latency API key |
121
+ | `base_url` | `https://api.0latency.ai` | API base URL override |
122
+ | `timeout` | `30.0` | Request timeout (seconds)|
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,97 @@
1
+ # zerolatency
2
+
3
+ Python SDK for the [0Latency](https://0latency.ai) memory API — persistent memory for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install zerolatency
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from zerolatency import Memory
15
+
16
+ memory = Memory("your-api-key")
17
+
18
+ # Store a memory
19
+ memory.add("User said they prefer dark mode and work in Python")
20
+
21
+ # Recall relevant memories
22
+ context = memory.recall("What are the user's preferences?")
23
+ print(context)
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Store memories
29
+
30
+ ```python
31
+ memory.add(
32
+ "User prefers concise answers",
33
+ agent_id="agent-123",
34
+ metadata={"source": "onboarding"},
35
+ )
36
+ ```
37
+
38
+ ### Recall memories
39
+
40
+ ```python
41
+ results = memory.recall("communication style", agent_id="agent-123", limit=5)
42
+ for m in results["memories"]:
43
+ print(m["content"])
44
+ ```
45
+
46
+ ### Extract memories from a conversation
47
+
48
+ ```python
49
+ conversation = [
50
+ {"role": "user", "content": "I'm a backend engineer who uses FastAPI."},
51
+ {"role": "assistant", "content": "Great! I'll keep that in mind."},
52
+ ]
53
+
54
+ job = memory.extract(conversation, agent_id="agent-123")
55
+ status = memory.extract_status(job["job_id"])
56
+ ```
57
+
58
+ ### Health check
59
+
60
+ ```python
61
+ print(memory.health())
62
+ ```
63
+
64
+ ### Context manager
65
+
66
+ ```python
67
+ with Memory("your-api-key") as memory:
68
+ memory.add("something to remember")
69
+ ```
70
+
71
+ ## Error handling
72
+
73
+ ```python
74
+ from zerolatency import Memory, AuthenticationError, RateLimitError, ZeroLatencyError
75
+
76
+ try:
77
+ memory = Memory("bad-key")
78
+ memory.add("test")
79
+ except AuthenticationError:
80
+ print("Check your API key")
81
+ except RateLimitError:
82
+ print("Slow down — retry after a backoff")
83
+ except ZeroLatencyError as e:
84
+ print(f"API error {e.status_code}: {e.message}")
85
+ ```
86
+
87
+ ## Configuration
88
+
89
+ | Parameter | Default | Description |
90
+ |------------|------------------------------|--------------------------|
91
+ | `api_key` | *required* | Your 0Latency API key |
92
+ | `base_url` | `https://api.0latency.ai` | API base URL override |
93
+ | `timeout` | `30.0` | Request timeout (seconds)|
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "zerolatency"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the 0Latency memory API — persistent memory for AI agents."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "0Latency", email = "dev@0latency.ai" }]
13
+ keywords = ["ai", "memory", "agents", "llm", "rag"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = ["httpx>=0.24"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://0latency.ai"
30
+ Documentation = "https://docs.0latency.ai"
31
+ Repository = "https://github.com/0latency/zerolatency-python"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/zerolatency"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+
39
+ [project.optional-dependencies]
40
+ dev = ["pytest>=7", "respx>=0.20", "ruff"]
@@ -0,0 +1,13 @@
1
+ """0Latency — Memory layer for AI agents."""
2
+
3
+ from .client import Memory
4
+ from .errors import AuthenticationError, RateLimitError, ZeroLatencyError
5
+
6
+ __all__ = [
7
+ "Memory",
8
+ "ZeroLatencyError",
9
+ "AuthenticationError",
10
+ "RateLimitError",
11
+ ]
12
+
13
+ __version__ = "0.1.0"
@@ -0,0 +1,165 @@
1
+ """0Latency Memory client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .errors import AuthenticationError, RateLimitError, ZeroLatencyError
10
+
11
+ DEFAULT_BASE_URL = "https://api.0latency.ai"
12
+ DEFAULT_TIMEOUT = 30.0
13
+
14
+
15
+ class Memory:
16
+ """Client for the 0Latency memory API.
17
+
18
+ Usage::
19
+
20
+ from zerolatency import Memory
21
+
22
+ memory = Memory("your-api-key")
23
+ memory.add("User prefers dark mode")
24
+ results = memory.recall("What does the user prefer?")
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ api_key: str,
30
+ base_url: str | None = None,
31
+ timeout: float = DEFAULT_TIMEOUT,
32
+ ) -> None:
33
+ self.api_key = api_key
34
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
35
+ self._client = httpx.Client(
36
+ base_url=self.base_url,
37
+ headers={
38
+ "Authorization": f"Bearer {api_key}",
39
+ "Content-Type": "application/json",
40
+ "User-Agent": "zerolatency-python/0.1.0",
41
+ },
42
+ timeout=timeout,
43
+ )
44
+
45
+ # -- public API ----------------------------------------------------------
46
+
47
+ def add(
48
+ self,
49
+ content: str,
50
+ agent_id: str | None = None,
51
+ metadata: dict[str, Any] | None = None,
52
+ ) -> dict[str, Any]:
53
+ """Store a memory.
54
+
55
+ Args:
56
+ content: The text content to remember.
57
+ agent_id: Optional agent identifier for scoping memories.
58
+ metadata: Optional key-value metadata to attach.
59
+
60
+ Returns:
61
+ API confirmation payload.
62
+ """
63
+ payload: dict[str, Any] = {"content": content}
64
+ if agent_id is not None:
65
+ payload["agent_id"] = agent_id
66
+ if metadata is not None:
67
+ payload["metadata"] = metadata
68
+ return self._post("/v1/memories", payload)
69
+
70
+ def recall(
71
+ self,
72
+ query: str,
73
+ agent_id: str | None = None,
74
+ limit: int = 10,
75
+ ) -> dict[str, Any]:
76
+ """Retrieve relevant memories.
77
+
78
+ Args:
79
+ query: Natural-language search query.
80
+ agent_id: Optional agent identifier for scoping.
81
+ limit: Maximum number of results (default 10).
82
+
83
+ Returns:
84
+ Matching memories from the API.
85
+ """
86
+ params: dict[str, Any] = {"query": query, "limit": limit}
87
+ if agent_id is not None:
88
+ params["agent_id"] = agent_id
89
+ return self._get("/v1/memories/recall", params)
90
+
91
+ def extract(
92
+ self,
93
+ conversation: list[dict[str, str]],
94
+ agent_id: str | None = None,
95
+ ) -> dict[str, Any]:
96
+ """Start async memory extraction from a conversation.
97
+
98
+ Args:
99
+ conversation: List of message dicts (e.g. ``[{"role": "user", "content": "..."}]``).
100
+ agent_id: Optional agent identifier.
101
+
102
+ Returns:
103
+ Dict containing ``job_id`` for status polling.
104
+ """
105
+ payload: dict[str, Any] = {"conversation": conversation}
106
+ if agent_id is not None:
107
+ payload["agent_id"] = agent_id
108
+ return self._post("/v1/memories/extract", payload)
109
+
110
+ def extract_status(self, job_id: str) -> dict[str, Any]:
111
+ """Check the status of an extraction job.
112
+
113
+ Args:
114
+ job_id: The job identifier returned by :meth:`extract`.
115
+
116
+ Returns:
117
+ Job status payload.
118
+ """
119
+ return self._get(f"/v1/memories/extract/{job_id}")
120
+
121
+ def health(self) -> dict[str, Any]:
122
+ """Check API health.
123
+
124
+ Returns:
125
+ Health status payload.
126
+ """
127
+ return self._get("/v1/health")
128
+
129
+ # -- lifecycle -----------------------------------------------------------
130
+
131
+ def close(self) -> None:
132
+ """Close the underlying HTTP connection."""
133
+ self._client.close()
134
+
135
+ def __enter__(self) -> Memory:
136
+ return self
137
+
138
+ def __exit__(self, *exc: object) -> None:
139
+ self.close()
140
+
141
+ # -- internals -----------------------------------------------------------
142
+
143
+ def _post(self, path: str, json: dict[str, Any]) -> dict[str, Any]:
144
+ return self._handle(self._client.post(path, json=json))
145
+
146
+ def _get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
147
+ return self._handle(self._client.get(path, params=params))
148
+
149
+ @staticmethod
150
+ def _handle(response: httpx.Response) -> dict[str, Any]:
151
+ if response.is_success:
152
+ return response.json() # type: ignore[no-any-return]
153
+
154
+ try:
155
+ body = response.json()
156
+ except Exception:
157
+ body = None
158
+
159
+ if response.status_code == 401:
160
+ raise AuthenticationError(response=body)
161
+ if response.status_code == 429:
162
+ raise RateLimitError(response=body)
163
+
164
+ msg = body.get("detail", response.text) if isinstance(body, dict) else response.text
165
+ raise ZeroLatencyError(msg, status_code=response.status_code, response=body)
@@ -0,0 +1,25 @@
1
+ """Exception classes for the 0Latency SDK."""
2
+
3
+
4
+ class ZeroLatencyError(Exception):
5
+ """Base exception for all 0Latency API errors."""
6
+
7
+ def __init__(self, message: str, status_code: int | None = None, response: dict | None = None) -> None:
8
+ self.message = message
9
+ self.status_code = status_code
10
+ self.response = response
11
+ super().__init__(self.message)
12
+
13
+
14
+ class AuthenticationError(ZeroLatencyError):
15
+ """Raised when the API key is invalid or missing."""
16
+
17
+ def __init__(self, message: str = "Invalid or missing API key.", response: dict | None = None) -> None:
18
+ super().__init__(message, status_code=401, response=response)
19
+
20
+
21
+ class RateLimitError(ZeroLatencyError):
22
+ """Raised when the API rate limit is exceeded."""
23
+
24
+ def __init__(self, message: str = "Rate limit exceeded. Please retry later.", response: dict | None = None) -> None:
25
+ super().__init__(message, status_code=429, response=response)
File without changes
@@ -0,0 +1,154 @@
1
+ """Tests for the zerolatency SDK client."""
2
+
3
+ import httpx
4
+ import pytest
5
+ import respx
6
+
7
+ from zerolatency import AuthenticationError, Memory, RateLimitError, ZeroLatencyError
8
+
9
+ BASE = "https://api.0latency.ai"
10
+
11
+
12
+ @pytest.fixture()
13
+ def memory():
14
+ client = Memory("test-key")
15
+ yield client
16
+ client.close()
17
+
18
+
19
+ # -- add ---------------------------------------------------------------------
20
+
21
+
22
+ @respx.mock
23
+ def test_add_basic(memory: Memory):
24
+ route = respx.post(f"{BASE}/v1/memories").mock(
25
+ return_value=httpx.Response(200, json={"id": "mem_1", "status": "ok"})
26
+ )
27
+ result = memory.add("remember this")
28
+ assert result == {"id": "mem_1", "status": "ok"}
29
+ assert route.called
30
+ payload = route.calls.last.request.content
31
+ assert b"remember this" in payload
32
+
33
+
34
+ @respx.mock
35
+ def test_add_with_agent_and_metadata(memory: Memory):
36
+ respx.post(f"{BASE}/v1/memories").mock(
37
+ return_value=httpx.Response(200, json={"id": "mem_2"})
38
+ )
39
+ result = memory.add("fact", agent_id="a1", metadata={"src": "test"})
40
+ assert result["id"] == "mem_2"
41
+
42
+
43
+ # -- recall ------------------------------------------------------------------
44
+
45
+
46
+ @respx.mock
47
+ def test_recall(memory: Memory):
48
+ respx.get(f"{BASE}/v1/memories/recall").mock(
49
+ return_value=httpx.Response(200, json={"memories": [{"content": "dark mode"}]})
50
+ )
51
+ result = memory.recall("preferences")
52
+ assert len(result["memories"]) == 1
53
+
54
+
55
+ @respx.mock
56
+ def test_recall_with_params(memory: Memory):
57
+ route = respx.get(f"{BASE}/v1/memories/recall").mock(
58
+ return_value=httpx.Response(200, json={"memories": []})
59
+ )
60
+ memory.recall("q", agent_id="a1", limit=5)
61
+ url = str(route.calls.last.request.url)
62
+ assert "agent_id=a1" in url
63
+ assert "limit=5" in url
64
+
65
+
66
+ # -- extract -----------------------------------------------------------------
67
+
68
+
69
+ @respx.mock
70
+ def test_extract(memory: Memory):
71
+ respx.post(f"{BASE}/v1/memories/extract").mock(
72
+ return_value=httpx.Response(200, json={"job_id": "job_1"})
73
+ )
74
+ result = memory.extract([{"role": "user", "content": "hi"}])
75
+ assert result["job_id"] == "job_1"
76
+
77
+
78
+ @respx.mock
79
+ def test_extract_status(memory: Memory):
80
+ respx.get(f"{BASE}/v1/memories/extract/job_1").mock(
81
+ return_value=httpx.Response(200, json={"status": "completed"})
82
+ )
83
+ result = memory.extract_status("job_1")
84
+ assert result["status"] == "completed"
85
+
86
+
87
+ # -- health ------------------------------------------------------------------
88
+
89
+
90
+ @respx.mock
91
+ def test_health(memory: Memory):
92
+ respx.get(f"{BASE}/v1/health").mock(
93
+ return_value=httpx.Response(200, json={"status": "healthy"})
94
+ )
95
+ assert memory.health()["status"] == "healthy"
96
+
97
+
98
+ # -- errors ------------------------------------------------------------------
99
+
100
+
101
+ @respx.mock
102
+ def test_auth_error(memory: Memory):
103
+ respx.post(f"{BASE}/v1/memories").mock(
104
+ return_value=httpx.Response(401, json={"detail": "unauthorized"})
105
+ )
106
+ with pytest.raises(AuthenticationError) as exc_info:
107
+ memory.add("test")
108
+ assert exc_info.value.status_code == 401
109
+
110
+
111
+ @respx.mock
112
+ def test_rate_limit_error(memory: Memory):
113
+ respx.post(f"{BASE}/v1/memories").mock(
114
+ return_value=httpx.Response(429, json={"detail": "too many requests"})
115
+ )
116
+ with pytest.raises(RateLimitError) as exc_info:
117
+ memory.add("test")
118
+ assert exc_info.value.status_code == 429
119
+
120
+
121
+ @respx.mock
122
+ def test_generic_error(memory: Memory):
123
+ respx.post(f"{BASE}/v1/memories").mock(
124
+ return_value=httpx.Response(500, json={"detail": "internal error"})
125
+ )
126
+ with pytest.raises(ZeroLatencyError) as exc_info:
127
+ memory.add("test")
128
+ assert exc_info.value.status_code == 500
129
+ assert "internal error" in exc_info.value.message
130
+
131
+
132
+ # -- context manager ---------------------------------------------------------
133
+
134
+
135
+ @respx.mock
136
+ def test_context_manager():
137
+ respx.get(f"{BASE}/v1/health").mock(
138
+ return_value=httpx.Response(200, json={"status": "healthy"})
139
+ )
140
+ with Memory("test-key") as mem:
141
+ assert mem.health()["status"] == "healthy"
142
+
143
+
144
+ # -- headers -----------------------------------------------------------------
145
+
146
+
147
+ @respx.mock
148
+ def test_auth_header(memory: Memory):
149
+ route = respx.get(f"{BASE}/v1/health").mock(
150
+ return_value=httpx.Response(200, json={})
151
+ )
152
+ memory.health()
153
+ auth = route.calls.last.request.headers["authorization"]
154
+ assert auth == "Bearer test-key"