adola 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.
- adola-0.1.0/LICENSE +22 -0
- adola-0.1.0/PKG-INFO +48 -0
- adola-0.1.0/README.md +25 -0
- adola-0.1.0/pyproject.toml +38 -0
- adola-0.1.0/setup.cfg +4 -0
- adola-0.1.0/src/adola/__init__.py +5 -0
- adola-0.1.0/src/adola/_client.py +232 -0
- adola-0.1.0/src/adola/_errors.py +20 -0
- adola-0.1.0/src/adola/py.typed +1 -0
- adola-0.1.0/src/adola/types.py +75 -0
- adola-0.1.0/src/adola.egg-info/PKG-INFO +48 -0
- adola-0.1.0/src/adola.egg-info/SOURCES.txt +14 -0
- adola-0.1.0/src/adola.egg-info/dependency_links.txt +1 -0
- adola-0.1.0/src/adola.egg-info/requires.txt +1 -0
- adola-0.1.0/src/adola.egg-info/top_level.txt +1 -0
- adola-0.1.0/tests/test_client.py +192 -0
adola-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adola
|
|
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.
|
|
22
|
+
|
adola-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adola
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Adola compression API
|
|
5
|
+
Author: Adola
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://adola.app
|
|
8
|
+
Project-URL: Documentation, https://adola.app/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/JBunga/adola-python
|
|
10
|
+
Keywords: adola,compression,tokens,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
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: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# adola
|
|
25
|
+
|
|
26
|
+
Python SDK for the Adola compression API.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install adola
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from adola import Adola
|
|
34
|
+
|
|
35
|
+
client = Adola(api_key="adola_...")
|
|
36
|
+
result = client.compress(
|
|
37
|
+
input="Adola compresses long prompts before they reach your model.",
|
|
38
|
+
query="What does Adola do?",
|
|
39
|
+
compression={"target_ratio": 0.4},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
print(result["output"])
|
|
43
|
+
print(result["receipt"]["tokens_saved"])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The client defaults to `https://api.adola.app`. Set `ADOLA_API_KEY` for auth and `ADOLA_BASE_URL` for local testing.
|
|
47
|
+
|
|
48
|
+
The package contains only the SDK client and does not include the Adola application codebase.
|
adola-0.1.0/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# adola
|
|
2
|
+
|
|
3
|
+
Python SDK for the Adola compression API.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install adola
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from adola import Adola
|
|
11
|
+
|
|
12
|
+
client = Adola(api_key="adola_...")
|
|
13
|
+
result = client.compress(
|
|
14
|
+
input="Adola compresses long prompts before they reach your model.",
|
|
15
|
+
query="What does Adola do?",
|
|
16
|
+
compression={"target_ratio": 0.4},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
print(result["output"])
|
|
20
|
+
print(result["receipt"]["tokens_saved"])
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The client defaults to `https://api.adola.app`. Set `ADOLA_API_KEY` for auth and `ADOLA_BASE_URL` for local testing.
|
|
24
|
+
|
|
25
|
+
The package contains only the SDK client and does not include the Adola application codebase.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "adola"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Adola compression API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.27.0"
|
|
13
|
+
]
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Adola" }
|
|
17
|
+
]
|
|
18
|
+
keywords = ["adola", "compression", "tokens", "sdk"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Typing :: Typed"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://adola.app"
|
|
31
|
+
Documentation = "https://adola.app/docs"
|
|
32
|
+
Repository = "https://github.com/JBunga/adola-python"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
adola = ["py.typed"]
|
adola-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from adola._errors import AdolaAPIError
|
|
10
|
+
from adola.types import (
|
|
11
|
+
CompressRequest,
|
|
12
|
+
CompressResponse,
|
|
13
|
+
CompressionOptions,
|
|
14
|
+
Model,
|
|
15
|
+
ProtectedOptions,
|
|
16
|
+
Span,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.adola.app"
|
|
20
|
+
USER_AGENT = "adola-python/0.1.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Adola:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
base_url: str | None = None,
|
|
29
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
30
|
+
http_client: httpx.Client | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.api_key = api_key or os.getenv("ADOLA_API_KEY")
|
|
33
|
+
if not self.api_key:
|
|
34
|
+
raise ValueError("api_key or ADOLA_API_KEY is required")
|
|
35
|
+
self.base_url = _normalize_base_url(base_url or os.getenv("ADOLA_BASE_URL") or DEFAULT_BASE_URL)
|
|
36
|
+
self._owns_client = http_client is None
|
|
37
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
38
|
+
|
|
39
|
+
def __enter__(self) -> "Adola":
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, *_exc: object) -> None:
|
|
43
|
+
self.close()
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
if self._owns_client:
|
|
47
|
+
self._client.close()
|
|
48
|
+
|
|
49
|
+
def models(self) -> list[Model]:
|
|
50
|
+
return self._request("GET", "/v1/models")
|
|
51
|
+
|
|
52
|
+
def compress(
|
|
53
|
+
self,
|
|
54
|
+
input: str | None = None,
|
|
55
|
+
*,
|
|
56
|
+
query: str | None = None,
|
|
57
|
+
spans: Sequence[Span] | None = None,
|
|
58
|
+
model: str = "rose-1",
|
|
59
|
+
compression: CompressionOptions | None = None,
|
|
60
|
+
protected: ProtectedOptions | None = None,
|
|
61
|
+
include_spans: bool = True,
|
|
62
|
+
) -> CompressResponse:
|
|
63
|
+
payload = _compress_payload(
|
|
64
|
+
input=input,
|
|
65
|
+
query=query,
|
|
66
|
+
spans=spans,
|
|
67
|
+
model=model,
|
|
68
|
+
compression=compression,
|
|
69
|
+
protected=protected,
|
|
70
|
+
include_spans=include_spans,
|
|
71
|
+
)
|
|
72
|
+
return self._request("POST", "/v1/compress", json=payload)
|
|
73
|
+
|
|
74
|
+
def batch_compress(self, requests: Sequence[CompressRequest]) -> list[CompressResponse]:
|
|
75
|
+
if not requests:
|
|
76
|
+
raise ValueError("requests must not be empty")
|
|
77
|
+
return self._request("POST", "/v1/batch/compress", json={"requests": list(requests)})
|
|
78
|
+
|
|
79
|
+
def _request(self, method: str, path: str, *, json: Any | None = None) -> Any:
|
|
80
|
+
try:
|
|
81
|
+
response = self._client.request(
|
|
82
|
+
method,
|
|
83
|
+
f"{self.base_url}{path}",
|
|
84
|
+
headers=_headers(self.api_key),
|
|
85
|
+
json=json,
|
|
86
|
+
)
|
|
87
|
+
except httpx.RequestError as exc:
|
|
88
|
+
raise AdolaAPIError(f"Request failed: {exc}") from exc
|
|
89
|
+
return _decode_response(response)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AsyncAdola:
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
api_key: str | None = None,
|
|
97
|
+
base_url: str | None = None,
|
|
98
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
99
|
+
http_client: httpx.AsyncClient | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.api_key = api_key or os.getenv("ADOLA_API_KEY")
|
|
102
|
+
if not self.api_key:
|
|
103
|
+
raise ValueError("api_key or ADOLA_API_KEY is required")
|
|
104
|
+
self.base_url = _normalize_base_url(base_url or os.getenv("ADOLA_BASE_URL") or DEFAULT_BASE_URL)
|
|
105
|
+
self._owns_client = http_client is None
|
|
106
|
+
self._client = http_client or httpx.AsyncClient(timeout=timeout)
|
|
107
|
+
|
|
108
|
+
async def __aenter__(self) -> "AsyncAdola":
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
async def __aexit__(self, *_exc: object) -> None:
|
|
112
|
+
await self.aclose()
|
|
113
|
+
|
|
114
|
+
async def aclose(self) -> None:
|
|
115
|
+
if self._owns_client:
|
|
116
|
+
await self._client.aclose()
|
|
117
|
+
|
|
118
|
+
async def models(self) -> list[Model]:
|
|
119
|
+
return await self._request("GET", "/v1/models")
|
|
120
|
+
|
|
121
|
+
async def compress(
|
|
122
|
+
self,
|
|
123
|
+
input: str | None = None,
|
|
124
|
+
*,
|
|
125
|
+
query: str | None = None,
|
|
126
|
+
spans: Sequence[Span] | None = None,
|
|
127
|
+
model: str = "rose-1",
|
|
128
|
+
compression: CompressionOptions | None = None,
|
|
129
|
+
protected: ProtectedOptions | None = None,
|
|
130
|
+
include_spans: bool = True,
|
|
131
|
+
) -> CompressResponse:
|
|
132
|
+
payload = _compress_payload(
|
|
133
|
+
input=input,
|
|
134
|
+
query=query,
|
|
135
|
+
spans=spans,
|
|
136
|
+
model=model,
|
|
137
|
+
compression=compression,
|
|
138
|
+
protected=protected,
|
|
139
|
+
include_spans=include_spans,
|
|
140
|
+
)
|
|
141
|
+
return await self._request("POST", "/v1/compress", json=payload)
|
|
142
|
+
|
|
143
|
+
async def batch_compress(self, requests: Sequence[CompressRequest]) -> list[CompressResponse]:
|
|
144
|
+
if not requests:
|
|
145
|
+
raise ValueError("requests must not be empty")
|
|
146
|
+
return await self._request("POST", "/v1/batch/compress", json={"requests": list(requests)})
|
|
147
|
+
|
|
148
|
+
async def _request(self, method: str, path: str, *, json: Any | None = None) -> Any:
|
|
149
|
+
try:
|
|
150
|
+
response = await self._client.request(
|
|
151
|
+
method,
|
|
152
|
+
f"{self.base_url}{path}",
|
|
153
|
+
headers=_headers(self.api_key),
|
|
154
|
+
json=json,
|
|
155
|
+
)
|
|
156
|
+
except httpx.RequestError as exc:
|
|
157
|
+
raise AdolaAPIError(f"Request failed: {exc}") from exc
|
|
158
|
+
return _decode_response(response)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _compress_payload(
|
|
162
|
+
*,
|
|
163
|
+
input: str | None,
|
|
164
|
+
query: str | None,
|
|
165
|
+
spans: Sequence[Span] | None,
|
|
166
|
+
model: str,
|
|
167
|
+
compression: CompressionOptions | None,
|
|
168
|
+
protected: ProtectedOptions | None,
|
|
169
|
+
include_spans: bool,
|
|
170
|
+
) -> CompressRequest:
|
|
171
|
+
if not input and not spans:
|
|
172
|
+
raise ValueError("input or spans is required")
|
|
173
|
+
payload: CompressRequest = {
|
|
174
|
+
"model": model, # type: ignore[typeddict-item]
|
|
175
|
+
"include_spans": include_spans,
|
|
176
|
+
}
|
|
177
|
+
if input is not None:
|
|
178
|
+
payload["input"] = input
|
|
179
|
+
if query is not None:
|
|
180
|
+
payload["query"] = query
|
|
181
|
+
if spans is not None:
|
|
182
|
+
payload["spans"] = list(spans)
|
|
183
|
+
if compression is not None:
|
|
184
|
+
payload["compression"] = compression
|
|
185
|
+
if protected is not None:
|
|
186
|
+
payload["protected"] = protected
|
|
187
|
+
return payload
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _decode_response(response: httpx.Response) -> Any:
|
|
191
|
+
if response.is_success:
|
|
192
|
+
return response.json()
|
|
193
|
+
message = _error_message(response)
|
|
194
|
+
raise AdolaAPIError(
|
|
195
|
+
message,
|
|
196
|
+
status_code=response.status_code,
|
|
197
|
+
request_id=response.headers.get("x-request-id"),
|
|
198
|
+
response=response,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _error_message(response: httpx.Response) -> str:
|
|
203
|
+
try:
|
|
204
|
+
body = response.json()
|
|
205
|
+
except ValueError:
|
|
206
|
+
return response.text or response.reason_phrase
|
|
207
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
208
|
+
if isinstance(detail, str):
|
|
209
|
+
return detail
|
|
210
|
+
if isinstance(detail, list):
|
|
211
|
+
messages = [
|
|
212
|
+
item.get("msg")
|
|
213
|
+
for item in detail
|
|
214
|
+
if isinstance(item, dict) and isinstance(item.get("msg"), str)
|
|
215
|
+
]
|
|
216
|
+
if messages:
|
|
217
|
+
return "; ".join(messages)
|
|
218
|
+
return response.reason_phrase
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _headers(api_key: str) -> dict[str, str]:
|
|
222
|
+
return {
|
|
223
|
+
"Accept": "application/json",
|
|
224
|
+
"Authorization": f"Bearer {api_key}",
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
"User-Agent": USER_AGENT,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _normalize_base_url(base_url: str) -> str:
|
|
231
|
+
return base_url.rstrip("/")
|
|
232
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AdolaAPIError(Exception):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
*,
|
|
11
|
+
status_code: int | None = None,
|
|
12
|
+
request_id: str | None = None,
|
|
13
|
+
response: Any | None = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.message = message
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.request_id = request_id
|
|
19
|
+
self.response = response
|
|
20
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CompressionOptions(TypedDict, total=False):
|
|
7
|
+
target_ratio: float
|
|
8
|
+
max_output_tokens: int | None
|
|
9
|
+
keep: int | None
|
|
10
|
+
preserve_order: bool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProtectedOptions(TypedDict, total=False):
|
|
14
|
+
xml_tags: list[str]
|
|
15
|
+
patterns: list[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Span(TypedDict, total=False):
|
|
19
|
+
id: str
|
|
20
|
+
text: str
|
|
21
|
+
protected: bool
|
|
22
|
+
metadata: dict[str, str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CompressRequest(TypedDict, total=False):
|
|
26
|
+
model: Literal["rose-1"]
|
|
27
|
+
query: str | None
|
|
28
|
+
input: str | None
|
|
29
|
+
spans: list[Span] | None
|
|
30
|
+
compression: CompressionOptions
|
|
31
|
+
protected: ProtectedOptions
|
|
32
|
+
include_spans: bool
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Risk(TypedDict):
|
|
36
|
+
level: str
|
|
37
|
+
flags: list[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Receipt(TypedDict):
|
|
41
|
+
original_tokens: int
|
|
42
|
+
output_tokens: int
|
|
43
|
+
tokens_saved: int
|
|
44
|
+
compression_ratio: float
|
|
45
|
+
selected_count: int
|
|
46
|
+
total_spans: int
|
|
47
|
+
protected_tokens: int
|
|
48
|
+
latency_ms: float
|
|
49
|
+
risk: Risk
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SelectedSpan(TypedDict):
|
|
53
|
+
id: str
|
|
54
|
+
index: int
|
|
55
|
+
text: str
|
|
56
|
+
tokens: int
|
|
57
|
+
protected: bool
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CompressResponse(TypedDict):
|
|
61
|
+
model: Literal["rose-1"]
|
|
62
|
+
output: str
|
|
63
|
+
receipt: Receipt
|
|
64
|
+
selected_spans: list[SelectedSpan]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Model(TypedDict):
|
|
68
|
+
id: Literal["rose-1"]
|
|
69
|
+
name: str
|
|
70
|
+
mode: Literal["context-compression"]
|
|
71
|
+
target: Literal["production-llm-systems"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BatchCompressRequest(TypedDict):
|
|
75
|
+
requests: list[CompressRequest]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adola
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Adola compression API
|
|
5
|
+
Author: Adola
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://adola.app
|
|
8
|
+
Project-URL: Documentation, https://adola.app/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/JBunga/adola-python
|
|
10
|
+
Keywords: adola,compression,tokens,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
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: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# adola
|
|
25
|
+
|
|
26
|
+
Python SDK for the Adola compression API.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install adola
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from adola import Adola
|
|
34
|
+
|
|
35
|
+
client = Adola(api_key="adola_...")
|
|
36
|
+
result = client.compress(
|
|
37
|
+
input="Adola compresses long prompts before they reach your model.",
|
|
38
|
+
query="What does Adola do?",
|
|
39
|
+
compression={"target_ratio": 0.4},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
print(result["output"])
|
|
43
|
+
print(result["receipt"]["tokens_saved"])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The client defaults to `https://api.adola.app`. Set `ADOLA_API_KEY` for auth and `ADOLA_BASE_URL` for local testing.
|
|
47
|
+
|
|
48
|
+
The package contains only the SDK client and does not include the Adola application codebase.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/adola/__init__.py
|
|
5
|
+
src/adola/_client.py
|
|
6
|
+
src/adola/_errors.py
|
|
7
|
+
src/adola/py.typed
|
|
8
|
+
src/adola/types.py
|
|
9
|
+
src/adola.egg-info/PKG-INFO
|
|
10
|
+
src/adola.egg-info/SOURCES.txt
|
|
11
|
+
src/adola.egg-info/dependency_links.txt
|
|
12
|
+
src/adola.egg-info/requires.txt
|
|
13
|
+
src/adola.egg-info/top_level.txt
|
|
14
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.27.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adola
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from adola import Adola, AdolaAPIError, AsyncAdola
|
|
12
|
+
|
|
13
|
+
FIXTURES = Path(__file__).resolve().parents[2] / "fixtures"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_fixture(name: str) -> object:
|
|
17
|
+
return json.loads((FIXTURES / name).read_text())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_models_sends_auth_and_parses_response() -> None:
|
|
21
|
+
calls: list[httpx.Request] = []
|
|
22
|
+
|
|
23
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
24
|
+
calls.append(request)
|
|
25
|
+
return httpx.Response(200, json=load_fixture("models-response.json"))
|
|
26
|
+
|
|
27
|
+
client = Adola(
|
|
28
|
+
api_key="test-key",
|
|
29
|
+
base_url="https://unit.test/",
|
|
30
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
models = client.models()
|
|
34
|
+
|
|
35
|
+
assert models[0]["id"] == "rose-1"
|
|
36
|
+
assert calls[0].url == "https://unit.test/v1/models"
|
|
37
|
+
assert calls[0].headers["authorization"] == "Bearer test-key"
|
|
38
|
+
assert calls[0].headers["user-agent"] == "adola-python/0.1.0"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_compress_sends_schema_payload() -> None:
|
|
42
|
+
seen: dict[str, object] = {}
|
|
43
|
+
|
|
44
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
45
|
+
seen["payload"] = json.loads(request.content)
|
|
46
|
+
return httpx.Response(200, json=load_fixture("compress-response.json"))
|
|
47
|
+
|
|
48
|
+
client = Adola(
|
|
49
|
+
api_key="test-key",
|
|
50
|
+
base_url="https://unit.test",
|
|
51
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
response = client.compress(
|
|
55
|
+
input="source text",
|
|
56
|
+
query="needle",
|
|
57
|
+
compression={"target_ratio": 0.5, "preserve_order": False},
|
|
58
|
+
protected={"xml_tags": ["safe"], "patterns": ["SECRET"]},
|
|
59
|
+
include_spans=False,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert response["receipt"]["tokens_saved"] == 6
|
|
63
|
+
assert seen["payload"] == {
|
|
64
|
+
"model": "rose-1",
|
|
65
|
+
"input": "source text",
|
|
66
|
+
"query": "needle",
|
|
67
|
+
"compression": {"target_ratio": 0.5, "preserve_order": False},
|
|
68
|
+
"protected": {"xml_tags": ["safe"], "patterns": ["SECRET"]},
|
|
69
|
+
"include_spans": False,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_compress_accepts_spans_without_input() -> None:
|
|
74
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
75
|
+
assert json.loads(request.content)["spans"] == [{"id": "a", "text": "span text"}]
|
|
76
|
+
return httpx.Response(200, json=load_fixture("compress-response.json"))
|
|
77
|
+
|
|
78
|
+
client = Adola(
|
|
79
|
+
api_key="test-key",
|
|
80
|
+
base_url="https://unit.test",
|
|
81
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert client.compress(spans=[{"id": "a", "text": "span text"}])["model"] == "rose-1"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_compress_requires_input_or_spans() -> None:
|
|
88
|
+
client = Adola(
|
|
89
|
+
api_key="test-key",
|
|
90
|
+
http_client=httpx.Client(transport=httpx.MockTransport(lambda _: httpx.Response(500))),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with pytest.raises(ValueError, match="input or spans"):
|
|
94
|
+
client.compress()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_batch_compress_posts_requests_array() -> None:
|
|
98
|
+
request_fixture = load_fixture("compress-request.json")
|
|
99
|
+
|
|
100
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
101
|
+
assert json.loads(request.content) == {"requests": [request_fixture]}
|
|
102
|
+
return httpx.Response(200, json=[load_fixture("compress-response.json")])
|
|
103
|
+
|
|
104
|
+
client = Adola(
|
|
105
|
+
api_key="test-key",
|
|
106
|
+
base_url="https://unit.test",
|
|
107
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
response = client.batch_compress([request_fixture]) # type: ignore[list-item]
|
|
111
|
+
|
|
112
|
+
assert response[0]["output"].startswith("Adola removes")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_api_error_includes_status_and_request_id() -> None:
|
|
116
|
+
def handler(_request: httpx.Request) -> httpx.Response:
|
|
117
|
+
return httpx.Response(
|
|
118
|
+
422,
|
|
119
|
+
headers={"x-request-id": "req_123"},
|
|
120
|
+
json={"detail": [{"msg": "input or spans is required"}]},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
client = Adola(
|
|
124
|
+
api_key="test-key",
|
|
125
|
+
base_url="https://unit.test",
|
|
126
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
with pytest.raises(AdolaAPIError) as exc_info:
|
|
130
|
+
client.models()
|
|
131
|
+
|
|
132
|
+
assert exc_info.value.status_code == 422
|
|
133
|
+
assert exc_info.value.request_id == "req_123"
|
|
134
|
+
assert str(exc_info.value) == "input or spans is required"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_request_error_is_wrapped() -> None:
|
|
138
|
+
def handler(_request: httpx.Request) -> httpx.Response:
|
|
139
|
+
raise httpx.ConnectError("no route")
|
|
140
|
+
|
|
141
|
+
client = Adola(
|
|
142
|
+
api_key="test-key",
|
|
143
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
with pytest.raises(AdolaAPIError, match="Request failed"):
|
|
147
|
+
client.models()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_env_base_url_and_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
151
|
+
monkeypatch.setenv("ADOLA_API_KEY", "env-key")
|
|
152
|
+
monkeypatch.setenv("ADOLA_BASE_URL", "https://env.test/")
|
|
153
|
+
|
|
154
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
155
|
+
assert request.url == "https://env.test/v1/models"
|
|
156
|
+
assert request.headers["authorization"] == "Bearer env-key"
|
|
157
|
+
return httpx.Response(200, json=load_fixture("models-response.json"))
|
|
158
|
+
|
|
159
|
+
client = Adola(http_client=httpx.Client(transport=httpx.MockTransport(handler)))
|
|
160
|
+
|
|
161
|
+
assert client.models()[0]["name"] == "Rose 1"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_async_client() -> None:
|
|
165
|
+
async def run() -> None:
|
|
166
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
167
|
+
assert request.headers["authorization"] == "Bearer async-key"
|
|
168
|
+
return httpx.Response(200, json=load_fixture("compress-response.json"))
|
|
169
|
+
|
|
170
|
+
async with AsyncAdola(
|
|
171
|
+
api_key="async-key",
|
|
172
|
+
base_url="https://unit.test",
|
|
173
|
+
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
|
|
174
|
+
) as client:
|
|
175
|
+
response = await client.compress(input="source")
|
|
176
|
+
|
|
177
|
+
assert response["receipt"]["tokens_saved"] == 6
|
|
178
|
+
|
|
179
|
+
asyncio.run(run())
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.skipif(not os.getenv("ADOLA_API_KEY"), reason="ADOLA_API_KEY is required")
|
|
183
|
+
def test_live_models_and_compress() -> None:
|
|
184
|
+
with Adola() as client:
|
|
185
|
+
assert client.models()[0]["id"] == "rose-1"
|
|
186
|
+
response = client.compress(
|
|
187
|
+
input="Adola trims prompt context before the request reaches a model.",
|
|
188
|
+
query="What does Adola trim?",
|
|
189
|
+
compression={"target_ratio": 0.5},
|
|
190
|
+
)
|
|
191
|
+
assert response["receipt"]["tokens_saved"] >= 0
|
|
192
|
+
|