avartha-python-sdk 0.0.7__py3-none-any.whl → 0.0.8__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.
@@ -8,7 +8,6 @@ import json
8
8
  from collections.abc import Mapping
9
9
  from typing import Any, Never
10
10
 
11
- from ._exceptions import UnsupportedOperationError
12
11
  from ._speech_config import RequestOptions
13
12
  from ._speech_http import (
14
13
  AsyncHttpResponse,
@@ -18,21 +17,13 @@ from ._speech_http import (
18
17
  segment,
19
18
  )
20
19
  from .types.conversational_ai import (
21
- ConversationInitiationClientDataRequestInput,
22
20
  ConversationSignedUrlResponseModel,
23
21
  GetConversationResponseModel,
24
22
  GetConversationsPageResponseModel,
25
- TelephonyCallConfigInput,
26
23
  TokenResponseModel,
27
- TwilioOutboundCallResponse,
28
24
  )
29
25
 
30
26
  _BASE = "v1/convai"
31
- _OUTBOUND_CALL = f"{_BASE}/twilio/outbound-call"
32
- _IDEMPOTENCY_HEADER = "Idempotency-Key"
33
- # A 409 is left to the caller: the key was reused for a different call, or the workspace is at
34
- # its call limit, and ringing someone moments later wouldn't be expected.
35
- _DIAL_RETRY_STATUSES = (408, 429)
36
27
 
37
28
 
38
29
  def _encoded(value: Mapping[str, Any] | None) -> str | None:
@@ -53,50 +44,6 @@ def _mint_params(
53
44
  }
54
45
 
55
46
 
56
- def _dial_body(
57
- agent_id: str,
58
- agent_phone_number_id: str,
59
- to_number: str,
60
- conversation_initiation_client_data: ConversationInitiationClientDataRequestInput | None,
61
- call_recording_enabled: bool | None,
62
- telephony_call_config: TelephonyCallConfigInput | None,
63
- ) -> dict[str, Any]:
64
- optional = {
65
- "conversation_initiation_client_data": conversation_initiation_client_data,
66
- "call_recording_enabled": call_recording_enabled,
67
- "telephony_call_config": telephony_call_config,
68
- }
69
- return {
70
- "agent_id": agent_id,
71
- "agent_phone_number_id": agent_phone_number_id,
72
- "to_number": to_number,
73
- **{name: value for name, value in optional.items() if value is not None},
74
- }
75
-
76
-
77
- def _dial_options(
78
- idempotency_key: str | None, request_options: RequestOptions | None
79
- ) -> RequestOptions:
80
- """Attach the key, and never retry without one: each keyless request rings someone."""
81
- options: RequestOptions = {**(request_options or {})}
82
- headers = dict(options.get("additional_headers") or {})
83
- key_header = _IDEMPOTENCY_HEADER.lower()
84
- if idempotency_key is not None:
85
- headers = {name: value for name, value in headers.items() if name.lower() != key_header}
86
- headers[_IDEMPOTENCY_HEADER] = idempotency_key
87
- options["additional_headers"] = headers
88
- if not any(name.lower() == key_header and value is not None for name, value in headers.items()):
89
- options["max_retries"] = 0
90
- return options
91
-
92
-
93
- def _refuse_register_call() -> Never:
94
- raise UnsupportedOperationError(
95
- "conversational_ai.twilio.register_call is not supported. Avartha answers calls to a "
96
- "connected number with the agent assigned to it, and outbound_call places calls."
97
- )
98
-
99
-
100
47
  class RawConversations:
101
48
  def __init__(self, client: SpeechHTTPClient) -> None:
102
49
  self._client = client
@@ -380,165 +327,20 @@ class AsyncConversations:
380
327
  return response.data
381
328
 
382
329
 
383
- class RawTwilio:
384
- def __init__(self, client: SpeechHTTPClient) -> None:
385
- self._client = client
386
-
387
- def outbound_call(
388
- self,
389
- *,
390
- agent_id: str,
391
- agent_phone_number_id: str,
392
- to_number: str,
393
- conversation_initiation_client_data: ConversationInitiationClientDataRequestInput
394
- | None = None,
395
- call_recording_enabled: bool | None = None,
396
- telephony_call_config: TelephonyCallConfigInput | None = None,
397
- idempotency_key: str | None = None,
398
- request_options: RequestOptions | None = None,
399
- ) -> HttpResponse[TwilioOutboundCallResponse]:
400
- return self._client.post(
401
- _OUTBOUND_CALL,
402
- TwilioOutboundCallResponse,
403
- params={},
404
- body=_dial_body(
405
- agent_id,
406
- agent_phone_number_id,
407
- to_number,
408
- conversation_initiation_client_data,
409
- call_recording_enabled,
410
- telephony_call_config,
411
- ),
412
- retry_statuses=_DIAL_RETRY_STATUSES,
413
- request_options=_dial_options(idempotency_key, request_options),
414
- )
415
-
416
-
417
- class Twilio:
418
- """Place phone calls from one of the workspace's Twilio numbers."""
419
-
420
- def __init__(self, client: SpeechHTTPClient) -> None:
421
- self.with_raw_response = RawTwilio(client)
422
-
423
- def outbound_call(
424
- self,
425
- *,
426
- agent_id: str,
427
- agent_phone_number_id: str,
428
- to_number: str,
429
- conversation_initiation_client_data: ConversationInitiationClientDataRequestInput
430
- | None = None,
431
- call_recording_enabled: bool | None = None,
432
- telephony_call_config: TelephonyCallConfigInput | None = None,
433
- idempotency_key: str | None = None,
434
- request_options: RequestOptions | None = None,
435
- ) -> TwilioOutboundCallResponse:
436
- """Ring to_number and connect the agent when someone answers.
437
-
438
- Pass idempotency_key, an Avartha extension, so a repeated request returns the first
439
- attempt instead of ringing again; only then does the SDK retry. Check success: False
440
- means Twilio refused the call or didn't confirm it, and an unconfirmed call may still
441
- ring.
442
- """
443
- return self.with_raw_response.outbound_call(
444
- agent_id=agent_id,
445
- agent_phone_number_id=agent_phone_number_id,
446
- to_number=to_number,
447
- conversation_initiation_client_data=conversation_initiation_client_data,
448
- call_recording_enabled=call_recording_enabled,
449
- telephony_call_config=telephony_call_config,
450
- idempotency_key=idempotency_key,
451
- request_options=request_options,
452
- ).data
453
-
454
- def register_call(self, **kwargs: Any) -> Never:
455
- _refuse_register_call()
456
-
457
-
458
- class AsyncRawTwilio:
459
- def __init__(self, client: AsyncSpeechHTTPClient) -> None:
460
- self._client = client
461
-
462
- async def outbound_call(
463
- self,
464
- *,
465
- agent_id: str,
466
- agent_phone_number_id: str,
467
- to_number: str,
468
- conversation_initiation_client_data: ConversationInitiationClientDataRequestInput
469
- | None = None,
470
- call_recording_enabled: bool | None = None,
471
- telephony_call_config: TelephonyCallConfigInput | None = None,
472
- idempotency_key: str | None = None,
473
- request_options: RequestOptions | None = None,
474
- ) -> AsyncHttpResponse[TwilioOutboundCallResponse]:
475
- return await self._client.post(
476
- _OUTBOUND_CALL,
477
- TwilioOutboundCallResponse,
478
- params={},
479
- body=_dial_body(
480
- agent_id,
481
- agent_phone_number_id,
482
- to_number,
483
- conversation_initiation_client_data,
484
- call_recording_enabled,
485
- telephony_call_config,
486
- ),
487
- retry_statuses=_DIAL_RETRY_STATUSES,
488
- request_options=_dial_options(idempotency_key, request_options),
489
- )
490
-
491
-
492
- class AsyncTwilio:
493
- """Place phone calls from one of the workspace's Twilio numbers."""
494
-
495
- def __init__(self, client: AsyncSpeechHTTPClient) -> None:
496
- self.with_raw_response = AsyncRawTwilio(client)
497
-
498
- async def outbound_call(
499
- self,
500
- *,
501
- agent_id: str,
502
- agent_phone_number_id: str,
503
- to_number: str,
504
- conversation_initiation_client_data: ConversationInitiationClientDataRequestInput
505
- | None = None,
506
- call_recording_enabled: bool | None = None,
507
- telephony_call_config: TelephonyCallConfigInput | None = None,
508
- idempotency_key: str | None = None,
509
- request_options: RequestOptions | None = None,
510
- ) -> TwilioOutboundCallResponse:
511
- """Ring to_number and connect the agent when someone answers.
512
-
513
- Pass idempotency_key, an Avartha extension, so a repeated request returns the first
514
- attempt instead of ringing again; only then does the SDK retry. Check success: False
515
- means Twilio refused the call or didn't confirm it, and an unconfirmed call may still
516
- ring.
517
- """
518
- response = await self.with_raw_response.outbound_call(
519
- agent_id=agent_id,
520
- agent_phone_number_id=agent_phone_number_id,
521
- to_number=to_number,
522
- conversation_initiation_client_data=conversation_initiation_client_data,
523
- call_recording_enabled=call_recording_enabled,
524
- telephony_call_config=telephony_call_config,
525
- idempotency_key=idempotency_key,
526
- request_options=request_options,
527
- )
528
- return response.data
529
-
530
- def register_call(self, **kwargs: Any) -> Never:
531
- _refuse_register_call()
532
-
533
-
534
330
  class _ConversationalAi:
535
331
  def __getattr__(self, name: str) -> Never:
536
332
  if name.startswith("_"):
537
333
  raise AttributeError(name)
334
+ if name == "twilio":
335
+ # Phone calls need workspace features that aren't available to every workspace.
336
+ raise NotImplementedError(
337
+ "conversational_ai.twilio is not available in this SDK. "
338
+ "conversational_ai.conversations is available."
339
+ )
538
340
  raise NotImplementedError(
539
341
  f"conversational_ai.{name} is not implemented. ElevenLabs-schema agents, tools, and "
540
342
  "knowledge bases will be implemented in a future release; "
541
- "conversational_ai.conversations and conversational_ai.twilio are available, "
343
+ "conversational_ai.conversations is available, "
542
344
  "and Control.voice_agents manages Avartha voice agents."
543
345
  )
544
346
 
@@ -546,10 +348,8 @@ class _ConversationalAi:
546
348
  class ConversationalAi(_ConversationalAi):
547
349
  def __init__(self, client: SpeechHTTPClient) -> None:
548
350
  self.conversations = Conversations(client)
549
- self.twilio = Twilio(client)
550
351
 
551
352
 
552
353
  class AsyncConversationalAi(_ConversationalAi):
553
354
  def __init__(self, client: AsyncSpeechHTTPClient) -> None:
554
355
  self.conversations = AsyncConversations(client)
555
- self.twilio = AsyncTwilio(client)
avartha/_realtime_tts.py CHANGED
@@ -8,6 +8,8 @@ text fragments unchanged, preserve errors, and use owned multi-context message t
8
8
  import asyncio
9
9
  import base64
10
10
  import json
11
+ import logging
12
+ import time
11
13
  from collections.abc import (
12
14
  AsyncGenerator,
13
15
  AsyncIterable,
@@ -53,6 +55,8 @@ from .types.speech import (
53
55
  VoiceSettings,
54
56
  )
55
57
 
58
+ log = logging.getLogger(__name__)
59
+
56
60
  ReceiveMessageMulti = AudioOutputMulti | FinalOutputMulti
57
61
  SendMessageMulti = (
58
62
  InitializeConnectionMulti | SendTextMulti | FlushContext | CloseContext | CloseSocket
@@ -106,38 +110,57 @@ def _query(
106
110
  *,
107
111
  request_options: RequestOptions | None = None,
108
112
  ) -> dict[str, Any]:
109
- """The connection query; refuses option values the runtime always rejects."""
110
- output_format = output_format or DEFAULT_OUTPUT_FORMAT
111
- query: dict[str, Any] = {
112
- "model_id": model_id,
113
- "output_format": output_format,
114
- "auto_mode": "true",
115
- }
116
- if inactivity_timeout is not None:
117
- if (
118
- isinstance(inactivity_timeout, bool)
119
- or not isinstance(inactivity_timeout, int)
120
- or inactivity_timeout not in _INACTIVITY_TIMEOUT_SECONDS
121
- ):
122
- raise ValueError("inactivity_timeout must be a whole number of seconds from 1 to 180")
123
- query["inactivity_timeout"] = inactivity_timeout
124
- if sync_alignment is not None:
125
- if not isinstance(sync_alignment, bool):
126
- raise ValueError("sync_alignment must be True or False")
127
- if sync_alignment and output_format.startswith("mp3_"):
128
- raise ValueError(
129
- "sync_alignment=True requires PCM or G.711 output; "
130
- "MP3 packets do not share the alignment clock"
131
- )
132
- query["sync_alignment"] = str(sync_alignment).lower()
133
- # Match HTTP request_options precedence: explicit extra query values override
134
- # defaults and named arguments, while None omits a parameter.
113
+ """The connection query; refuses option values the runtime always rejects.
114
+
115
+ Extra query values in `request_options` override defaults and named arguments,
116
+ as for HTTP requests, and None omits a parameter. Named arguments are checked
117
+ as given, then the merged values are checked again, so an override cannot send
118
+ a value the runtime refuses.
119
+ """
120
+ if inactivity_timeout is not None and not _valid_inactivity_timeout(inactivity_timeout):
121
+ raise ValueError(_INACTIVITY_TIMEOUT_ERROR)
122
+ if sync_alignment is not None and not isinstance(sync_alignment, bool):
123
+ raise ValueError("sync_alignment must be True or False")
135
124
  extra = request_options.get("additional_query_parameters", {}) if request_options else {}
136
- query = remove_none_from_dict({**query, **extra})
137
- return {
125
+ merged = remove_none_from_dict(
126
+ {
127
+ "model_id": model_id,
128
+ "output_format": output_format or DEFAULT_OUTPUT_FORMAT,
129
+ "auto_mode": "true",
130
+ "inactivity_timeout": inactivity_timeout,
131
+ "sync_alignment": sync_alignment,
132
+ **extra,
133
+ }
134
+ )
135
+ query = {
138
136
  key: str(value).lower() if isinstance(value, bool) else value
139
- for key, value in query.items()
137
+ for key, value in merged.items()
140
138
  }
139
+ if "inactivity_timeout" in query and not _valid_inactivity_timeout(query["inactivity_timeout"]):
140
+ raise ValueError(_INACTIVITY_TIMEOUT_ERROR)
141
+ alignment = query.get("sync_alignment")
142
+ if alignment is not None and alignment not in ("true", "false"):
143
+ raise ValueError("sync_alignment must be True or False")
144
+ if alignment == "true" and str(query.get("output_format", "")).startswith("mp3_"):
145
+ raise ValueError(
146
+ "sync_alignment=True requires PCM or G.711 output; "
147
+ "MP3 packets do not share the alignment clock"
148
+ )
149
+ return query
150
+
151
+
152
+ _INACTIVITY_TIMEOUT_ERROR = "inactivity_timeout must be a whole number of seconds from 1 to 180"
153
+
154
+
155
+ def _valid_inactivity_timeout(value: Any) -> bool:
156
+ """An int, or the runtime's digits-only query spelling, from 1 to 180."""
157
+ if isinstance(value, str) and value.isascii() and value.isdigit():
158
+ value = int(value)
159
+ return (
160
+ isinstance(value, int)
161
+ and not isinstance(value, bool)
162
+ and value in _INACTIVITY_TIMEOUT_SECONDS
163
+ )
141
164
 
142
165
 
143
166
  def _headers(base: Mapping[str, str], request_options: RequestOptions | None) -> dict[str, str]:
@@ -158,6 +181,25 @@ def _audio(payload: Any) -> bytes | None:
158
181
  return base64.b64decode(encoded) if encoded else None
159
182
 
160
183
 
184
+ def _warning(payload: Any) -> bool:
185
+ """Log a runtime warning; True when the frame carries nothing else to deliver.
186
+
187
+ The runtime sends one, for example, when voice_settings it accepts for
188
+ compatibility have no effect. A warning never fails the utterance.
189
+ """
190
+ if not isinstance(payload, Mapping) or not isinstance(payload.get("warning"), str):
191
+ return False
192
+ context_id = payload.get("contextId") or payload.get("context_id")
193
+ where = f" for context {context_id!r}" if isinstance(context_id, str) else ""
194
+ log.warning("Text-to-speech warning%s: %s", where, payload["warning"])
195
+ return not (
196
+ payload.get("audio")
197
+ or payload.get("isFinal")
198
+ or payload.get("is_final")
199
+ or payload.get("error") is not None
200
+ )
201
+
202
+
161
203
  def _raise_stream_error(payload: Any) -> None:
162
204
  """A failed utterance is terminal even if the socket remains open."""
163
205
  if isinstance(payload, Mapping) and payload.get("error") is not None:
@@ -275,8 +317,10 @@ class AvarthaRealtimeTextToSpeechClient:
275
317
 
276
318
  `output_format` defaults to `DEFAULT_OUTPUT_FORMAT`; see
277
319
  `TextToSpeechOutputFormat` for PCM, MP3 and G.711 delivery formats.
278
- `voice_settings` is forwarded when supplied, but the gateway rejects any
279
- non-null value; leave it unset unless the platform has gained support for it.
320
+ `voice_settings` is forwarded when supplied. Valid `stability`,
321
+ `similarity_boost`, `style` and `use_speaker_boost` values are accepted for
322
+ compatibility but do not change synthesis; the runtime's warning is logged.
323
+ `speed` must be omitted or 1.0.
280
324
 
281
325
  `inactivity_timeout` is an Avartha extension: the seconds (1 to 180) the
282
326
  runtime waits for more text before ending the utterance; omission uses
@@ -338,6 +382,7 @@ class AvarthaRealtimeTextToSpeechClient:
338
382
  while True:
339
383
  received = json.loads(socket.recv())
340
384
  _raise_stream_error(received)
385
+ _warning(received)
341
386
  if audio := _audio(received):
342
387
  yield audio
343
388
  except websockets.exceptions.ConnectionClosed as closed:
@@ -481,6 +526,7 @@ class AvarthaAsyncRealtimeTextToSpeechClient:
481
526
  raise error
482
527
  received = json.loads(await receiver)
483
528
  _raise_stream_error(received)
529
+ _warning(received)
484
530
  if audio := _audio(received):
485
531
  yield audio
486
532
  except websockets.exceptions.ConnectionClosed as closed:
@@ -633,7 +679,10 @@ class _MultiContextProtocol:
633
679
  def close_socket(self) -> dict[str, Any]:
634
680
  return _frame(CloseSocket(close_socket=True))
635
681
 
636
- def receive(self, payload: Mapping[str, Any]) -> ReceiveMessageMulti:
682
+ def receive(self, payload: Mapping[str, Any]) -> ReceiveMessageMulti | None:
683
+ """The typed message, or None for a warning that carries nothing else."""
684
+ if _warning(payload):
685
+ return None
637
686
  context_id = payload.get("contextId") or payload.get("context_id")
638
687
  context = self._contexts.get(context_id) if isinstance(context_id, str) else None
639
688
  if payload.get("error") is not None or ("code" in payload and "message" in payload):
@@ -719,9 +768,15 @@ class MultiContextSession:
719
768
  """Return the next message, or None after a complete, cleanly closed stream.
720
769
 
721
770
  Raises `ApiError` for gateway errors or closure before all finals arrive.
771
+ Runtime warnings are logged and skipped; `timeout` covers the whole wait.
722
772
  """
773
+ deadline = None if timeout is None else time.monotonic() + timeout
723
774
  try:
724
- return self._protocol.receive(json.loads(self._socket.recv(timeout)))
775
+ while True:
776
+ remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
777
+ message = self._protocol.receive(json.loads(self._socket.recv(remaining)))
778
+ if message is not None:
779
+ return message
725
780
  except websockets.exceptions.ConnectionClosed as closed:
726
781
  if closed.code == 1000 and not self._protocol.draining:
727
782
  return None
@@ -897,14 +952,17 @@ class AsyncMultiContextSession:
897
952
  raise
898
953
 
899
954
  async def receive(self, timeout: float | None = None) -> ReceiveMessageMulti | None:
900
- """Return the next message, or None after a complete, cleanly closed stream."""
955
+ """Return the next message, or None after a complete, cleanly closed stream.
956
+
957
+ Runtime warnings are logged and skipped; `timeout` covers the whole wait.
958
+ """
901
959
  self._check_send_failure()
902
960
  try:
903
- if timeout is None:
904
- payload = await self._socket.recv()
905
- else:
906
- payload = await asyncio.wait_for(self._socket.recv(), timeout)
907
- return self._protocol.receive(json.loads(payload))
961
+ async with asyncio.timeout(timeout):
962
+ while True:
963
+ message = self._protocol.receive(json.loads(await self._socket.recv()))
964
+ if message is not None:
965
+ return message
908
966
  except websockets.exceptions.ConnectionClosed as closed:
909
967
  if closed.code == 1000 and not self._protocol.draining:
910
968
  return None
avartha/_speech_http.py CHANGED
@@ -64,9 +64,6 @@ class AsyncHttpResponse[T]:
64
64
  return AsyncHttpResponse(self._response, transform(self.data))
65
65
 
66
66
 
67
- _RETRY_STATUSES = (408, 409, 429)
68
-
69
-
70
67
  def segment(value: str) -> str:
71
68
  if value in {".", ".."}:
72
69
  raise ValueError("Resource identifiers cannot be path traversal segments")
@@ -157,19 +154,17 @@ class _Settings:
157
154
  path: str,
158
155
  params: Mapping[str, Any],
159
156
  options: RequestOptions,
160
- body: Mapping[str, Any] | None = None,
161
157
  ) -> httpx.Request:
162
158
  url = not_vendor_host(f"{self.base_url.rstrip('/')}/{path}", field="base_url")
163
159
  query = {**params, **(options.get("additional_query_parameters") or {})}
164
160
  headers = {**self.headers, **(options.get("additional_headers") or {})}
165
- content = {**(body or {}), **(options.get("additional_body_parameters") or {})}
166
161
  timeout = options.get("timeout_in_seconds")
167
162
  return client.build_request(
168
163
  method,
169
164
  url,
170
165
  params=_query(jsonable_encoder(query)) or None,
171
166
  headers={key: value for key, value in headers.items() if value is not None},
172
- json=jsonable_encoder(content) or None,
167
+ json=jsonable_encoder(options.get("additional_body_parameters")) or None,
173
168
  timeout=self.timeout if timeout is None else timeout,
174
169
  )
175
170
 
@@ -207,12 +202,10 @@ class SpeechHTTPClient:
207
202
  schema: type[T],
208
203
  *,
209
204
  params: Mapping[str, Any],
210
- body: Mapping[str, Any] | None = None,
211
- retry_statuses: tuple[int, ...] = _RETRY_STATUSES,
212
205
  request_options: RequestOptions | None = None,
213
206
  ) -> HttpResponse[T]:
214
- """Send a POST; retried like GET unless request_options sets max_retries to 0."""
215
- return self._send("POST", path, schema, params, request_options, body, retry_statuses)
207
+ """Send a body-less POST; retried like GET, so use it only for idempotent routes."""
208
+ return self._send("POST", path, schema, params, request_options)
216
209
 
217
210
  def _send[T](
218
211
  self,
@@ -221,8 +214,6 @@ class SpeechHTTPClient:
221
214
  schema: type[T],
222
215
  params: Mapping[str, Any],
223
216
  request_options: RequestOptions | None,
224
- body: Mapping[str, Any] | None = None,
225
- retry_statuses: tuple[int, ...] = _RETRY_STATUSES,
226
217
  ) -> HttpResponse[T]:
227
218
  if self._closed:
228
219
  raise RuntimeError("Cannot send a request, as the client has been closed.")
@@ -230,7 +221,7 @@ class SpeechHTTPClient:
230
221
  self._client = httpx.Client()
231
222
  options = request_options or {}
232
223
  for attempt in range(max(0, options.get("max_retries", 2)) + 1):
233
- request = self._settings.request(self._client, method, path, params, options, body)
224
+ request = self._settings.request(self._client, method, path, params, options)
234
225
  with wrap_httpx_errors():
235
226
  for redirects in range(self._client.max_redirects + 1):
236
227
  response = self._client.send(request, follow_redirects=False)
@@ -246,7 +237,7 @@ class SpeechHTTPClient:
246
237
  finally:
247
238
  response.close()
248
239
  if (
249
- response.status_code >= 500 or response.status_code in retry_statuses
240
+ response.status_code >= 500 or response.status_code in (408, 409, 429)
250
241
  ) and attempt < options.get("max_retries", 2):
251
242
  delay = _retry_delay(response, attempt)
252
243
  response.close()
@@ -298,12 +289,10 @@ class AsyncSpeechHTTPClient:
298
289
  schema: type[T],
299
290
  *,
300
291
  params: Mapping[str, Any],
301
- body: Mapping[str, Any] | None = None,
302
- retry_statuses: tuple[int, ...] = _RETRY_STATUSES,
303
292
  request_options: RequestOptions | None = None,
304
293
  ) -> AsyncHttpResponse[T]:
305
- """Send a POST; retried like GET unless request_options sets max_retries to 0."""
306
- return await self._send("POST", path, schema, params, request_options, body, retry_statuses)
294
+ """Send a body-less POST; retried like GET, so use it only for idempotent routes."""
295
+ return await self._send("POST", path, schema, params, request_options)
307
296
 
308
297
  async def _send[T](
309
298
  self,
@@ -312,8 +301,6 @@ class AsyncSpeechHTTPClient:
312
301
  schema: type[T],
313
302
  params: Mapping[str, Any],
314
303
  request_options: RequestOptions | None,
315
- body: Mapping[str, Any] | None = None,
316
- retry_statuses: tuple[int, ...] = _RETRY_STATUSES,
317
304
  ) -> AsyncHttpResponse[T]:
318
305
  if self._closed:
319
306
  raise RuntimeError("Cannot send a request, as the client has been closed.")
@@ -321,7 +308,7 @@ class AsyncSpeechHTTPClient:
321
308
  self._client = httpx.AsyncClient()
322
309
  options = request_options or {}
323
310
  for attempt in range(max(0, options.get("max_retries", 2)) + 1):
324
- request = self._settings.request(self._client, method, path, params, options, body)
311
+ request = self._settings.request(self._client, method, path, params, options)
325
312
  with wrap_httpx_errors():
326
313
  for redirects in range(self._client.max_redirects + 1):
327
314
  response = await self._client.send(request, follow_redirects=False)
@@ -337,7 +324,7 @@ class AsyncSpeechHTTPClient:
337
324
  finally:
338
325
  await response.aclose()
339
326
  if (
340
- response.status_code >= 500 or response.status_code in retry_statuses
327
+ response.status_code >= 500 or response.status_code in (408, 409, 429)
341
328
  ) and attempt < options.get("max_retries", 2):
342
329
  delay = _retry_delay(response, attempt)
343
330
  await response.aclose()
@@ -1,7 +1,6 @@
1
- """Avartha's conversation route types; unlisted server response fields stay available."""
1
+ """Responses from Avartha's conversation routes; unlisted server fields stay available."""
2
2
 
3
3
  import typing
4
- from collections.abc import Mapping
5
4
 
6
5
  import pydantic
7
6
 
@@ -51,27 +50,3 @@ class GetConversationResponseModel(ConversationSummaryResponseModel):
51
50
  has_audio: bool | None = pydantic.Field(default=None)
52
51
  has_user_audio: bool | None = pydantic.Field(default=None)
53
52
  has_response_audio: bool | None = pydantic.Field(default=None)
54
-
55
-
56
- class ConversationInitiationClientDataRequestInput(typing.TypedDict, total=False):
57
- """What a placed call accepts of the ElevenLabs model of this name."""
58
-
59
- dynamic_variables: Mapping[str, typing.Any] | None
60
-
61
-
62
- class TelephonyCallConfigInput(typing.TypedDict, total=False):
63
- """What a placed call accepts of the ElevenLabs model of this name.
64
-
65
- The platform refuses `True` for recording and any object for machine detection.
66
- """
67
-
68
- ringing_timeout_secs: int | None
69
- twilio_call_recording_enabled: bool | None
70
- twilio_machine_detection: Mapping[str, typing.Any] | None
71
-
72
-
73
- class TwilioOutboundCallResponse(_Response):
74
- success: bool = pydantic.Field()
75
- message: str = pydantic.Field()
76
- conversation_id: str | None = pydantic.Field(default=None)
77
- call_sid: str | None = pydantic.Field(default=None, alias="callSid")
@@ -85,11 +85,6 @@ class VoiceAgentToolBinding(TypedDict):
85
85
  tool_settings: NotRequired[dict[str, VoiceToolSettings]]
86
86
 
87
87
 
88
- class VoiceAgentKnowledgeBinding(TypedDict):
89
- kind: Literal["document", "folder"]
90
- id: str
91
-
92
-
93
88
  class VoiceAgentConfig(TypedDict):
94
89
  """Versioned agent configuration, always sent and replaced as a whole."""
95
90
 
@@ -111,7 +106,6 @@ class VoiceAgentConfig(TypedDict):
111
106
  built_in_tools: NotRequired[dict[str, VoiceAgentBuiltInTool]]
112
107
  tools: NotRequired[list[str]]
113
108
  tool_bindings: NotRequired[list[VoiceAgentToolBinding]]
114
- knowledge_bindings: NotRequired[list[VoiceAgentKnowledgeBinding]]
115
109
  capabilities: NotRequired[list[str]]
116
110
  chat_artifacts_enabled: NotRequired[bool]
117
111
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avartha-python-sdk
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: Python clients for Avartha Realtime text, streaming speech, and platform management
5
5
  Author-email: "Avartha Inc." <team@avartha.ai>
6
6
  License-Expression: LicenseRef-Avartha-Proprietary
@@ -33,11 +33,12 @@ Dynamic: license-file
33
33
 
34
34
  Synchronous and asynchronous Python clients for Avartha Realtime text,
35
35
  streaming speech, and platform management. Requires Python 3.12+.
36
+ The guides and examples below use SDK 0.0.7 or later.
36
37
 
37
38
  ## Install
38
39
 
39
40
  ```sh
40
- python -m pip install avartha-python-sdk
41
+ python -m pip install --upgrade 'avartha-python-sdk>=0.0.7'
41
42
  ```
42
43
 
43
44
  Configure your Avartha API key and platform root:
@@ -55,15 +56,18 @@ to your workspace. Unsupported tier values and dedicated inference URLs raise
55
56
  ## Discover models
56
57
 
57
58
  ```python
58
- from avartha import OpenAI
59
+ from avartha import Avartha
59
60
 
60
- with OpenAI() as client:
61
+ with Avartha() as client:
61
62
  for model in client.models.list():
62
63
  print(model.id, model.to_dict().get("protocols", []))
64
+ for model in client.elevenlabs.models.list():
65
+ print(model.model_id, getattr(model, "protocols", []))
63
66
  ```
64
67
 
65
- Choose a model supporting `openai_realtime` for text, `elevenlabs_tts` for
66
- speech synthesis, or `elevenlabs_asr` for speech recognition.
68
+ `client.models` lists Voice LLMs; choose `openai_realtime` for text.
69
+ `client.elevenlabs.models` lists speech models; choose `elevenlabs_tts` for
70
+ speech synthesis or `elevenlabs_asr` for speech recognition.
67
71
 
68
72
  ## Generate text
69
73
 
@@ -111,11 +115,10 @@ Inference consumes credits.
111
115
  | Workspaces, members, invitations | `Control` / `AsyncControl` |
112
116
  | Voice agents: create, edit, publish, restore, delete | `Control` / `AsyncControl`: `voice_agents` |
113
117
  | Live conversations with published agents | `avartha.conversational_ai`: `Conversation`, `AsyncConversation` |
114
- | Placing phone calls from Twilio numbers | `ElevenLabs` / `AsyncElevenLabs`: `conversational_ai.twilio.outbound_call` |
115
118
  | Combined inference and management | `Avartha` / `AsyncAvartha` |
116
119
 
117
- Managed inference uses WebSockets; Chat Completions, Responses, HTTP TTS,
118
- and file transcription are unavailable. ElevenLabs-schema agent management is not
120
+ This SDK provides model inference over WebSockets; it does not wrap HTTP
121
+ Chat Completions, Responses, speech synthesis, or file transcription. ElevenLabs-schema agent management is not
119
122
  implemented, and accessing it under `conversational_ai` raises `NotImplementedError`.
120
123
 
121
124
  ## Migrating and handling errors
@@ -140,4 +143,4 @@ never retried automatically.
140
143
  The [source archive on PyPI](https://pypi.org/project/avartha-python-sdk/#files)
141
144
  includes `README.md`, customer guides in `docs/`, and runnable examples in
142
145
  `examples/`. Download and unpack the archive to read the migration, compatibility,
143
- and platform-management guides and run the text, speech, and endpoint examples.
146
+ and platform-management guides and run the text, speech, and voice-agent examples.
@@ -1,6 +1,6 @@
1
1
  avartha/__init__.py,sha256=Myea2lrcfw5ZmPj1ZbKOhKdi5CQY44VEKoFTCy8uqyQ,2900
2
2
  avartha/_config.py,sha256=mzaHiqYH_7N4okLijiRbJlCNyxvZn4Ni_A_OAZDtOTA,6314
3
- avartha/_conversation_resources.py,sha256=uLnnuCF_wtd09mNHtk0kwQKv2Oe8kGSEmrUWyNc47Gw,20832
3
+ avartha/_conversation_resources.py,sha256=DmMKUNBh6NPZWFGpm734tzE9HecCWsEFBa30CXTXbOM,13134
4
4
  avartha/_exceptions.py,sha256=_QY7SEw2nqyOz3pNxDWi40GW1xqB3EZxJ0t4iJ-YBpQ,1588
5
5
  avartha/_http_errors.py,sha256=gU8eapqYcI7Q3M8lAxwVH-qwz-TnQf2loBThFKG0ka8,406
6
6
  avartha/_models.py,sha256=KB4PeFvktbSOKbT-qMX5tjsExEg9cwufZ3ourx8GcVI,7331
@@ -11,10 +11,10 @@ avartha/_realtime_asr.py,sha256=KIonb9PUdkKnsP_nQKJOrcsLgjIh9jUnV_ngXFWsWzM,1819
11
11
  avartha/_realtime_openai.py,sha256=kqdn6K1IMQFUgu_RQXhQxmkWnq2BxVUbicF4_2vhp4o,23859
12
12
  avartha/_realtime_resources.py,sha256=rZj8hBYuLP9kTX56hhXTGE8R3crW_Wfmn46VY_dL3TM,7489
13
13
  avartha/_realtime_sampling.py,sha256=m4-aPCcvxCxA2l61cRCD2IRAbtKASMSadJVmHu8WkoU,3238
14
- avartha/_realtime_tts.py,sha256=jMK_uRUNY4yuWfaem_tdLp_J8ZgQEBNzvzVlzGDN-xY,38521
14
+ avartha/_realtime_tts.py,sha256=gxL0mPlTnQ_J9zPinfRDXA1T-XQbBJcUa-iKW-QwlSg,40869
15
15
  avartha/_sentinels.py,sha256=eX9pg8L8DMWOLrNLfJKWjcFHC5GZO0I_snULCum0ehg,433
16
16
  avartha/_speech_config.py,sha256=jL0M8Z6MGXzfgf3Lm7QMMEKDtVXCW5-jrqz5WEtu9g4,1781
17
- avartha/_speech_http.py,sha256=1oenxxk5Qvj6JqkVgl11sDeogTBp1af-Kxp2nAnKRJg,12917
17
+ avartha/_speech_http.py,sha256=86HO-ocVdGILaPRxLFi5LTzCqleFxwCWJHxMwePYLsE,12308
18
18
  avartha/_speech_resources.py,sha256=zBJHkQkllmWG_bmTIhwmjz9Nf7w4JMNkKJimnaIL3_4,18628
19
19
  avartha/_unsupported.py,sha256=qrbsxi2SL6co5M2uLs4fTVOoISuUm58khoayeL22gPE,637
20
20
  avartha/_websocket.py,sha256=9R05huh1n8spuyg1n_aIJN0Uk_kLG7BbNPD88wogIKY,855
@@ -35,10 +35,10 @@ avartha/resources/models.py,sha256=49IEOptS4QztywPbR0QQ0q-TvyHcKMpqklGNZ8HNXH0,6
35
35
  avartha/resources/realtime/__init__.py,sha256=-eWjNw1X8STRlDvKHgpMf4CvKiTbWgPJ6L_crxVpoTA,439
36
36
  avartha/resources/realtime/realtime.py,sha256=zqA5c_-9bzROwXFNyQe5s-tSCu8B6N-NqiimJlmpPqk,1061
37
37
  avartha/types/__init__.py,sha256=dTxUAbFhfjXbh9N2TtOSg0YeeWU5Lobf2gq1Nql9qZA,171
38
- avartha/types/conversational_ai.py,sha256=WxmUaCtaSfkeym6tNhG_JllKvT_-VnAvfy_kNcp6-QM,2919
38
+ avartha/types/conversational_ai.py,sha256=v67gXcim9jzeT2bSdgV_9bWAIXHH5F8safyMTBcj5-8,2035
39
39
  avartha/types/model.py,sha256=1bDAGerzYfk0I94TbDJriq3Z-l6W7OAqdVj2obq-ZZc,261
40
40
  avartha/types/speech_language.py,sha256=PT0uG-fwX2dGHwc1xcoM1VSMaNkOILS1AKcSXX8eyd0,388
41
- avartha/types/voice_agents.py,sha256=WokYiN38F-VuBkiyFHjblahz8reHwMuSY3zEhSzr6zs,5922
41
+ avartha/types/voice_agents.py,sha256=GLoJW1dt-fZ328QPsfRgf59oEFUoslmTcQqciJZHASk,5753
42
42
  avartha/types/websocket_connection_options.py,sha256=EuXoeyPKU4EMHqViOIzYZzvprE9FPPgGrUhHb_2Fcrg,816
43
43
  avartha/types/websocket_reconnection.py,sha256=sQ9X1jp31R2Ut5YdUHAtxMORw8KV-XKsPoczmGaNU6Q,580
44
44
  avartha/types/audio/__init__.py,sha256=dnsktnAoypmB2Zr3tll_k6q4M7_3scl-HdyJwfTsA0A,119
@@ -253,9 +253,9 @@ avartha/types/speech/voice_sharing_response.py,sha256=KRO9Rq80wTYV5of2bRrFTVJSDf
253
253
  avartha/types/speech/voice_sharing_response_category.py,sha256=0yFm6b7R4piHdNTdBWTQYKpNPxW8r2HJvnMdVpT7s5U,241
254
254
  avartha/types/speech/voice_sharing_state.py,sha256=8EBRbrRaDhUEap39kDXmt54AYeG172fnuBmRV1TSHP4,201
255
255
  avartha/types/speech/voice_verification_response.py,sha256=IL_Z1OTIiR9mvuaWzF9-GsL7pMRZ5fJeV_W3FfjLTTY,770
256
- avartha_python_sdk-0.0.7.dist-info/licenses/LICENSE,sha256=1ulXrK7aq7kBXX1scK6IiOgnbwqpmK6kWIB3CYznVG8,1135
257
- avartha_python_sdk-0.0.7.dist-info/licenses/THIRD_PARTY_NOTICES,sha256=R62M0553miTc6E42IVFLUIYkLFim_mf_Abq1lty0IaQ,13081
258
- avartha_python_sdk-0.0.7.dist-info/METADATA,sha256=wibaSI9cuzi40eo1iUSdw7okkhM6n0YFL-VKC-AMjY4,5970
259
- avartha_python_sdk-0.0.7.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
260
- avartha_python_sdk-0.0.7.dist-info/top_level.txt,sha256=cj2kDXl33tO2IPWPduSTukcwozPnYKOsVUVE9QZRUr0,8
261
- avartha_python_sdk-0.0.7.dist-info/RECORD,,
256
+ avartha_python_sdk-0.0.8.dist-info/licenses/LICENSE,sha256=1ulXrK7aq7kBXX1scK6IiOgnbwqpmK6kWIB3CYznVG8,1135
257
+ avartha_python_sdk-0.0.8.dist-info/licenses/THIRD_PARTY_NOTICES,sha256=R62M0553miTc6E42IVFLUIYkLFim_mf_Abq1lty0IaQ,13081
258
+ avartha_python_sdk-0.0.8.dist-info/METADATA,sha256=Aekvr_tp9UEQaiGEZ9woczskwXm9rw2_4Yqzddvdnf4,6138
259
+ avartha_python_sdk-0.0.8.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
260
+ avartha_python_sdk-0.0.8.dist-info/top_level.txt,sha256=cj2kDXl33tO2IPWPduSTukcwozPnYKOsVUVE9QZRUr0,8
261
+ avartha_python_sdk-0.0.8.dist-info/RECORD,,