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.
- zerolatency-0.1.0/.gitignore +5 -0
- zerolatency-0.1.0/LICENSE +21 -0
- zerolatency-0.1.0/PKG-INFO +126 -0
- zerolatency-0.1.0/README.md +97 -0
- zerolatency-0.1.0/pyproject.toml +40 -0
- zerolatency-0.1.0/src/zerolatency/__init__.py +13 -0
- zerolatency-0.1.0/src/zerolatency/client.py +165 -0
- zerolatency-0.1.0/src/zerolatency/errors.py +25 -0
- zerolatency-0.1.0/tests/__init__.py +0 -0
- zerolatency-0.1.0/tests/test_client.py +154 -0
|
@@ -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"
|