diff --git a/.github/workflows/sdk-smoke.yml b/.github/workflows/sdk-smoke.yml index 8589e0e0d..8022dd5c4 100644 --- a/.github/workflows/sdk-smoke.yml +++ b/.github/workflows/sdk-smoke.yml @@ -62,6 +62,7 @@ jobs: tests/test_retell_transport_validation.py tests/test_call_originator_factory.py tests/test_retell_evidence.py + tests/test_retell_webcall_bridge.py tests/harness/test_process_runtime.py tests/harness/test_source_folder.py tests/test_harness_sandbox_server.py diff --git a/pyproject.toml b/pyproject.toml index 7b07cee5f..db1cd6cff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ evaluation = [] optimize = [] livekit = [ "aiohttp>=3.10", + "aiortc>=1.9,<2", "audioop-lts>=0.2.1; python_version >= '3.13'", "livekit-agents[cartesia,deepgram,openai,silero,google]>=1.8.3,<2", "livekit-plugins-elevenlabs>=1.8.3,<2", @@ -123,12 +124,14 @@ notebook = [ ] trinity = [ "aiohttp>=3.10", + "aiortc>=1.9,<2", "audioop-lts>=0.2.1; python_version >= '3.13'", "livekit-agents[cartesia,deepgram,openai,silero,google]>=1.8.3,<2", "livekit-plugins-elevenlabs>=1.8.3,<2", ] all = [ "aiohttp>=3.10", + "aiortc>=1.9,<2", "audioop-lts>=0.2.1; python_version >= '3.13'", "chromadb>=0.4.0", "livekit-agents[cartesia,deepgram,openai,silero,google]>=1.8.3,<2", diff --git a/src/fi/alk/harness/call_runner.py b/src/fi/alk/harness/call_runner.py index 5edcbc7b7..3f2645a2c 100644 --- a/src/fi/alk/harness/call_runner.py +++ b/src/fi/alk/harness/call_runner.py @@ -44,6 +44,7 @@ from typing import Any, Awaitable, Callable, Mapping, Protocol from fi import agentcc, simulate +from fi.simulate.agent.definition import retell_web_call_url as _retell_web_call_url from fi.simulate.runtime import ( SimulationSpec, new_run_id, @@ -598,14 +599,7 @@ def setting(name: str) -> str: target={ "provider": "retell", "agent_id": provider_target_id, - "api_url": str( - simulator_config.get("retell_api_url") - or "https://api.retellai.com/v2/create-web-call" - ), - "livekit_url": str( - simulator_config.get("retell_livekit_url") - or "wss://retell-ai-4ihahnq7.livekit.cloud" - ), + "api_url": _retell_web_call_url(simulator_config.get("retell_api_url")), "api_key_env": RETELL_API_KEY_ALIAS, }, transport={"kind": "retell_webcall"}, diff --git a/src/fi/simulate/agent/definition.py b/src/fi/simulate/agent/definition.py index f519ed533..9a79f9c83 100644 --- a/src/fi/simulate/agent/definition.py +++ b/src/fi/simulate/agent/definition.py @@ -1,7 +1,15 @@ import re from typing import Annotated, Literal, Optional +from urllib.parse import urlsplit, urlunsplit -from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, model_validator +from pydantic import ( + AnyHttpUrl, + AnyUrl, + BaseModel, + Field, + field_validator, + model_validator, +) # Always .fullmatch, never .match: "$" also matches immediately before a @@ -229,17 +237,35 @@ class VapiTargetConfig(BaseModel): ) +RETELL_WEB_CALL_URL = "https://api.retellai.com/v3/create-web-call" +_RETELL_LEGACY_WEB_CALL_PATH = "/v2/create-web-call" +_RETELL_WEB_CALL_PATH = "/v3/create-web-call" + + +def retell_web_call_url(configured: object) -> str: + """Return the v3 web-call URL for a configured value. + + Retell retired ``/v2/create-web-call``. Only that path is rewritten, so a + custom scheme and host (a proxy) are kept. An empty value gives the default. + """ + url = str(configured or "").strip() + if not url: + return RETELL_WEB_CALL_URL + parts = urlsplit(url) + path = parts.path.rstrip("/") + if path.endswith(_RETELL_LEGACY_WEB_CALL_PATH): + prefix = path[: -len(_RETELL_LEGACY_WEB_CALL_PATH)] + return urlunsplit(parts._replace(path=prefix + _RETELL_WEB_CALL_PATH)) + return url + + class RetellTargetConfig(BaseModel): """Non-secret configuration for a Retell agent under test.""" provider: Literal["retell"] = "retell" agent_id: str = Field(..., min_length=1) api_url: AnyHttpUrl = Field( - "https://api.retellai.com/v2/create-web-call", - validate_default=True, - ) - livekit_url: AnyUrl = Field( - "wss://retell-ai-4ihahnq7.livekit.cloud", + "https://api.retellai.com/v3/create-web-call", validate_default=True, ) api_key_env: str = Field( @@ -247,11 +273,10 @@ class RetellTargetConfig(BaseModel): pattern=r"^[A-Za-z_][A-Za-z0-9_]*$", ) - @model_validator(mode="after") - def _check_livekit_url(self) -> "RetellTargetConfig": - if self.livekit_url.scheme not in {"ws", "wss"}: - raise ValueError("retell_livekit_url_invalid: URL must use ws:// or wss://") - return self + @field_validator("api_url", mode="before") + @classmethod + def _upgrade_legacy_api_url(cls, value: object) -> object: + return retell_web_call_url(value) VoiceProviderTarget = Annotated[ diff --git a/src/fi/simulate/simulation/bridge/__init__.py b/src/fi/simulate/simulation/bridge/__init__.py index fb8cf08f7..9e00f3050 100644 --- a/src/fi/simulate/simulation/bridge/__init__.py +++ b/src/fi/simulate/simulation/bridge/__init__.py @@ -1,9 +1,23 @@ +from typing import TYPE_CHECKING, Any + from fi.simulate.simulation.bridge.livekit import LiveKitAudioBridge -from fi.simulate.simulation.bridge.retell import RetellWebCallConnector from fi.simulate.simulation.bridge.vapi import VapiWebSocketConnector +if TYPE_CHECKING: + from fi.simulate.simulation.bridge.retell import RetellWebCallConnector + __all__ = [ "LiveKitAudioBridge", "RetellWebCallConnector", "VapiWebSocketConnector", ] + + +def __getattr__(name: str) -> Any: + # The Retell connector needs aiortc, so it loads on first use: a Vapi, SIP + # or native LiveKit run never imports it. + if name == "RetellWebCallConnector": + from fi.simulate.simulation.bridge.retell import RetellWebCallConnector + + return RetellWebCallConnector + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/fi/simulate/simulation/bridge/connector.py b/src/fi/simulate/simulation/bridge/connector.py index f0c3761b6..27ca9a64a 100644 --- a/src/fi/simulate/simulation/bridge/connector.py +++ b/src/fi/simulate/simulation/bridge/connector.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from collections.abc import AsyncIterator -from dataclasses import dataclass +from dataclasses import dataclass, field class ProviderConnector(ABC): @@ -39,7 +39,7 @@ def call_id(self) -> str | None: @dataclass(frozen=True) class ConnectorConfig: - api_key: str + api_key: str = field(repr=False) assistant_id: str api_url: str livekit_url: str = "" diff --git a/src/fi/simulate/simulation/bridge/livekit.py b/src/fi/simulate/simulation/bridge/livekit.py index 6aed992a0..0c1204088 100644 --- a/src/fi/simulate/simulation/bridge/livekit.py +++ b/src/fi/simulate/simulation/bridge/livekit.py @@ -214,6 +214,9 @@ async def _pump() -> None: while True: chunk = await queue.get() if chunk is None: + # The pump has finished. Surface its error, such as a coded + # provider timeout, instead of treating it as a normal end. + await pump break pcm, sample_rate = chunk if sample_rate != ROOM_SAMPLE_RATE: diff --git a/src/fi/simulate/simulation/bridge/retell.py b/src/fi/simulate/simulation/bridge/retell.py index 174a7f01d..614b82526 100644 --- a/src/fi/simulate/simulation/bridge/retell.py +++ b/src/fi/simulate/simulation/bridge/retell.py @@ -1,12 +1,25 @@ from __future__ import annotations import asyncio +import fractions +import json import logging import os +import time from collections.abc import AsyncIterator +from typing import Any +from urllib.parse import urlsplit import aiohttp -from livekit import rtc +import av +from aiortc import ( + MediaStreamTrack, + RTCConfiguration, + RTCIceServer, + RTCPeerConnection, + RTCSessionDescription, +) +from aiortc.mediastreams import MediaStreamError from fi.simulate.agent.definition import RetellTargetConfig from fi.simulate.simulation.bridge.audio import PCMResampler @@ -14,18 +27,105 @@ logger = logging.getLogger(__name__) RETELL_SAMPLE_RATE = 48000 +RETELL_API_URL = "https://api.retellai.com/v3/create-web-call" +FRAME_SAMPLES = 960 +FRAME_BYTES = FRAME_SAMPLES * 2 +MAX_PENDING_BYTES = RETELL_SAMPLE_RATE * 2 * 10 +AGENT_TRACK_TIMEOUT_SECONDS = 30.0 +RECV_FRAME_TIMEOUT_SECONDS = 30.0 +REQUEST_TIMEOUT_SECONDS = 15.0 +CLOSING_STATES = {"ended", "replaced"} +_TIMEOUT_ERRORS = (asyncio.TimeoutError, TimeoutError) + + +class RetellWebCallError(RuntimeError): + def __init__(self, code: str, status: int | None = None) -> None: + super().__init__(code if status is None else f"{code}:{status}") + self.code = code + self.status = status + + +class _CallerAudioTrack(MediaStreamTrack): + kind = "audio" + + def __init__(self) -> None: + super().__init__() + self._pending = bytearray() + self._pts = 0 + self._started_at: float | None = None + + def push(self, data: bytes) -> None: + self._pending.extend(data) + overflow = len(self._pending) - MAX_PENDING_BYTES + if overflow > 0: + del self._pending[:overflow] + + async def recv(self) -> av.AudioFrame: + if self.readyState != "live": + raise MediaStreamError + if self._started_at is None: + # Audio queued while the connection was coming up would otherwise + # become a permanent delay on everything the caller says. + self._pending.clear() + self._started_at = time.monotonic() + delay = self._started_at + self._pts / RETELL_SAMPLE_RATE - time.monotonic() + if delay > 0: + await asyncio.sleep(delay) + chunk = bytes(self._pending[:FRAME_BYTES]) + del self._pending[:FRAME_BYTES] + frame = av.AudioFrame(format="s16", layout="mono", samples=FRAME_SAMPLES) + frame.planes[0].update(chunk.ljust(frame.planes[0].buffer_size, b"\x00")) + frame.sample_rate = RETELL_SAMPLE_RATE + frame.pts = self._pts + frame.time_base = fractions.Fraction(1, RETELL_SAMPLE_RATE) + self._pts += FRAME_SAMPLES + return frame + + +def _ice_servers(servers: Any) -> list[RTCIceServer]: + result = [] + for server in servers if isinstance(servers, list) else []: + if not isinstance(server, dict): + continue + urls = server.get("urls") or server.get("url") + if not urls: + continue + result.append( + RTCIceServer( + urls=urls, + username=server.get("username"), + credential=server.get("credential"), + ) + ) + return result + + +def _mono_pcm(frame: av.AudioFrame, resampler: av.AudioResampler) -> bytes: + pcm = bytearray() + for converted in resampler.resample(frame): + pcm.extend(bytes(converted.planes[0])[: converted.samples * 2]) + return bytes(pcm) + + +def _sessions_url(api_url: str, call_id: str) -> str: + parts = urlsplit(api_url) + return f"{parts.scheme}://{parts.netloc}/webrtc-proxy/{call_id}/v1/webrtc/sessions" class RetellWebCallConnector(ProviderConnector): def __init__(self, config: ConnectorConfig) -> None: self._config = config - self._room: rtc.Room | None = None - self._audio_source: rtc.AudioSource | None = None - self._track_future: asyncio.Future[rtc.RemoteAudioTrack] | None = None + self._http: aiohttp.ClientSession | None = None + self._pc: RTCPeerConnection | None = None + self._caller_track: _CallerAudioTrack | None = None + self._track_future: asyncio.Future[MediaStreamTrack] | None = None self._agent_disconnected = asyncio.Event() - self._agent_identity: str | None = None self._agent_ready = asyncio.Event() + self._pc_connected = asyncio.Event() self._call_id: str | None = None + self._access_token: str | None = None + self._session_url: str | None = None + self._ice: list[RTCIceServer] = [] self._connected = False self._resamplers: dict[int, PCMResampler] = {} @@ -39,7 +139,6 @@ def from_target(cls, target: RetellTargetConfig) -> "RetellWebCallConnector": api_key=api_key, assistant_id=target.agent_id, api_url=str(target.api_url), - livekit_url=str(target.livekit_url), ) ) @@ -60,87 +159,156 @@ def from_env(cls) -> "RetellWebCallConnector": return cls.from_target( RetellTargetConfig( agent_id=agent_id, - api_url=os.environ.get( - "RETELL_API_URL", - "https://api.retellai.com/v2/create-web-call", - ), - livekit_url=os.environ.get( - "RETELL_LIVEKIT_URL", - "wss://retell-ai-4ihahnq7.livekit.cloud", - ), + api_url=os.environ.get("RETELL_API_URL", RETELL_API_URL), ) ) async def connect(self) -> None: - async with aiohttp.ClientSession() as session: - async with session.post( + self._http = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_SECONDS) + ) + try: + await self._create_call() + await self._open_session() + if self._track_future is None: + raise RetellWebCallError("retell_webcall_track_future_missing") + loop = asyncio.get_running_loop() + deadline = loop.time() + AGENT_TRACK_TIMEOUT_SECONDS + try: + await asyncio.wait_for( + asyncio.shield(self._track_future), + timeout=AGENT_TRACK_TIMEOUT_SECONDS, + ) + except _TIMEOUT_ERRORS as exc: + raise RetellWebCallError("retell_webcall_agent_track_timeout") from exc + await self._wait_until_connected(max(0.0, deadline - loop.time())) + logger.info("retell_webcall_connected", extra={"call_id": self._call_id}) + except Exception: + await self.disconnect() + raise + + async def _wait_until_connected(self, timeout: float) -> None: + pc = self._pc + if pc is None: + raise RetellWebCallError("retell_webcall_connect_failed") + if pc.connectionState == "connected": + return + connected = asyncio.ensure_future(self._pc_connected.wait()) + dead = asyncio.ensure_future(self._agent_disconnected.wait()) + try: + done, _ = await asyncio.wait( + {connected, dead}, + timeout=timeout, + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + for task in (connected, dead): + task.cancel() + await asyncio.gather(connected, dead, return_exceptions=True) + if connected in done and pc.connectionState == "connected": + return + if not done: + raise RetellWebCallError("retell_webcall_connect_timeout") + raise RetellWebCallError("retell_webcall_connect_failed") + + async def _create_call(self) -> None: + assert self._http is not None + try: + async with self._http.post( self._config.api_url, headers={"Authorization": f"Bearer {self._config.api_key}"}, json={"agent_id": self._config.assistant_id}, ) as response: if response.status != 201: - raise RuntimeError( - f"retell_webcall_create_failed:{response.status}" + raise RetellWebCallError( + "retell_webcall_create_failed", response.status ) payload = await response.json() - access_token = ( - payload.get("access_token") if isinstance(payload, dict) else None - ) - call_id = payload.get("call_id") if isinstance(payload, dict) else None - if not isinstance(access_token, str) or not access_token.strip(): - raise ValueError("retell_webcall_response_missing_access_token") - if not isinstance(call_id, str) or not call_id.strip(): - raise ValueError("retell_webcall_response_missing_call_id") - self._call_id = call_id - self._room = rtc.Room() + except _TIMEOUT_ERRORS as exc: + raise RetellWebCallError("retell_webcall_create_timeout") from exc + if not isinstance(payload, dict): + raise RetellWebCallError("retell_webcall_response_invalid") + for field in ("call_id", "access_token"): + value = payload.get(field) + if not isinstance(value, str) or not value.strip(): + raise RetellWebCallError(f"retell_webcall_response_missing_{field}") + if payload.get("transport", "gateway") != "gateway": + raise RetellWebCallError("retell_webcall_transport_unsupported") + self._call_id = payload["call_id"] + self._access_token = payload["access_token"] + self._ice = _ice_servers(payload.get("ice_servers")) + + async def _open_session(self) -> None: + assert self._http is not None and self._call_id is not None self._track_future = asyncio.get_running_loop().create_future() + pc = RTCPeerConnection(RTCConfiguration(iceServers=self._ice)) + self._pc = pc + self._caller_track = _CallerAudioTrack() + pc.addTrack(self._caller_track) + control = pc.createDataChannel("control") - @self._room.on("track_subscribed") - def _on_track(track, _publication, participant) -> None: - if not isinstance(track, rtc.RemoteAudioTrack): + @control.on("message") + def _on_control(message: str | bytes) -> None: + self._handle_control(message) + + @pc.on("track") + def _on_track(track: MediaStreamTrack) -> None: + if track.kind != "audio": return if self._track_future is None or self._track_future.done(): return - identity = getattr(participant, "identity", None) - if identity is not None: - self._agent_identity = str(identity) self._track_future.set_result(track) self._agent_ready.set() - @self._room.on("participant_disconnected") - def _on_participant_disconnected(participant) -> None: - identity = getattr(participant, "identity", None) - if self._agent_identity is None or str(identity) == self._agent_identity: + @pc.on("connectionstatechange") + def _on_state() -> None: + if pc.connectionState == "connected": + self._pc_connected.set() + if pc.connectionState in {"failed", "closed"}: self._agent_disconnected.set() - @self._room.on("disconnected") - def _on_disconnected(*_args) -> None: - self._agent_disconnected.set() + await pc.setLocalDescription(await pc.createOffer()) + sessions_url = _sessions_url(self._config.api_url, self._call_id) + try: + async with self._http.post( + sessions_url, + headers=self._session_headers(), + json={"identity": "client", "sdp": pc.localDescription.sdp}, + ) as response: + if response.status not in (200, 201): + raise RetellWebCallError( + "retell_webcall_session_failed", response.status + ) + answer = await response.json() + except _TIMEOUT_ERRORS as exc: + raise RetellWebCallError("retell_webcall_session_timeout") from exc + session_id = answer.get("session_id") if isinstance(answer, dict) else None + sdp = answer.get("sdp") if isinstance(answer, dict) else None + if not isinstance(session_id, str) or not isinstance(sdp, str): + raise RetellWebCallError("retell_webcall_session_response_invalid") + self._session_url = f"{sessions_url}/{session_id}" + await pc.setRemoteDescription(RTCSessionDescription(sdp=sdp, type="answer")) + self._connected = True + def _handle_control(self, message: str | bytes) -> None: try: - await self._room.connect(self._config.livekit_url, access_token) - self._audio_source = rtc.AudioSource(RETELL_SAMPLE_RATE, 1) - track = rtc.LocalAudioTrack.create_audio_track( - "bridge-audio", self._audio_source - ) - await self._room.local_participant.publish_track( - track, - rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE), + event = json.loads(message) + except (TypeError, ValueError): + return + if not isinstance(event, dict) or event.get("type") != "status": + return + if event.get("state") in CLOSING_STATES: + logger.info( + "retell_webcall_status", + extra={"call_id": self._call_id, "state": event.get("state")}, ) - self._connected = True - if self._track_future is None: - raise RuntimeError("retell_webcall_track_future_missing") - await asyncio.wait_for(asyncio.shield(self._track_future), timeout=30.0) - logger.info("retell_webcall_connected", extra={"call_id": call_id}) - except asyncio.TimeoutError as exc: - await self.disconnect() - raise RuntimeError("retell_webcall_agent_track_timeout") from exc - except Exception: - await self.disconnect() - raise + self._agent_disconnected.set() + + def _session_headers(self) -> dict[str, str]: + return {"Authorization": f"Bearer {self._access_token}"} async def send_audio(self, data: bytes, sample_rate: int) -> None: - if not self._audio_source or not self._connected: + if self._caller_track is None or not self._connected: return if sample_rate != RETELL_SAMPLE_RATE: resampler = self._resamplers.setdefault( @@ -148,50 +316,76 @@ async def send_audio(self, data: bytes, sample_rate: int) -> None: PCMResampler(from_rate=sample_rate, to_rate=RETELL_SAMPLE_RATE), ) data = resampler.convert(data) - await self._audio_source.capture_frame( - rtc.AudioFrame( - data=data, - sample_rate=RETELL_SAMPLE_RATE, - num_channels=1, - samples_per_channel=len(data) // 2, - ) - ) + self._caller_track.push(data) async def recv_audio(self) -> AsyncIterator[tuple[bytes, int]]: if self._track_future is None: return try: - track = await asyncio.wait_for(self._track_future, timeout=30.0) + track = await asyncio.wait_for( + self._track_future, timeout=AGENT_TRACK_TIMEOUT_SECONDS + ) except asyncio.TimeoutError as exc: self._connected = False - raise RuntimeError("retell_webcall_agent_track_timeout") from exc - stream = rtc.AudioStream(track) - - async def close_on_disconnect(): - await self._agent_disconnected.wait() - await stream.aclose() - - disconnect = asyncio.create_task(close_on_disconnect()) + raise RetellWebCallError("retell_webcall_agent_track_timeout") from exc + resampler = av.AudioResampler(format="s16", layout="mono") + ended = asyncio.create_task(self._agent_disconnected.wait()) + frame_task: asyncio.Future[av.AudioFrame] | None = None try: - async for event in stream: - frame = event.frame - yield frame.data.tobytes(), frame.sample_rate + while not self._agent_disconnected.is_set(): + frame_task = asyncio.ensure_future(track.recv()) + done, _ = await asyncio.wait( + {frame_task, ended}, + timeout=RECV_FRAME_TIMEOUT_SECONDS, + return_when=asyncio.FIRST_COMPLETED, + ) + if frame_task not in done: + if ended in done: + break + raise RetellWebCallError("retell_webcall_agent_audio_timeout") + try: + frame = frame_task.result() + except MediaStreamError: + break + frame_task = None + pcm = _mono_pcm(frame, resampler) + if pcm: + yield pcm, frame.sample_rate finally: self._connected = False - disconnect.cancel() - await asyncio.gather(disconnect, return_exceptions=True) - await stream.aclose() + pending = [task for task in (frame_task, ended) if task is not None] + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) logger.info("retell_audio_stream_closed call_id=%s", self._call_id) async def disconnect(self) -> None: self._connected = False + self._agent_disconnected.set() try: - if self._room: - await self._room.disconnect() + if self._caller_track is not None: + self._caller_track.stop() + if self._pc is not None: + await self._pc.close() + finally: + self._pc = None + await self._end_session() + + async def _end_session(self) -> None: + http, self._http = self._http, None + session_url, self._session_url = self._session_url, None + if http is None: + return + try: + if session_url and self._access_token: + async with http.delete(session_url, headers=self._session_headers()): + pass + except (aiohttp.ClientError, asyncio.TimeoutError): + logger.warning( + "retell_webcall_session_end_failed", extra={"call_id": self._call_id} + ) finally: - if self._audio_source is not None: - await self._audio_source.aclose() - self._audio_source = None + await http.close() @property def is_connected(self) -> bool: diff --git a/src/fi/simulate/simulation/engines/livekit.py b/src/fi/simulate/simulation/engines/livekit.py index effd1e893..efa472671 100644 --- a/src/fi/simulate/simulation/engines/livekit.py +++ b/src/fi/simulate/simulation/engines/livekit.py @@ -3955,10 +3955,19 @@ def on_room_disconnected(*_args) -> None: else: monitor_failures[name] = exc for name, exc in monitor_failures.items(): + extra: dict[str, object] = { + "monitor": name, + "target_identity": target_identity, + } + if name == "provider_disconnected" and isinstance(exc, Exception): + # Only the sanitized code and status, never the message. + extra.update( + _safe_provider_error_details(exc, operation="provider_audio") + ) logger.warning( "conversation end monitor failed", exc_info=redacted_exc_info(exc), - extra={"monitor": name, "target_identity": target_identity}, + extra=extra, ) # A bridge task error is still a real provider-side disconnect. if "provider_disconnected" in monitor_failures: diff --git a/tests/test_retell_webcall_bridge.py b/tests/test_retell_webcall_bridge.py index 8eae2efe1..50fd0e78d 100644 --- a/tests/test_retell_webcall_bridge.py +++ b/tests/test_retell_webcall_bridge.py @@ -1,27 +1,46 @@ from __future__ import annotations import asyncio +import json +import subprocess +import sys from types import SimpleNamespace +import av import pytest +from aiortc.mediastreams import MediaStreamError -from fi.simulate.agent.definition import RetellTargetConfig +from fi.alk.harness.call_runner import _retell_web_call_url +from fi.simulate.agent.definition import RetellTargetConfig, retell_web_call_url from fi.simulate.simulation.bridge import livekit as bridge_livekit from fi.simulate.simulation.bridge import retell from fi.simulate.simulation.bridge.connector import ConnectorConfig -from fi.simulate.simulation.bridge.retell import RetellWebCallConnector - - -class _AudioSource: - def __init__(self): - self.closed = False - - async def aclose(self): - self.closed = True +from fi.simulate.simulation.bridge.retell import ( + FRAME_SAMPLES, + RetellWebCallConnector, + RetellWebCallError, +) +from fi.simulate.simulation.engines.livekit import _safe_provider_error_details + +API_URL = "https://api.retellai.com/v3/create-web-call" +SESSIONS_URL = ( + "https://api.retellai.com/webrtc-proxy/call_retell_123/v1/webrtc/sessions" +) +CALL = { + "call_id": "call_retell_123", + "access_token": "call-token", + "transport": "gateway", + "ice_servers": [ + {"urls": ["turn:turn.retell.example:3478"], "username": "u", "credential": "c"} + ], +} +ANSWER = {"session_id": "session_1", "sdp": "v=0\r\nanswer"} class _Response: - status = 201 + def __init__(self, status, body=None): + self.status = status + self._body = body async def __aenter__(self): return self @@ -30,36 +49,41 @@ async def __aexit__(self, *_args): return None async def json(self): - return {"access_token": "livekit-token", "call_id": "call_retell_123"} + return self._body -class _Session: - def __init__(self) -> None: - self.request: dict[str, object] = {} +class _TimedOut: + """A request that stalls until aiohttp's own timeout fires.""" async def __aenter__(self): - return self + raise retell.aiohttp.ServerTimeoutError("timed out") async def __aexit__(self, *_args): return None + +class _Http: + def __init__(self, responses): + self.responses = list(responses) + self.requests = [] + self.closed = False + self.session_kwargs = None + def post(self, url, *, headers, json): - self.request = {"url": url, "headers": headers, "json": json} - return _Response() + self.requests.append(("POST", url, headers, json)) + return self.responses.pop(0) + def delete(self, url, *, headers): + self.requests.append(("DELETE", url, headers, None)) + return _Response(204) -class _RemoteAudioTrack: - pass + async def close(self): + self.closed = True -class _Room: - def __init__(self) -> None: +class _Emitter: + def __init__(self): self.handlers = {} - self.local_participant = SimpleNamespace( - publish_track=self._publish_track, - ) - self.connection = None - self.disconnected = False def on(self, event): def decorator(callback): @@ -68,63 +92,478 @@ def decorator(callback): return decorator - async def connect(self, url, token): - self.connection = (url, token) - async def _publish_track(self, track, options): - self.published = (track, options) - self.handlers["track_subscribed"](_RemoteAudioTrack(), None, None) +class _Track: + kind = "audio" + + def __init__(self, frames=()): + self.frames = list(frames) + + async def recv(self): + if self.frames: + return self.frames.pop(0) + await asyncio.Event().wait() + + +class _PeerConnection(_Emitter): + def __init__(self, remote_track=None, auto_connect=True): + super().__init__() + self.calls = [] + self.auto_connect = auto_connect + self.tracks = [] + self.channel = _Emitter() + self.channel_label = None + self.localDescription = None + self.remote = None + self.connectionState = "new" + self.closed = False + self.remote_track = remote_track or _Track() + + def addTrack(self, track): + self.tracks.append(track) + + def createDataChannel(self, label): + self.calls.append("createDataChannel") + self.channel_label = label + return self.channel + + async def createOffer(self): + self.calls.append("createOffer") + return SimpleNamespace(sdp="v=0\r\noffer", type="offer") + + async def setLocalDescription(self, description): + self.calls.append("setLocalDescription") + self.localDescription = description + + def set_state(self, state): + self.connectionState = state + handler = self.handlers.get("connectionstatechange") + if handler is not None: + handler() + + async def setRemoteDescription(self, description): + self.remote = description + self.handlers["track"](self.remote_track) + if self.auto_connect: + self.set_state("connected") + + async def close(self): + self.closed = True + self.connectionState = "closed" + + +def _connector(): + return RetellWebCallConnector( + ConnectorConfig(api_key="api-key", assistant_id="agent_123", api_url=API_URL) + ) + + +def _install(monkeypatch, http, pc): + def make_session(**kwargs): + http.session_kwargs = kwargs + return http + + monkeypatch.setattr(retell.aiohttp, "ClientSession", make_session) + monkeypatch.setattr(retell, "RTCPeerConnection", lambda configuration: pc) - async def disconnect(self): - self.disconnected = True +def _ready(monkeypatch, remote_track=None): + http = _Http([_Response(201, CALL), _Response(201, ANSWER)]) + pc = _PeerConnection(remote_track=remote_track) + _install(monkeypatch, http, pc) + return http, pc, _connector() -def test_retell_webcall_connector_creates_and_joins_call(monkeypatch) -> None: - session = _Session() - room = _Room() - monkeypatch.setattr(retell.aiohttp, "ClientSession", lambda: session) - monkeypatch.setattr(retell.rtc, "Room", lambda: room) - monkeypatch.setattr(retell.rtc, "RemoteAudioTrack", _RemoteAudioTrack) - monkeypatch.setattr(retell.rtc, "AudioSource", lambda *_args: _AudioSource()) - monkeypatch.setattr( - retell.rtc.LocalAudioTrack, - "create_audio_track", - lambda *_args: "bridge-track", + +def _stereo_frame(sample_value: int) -> av.AudioFrame: + frame = av.AudioFrame(format="s16", layout="stereo", samples=FRAME_SAMPLES) + data = sample_value.to_bytes(2, "little", signed=True) * (FRAME_SAMPLES * 2) + frame.planes[0].update(data.ljust(frame.planes[0].buffer_size, b"\x00")) + frame.sample_rate = 48000 + return frame + + +async def _drain(connector): + async for _ in connector.recv_audio(): + pass + + +def test_connect_creates_a_v3_call_and_opens_a_gateway_session(monkeypatch) -> None: + http, pc, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + assert connector.is_connected and connector.is_agent_ready + await connector.disconnect() + + asyncio.run(run()) + + create, session, delete = http.requests + assert create == ( + "POST", + API_URL, + {"Authorization": "Bearer api-key"}, + {"agent_id": "agent_123"}, ) - monkeypatch.setattr( - retell.rtc, - "TrackPublishOptions", - lambda **kwargs: SimpleNamespace(**kwargs), + assert session == ( + "POST", + SESSIONS_URL, + {"Authorization": "Bearer call-token"}, + {"identity": "client", "sdp": "v=0\r\noffer"}, ) - connector = RetellWebCallConnector( - ConnectorConfig( - api_key="test-key", - assistant_id="agent_123", - api_url="https://api.retellai.com/v2/create-web-call", - livekit_url="wss://retell.example.com", - ) + assert delete == ( + "DELETE", + f"{SESSIONS_URL}/session_1", + {"Authorization": "Bearer call-token"}, + None, ) + assert pc.channel_label == "control" + assert pc.remote.sdp == ANSWER["sdp"] and pc.remote.type == "answer" + assert pc.closed and http.closed + assert connector.call_id == "call_retell_123" + + +def test_ice_servers_from_the_call_reach_the_peer_connection(monkeypatch) -> None: + http = _Http([_Response(201, CALL), _Response(201, ANSWER)]) + seen = {} + + def make_pc(configuration): + seen["servers"] = configuration.iceServers + return _PeerConnection() - async def run() -> None: + monkeypatch.setattr(retell.aiohttp, "ClientSession", lambda **_kwargs: http) + monkeypatch.setattr(retell, "RTCPeerConnection", make_pc) + connector = _connector() + + async def run(): await connector.connect() await connector.disconnect() asyncio.run(run()) - assert connector.call_id == "call_retell_123" - assert session.request["json"] == {"agent_id": "agent_123"} - assert room.connection == ("wss://retell.example.com", "livekit-token") - assert room.disconnected is True + (server,) = seen["servers"] + assert server.urls == ["turn:turn.retell.example:3478"] + assert (server.username, server.credential) == ("u", "c") + + +def test_refused_create_reports_its_http_status(monkeypatch) -> None: + http = _Http([_Response(422, {"message": "agent missing"})]) + _install(monkeypatch, http, _PeerConnection()) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(_connector().connect()) + + assert (exc.value.code, exc.value.status) == ("retell_webcall_create_failed", 422) + details = _safe_provider_error_details(exc.value, operation="web_bridge_start") + assert details["http_status"] == 422 + assert details["provider_code"] == "retell_webcall_create_failed" + assert http.closed + + +def test_refused_session_reports_its_http_status(monkeypatch) -> None: + http = _Http([_Response(201, CALL), _Response(401, {"message": "expired"})]) + pc = _PeerConnection() + _install(monkeypatch, http, pc) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(_connector().connect()) + + assert (exc.value.code, exc.value.status) == ("retell_webcall_session_failed", 401) + assert pc.closed and http.closed + + +@pytest.mark.parametrize( + ("body", "code"), + [ + ({**CALL, "transport": "livekit"}, "retell_webcall_transport_unsupported"), + ({**CALL, "access_token": ""}, "retell_webcall_response_missing_access_token"), + ( + {k: v for k, v in CALL.items() if k != "call_id"}, + "retell_webcall_response_missing_call_id", + ), + (["not", "an", "object"], "retell_webcall_response_invalid"), + ], +) +def test_unusable_create_response_is_refused(monkeypatch, body, code) -> None: + _install(monkeypatch, _Http([_Response(201, body)]), _PeerConnection()) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(_connector().connect()) + + assert exc.value.code == code + + +def test_missing_agent_track_times_out_and_ends_the_session(monkeypatch) -> None: + http, pc, connector = _ready(monkeypatch) + + async def no_track(description): + pc.remote = description + + pc.setRemoteDescription = no_track + monkeypatch.setattr(retell, "AGENT_TRACK_TIMEOUT_SECONDS", 0.05) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(connector.connect()) + + assert exc.value.code == "retell_webcall_agent_track_timeout" + assert pc.closed + assert http.requests[-1][:2] == ("DELETE", f"{SESSIONS_URL}/session_1") + + +def test_agent_audio_arrives_as_mono_pcm(monkeypatch) -> None: + track = _Track([_stereo_frame(1000), _stereo_frame(-1000)]) + _, pc, connector = _ready(monkeypatch, remote_track=track) + + async def run(): + await connector.connect() + chunks = [] + async for pcm, rate in connector.recv_audio(): + chunks.append((pcm, rate)) + if len(chunks) == 2: + pc.channel.handlers["message"]( + json.dumps({"type": "status", "state": "ended"}) + ) + await connector.disconnect() + return chunks + + chunks = asyncio.run(run()) + + assert [rate for _, rate in chunks] == [48000, 48000] + first, second = (pcm for pcm, _ in chunks) + assert len(first) == FRAME_SAMPLES * 2 + assert first[:2] == (1000).to_bytes(2, "little", signed=True) + assert second[:2] == (-1000).to_bytes(2, "little", signed=True) + + +@pytest.mark.parametrize("state", ["ended", "replaced"]) +def test_closing_status_on_control_ends_the_audio_stream(monkeypatch, state) -> None: + _, pc, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + receiver = asyncio.create_task(_drain(connector)) + await asyncio.sleep(0) + pc.channel.handlers["message"](json.dumps({"type": "status", "state": state})) + await asyncio.wait_for(receiver, timeout=1) + assert connector.is_connected is False + await connector.disconnect() + + asyncio.run(run()) + + +def test_connected_status_and_other_messages_keep_the_call_open(monkeypatch) -> None: + _, pc, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + for message in ( + '{"type":"status","state":"connected"}', + '{"type":"pong","ts":1}', + "not json", + b"\x00", + ): + pc.channel.handlers["message"](message) + assert connector._agent_disconnected.is_set() is False + await connector.disconnect() + + asyncio.run(run()) + + +def test_failed_peer_connection_ends_the_audio_stream(monkeypatch) -> None: + _, pc, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + receiver = asyncio.create_task(_drain(connector)) + await asyncio.sleep(0) + pc.connectionState = "failed" + pc.handlers["connectionstatechange"]() + await asyncio.wait_for(receiver, timeout=1) + await connector.disconnect() + + asyncio.run(run()) + + +def test_silent_agent_track_raises_a_coded_audio_timeout(monkeypatch) -> None: + _, _, connector = _ready(monkeypatch) + monkeypatch.setattr(retell, "RECV_FRAME_TIMEOUT_SECONDS", 0.05) + + async def run(): + await connector.connect() + try: + await asyncio.wait_for(_drain(connector), timeout=1) + finally: + await connector.disconnect() + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(run()) + + assert exc.value.code == "retell_webcall_agent_audio_timeout" + details = _safe_provider_error_details(exc.value, operation="provider_audio") + assert details["provider_code"] == "retell_webcall_agent_audio_timeout" + + +def test_bridge_fails_the_call_when_the_provider_audio_stream_errors() -> None: + class _FailingConnector: + async def recv_audio(self): + raise RetellWebCallError("retell_webcall_agent_audio_timeout") + yield b"", 0 # pragma: no cover + + bridge = object.__new__(bridge_livekit.LiveKitAudioBridge) + + async def _no_playout() -> None: + return None + + bridge._audio_source = SimpleNamespace(wait_for_playout=_no_playout) + bridge._connector = _FailingConnector() + bridge._last_audio_at = bridge._last_provider_audio_at = 0.0 + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(bridge._provider_to_room()) + + assert exc.value.code == "retell_webcall_agent_audio_timeout" + + +def test_caller_audio_is_sent_in_20ms_frames_then_silence() -> None: + sample = (7).to_bytes(2, "little", signed=True) + + async def run(): + track = retell._CallerAudioTrack() + await track.recv() + track.push(sample * (FRAME_SAMPLES + 10)) + frames = [await track.recv() for _ in range(3)] + track.stop() + return frames + + first, second, third = asyncio.run(run()) + + for frame in (first, second, third): + assert (frame.samples, frame.sample_rate) == (FRAME_SAMPLES, 48000) + assert [first.pts, second.pts, third.pts] == [ + FRAME_SAMPLES, + 2 * FRAME_SAMPLES, + 3 * FRAME_SAMPLES, + ] + assert bytes(first.planes[0])[: FRAME_SAMPLES * 2] == sample * FRAME_SAMPLES + second_pcm = bytes(second.planes[0])[: FRAME_SAMPLES * 2] + assert second_pcm[:20] == sample * 10 + assert set(second_pcm[20:]) == {0} + assert set(bytes(third.planes[0])[: FRAME_SAMPLES * 2]) == {0} + + +def test_caller_audio_buffer_is_bounded() -> None: + track = retell._CallerAudioTrack() + track.push(b"\x01\x00" * retell.MAX_PENDING_BYTES) + assert len(track._pending) == retell.MAX_PENDING_BYTES + + +def test_caller_audio_overflow_drops_the_oldest_bytes() -> None: + track = retell._CallerAudioTrack() + track.push(b"\x01" * retell.MAX_PENDING_BYTES) + track.push(b"\x02" * 1000) + + assert len(track._pending) == retell.MAX_PENDING_BYTES + assert bytes(track._pending[-1000:]) == b"\x02" * 1000 + assert track._pending[0] == 1 + assert track._pending.count(1) == retell.MAX_PENDING_BYTES - 1000 -def test_retell_webcall_connector_uses_explicit_target(monkeypatch) -> None: +def test_audio_queued_before_the_first_recv_is_dropped() -> None: + sample = (9).to_bytes(2, "little", signed=True) + + async def run(): + track = retell._CallerAudioTrack() + track.push(sample * FRAME_SAMPLES * 5) + first = await track.recv() + pending_after_first = len(track._pending) + track.push(sample * FRAME_SAMPLES) + second = await track.recv() + return first, pending_after_first, second + + first, pending_after_first, second = asyncio.run(run()) + + assert set(bytes(first.planes[0])[: FRAME_SAMPLES * 2]) == {0} + assert pending_after_first == 0 + assert bytes(second.planes[0])[: FRAME_SAMPLES * 2] == sample * FRAME_SAMPLES + + +def test_caller_frames_are_paced_in_real_time(monkeypatch) -> None: + sleeps = [] + real_sleep = asyncio.sleep + + async def recording_sleep(delay, *args, **kwargs): + sleeps.append(delay) + await real_sleep(0) + + monkeypatch.setattr(asyncio, "sleep", recording_sleep) + + async def run(): + track = retell._CallerAudioTrack() + await track.recv() + await track.recv() + track.stop() + + asyncio.run(run()) + + assert len(sleeps) == 1 + # Any wait up to one frame proves the sender paces; the exact amount depends + # on how long the runner took between the two reads. + assert 0 < sleeps[0] <= FRAME_SAMPLES / retell.RETELL_SAMPLE_RATE + + +def test_send_audio_resamples_to_48k(monkeypatch) -> None: + _, _, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + await connector.send_audio(b"\x00\x00" * 160, 16000) + pending = len(connector._caller_track._pending) + await connector.disconnect() + return pending + + assert asyncio.run(run()) == pytest.approx(480 * 2, abs=8) + + +def test_disconnect_closes_http_even_when_session_end_fails(monkeypatch) -> None: + http, pc, connector = _ready(monkeypatch) + + def failing_delete(url, *, headers): + raise retell.aiohttp.ClientConnectionError("gone") + + http.delete = failing_delete + + async def run(): + await connector.connect() + await connector.disconnect() + + asyncio.run(run()) + + assert pc.closed and http.closed + + +def test_cancelled_audio_receiver_leaves_no_task_behind(monkeypatch) -> None: + _, _, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + receiver = asyncio.create_task(_drain(connector)) + await asyncio.sleep(0.01) + receiver.cancel() + await asyncio.gather(receiver, return_exceptions=True) + await connector.disconnect() + await asyncio.sleep(0) + current = asyncio.current_task() + return [t for t in asyncio.all_tasks() if t is not current and not t.done()] + + assert asyncio.run(run()) == [] + + +def test_connector_uses_explicit_target(monkeypatch) -> None: monkeypatch.setenv("HEALTHCARE_RETELL_KEY", "test-key") connector = RetellWebCallConnector.from_target( RetellTargetConfig( agent_id="agent_healthcare", - api_url="https://retell.healthcare.example/v2/create-web-call", - livekit_url="wss://retell-healthcare.example.com", + api_url="https://retell.healthcare.example/v3/create-web-call", api_key_env="HEALTHCARE_RETELL_KEY", ) ) @@ -133,11 +572,28 @@ def test_retell_webcall_connector_uses_explicit_target(monkeypatch) -> None: assert connector._config.api_key == "test-key" assert ( connector._config.api_url - == "https://retell.healthcare.example/v2/create-web-call" + == "https://retell.healthcare.example/v3/create-web-call" ) -def test_retell_webcall_connector_requires_credentials(monkeypatch) -> None: +def test_target_defaults_to_v3_and_ignores_the_old_livekit_url() -> None: + target = RetellTargetConfig( + agent_id="agent_1", livekit_url="wss://retell-ai-4ihahnq7.livekit.cloud" + ) + assert str(target.api_url) == API_URL + assert "livekit_url" not in target.model_dump() + + +def test_sessions_url_follows_the_configured_api_host() -> None: + assert ( + retell._sessions_url( + "https://retell.proxy.example/v3/create-web-call", "call_9" + ) + == "https://retell.proxy.example/webrtc-proxy/call_9/v1/webrtc/sessions" + ) + + +def test_connector_requires_credentials(monkeypatch) -> None: monkeypatch.delenv("RETELL_API_KEY", raising=False) monkeypatch.delenv("RETELL_AGENT_ID", raising=False) @@ -145,141 +601,275 @@ def test_retell_webcall_connector_requires_credentials(monkeypatch) -> None: RetellWebCallConnector.from_env() -def test_unrelated_participant_disconnect_does_not_end_agent_audio(monkeypatch) -> None: - session = _Session() - room = _Room() +def test_from_env_defaults_to_v3(monkeypatch) -> None: + monkeypatch.setenv("RETELL_API_KEY", "key") + monkeypatch.setenv("RETELL_AGENT_ID", "agent_1") + monkeypatch.delenv("RETELL_API_URL", raising=False) + + assert RetellWebCallConnector.from_env()._config.api_url == API_URL + + +@pytest.mark.parametrize( + ("configured", "expected"), + [ + (None, API_URL), + ("", API_URL), + ("https://api.retellai.com/v2/create-web-call", API_URL), + ("https://api.retellai.com/v2/create-web-call/", API_URL), + ( + "https://retell.proxy.example/v3/create-web-call", + "https://retell.proxy.example/v3/create-web-call", + ), + ( + "https://proxy.example/v2/create-web-call", + "https://proxy.example/v3/create-web-call", + ), + ( + "http://localhost:8080/retell/v2/create-web-call/", + "http://localhost:8080/retell/v3/create-web-call", + ), + ], +) +def test_retired_v2_url_is_upgraded_and_keeps_its_host(configured, expected) -> None: + assert retell_web_call_url(configured) == expected + assert _retell_web_call_url(configured) == expected + + +def test_target_config_upgrades_a_v2_api_url() -> None: + target = RetellTargetConfig( + agent_id="agent_1", api_url="https://proxy.example/v2/create-web-call" + ) + assert str(target.api_url) == "https://proxy.example/v3/create-web-call" - async def publish(track, options): - room.published = (track, options) - room.handlers["track_subscribed"]( - _RemoteAudioTrack(), - None, - SimpleNamespace(identity="retell-agent"), - ) - room.local_participant.publish_track = publish - monkeypatch.setattr(retell.aiohttp, "ClientSession", lambda: session) - monkeypatch.setattr(retell.rtc, "Room", lambda: room) - monkeypatch.setattr(retell.rtc, "RemoteAudioTrack", _RemoteAudioTrack) - monkeypatch.setattr(retell.rtc, "AudioSource", lambda *_args: _AudioSource()) - monkeypatch.setattr( - retell.rtc.LocalAudioTrack, - "create_audio_track", - lambda *_args: "bridge-track", - ) - monkeypatch.setattr( - retell.rtc, - "TrackPublishOptions", - lambda **kwargs: SimpleNamespace(**kwargs), - ) - connector = RetellWebCallConnector( - ConnectorConfig( - api_key="test-key", - assistant_id="agent_123", - api_url="https://api.retellai.com/v2/create-web-call", - livekit_url="wss://retell.example.com", - ) - ) +def test_from_env_with_a_v2_url_posts_to_v3(monkeypatch) -> None: + monkeypatch.setenv("RETELL_API_KEY", "key") + monkeypatch.setenv("RETELL_AGENT_ID", "agent_1") + monkeypatch.setenv("RETELL_API_URL", "https://proxy.example/v2/create-web-call") + http = _Http([_Response(201, CALL), _Response(201, ANSWER)]) + _install(monkeypatch, http, _PeerConnection()) + connector = RetellWebCallConnector.from_env() + + async def run(): + await connector.connect() + await connector.disconnect() - asyncio.run(connector.connect()) - room.handlers["participant_disconnected"]( - SimpleNamespace(identity="unrelated-participant") + asyncio.run(run()) + + assert http.requests[0][1] == "https://proxy.example/v3/create-web-call" + assert http.requests[1][1] == ( + "https://proxy.example/webrtc-proxy/call_retell_123/v1/webrtc/sessions" ) - assert connector._agent_disconnected.is_set() is False - room.handlers["participant_disconnected"](SimpleNamespace(identity="retell-agent")) - assert connector._agent_disconnected.is_set() is True - asyncio.run(connector.disconnect()) -@pytest.mark.parametrize("side", ["provider", "room"]) -def test_cancelled_audio_receiver_stops_its_native_stream(monkeypatch, side): - stream_started = asyncio.Event() - stopped = asyncio.Event() - streams = [] +def test_stalled_create_post_reports_a_create_timeout(monkeypatch) -> None: + http = _Http([_TimedOut()]) + _install(monkeypatch, http, _PeerConnection()) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(_connector().connect()) + + assert exc.value.code == "retell_webcall_create_timeout" + assert http.closed - class Stream: - def __init__(self, track): - self.producer = asyncio.create_task(stopped.wait()) - streams.append(self) - def __aiter__(self): - return self +def test_stalled_session_post_reports_a_session_timeout(monkeypatch) -> None: + http = _Http([_Response(201, CALL), _TimedOut()]) + pc = _PeerConnection() + _install(monkeypatch, http, pc) - async def __anext__(self): - stream_started.set() - await asyncio.Event().wait() + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(_connector().connect()) - async def aclose(self): - stopped.set() - await self.producer + assert exc.value.code == "retell_webcall_session_timeout" + assert pc.closed and http.closed - monkeypatch.setattr(retell.rtc, "AudioStream", Stream) - monkeypatch.setattr(retell.rtc, "Room", _Room) + +def test_connect_waits_for_the_peer_connection_to_come_up(monkeypatch) -> None: + _, pc, connector = _ready(monkeypatch) + pc.auto_connect = False async def run(): - connector = RetellWebCallConnector( - ConnectorConfig(api_key="key", assistant_id="agent", api_url="https://retell.example") - ) - connector._connected = True - connector._track_future = asyncio.get_running_loop().create_future() - connector._track_future.set_result(object()) - if side == "provider": - async def receive(): - async for _ in connector.recv_audio(): - pass - task = asyncio.create_task(receive()) - else: - bridge = bridge_livekit.LiveKitAudioBridge( - url="ws://localhost", api_key="key", api_secret="secret", - room_name="room", identity="bridge", connector=connector, - ) - bridge._track_future = connector._track_future - task = asyncio.create_task(bridge._room_to_provider()) - await stream_started.wait() - task.cancel() - await asyncio.gather(task, return_exceptions=True) - try: - assert streams[0].producer.done(), "Native audio subscription outlived its receiver" - finally: - await streams[0].aclose() + task = asyncio.create_task(connector.connect()) + await asyncio.sleep(0.05) + assert not task.done() + pc.set_state("connected") + await asyncio.wait_for(task, timeout=1) + assert connector.is_connected + await connector.disconnect() asyncio.run(run()) -@pytest.mark.parametrize("side", ["provider", "room"]) -def test_disconnect_closes_audio_source_even_when_remote_close_fails(monkeypatch, side): - monkeypatch.setattr(retell.rtc, "Room", _Room) +def test_peer_connection_failing_during_connect_raises_a_coded_error( + monkeypatch, +) -> None: + http, pc, connector = _ready(monkeypatch) + pc.auto_connect = False + + async def run(): + task = asyncio.create_task(connector.connect()) + await asyncio.sleep(0.05) + pc.set_state("failed") + await asyncio.wait_for(task, timeout=1) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(run()) + + assert exc.value.code == "retell_webcall_connect_failed" + assert pc.closed and http.closed + - async def failing_disconnect(): - raise ConnectionError("Remote connection is already gone") +def test_peer_connection_closing_during_connect_raises_a_coded_error( + monkeypatch, +) -> None: + _, pc, connector = _ready(monkeypatch) + pc.auto_connect = False async def run(): - source = _AudioSource() + task = asyncio.create_task(connector.connect()) + await asyncio.sleep(0.05) + pc.set_state("closed") + await asyncio.wait_for(task, timeout=1) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(run()) + + assert exc.value.code == "retell_webcall_connect_failed" + + +def test_peer_connection_that_never_connects_times_out(monkeypatch) -> None: + http, pc, connector = _ready(monkeypatch) + pc.auto_connect = False + monkeypatch.setattr(retell, "AGENT_TRACK_TIMEOUT_SECONDS", 0.05) + + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(connector.connect()) + + assert exc.value.code == "retell_webcall_connect_timeout" + assert pc.closed and http.closed + + +def test_control_channel_is_created_before_the_offer(monkeypatch) -> None: + _, pc, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + await connector.disconnect() + + asyncio.run(run()) + + assert pc.calls.index("createDataChannel") < pc.calls.index("createOffer") + assert pc.calls.index("createDataChannel") < pc.calls.index("setLocalDescription") + + +def test_http_session_has_a_request_timeout(monkeypatch) -> None: + http, _, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + await connector.disconnect() + + asyncio.run(run()) + + timeout = http.session_kwargs["timeout"] + assert isinstance(timeout, retell.aiohttp.ClientTimeout) + assert timeout.total == retell.REQUEST_TIMEOUT_SECONDS + + +def test_closed_peer_connection_ends_the_audio_stream(monkeypatch) -> None: + _, pc, connector = _ready(monkeypatch) + + async def run(): + await connector.connect() + receiver = asyncio.create_task(_drain(connector)) + await asyncio.sleep(0) + pc.set_state("closed") + await asyncio.wait_for(receiver, timeout=1) + assert connector.is_connected is False + await connector.disconnect() + + asyncio.run(run()) + + +def test_media_stream_error_ends_the_audio_stream_cleanly(monkeypatch) -> None: + class _EndedTrack(_Track): + async def recv(self): + raise MediaStreamError + + _, _, connector = _ready(monkeypatch, remote_track=_EndedTrack()) + + async def run(): + await connector.connect() + await asyncio.wait_for(_drain(connector), timeout=1) + assert connector.is_connected is False + await connector.disconnect() + + asyncio.run(run()) + + +def test_errors_and_error_details_carry_no_credentials(monkeypatch) -> None: + api_key = "sk-secret-api-key-4471" + token = "tok-secret-access-9083" + call = {**CALL, "access_token": token} + scenarios = [ + [_Response(422, {"message": "agent missing"})], + [_Response(201, call), _Response(401, {"message": "expired"})], + [_Response(201, call), _TimedOut()], + ] + for responses in scenarios: + _install(monkeypatch, _Http(responses), _PeerConnection()) connector = RetellWebCallConnector( - ConnectorConfig(api_key="key", assistant_id="agent", api_url="https://retell.example") + ConnectorConfig(api_key=api_key, assistant_id="agent_1", api_url=API_URL) ) - if side == "provider": - connector._room = _Room() - connector._room.disconnect = failing_disconnect - connector._audio_source = source - close = connector.disconnect - else: - connector.disconnect = failing_disconnect - bridge = bridge_livekit.LiveKitAudioBridge( - url="ws://localhost", api_key="key", api_secret="secret", - room_name="room", identity="bridge", connector=connector, - ) - bridge._audio_source = source - close = bridge.aclose - with pytest.raises(ConnectionError): - await close() - assert source.closed, "Owned native audio source survived a failed disconnect" - asyncio.run(run()) + with pytest.raises(RetellWebCallError) as exc: + asyncio.run(connector.connect()) + + details = _safe_provider_error_details(exc.value, operation="web_bridge_start") + for text in (str(exc.value), repr(exc.value), json.dumps(details)): + assert api_key not in text and token not in text + + +def test_connector_config_repr_hides_the_api_key() -> None: + config = ConnectorConfig( + api_key="sk-secret-api-key-4471", assistant_id="agent_1", api_url=API_URL + ) + assert "sk-secret-api-key-4471" not in repr(config) + assert "agent_1" in repr(config) + + +def test_importing_the_bridge_package_does_not_load_aiortc() -> None: + code = ( + "import sys, importlib.abc\n" + "class Block(importlib.abc.MetaPathFinder):\n" + " def find_spec(self, name, path=None, target=None):\n" + " if name == 'aiortc' or name.startswith('aiortc.'):\n" + " raise ImportError('blocked ' + name)\n" + "sys.meta_path.insert(0, Block())\n" + "import fi.simulate.simulation.bridge\n" + "assert 'aiortc' not in sys.modules\n" + "assert 'fi.simulate.simulation.bridge.retell' not in sys.modules\n" + ) + result = subprocess.run( + [sys.executable, "-c", code], + check=False, + capture_output=True, + text=True, + timeout=120, + ) + assert result.returncode == 0, result.stderr + + +def test_bridge_package_loads_the_retell_connector_on_first_use() -> None: + import fi.simulate.simulation.bridge as bridge_package + assert bridge_package.RetellWebCallConnector is RetellWebCallConnector + with pytest.raises(AttributeError): + bridge_package.__getattr__("NoSuchConnector") -def test_cancelling_bridge_run_reaps_all_media_tasks(monkeypatch): - monkeypatch.setattr(retell.rtc, "Room", _Room) + +def test_cancelling_bridge_run_reaps_all_media_tasks(): tasks = [] ready = asyncio.Event() @@ -290,19 +880,27 @@ async def media(): await asyncio.Event().wait() async def run(): - connector = RetellWebCallConnector( - ConnectorConfig(api_key="key", assistant_id="agent", api_url="https://retell.example") - ) bridge = bridge_livekit.LiveKitAudioBridge( - url="ws://localhost", api_key="key", api_secret="secret", - room_name="room", identity="bridge", connector=connector, + url="ws://localhost", + api_key="key", + api_secret="secret", + room_name="room", + identity="bridge", + connector=_connector(), ) - for name in ("_room_to_provider", "_provider_to_room", "_watchdog", "_wait_for_room_disconnect"): + for name in ( + "_room_to_provider", + "_provider_to_room", + "_watchdog", + "_wait_for_room_disconnect", + ): setattr(bridge, name, media) task = asyncio.create_task(bridge.run()) await ready.wait() task.cancel() await asyncio.gather(task, return_exceptions=True) - assert all(t.done() for t in tasks), "Bridge returned while native media tasks were alive" + assert all(t.done() for t in tasks), ( + "Bridge returned while media tasks were alive" + ) asyncio.run(run()) diff --git a/uv.lock b/uv.lock index 6578ffc64..2c63496e8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -8,9 +8,12 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -91,6 +94,8 @@ a2a = [ ] all = [ { name = "aiohttp" }, + { name = "aiortc", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aiortc", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "chromadb" }, { name = "livekit-agents", extra = ["cartesia", "deepgram", "google", "openai", "silero"] }, @@ -122,6 +127,8 @@ langchain = [ ] livekit = [ { name = "aiohttp" }, + { name = "aiortc", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aiortc", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "livekit-agents", extra = ["cartesia", "deepgram", "google", "openai", "silero"] }, { name = "livekit-plugins-elevenlabs" }, @@ -143,6 +150,8 @@ pipecat = [ ] trinity = [ { name = "aiohttp" }, + { name = "aiortc", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aiortc", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "livekit-agents", extra = ["cartesia", "deepgram", "google", "openai", "silero"] }, { name = "livekit-plugins-elevenlabs" }, @@ -165,6 +174,9 @@ requires-dist = [ { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.10" }, { name = "aiohttp", marker = "extra == 'livekit'", specifier = ">=3.10" }, { name = "aiohttp", marker = "extra == 'trinity'", specifier = ">=3.10" }, + { name = "aiortc", marker = "extra == 'all'", specifier = ">=1.9,<2" }, + { name = "aiortc", marker = "extra == 'livekit'", specifier = ">=1.9,<2" }, + { name = "aiortc", marker = "extra == 'trinity'", specifier = ">=1.9,<2" }, { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'all'", specifier = ">=0.2.1" }, { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'livekit'", specifier = ">=0.2.1" }, { name = "audioop-lts", marker = "python_full_version >= '3.13' and extra == 'trinity'", specifier = ">=0.2.1" }, @@ -389,20 +401,117 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, ] +[[package]] +name = "aioice" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "dnspython", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ifaddr", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/04/df7286233f468e19e9bedff023b6b246182f0b2ccb04ceeb69b2994021c6/aioice-0.10.2.tar.gz", hash = "sha256:bf236c6829ee33c8e540535d31cd5a066b531cb56de2be94c46be76d68b1a806", size = 44307, upload-time = "2025-11-28T15:56:48.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e3/0d23b1f930c17d371ce1ec36ee529f22fd19ebc2a07fe3418e3d1d884ce2/aioice-0.10.2-py3-none-any.whl", hash = "sha256:14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", size = 24875, upload-time = "2025-11-28T15:56:47.847Z" }, +] + +[[package]] +name = "aioice" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "dnspython", version = "2.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ifaddr", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/16/fd4e7d1ccab77b6ae655bf6e796161586eeab18f5047c584bbedea846d66/aioice-0.13.0.tar.gz", hash = "sha256:02c2dee812f1a68602689a444bd5125a144b6ee43bca74229a1e0f7842d1897c", size = 68714, upload-time = "2026-11-05T17:01:35.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/06/64a79f0b7d730fc10b35fe31a396816030c0ab635be221c6c8587b00fc7f/aioice-0.13.0-py3-none-any.whl", hash = "sha256:15937b43f5f7a3587148049f1c93ff95fe4add1b7e0f5931eeec6408bc188d06", size = 29732, upload-time = "2026-11-05T17:01:34.897Z" }, +] + [[package]] name = "aiologic" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f6/27/206615942005471499f6fbc36621582e24d0686f33c74b2d018fcfd4fe67/aiologic-0.16.0-py3-none-any.whl", hash = "sha256:e00ce5f68c5607c864d26aec99c0a33a83bdf8237aa7312ffbb96805af67d8b6", size = 135193, upload-time = "2025-11-27T23:48:40.099Z" }, ] +[[package]] +name = "aiortc" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "aioice", version = "0.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "av", marker = "python_full_version < '3.11'" }, + { name = "cryptography", marker = "python_full_version < '3.11'" }, + { name = "google-crc32c", marker = "python_full_version < '3.11'" }, + { name = "pyee", version = "13.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pylibsrtp", marker = "python_full_version < '3.11'" }, + { name = "pyopenssl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/42/af1e5755f4cdeb5926ef4aee4bd99d2fbc04b497dfe09f036d359219993e/aiortc-1.15.0.tar.gz", hash = "sha256:ee6c0757ca070cf6d6bee441936d6ede24eef51211bbff6653409c540f72e625", size = 1182039, upload-time = "2026-07-13T19:26:43.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/5f/8435ba02c9278b6cec6f168db92e1d3280dd3af8f2225e20dc7c3be5ab22/aiortc-1.15.0-py3-none-any.whl", hash = "sha256:4e1e54bff31a9c2cb654c7b7edc068085a7df53365e5df24a5cb24168e3f95f7", size = 93678, upload-time = "2026-07-13T19:26:42.241Z" }, +] + +[[package]] +name = "aiortc" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "aioice", version = "0.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "av", marker = "python_full_version >= '3.11'" }, + { name = "cryptography", marker = "python_full_version >= '3.11'" }, + { name = "google-crc32c", marker = "python_full_version >= '3.11'" }, + { name = "pyee", version = "13.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pyee", version = "16.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pylibsrtp", marker = "python_full_version >= '3.11'" }, + { name = "pyopenssl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/19/4deceaea278a70c57c9b045ecc3a5a85ce37d3e6679195b1d2161694d577/aiortc-1.16.0.tar.gz", hash = "sha256:892a1d22ec500a41c6a2377a372db575929d9e9f614e9a2276b145cf40924798", size = 1196535, upload-time = "2026-11-06T09:10:23.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/1c/3d2560721f57d6adfdfcde2f24504a6b77b9bcc7e8ea6946c80c26bd8806/aiortc-1.16.0-py3-none-any.whl", hash = "sha256:084844ba0542fc9f9a41d135e2c718c2122746e2d70e62974435de9b98ec92b2", size = 97125, upload-time = "2026-11-06T09:10:21.718Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -986,7 +1095,7 @@ name = "coloredlogs" version = "15.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "humanfriendly" }, + { name = "humanfriendly", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } wheels = [ @@ -1079,7 +1188,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" }, @@ -1114,34 +1223,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime" }, + { name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft" }, + { name = "nvidia-cufft", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] cufile = [ - { name = "nvidia-cufile" }, + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti" }, + { name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand" }, + { name = "nvidia-curand", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver" }, + { name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx" }, + { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] [[package]] @@ -1149,8 +1258,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -1204,6 +1313,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "dnspython" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/49/900383b471c645a3c397f47b3211d1fcecb3a540c98f683494cdf86b33de/dnspython-2.9.2.tar.gz", hash = "sha256:8d4d39a513e649d634004ce4d39ade2352efcea472a0247535a5c2d18a9d7ba9", size = 455484, upload-time = "2026-11-03T23:56:53.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/0c/5346358b3a20275d256978c35203ddc2c8617f78c1b23795b79ba22ced6d/dnspython-2.9.2-py3-none-any.whl", hash = "sha256:af15f7d385a6ae2fb35a796fde2138004c8c42c7389090e2addc83e3b3340fd8", size = 360455, upload-time = "2026-11-03T23:56:52.088Z" }, +] + [[package]] name = "docstring-parser" version = "0.18.0" @@ -1236,7 +1380,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1627,6 +1771,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/44/675358f0b939f14ae6481dddecbde5fd91b92b51f7991a70d98d3c68c02e/google_cloud_texttospeech-2.37.0-py3-none-any.whl", hash = "sha256:911f42f327027975d7781efcace1993afdf311b692b95b6814b71085750cc38a", size = 199697, upload-time = "2026-06-22T23:20:37.235Z" }, ] +[[package]] +name = "google-crc32c" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/25/9cb0c1c31c45b893eb8f11ae70b3f4309432d59b5acaebca5dbe791729a4/google_crc32c-1.9.0.tar.gz", hash = "sha256:7b8c84c3d159ab6817fe3f74e6e6cef099c3f95dcec3abc0d8afb1404642efbe", size = 14857, upload-time = "2026-09-24T21:39:32.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/7c/e89a13c971bcab4a0464ecc78f8dc162c5c7ec8986a54dd867fc093b2c6f/google_crc32c-1.9.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6b529a6a287104ec79d281c411685231200ce954a29c28ab8e5093cb6e130fb", size = 31959, upload-time = "2026-09-24T21:19:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/c6/06/510062c2acbdbf602d759b7b0086032c487126106bb25197b9da1ff1047f/google_crc32c-1.9.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:51cb4e23a38ad4f495f35f87c233ca3ea6b9c4559e7ac383cdef786fab0f7977", size = 31801, upload-time = "2026-09-24T21:22:24.222Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c6/53eaa12dc62625f4605760b09854a0814ef6afebdc0e611a38c7b15aa6d1/google_crc32c-1.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8535e75dfead304f30e9122b9ea2c0a570dbaa52c176a0a591540c7914c1e46d", size = 37203, upload-time = "2026-09-24T21:38:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/770c2713df471df73998f79758739da83e410ef576bfafd05e5e845959ff/google_crc32c-1.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:280f3a3e47af0eeba3a3e5aa7d311af77001812b8df80fb8beafcd0b40eaf7f1", size = 35816, upload-time = "2026-09-24T21:38:05.798Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f3/181945217690644aa502220a3ec9bf0d2ef9af930bbfffee555fe5236e2f/google_crc32c-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:56610f548f1b35c9568b9d1de30423480f505dae4991556072d5802820ff35c4", size = 35097, upload-time = "2026-09-24T21:39:27.402Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/a2f07f15e624f0de79359b1a6c1deb59ec5061bd3b38744b3b2849400662/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:457d0d9a4718fd52b1494eac5c200ad25beeadbdc91843d550a003910838589f", size = 31958, upload-time = "2026-09-24T21:19:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/923743597b774bbcf12a7c3e00e48d745e15fd616ad7489a40a63fff8f2f/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ccfe40021fd6afe23361175cf7551e3cef5fd34dc1ebe319f14993a83579e0eb", size = 31805, upload-time = "2026-09-24T21:22:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/df/a6/4d0352fe889663e0d81cea7fc664ec9158727384de4a44ab10e9967a7682/google_crc32c-1.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbef61a3794e011c65fb4396a196cf123a7f474fe5a443db8e5dd7d751b9e6d4", size = 38119, upload-time = "2026-09-24T21:38:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e3/26685384e4b66ff0928d9566ef6110a7df76029175a1842329d7e3515f10/google_crc32c-1.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86764b99e7a607830d93cb5b75e0ec3ff6cb06d3c274624418473cee701900d4", size = 36677, upload-time = "2026-09-24T21:38:08.082Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/4e90102e84880e97d3cf935f2672ecd29191bdeacf57f01740f92debda00/google_crc32c-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:43a2dc26f9be213fbe0b4fc4a1088c5d45cbfcb3247420ccc820f0fc3edeea86", size = 35096, upload-time = "2026-09-24T21:39:28.201Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/0730e1b3a14d054d1466f2fec88dadf978509c749a3d96d8b069cc56d38a/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53fdafef58e230d0c946ab5f8446d123d9f548230a73b29c8b41c9546f268bc1", size = 31961, upload-time = "2026-09-24T21:19:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/d085abaf2fd907121975b92245bb3480fb8be40c37d03f9d6c41857f84c3/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:8b91f41645b15a720357183fa5716682ada441873e3c462c15f9714be36f146b", size = 31804, upload-time = "2026-09-24T21:22:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/94/78/dd1935432337e5da7af391a6fc9f161c1c8e9b9002a402b9190135fe1b59/google_crc32c-1.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16865b477d7941712cb0e0aad8ad4815e984fb5fc16d3fdaef7d986e26e53c95", size = 37775, upload-time = "2026-09-24T21:38:09.249Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/9db03635bb10188d93dcbab9baa2a8670a0da4e868b4370cdbd98d65fed8/google_crc32c-1.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3abb18297d9ef0ab120531838be0e6d68c9fa876570e11c229c48f2edac23ce7", size = 36347, upload-time = "2026-09-24T21:38:10.141Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/94dee516c846bd9382c3f566d8f8e5fb9e90599e45afeb697f9fc2533528/google_crc32c-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb63a8d7fa2e95dcff1ca16af2f4d88b526fa5ff72d1696285884ac2d49b6963", size = 35097, upload-time = "2026-09-24T21:39:28.934Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/cb484e8b6174f130f8c6dc79c733a9dd8869b410ad6511fb6104c46b973a/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:f1dc17d987ddcc5eba12a7ce48f0eb93141dea236b170c1101151396edf2f0cf", size = 31960, upload-time = "2026-09-24T21:19:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/af/25/3e8e567bd48448e225ea27318ccf2b94e05124e7b8b97b13eaec9e127199/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f894a2877650b56201d26a012a257b76d54a68834dc3913a93830ca8a047b075", size = 31803, upload-time = "2026-09-24T21:22:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/f0/18/bee0dd59ae622482dc6463636c79e4bde7c954d061c859c9256362c9931a/google_crc32c-1.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4488f1553a9ab7e86cdedc833374a7e904031803b995dc0bd0be48c271fa6556", size = 37776, upload-time = "2026-09-24T21:38:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b6/e76e80fed5f2558273c7839e622f98095c9b36c719c7147e38e3c055cb70/google_crc32c-1.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0568b17ed90ac596f29400d99e243fd0cc6276766183def888d1bf8d1dc13827", size = 36350, upload-time = "2026-09-24T21:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/87/34/165542bfa99dfef91a76471cc48cce74b8ff4e295722896087ab2b8e8611/google_crc32c-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:8583ec21d56b565d68ab2963cc7e21b3b271247c29b04286068255ef65f221bd", size = 35090, upload-time = "2026-09-24T21:39:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/43ea41f4061a1cad87b2b6559c98e960e45bf551fe66f83d833b98aaf0c9/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6a3b2c8a343c570ed8100a7627c20badfd92c6caa2067093a86be45af27f5b1b", size = 31963, upload-time = "2026-09-24T21:19:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/45/d2/a968c0c29ccd2b0c980ff4f9e3f7035cee28c23a1c57541825cc8221858c/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:13179f7e3282617923e957b8e54b8f9c3968030f48640a9f47fd7c5c38c4a215", size = 31805, upload-time = "2026-09-24T21:22:27.917Z" }, + { url = "https://files.pythonhosted.org/packages/03/73/388e493d6c3e252e37165d22efe5a1361f872a24425391b999822861b23a/google_crc32c-1.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:265233aff33d835f5b909584fe36ab29647b598c271b661a300001099109e53e", size = 37925, upload-time = "2026-09-24T21:38:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/36/190d32caa363ef25d685f422ed1bbf93ff1140fb22fd4d90f24cec209977/google_crc32c-1.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dee799544cae42a42b17a88e38b59cf2c271051dc001da2117a8ff240ffa0548", size = 36495, upload-time = "2026-09-24T21:38:14.211Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fd/81cefea6adae7bd92abb23d4567d199f6485a20ec0a305ca5fa04c52b9c5/google_crc32c-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:af73200fa9791ccd380f3598235dba8d82b8af0905df045b3dc60b59836e8ddd", size = 35535, upload-time = "2026-09-24T21:39:30.52Z" }, + { url = "https://files.pythonhosted.org/packages/c5/18/19d4f17f3f33f8fdffcb3e1e69219d6f7ec2c359c160867b04dac1d0a64d/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e6e8be8a94436079cb5340f6d495d9d7ba30124d8b952703994c739c7c06e236", size = 31977, upload-time = "2026-09-24T21:19:03.976Z" }, + { url = "https://files.pythonhosted.org/packages/81/b4/8010372c4b46f2ee2352dfdb630c397570cd85522a315df024ad2f9459aa/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:f2b64641bca27497b986b9d87883014035aa904cb4fa333407c6752b3afee9ba", size = 31809, upload-time = "2026-09-24T21:22:29.1Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f8/7e33845d6b90ce1cf37cfabf25cb859277c7d3533ef1b6b1e1ca58581549/google_crc32c-1.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f97c3806dcea41c29c04965347b0e12481561b75e0045dc7a4f69d75dec5d9b1", size = 38359, upload-time = "2026-09-24T21:38:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/556b2423f449a7515af6b8222a4d7833cbe09ff3e8d2f0b80471f5f6d02e/google_crc32c-1.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0abe7e202c25909869c35672ab0f2fe748a7acf276eb78577332a7c38999740f", size = 36858, upload-time = "2026-09-24T21:38:15.799Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/4733f1b7c921d04a2bb9b9916cf66498bf7ad0860a06289413830da83192/google_crc32c-1.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:5695c8b9327e040b2aba12c6659b0acb5995314ef0af0192da66e662e011103b", size = 35534, upload-time = "2026-09-24T21:39:31.337Z" }, +] + [[package]] name = "google-genai" version = "2.25.0" @@ -1999,7 +2181,7 @@ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "pyreadline3", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ @@ -2107,6 +2289,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.1" @@ -2170,17 +2361,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "exceptiongroup" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -2198,22 +2389,25 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/96/b150fe7e25a5a29ae9ac1374e71488639605d39a1ea4abb74c9ce33af235/ipython-9.16.1.tar.gz", hash = "sha256:5a3d1f9a47ff216d6cf9cf863124f6a2c1a198d1354c546a4d24a370a283b64c", size = 4515302, upload-time = "2026-08-03T08:36:15.571Z" } wheels = [ @@ -2225,7 +2419,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -3544,9 +3738,12 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ @@ -3681,9 +3878,12 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ @@ -3765,7 +3965,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -3804,7 +4004,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -3816,7 +4016,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -3846,9 +4046,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -3860,7 +4060,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -3929,12 +4129,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "coloredlogs" }, - { name = "flatbuffers" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, + { name = "coloredlogs", marker = "python_full_version < '3.11'" }, + { name = "flatbuffers", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", marker = "python_full_version < '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, @@ -3972,16 +4172,19 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "flatbuffers" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, + { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, + { name = "sympy", marker = "python_full_version >= '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" }, @@ -4339,10 +4542,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -4406,14 +4609,17 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ @@ -4489,7 +4695,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -4602,24 +4808,24 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "aiofiles" }, - { name = "aiohttp" }, - { name = "docstring-parser" }, - { name = "loguru" }, - { name = "markdown" }, - { name = "nltk" }, - { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" } }, - { name = "openai" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "pyloudnorm" }, - { name = "resampy" }, - { name = "soxr" }, - { name = "transformers" }, - { name = "wait-for2" }, + { name = "aiofiles", marker = "python_full_version < '3.11'" }, + { name = "aiohttp", marker = "python_full_version < '3.11'" }, + { name = "docstring-parser", marker = "python_full_version < '3.11'" }, + { name = "loguru", marker = "python_full_version < '3.11'" }, + { name = "markdown", marker = "python_full_version < '3.11'" }, + { name = "nltk", marker = "python_full_version < '3.11'" }, + { name = "numba", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "openai", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "pyloudnorm", marker = "python_full_version < '3.11'" }, + { name = "resampy", marker = "python_full_version < '3.11'" }, + { name = "soxr", marker = "python_full_version < '3.11'" }, + { name = "transformers", marker = "python_full_version < '3.11'" }, + { name = "wait-for2", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/c6/121d4a6088051695eb382457a09fd8515cc6c37de23c359f9b16be18124f/pipecat_ai-0.0.108.tar.gz", hash = "sha256:d6707333c9e1f909d654b329d2d85288b693a36a4e31e820e522df94e3308bb8", size = 11132175, upload-time = "2026-03-28T04:49:49.969Z" } wheels = [ @@ -4637,29 +4843,32 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "aiofiles" }, - { name = "aiohttp" }, + { name = "aiofiles", marker = "python_full_version >= '3.11'" }, + { name = "aiohttp", marker = "python_full_version >= '3.11'" }, { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, - { name = "docstring-parser" }, - { name = "loguru" }, - { name = "markdown" }, - { name = "nltk" }, - { name = "numba" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "onnxruntime", version = "1.24.4", source = { registry = "https://pypi.org/simple" } }, - { name = "openai" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "pyloudnorm" }, - { name = "resampy" }, - { name = "soxr" }, - { name = "wait-for2", marker = "python_full_version < '3.12'" }, + { name = "docstring-parser", marker = "python_full_version >= '3.11'" }, + { name = "loguru", marker = "python_full_version >= '3.11'" }, + { name = "markdown", marker = "python_full_version >= '3.11'" }, + { name = "nltk", marker = "python_full_version >= '3.11'" }, + { name = "numba", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnxruntime", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "openai", marker = "python_full_version >= '3.11'" }, + { name = "pillow", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pyloudnorm", marker = "python_full_version >= '3.11'" }, + { name = "resampy", marker = "python_full_version >= '3.11'" }, + { name = "soxr", marker = "python_full_version >= '3.11'" }, + { name = "wait-for2", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/cc/8352e30c47ee4fd075b9a0f3d91183294582578f07289180a30fc8228409/pipecat_ai-1.3.0.tar.gz", hash = "sha256:abeb9d95b1df2f35b855334cda1899fc603124d5448967c82ba08a94c604318f", size = 11260868, upload-time = "2026-05-29T01:03:00.532Z" } wheels = [ @@ -5334,6 +5543,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + +[[package]] +name = "pyee" +version = "16.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/f7/c05c22ce87ee176fca235850788cfd1a9241adce21fdfb3326c1ae838bf5/pyee-16.0.1.tar.gz", hash = "sha256:bdc6e9214429ae5ec10b8a51604cf68c363b11cbc105e3da72204fce8c111e59", size = 143618, upload-time = "2026-10-31T17:23:34.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ff/a5c6a937c4061f83de578320d749bcb740957817ed52f483cafb184e6a1f/pyee-16.0.1-py3-none-any.whl", hash = "sha256:756e8d455970f8c5598e760495ec9195c6d31a64beb3f3f666662fc169804076", size = 17616, upload-time = "2026-10-31T17:23:30.869Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -5360,6 +5610,28 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pylibsrtp" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/a6/6e532bec974aaecbf9fe4e12538489fb1c28456e65088a50f305aeab9f89/pylibsrtp-1.0.0.tar.gz", hash = "sha256:b39dff075b263a8ded5377f2490c60d2af452c9f06c4d061c7a2b640612b34d4", size = 10858, upload-time = "2025-10-13T16:12:31.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/af/89e61a62fa3567f1b7883feb4d19e19564066c2fcd41c37e08d317b51881/pylibsrtp-1.0.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:822c30ea9e759b333dc1f56ceac778707c51546e97eb874de98d7d378c000122", size = 1865017, upload-time = "2025-10-13T16:12:15.62Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0e/8d215484a9877adcf2459a8b28165fc89668b034565277fd55d666edd247/pylibsrtp-1.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaad74e5c8cbc1c32056c3767fea494c1e62b3aea2c908eda2a1051389fdad76", size = 2182739, upload-time = "2025-10-13T16:12:17.121Z" }, + { url = "https://files.pythonhosted.org/packages/57/3f/76a841978877ae13eac0d4af412c13bbd5d83b3df2c1f5f2175f2e0f68e5/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9209b86e662ebbd17c8a9e8549ba57eca92a3e87fb5ba8c0e27b8c43cd08a767", size = 2732922, upload-time = "2025-10-13T16:12:18.348Z" }, + { url = "https://files.pythonhosted.org/packages/0e/14/cf5d2a98a66fdfe258f6b036cda570f704a644fa861d7883a34bc359501e/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", size = 2434534, upload-time = "2025-10-13T16:12:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/bd/08/a3f6e86c04562f7dce6717cd2206a0f84ca85c5e38121d998e0e330194c3/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:81fb8879c2e522021a7cbd3f4bda1b37c192e1af939dfda3ff95b4723b329663", size = 2345818, upload-time = "2025-10-13T16:12:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d5/130c2b5b4b51df5631684069c6f0a6761c59d096a33d21503ac207cf0e47/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ddb562e443cf2e557ea2dfaeef0d7e6b90e96dd38eb079b4ab2c8e34a79f50b", size = 2774490, upload-time = "2025-10-13T16:12:22.659Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/715a453bfee3bea92a243888ad359094a7727cc6d393f21281320fe7798c/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f02e616c9dfab2b03b32d8cc7b748f9d91814c0211086f987629a60f05f6e2cc", size = 2372603, upload-time = "2025-10-13T16:12:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/e3/56/52fa74294254e1f53a4ff170ee2006e57886cf4bb3db46a02b4f09e1d99f/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c134fa09e7b80a5b7fed626230c5bc257fd771bd6978e754343e7a61d96bc7e6", size = 2451269, upload-time = "2025-10-13T16:12:25.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/51/2e9b34f484cbdd3bac999bf1f48b696d7389433e900639089e8fc4e0da0d/pylibsrtp-1.0.0-cp310-abi3-win32.whl", hash = "sha256:bae377c3b402b17b9bbfbfe2534c2edba17aa13bea4c64ce440caacbe0858b55", size = 1247503, upload-time = "2025-10-13T16:12:27.39Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/43db21af194580aba2d9a6d4c7bd8c1a6e887fa52cd810b88f89096ecad2/pylibsrtp-1.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:8d6527c4a78a39a8d397f8862a8b7cdad4701ee866faf9de4ab8c70be61fd34d", size = 1601659, upload-time = "2025-10-13T16:12:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, +] + [[package]] name = "pyloudnorm" version = "0.2.0" @@ -5375,6 +5647,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/b6/65a49a05614b2548edbba3aab118f2ebe7441dfd778accdcdce9f6567f20/pyloudnorm-0.2.0-py3-none-any.whl", hash = "sha256:9bb69afb904f59d007a7f9ba3d75d16fb8aeef35c44d6df822a9f192d69cf13f", size = 10879, upload-time = "2026-01-04T11:43:34.534Z" }, ] +[[package]] +name = "pyopenssl" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, +] + [[package]] name = "pypika" version = "0.51.1" @@ -6096,9 +6381,12 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } wheels = [ @@ -6290,10 +6578,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -6340,16 +6628,19 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "joblib" }, - { name = "narwhals" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "narwhals", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } wheels = [ @@ -6393,7 +6684,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -6455,12 +6746,15 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [