avartha-python-sdk 0.0.1__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.
- avartha/__init__.py +42 -0
- avartha/_config.py +140 -0
- avartha/_realtime_tts.py +138 -0
- avartha/_version.py +24 -0
- avartha/client.py +159 -0
- avartha/control.py +441 -0
- avartha/conversational_ai/__init__.py +19 -0
- avartha/conversational_ai/conversation.py +19 -0
- avartha/conversational_ai/default_audio_interface.py +5 -0
- avartha/elevenlabs.py +120 -0
- avartha/errors.py +35 -0
- avartha/openai.py +106 -0
- avartha/py.typed +0 -0
- avartha_python_sdk-0.0.1.dist-info/METADATA +417 -0
- avartha_python_sdk-0.0.1.dist-info/RECORD +18 -0
- avartha_python_sdk-0.0.1.dist-info/WHEEL +5 -0
- avartha_python_sdk-0.0.1.dist-info/licenses/LICENSE +23 -0
- avartha_python_sdk-0.0.1.dist-info/top_level.txt +1 -0
avartha/control.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""Customer control plane at /control/v1, authenticated with Avartha bearer keys.
|
|
2
|
+
|
|
3
|
+
Responses retain their JSON fields so additive platform changes remain visible.
|
|
4
|
+
Writes are never automatically retried: endpoint creation may provision hardware.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import math
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import Self
|
|
13
|
+
from urllib.parse import quote, unquote, urlsplit
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from pydantic import JsonValue
|
|
17
|
+
|
|
18
|
+
from . import _config
|
|
19
|
+
from .errors import EndpointFailedError, PlatformAPIError
|
|
20
|
+
|
|
21
|
+
type Object = dict[str, JsonValue]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _object(value: JsonValue) -> Object:
|
|
25
|
+
if not isinstance(value, dict):
|
|
26
|
+
raise ValueError("Expected a JSON object from the platform.")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _objects(value: JsonValue) -> list[Object]:
|
|
31
|
+
if not isinstance(value, list):
|
|
32
|
+
raise ValueError("Expected a JSON array from the platform.")
|
|
33
|
+
return [_object(item) for item in value]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _segment(value: str) -> str:
|
|
37
|
+
if not value or value in {".", ".."} or any(c in value for c in "/\\"):
|
|
38
|
+
raise ValueError("Resource identifiers must be nonempty path segments.")
|
|
39
|
+
return quote(value, safe="")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _target(base_url: str, path: str) -> str:
|
|
43
|
+
parts = urlsplit(path)
|
|
44
|
+
decoded = unquote(parts.path)
|
|
45
|
+
if (
|
|
46
|
+
parts.scheme
|
|
47
|
+
or parts.netloc
|
|
48
|
+
or parts.query
|
|
49
|
+
or parts.fragment
|
|
50
|
+
or "\\" in decoded
|
|
51
|
+
or any(part in {".", ".."} for part in decoded.split("/"))
|
|
52
|
+
):
|
|
53
|
+
raise ValueError("Use a relative control-plane path without query or traversal segments.")
|
|
54
|
+
return f"{base_url}/{path.lstrip('/')}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _decode(response: httpx.Response) -> JsonValue:
|
|
58
|
+
if not response.is_success:
|
|
59
|
+
raise PlatformAPIError(response)
|
|
60
|
+
if response.status_code == 204 or not response.content:
|
|
61
|
+
return None
|
|
62
|
+
return response.json()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _positive(value: float, name: str) -> None:
|
|
66
|
+
if not math.isfinite(value) or value <= 0:
|
|
67
|
+
raise ValueError(f"{name} must be finite and positive.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _ready(endpoint: Object) -> bool:
|
|
71
|
+
if endpoint.get("status") in {"failed", "stopped", "stopping"}:
|
|
72
|
+
raise EndpointFailedError(endpoint)
|
|
73
|
+
replicas = endpoint.get("replicas")
|
|
74
|
+
accepting_ready = replicas.get("accepting_ready") if isinstance(replicas, dict) else None
|
|
75
|
+
return (
|
|
76
|
+
endpoint.get("status") == "running"
|
|
77
|
+
and isinstance(accepting_ready, int)
|
|
78
|
+
and accepting_ready > 0
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Control:
|
|
83
|
+
"""Platform root configuration plus catalog, organizations and endpoints."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
api_key: str | None = None,
|
|
89
|
+
base_url: str | None = None,
|
|
90
|
+
timeout: float = 60,
|
|
91
|
+
http_client: httpx.Client | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
_positive(timeout, "timeout")
|
|
94
|
+
self.base_url = _config.platform_url(base_url) + "/control/v1"
|
|
95
|
+
self._headers = {"Authorization": f"Bearer {_config.api_key(api_key)}"}
|
|
96
|
+
self._timeout = timeout
|
|
97
|
+
self._owns_client = http_client is None
|
|
98
|
+
self._client = http_client if http_client is not None else httpx.Client()
|
|
99
|
+
self.catalog = Catalog(self)
|
|
100
|
+
self.organizations = Organizations(self)
|
|
101
|
+
self.endpoints = Endpoints(self)
|
|
102
|
+
|
|
103
|
+
def request(
|
|
104
|
+
self,
|
|
105
|
+
method: str,
|
|
106
|
+
path: str,
|
|
107
|
+
*,
|
|
108
|
+
json: Object | None = None,
|
|
109
|
+
params: Mapping[str, str] | None = None,
|
|
110
|
+
timeout: float | None = None,
|
|
111
|
+
) -> JsonValue:
|
|
112
|
+
"""Call another verified customer route relative to /control/v1."""
|
|
113
|
+
response = self._client.request(
|
|
114
|
+
method,
|
|
115
|
+
_target(self.base_url, path),
|
|
116
|
+
headers=self._headers,
|
|
117
|
+
json=json,
|
|
118
|
+
params=params,
|
|
119
|
+
timeout=self._timeout if timeout is None else timeout,
|
|
120
|
+
follow_redirects=False,
|
|
121
|
+
)
|
|
122
|
+
return _decode(response)
|
|
123
|
+
|
|
124
|
+
def close(self) -> None:
|
|
125
|
+
if self._owns_client:
|
|
126
|
+
self._client.close()
|
|
127
|
+
|
|
128
|
+
def __enter__(self) -> Self:
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def __exit__(
|
|
132
|
+
self,
|
|
133
|
+
exc_type: type[BaseException] | None,
|
|
134
|
+
exc: BaseException | None,
|
|
135
|
+
traceback: TracebackType | None,
|
|
136
|
+
) -> None:
|
|
137
|
+
self.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Catalog:
|
|
141
|
+
def __init__(self, control: Control) -> None:
|
|
142
|
+
self._control = control
|
|
143
|
+
|
|
144
|
+
def models(self) -> list[Object]:
|
|
145
|
+
return _objects(self._control.request("GET", "catalog/models"))
|
|
146
|
+
|
|
147
|
+
def skus(self, *, provider: str) -> JsonValue:
|
|
148
|
+
return self._control.request("GET", "catalog/skus", params={"provider": provider})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Organizations:
|
|
152
|
+
def __init__(self, control: Control) -> None:
|
|
153
|
+
self._control = control
|
|
154
|
+
|
|
155
|
+
def list(self) -> list[Object]:
|
|
156
|
+
return _objects(self._control.request("GET", "organizations"))
|
|
157
|
+
|
|
158
|
+
def get(self, organization_id: str) -> Object:
|
|
159
|
+
return _object(self._control.request("GET", f"organizations/{_segment(organization_id)}"))
|
|
160
|
+
|
|
161
|
+
def limits(self, organization_id: str) -> Object:
|
|
162
|
+
"""Return quota policy, current usage, and any pending increase request."""
|
|
163
|
+
return _object(
|
|
164
|
+
self._control.request("GET", f"organizations/{_segment(organization_id)}/limits")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Endpoints:
|
|
169
|
+
def __init__(self, control: Control) -> None:
|
|
170
|
+
self._control = control
|
|
171
|
+
|
|
172
|
+
def list(self, *, workspace_id: str) -> list[Object]:
|
|
173
|
+
return _objects(
|
|
174
|
+
self._control.request("GET", "endpoints", params={"workspace_id": workspace_id})
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def get(self, endpoint_id: str, *, timeout: float | None = None) -> Object:
|
|
178
|
+
return _object(
|
|
179
|
+
self._control.request("GET", f"endpoints/{_segment(endpoint_id)}", timeout=timeout)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def create(
|
|
183
|
+
self,
|
|
184
|
+
*,
|
|
185
|
+
workspace_id: str,
|
|
186
|
+
model_slug: str,
|
|
187
|
+
name: str,
|
|
188
|
+
sku: Object,
|
|
189
|
+
cloud_connection_id: str | None = None,
|
|
190
|
+
) -> Object:
|
|
191
|
+
"""Persist launch intent; use wait_until_ready to observe provisioning."""
|
|
192
|
+
body: Object = {
|
|
193
|
+
"workspace_id": workspace_id,
|
|
194
|
+
"model_slug": model_slug,
|
|
195
|
+
"name": name,
|
|
196
|
+
"sku": sku,
|
|
197
|
+
}
|
|
198
|
+
if cloud_connection_id is not None:
|
|
199
|
+
body["cloud_connection_id"] = cloud_connection_id
|
|
200
|
+
return _object(self._control.request("POST", "endpoints", json=body))
|
|
201
|
+
|
|
202
|
+
def stop(self, endpoint_id: str) -> Object:
|
|
203
|
+
return _object(self._control.request("POST", f"endpoints/{_segment(endpoint_id)}/stop"))
|
|
204
|
+
|
|
205
|
+
def delete(self, endpoint_id: str) -> None:
|
|
206
|
+
self._control.request("DELETE", f"endpoints/{_segment(endpoint_id)}")
|
|
207
|
+
|
|
208
|
+
def management(self, endpoint_id: str) -> Object:
|
|
209
|
+
return _object(
|
|
210
|
+
self._control.request("GET", f"endpoints/{_segment(endpoint_id)}/management")
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def update_scaling(
|
|
214
|
+
self, endpoint_id: str, *, expected_generation: int, policy: Object
|
|
215
|
+
) -> JsonValue:
|
|
216
|
+
return self._control.request(
|
|
217
|
+
"PATCH",
|
|
218
|
+
f"endpoints/{_segment(endpoint_id)}/scaling",
|
|
219
|
+
json={"expected_generation": expected_generation, "policy": policy},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def update_routing(
|
|
223
|
+
self, endpoint_id: str, *, expected_generation: int, policy: Object
|
|
224
|
+
) -> JsonValue:
|
|
225
|
+
return self._control.request(
|
|
226
|
+
"PATCH",
|
|
227
|
+
f"endpoints/{_segment(endpoint_id)}/routing",
|
|
228
|
+
json={"expected_generation": expected_generation, "policy": policy},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def wait_until_ready(
|
|
232
|
+
self,
|
|
233
|
+
endpoint_id: str,
|
|
234
|
+
*,
|
|
235
|
+
timeout: float = 600,
|
|
236
|
+
poll_interval: float = 2,
|
|
237
|
+
) -> Object:
|
|
238
|
+
"""Wait for a running endpoint with an accepting replica; fail on teardown.
|
|
239
|
+
|
|
240
|
+
timeout bounds polling and each HTTP request's timeout budget. Network
|
|
241
|
+
errors propagate; no hidden retry or replay of a creation request occurs.
|
|
242
|
+
"""
|
|
243
|
+
_positive(timeout, "timeout")
|
|
244
|
+
_positive(poll_interval, "poll_interval")
|
|
245
|
+
deadline = time.monotonic() + timeout
|
|
246
|
+
while True:
|
|
247
|
+
remaining = deadline - time.monotonic()
|
|
248
|
+
if remaining <= 0:
|
|
249
|
+
raise TimeoutError(
|
|
250
|
+
f"Endpoint {endpoint_id} was not ready within {timeout} seconds."
|
|
251
|
+
)
|
|
252
|
+
endpoint = self.get(endpoint_id, timeout=min(remaining, self._control._timeout))
|
|
253
|
+
if _ready(endpoint):
|
|
254
|
+
return endpoint
|
|
255
|
+
remaining = deadline - time.monotonic()
|
|
256
|
+
if remaining > 0:
|
|
257
|
+
time.sleep(min(poll_interval, remaining))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class AsyncControl:
|
|
261
|
+
"""Platform root configuration plus catalog, organizations and endpoints."""
|
|
262
|
+
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
*,
|
|
266
|
+
api_key: str | None = None,
|
|
267
|
+
base_url: str | None = None,
|
|
268
|
+
timeout: float = 60,
|
|
269
|
+
http_client: httpx.AsyncClient | None = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
_positive(timeout, "timeout")
|
|
272
|
+
self.base_url = _config.platform_url(base_url) + "/control/v1"
|
|
273
|
+
self._headers = {"Authorization": f"Bearer {_config.api_key(api_key)}"}
|
|
274
|
+
self._timeout = timeout
|
|
275
|
+
self._owns_client = http_client is None
|
|
276
|
+
self._client = http_client if http_client is not None else httpx.AsyncClient()
|
|
277
|
+
self.catalog = AsyncCatalog(self)
|
|
278
|
+
self.organizations = AsyncOrganizations(self)
|
|
279
|
+
self.endpoints = AsyncEndpoints(self)
|
|
280
|
+
|
|
281
|
+
async def request(
|
|
282
|
+
self,
|
|
283
|
+
method: str,
|
|
284
|
+
path: str,
|
|
285
|
+
*,
|
|
286
|
+
json: Object | None = None,
|
|
287
|
+
params: Mapping[str, str] | None = None,
|
|
288
|
+
timeout: float | None = None,
|
|
289
|
+
) -> JsonValue:
|
|
290
|
+
"""Call another verified customer route relative to /control/v1."""
|
|
291
|
+
response = await self._client.request(
|
|
292
|
+
method,
|
|
293
|
+
_target(self.base_url, path),
|
|
294
|
+
headers=self._headers,
|
|
295
|
+
json=json,
|
|
296
|
+
params=params,
|
|
297
|
+
timeout=self._timeout if timeout is None else timeout,
|
|
298
|
+
follow_redirects=False,
|
|
299
|
+
)
|
|
300
|
+
return _decode(response)
|
|
301
|
+
|
|
302
|
+
async def close(self) -> None:
|
|
303
|
+
if self._owns_client:
|
|
304
|
+
await self._client.aclose()
|
|
305
|
+
|
|
306
|
+
async def __aenter__(self) -> Self:
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
async def __aexit__(
|
|
310
|
+
self,
|
|
311
|
+
exc_type: type[BaseException] | None,
|
|
312
|
+
exc: BaseException | None,
|
|
313
|
+
traceback: TracebackType | None,
|
|
314
|
+
) -> None:
|
|
315
|
+
await self.close()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class AsyncCatalog:
|
|
319
|
+
def __init__(self, control: AsyncControl) -> None:
|
|
320
|
+
self._control = control
|
|
321
|
+
|
|
322
|
+
async def models(self) -> list[Object]:
|
|
323
|
+
return _objects(await self._control.request("GET", "catalog/models"))
|
|
324
|
+
|
|
325
|
+
async def skus(self, *, provider: str) -> JsonValue:
|
|
326
|
+
return await self._control.request("GET", "catalog/skus", params={"provider": provider})
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class AsyncOrganizations:
|
|
330
|
+
def __init__(self, control: AsyncControl) -> None:
|
|
331
|
+
self._control = control
|
|
332
|
+
|
|
333
|
+
async def list(self) -> list[Object]:
|
|
334
|
+
return _objects(await self._control.request("GET", "organizations"))
|
|
335
|
+
|
|
336
|
+
async def get(self, organization_id: str) -> Object:
|
|
337
|
+
return _object(
|
|
338
|
+
await self._control.request("GET", f"organizations/{_segment(organization_id)}")
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
async def limits(self, organization_id: str) -> Object:
|
|
342
|
+
"""Return quota policy, current usage, and any pending increase request."""
|
|
343
|
+
return _object(
|
|
344
|
+
await self._control.request("GET", f"organizations/{_segment(organization_id)}/limits")
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class AsyncEndpoints:
|
|
349
|
+
def __init__(self, control: AsyncControl) -> None:
|
|
350
|
+
self._control = control
|
|
351
|
+
|
|
352
|
+
async def list(self, *, workspace_id: str) -> list[Object]:
|
|
353
|
+
return _objects(
|
|
354
|
+
await self._control.request("GET", "endpoints", params={"workspace_id": workspace_id})
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
async def get(self, endpoint_id: str, *, timeout: float | None = None) -> Object:
|
|
358
|
+
return _object(
|
|
359
|
+
await self._control.request(
|
|
360
|
+
"GET", f"endpoints/{_segment(endpoint_id)}", timeout=timeout
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
async def create(
|
|
365
|
+
self,
|
|
366
|
+
*,
|
|
367
|
+
workspace_id: str,
|
|
368
|
+
model_slug: str,
|
|
369
|
+
name: str,
|
|
370
|
+
sku: Object,
|
|
371
|
+
cloud_connection_id: str | None = None,
|
|
372
|
+
) -> Object:
|
|
373
|
+
"""Persist launch intent; use wait_until_ready to observe provisioning."""
|
|
374
|
+
body: Object = {
|
|
375
|
+
"workspace_id": workspace_id,
|
|
376
|
+
"model_slug": model_slug,
|
|
377
|
+
"name": name,
|
|
378
|
+
"sku": sku,
|
|
379
|
+
}
|
|
380
|
+
if cloud_connection_id is not None:
|
|
381
|
+
body["cloud_connection_id"] = cloud_connection_id
|
|
382
|
+
return _object(await self._control.request("POST", "endpoints", json=body))
|
|
383
|
+
|
|
384
|
+
async def stop(self, endpoint_id: str) -> Object:
|
|
385
|
+
return _object(
|
|
386
|
+
await self._control.request("POST", f"endpoints/{_segment(endpoint_id)}/stop")
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
async def delete(self, endpoint_id: str) -> None:
|
|
390
|
+
await self._control.request("DELETE", f"endpoints/{_segment(endpoint_id)}")
|
|
391
|
+
|
|
392
|
+
async def management(self, endpoint_id: str) -> Object:
|
|
393
|
+
return _object(
|
|
394
|
+
await self._control.request("GET", f"endpoints/{_segment(endpoint_id)}/management")
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
async def update_scaling(
|
|
398
|
+
self, endpoint_id: str, *, expected_generation: int, policy: Object
|
|
399
|
+
) -> JsonValue:
|
|
400
|
+
return await self._control.request(
|
|
401
|
+
"PATCH",
|
|
402
|
+
f"endpoints/{_segment(endpoint_id)}/scaling",
|
|
403
|
+
json={"expected_generation": expected_generation, "policy": policy},
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
async def update_routing(
|
|
407
|
+
self, endpoint_id: str, *, expected_generation: int, policy: Object
|
|
408
|
+
) -> JsonValue:
|
|
409
|
+
return await self._control.request(
|
|
410
|
+
"PATCH",
|
|
411
|
+
f"endpoints/{_segment(endpoint_id)}/routing",
|
|
412
|
+
json={"expected_generation": expected_generation, "policy": policy},
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
async def wait_until_ready(
|
|
416
|
+
self,
|
|
417
|
+
endpoint_id: str,
|
|
418
|
+
*,
|
|
419
|
+
timeout: float = 600,
|
|
420
|
+
poll_interval: float = 2,
|
|
421
|
+
) -> Object:
|
|
422
|
+
"""Wait for a running endpoint with an accepting replica; fail on teardown.
|
|
423
|
+
|
|
424
|
+
timeout bounds polling and each HTTP request's timeout budget. Network
|
|
425
|
+
errors propagate; no hidden retry or replay of a creation request occurs.
|
|
426
|
+
"""
|
|
427
|
+
_positive(timeout, "timeout")
|
|
428
|
+
_positive(poll_interval, "poll_interval")
|
|
429
|
+
deadline = time.monotonic() + timeout
|
|
430
|
+
while True:
|
|
431
|
+
remaining = deadline - time.monotonic()
|
|
432
|
+
if remaining <= 0:
|
|
433
|
+
raise TimeoutError(
|
|
434
|
+
f"Endpoint {endpoint_id} was not ready within {timeout} seconds."
|
|
435
|
+
)
|
|
436
|
+
endpoint = await self.get(endpoint_id, timeout=min(remaining, self._control._timeout))
|
|
437
|
+
if _ready(endpoint):
|
|
438
|
+
return endpoint
|
|
439
|
+
remaining = deadline - time.monotonic()
|
|
440
|
+
if remaining > 0:
|
|
441
|
+
await asyncio.sleep(min(poll_interval, remaining))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""ElevenLabs-compatible conversation runtime and client tool registration."""
|
|
2
|
+
|
|
3
|
+
from .conversation import (
|
|
4
|
+
AsyncAudioInterface,
|
|
5
|
+
AsyncConversation,
|
|
6
|
+
AudioInterface,
|
|
7
|
+
ClientTools,
|
|
8
|
+
Conversation,
|
|
9
|
+
ConversationInitiationData,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AsyncAudioInterface",
|
|
14
|
+
"AsyncConversation",
|
|
15
|
+
"AudioInterface",
|
|
16
|
+
"ClientTools",
|
|
17
|
+
"Conversation",
|
|
18
|
+
"ConversationInitiationData",
|
|
19
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Use the upstream conversation runtime without changing its protocol."""
|
|
2
|
+
|
|
3
|
+
from elevenlabs.conversational_ai.conversation import (
|
|
4
|
+
AsyncAudioInterface,
|
|
5
|
+
AsyncConversation,
|
|
6
|
+
AudioInterface,
|
|
7
|
+
ClientTools,
|
|
8
|
+
Conversation,
|
|
9
|
+
ConversationInitiationData,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AsyncAudioInterface",
|
|
14
|
+
"AsyncConversation",
|
|
15
|
+
"AudioInterface",
|
|
16
|
+
"ClientTools",
|
|
17
|
+
"Conversation",
|
|
18
|
+
"ConversationInitiationData",
|
|
19
|
+
]
|
avartha/elevenlabs.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""ElevenLabs clients configured for Avartha inference.
|
|
2
|
+
|
|
3
|
+
All generated resources and wire types are upstream objects. A supplied HTTP
|
|
4
|
+
client remains caller-owned; close() only closes a client created here.
|
|
5
|
+
|
|
6
|
+
Realtime TTS is the exception, so the sync constructor replaces `_text_to_speech`
|
|
7
|
+
with the corrected client from `_realtime_tts`: upstream's helper sends an
|
|
8
|
+
envelope the gateway refuses. That assignment is the only private-attribute
|
|
9
|
+
access here besides the `_ws_base_url` upstream pins to wss even for
|
|
10
|
+
http://localhost.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
from typing import Any, Self, cast
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from elevenlabs.client import AsyncElevenLabs as _AsyncElevenLabs
|
|
18
|
+
from elevenlabs.client import ElevenLabs as _ElevenLabs
|
|
19
|
+
|
|
20
|
+
from . import _config
|
|
21
|
+
from ._config import Tier
|
|
22
|
+
from ._realtime_tts import AvarthaRealtimeTextToSpeechClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ElevenLabs(_ElevenLabs):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
base_url: str | None = None,
|
|
31
|
+
tier: Tier = "serverless",
|
|
32
|
+
headers: dict[str, str] | None = None,
|
|
33
|
+
timeout: float | None = 240,
|
|
34
|
+
follow_redirects: bool | None = True,
|
|
35
|
+
httpx_client: httpx.Client | None = None,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> None:
|
|
38
|
+
key, url = _config.api_key(api_key), _config.elevenlabs_url(base_url, tier=tier)
|
|
39
|
+
self._owned_http_client = None
|
|
40
|
+
if httpx_client is None:
|
|
41
|
+
httpx_client = httpx.Client(timeout=timeout, follow_redirects=bool(follow_redirects))
|
|
42
|
+
self._owned_http_client = httpx_client
|
|
43
|
+
super().__init__(
|
|
44
|
+
api_key=key,
|
|
45
|
+
base_url=url,
|
|
46
|
+
headers=headers,
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
follow_redirects=follow_redirects,
|
|
49
|
+
httpx_client=httpx_client,
|
|
50
|
+
**kwargs,
|
|
51
|
+
)
|
|
52
|
+
self._text_to_speech = AvarthaRealtimeTextToSpeechClient(
|
|
53
|
+
client_wrapper=self._client_wrapper
|
|
54
|
+
)
|
|
55
|
+
self._text_to_speech._ws_base_url = _config.websocket_url(url)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def text_to_speech(self) -> AvarthaRealtimeTextToSpeechClient:
|
|
59
|
+
return cast(AvarthaRealtimeTextToSpeechClient, self._text_to_speech)
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
if self._owned_http_client is not None:
|
|
63
|
+
self._owned_http_client.close()
|
|
64
|
+
|
|
65
|
+
def __enter__(self) -> Self:
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(
|
|
69
|
+
self,
|
|
70
|
+
exc_type: type[BaseException] | None,
|
|
71
|
+
exc: BaseException | None,
|
|
72
|
+
traceback: TracebackType | None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.close()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AsyncElevenLabs(_AsyncElevenLabs):
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
api_key: str | None = None,
|
|
82
|
+
base_url: str | None = None,
|
|
83
|
+
tier: Tier = "serverless",
|
|
84
|
+
headers: dict[str, str] | None = None,
|
|
85
|
+
timeout: float | None = 240,
|
|
86
|
+
follow_redirects: bool | None = True,
|
|
87
|
+
httpx_client: httpx.AsyncClient | None = None,
|
|
88
|
+
**kwargs: Any,
|
|
89
|
+
) -> None:
|
|
90
|
+
key, url = _config.api_key(api_key), _config.elevenlabs_url(base_url, tier=tier)
|
|
91
|
+
self._owned_http_client = None
|
|
92
|
+
if httpx_client is None:
|
|
93
|
+
httpx_client = httpx.AsyncClient(
|
|
94
|
+
timeout=timeout, follow_redirects=bool(follow_redirects)
|
|
95
|
+
)
|
|
96
|
+
self._owned_http_client = httpx_client
|
|
97
|
+
super().__init__(
|
|
98
|
+
api_key=key,
|
|
99
|
+
base_url=url,
|
|
100
|
+
headers=headers,
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
follow_redirects=follow_redirects,
|
|
103
|
+
httpx_client=httpx_client,
|
|
104
|
+
**kwargs,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def close(self) -> None:
|
|
108
|
+
if self._owned_http_client is not None:
|
|
109
|
+
await self._owned_http_client.aclose()
|
|
110
|
+
|
|
111
|
+
async def __aenter__(self) -> Self:
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
async def __aexit__(
|
|
115
|
+
self,
|
|
116
|
+
exc_type: type[BaseException] | None,
|
|
117
|
+
exc: BaseException | None,
|
|
118
|
+
traceback: TracebackType | None,
|
|
119
|
+
) -> None:
|
|
120
|
+
await self.close()
|
avartha/errors.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Platform errors. Inference errors keep their upstream SDK exception classes."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from pydantic import JsonValue
|
|
5
|
+
|
|
6
|
+
from ._config import ConfigurationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlatformAPIError(Exception):
|
|
10
|
+
"""Non-success platform response, including field-scoped validation errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, response: httpx.Response) -> None:
|
|
13
|
+
self.response = response
|
|
14
|
+
self.status_code = response.status_code
|
|
15
|
+
self.request_id = response.headers.get("x-request-id")
|
|
16
|
+
self.retry_after = response.headers.get("retry-after")
|
|
17
|
+
try:
|
|
18
|
+
self.body: JsonValue = response.json()
|
|
19
|
+
except ValueError:
|
|
20
|
+
self.body = response.text
|
|
21
|
+
self.field_errors = self.body.get("errors", []) if isinstance(self.body, dict) else []
|
|
22
|
+
message = self.body.get("error", self.body) if isinstance(self.body, dict) else self.body
|
|
23
|
+
super().__init__(f"Platform API returned HTTP {self.status_code}: {message}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EndpointFailedError(Exception):
|
|
27
|
+
def __init__(self, endpoint: dict[str, JsonValue]) -> None:
|
|
28
|
+
self.endpoint = endpoint
|
|
29
|
+
super().__init__(
|
|
30
|
+
f"Endpoint {endpoint.get('id')} entered {endpoint.get('status')}: "
|
|
31
|
+
f"{endpoint.get('error')}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["ConfigurationError", "EndpointFailedError", "PlatformAPIError"]
|