diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3576973..d6dc3e22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,17 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/toddlzt-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -31,7 +36,7 @@ jobs: run: ./scripts/lint upload: - if: github.repository == 'stainless-sdks/toddlzt-python' + if: github.repository == 'stainless-sdks/toddlzt-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 name: upload permissions: @@ -58,6 +63,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/toddlzt-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ea3d667e..0a7c0458 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.36.0" + ".": "4.37.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 20f60d86..51983f36 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/retell%2Ftoddlzt-56f2d45e8cd73d9affbc60ffb07513d0ac62f19631854d3f376d066969f71bad.yml -openapi_spec_hash: ab9dac6b50dd93a8ef459e6e68660bac +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/retell%2Ftoddlzt-b9085cd6eb0e7f2798412d918e4327040bf1f7076a85001ea56ee7d55f7a306c.yml +openapi_spec_hash: a7b3e777b15c7901c94efa5a3132b9c0 config_hash: f4bc63f2350a2a4988750b41a0737f9d diff --git a/CHANGELOG.md b/CHANGELOG.md index b2753ed7..b93d2f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## 4.37.0 (2025-06-29) + +Full Changelog: [v4.36.0...v4.37.0](https://github.com/RetellAI/retell-python-sdk/compare/v4.36.0...v4.37.0) + +### Features + +* **api:** api update ([95c3188](https://github.com/RetellAI/retell-python-sdk/commit/95c3188963aee89f451b3afd5b976ed2a222bd96)) +* **api:** api update ([8231ac4](https://github.com/RetellAI/retell-python-sdk/commit/8231ac42d5758c5e9a211884222caddb43544c04)) +* **api:** api update ([a4f71a1](https://github.com/RetellAI/retell-python-sdk/commit/a4f71a144677a3d9046cdfea1928a1683bce248a)) +* **api:** api update ([9771f59](https://github.com/RetellAI/retell-python-sdk/commit/9771f599c1798c95828a9582c8d460723facd5d3)) +* **api:** api update ([0703f82](https://github.com/RetellAI/retell-python-sdk/commit/0703f822fa8c51efe5cb0ca70174d8dacadc985a)) +* **api:** api update ([2a5ba8b](https://github.com/RetellAI/retell-python-sdk/commit/2a5ba8bd792e3c10ec7ac0ca5025f300a06fe311)) +* **api:** api update ([3600f4c](https://github.com/RetellAI/retell-python-sdk/commit/3600f4cf8833658591ef72e58e33df57dc223eb4)) +* **api:** api update ([e33361b](https://github.com/RetellAI/retell-python-sdk/commit/e33361bc15fc80a3fcbb9be089742f7dfda7568b)) +* **api:** api update ([15b8af3](https://github.com/RetellAI/retell-python-sdk/commit/15b8af3658b36e040a21fc5be3c28d2e3a45bf79)) +* **client:** add support for aiohttp ([c3c2840](https://github.com/RetellAI/retell-python-sdk/commit/c3c2840e285d73c451f0a4e18ffcdf6d7bdff06a)) + + +### Bug Fixes + +* **ci:** correct conditional ([3a5605f](https://github.com/RetellAI/retell-python-sdk/commit/3a5605f8876535b1b8ee4b61d018afeaa6b21fdb)) +* **ci:** release-doctor — report correct token name ([cced89c](https://github.com/RetellAI/retell-python-sdk/commit/cced89c6e18c338a66051764fdbcd7cd8decbba4)) +* **tests:** fix: tests which call HTTP endpoints directly with the example parameters ([82eb666](https://github.com/RetellAI/retell-python-sdk/commit/82eb66606315b29827250d1db55cbc8d1201a770)) + + +### Chores + +* **ci:** enable for pull requests ([3255f4a](https://github.com/RetellAI/retell-python-sdk/commit/3255f4a2a7e57ae4dfc3e704c1895ca76d6eaeb4)) +* **ci:** only run for pushes and fork pull requests ([619e43a](https://github.com/RetellAI/retell-python-sdk/commit/619e43a84070a10d57973a7d79d33131e639f2fa)) +* **internal:** update conftest.py ([56ff61e](https://github.com/RetellAI/retell-python-sdk/commit/56ff61e8c2a353faf6b668d40b7ea96d57576789)) +* **readme:** update badges ([faaed6a](https://github.com/RetellAI/retell-python-sdk/commit/faaed6a89f513e4de848ca569140f15989ec84f3)) +* **tests:** add tests for httpx client instantiation & proxies ([f055bdf](https://github.com/RetellAI/retell-python-sdk/commit/f055bdf00ae2ab8d39684fd7674ac04845858170)) +* **tests:** skip some failing tests on the latest python versions ([8273205](https://github.com/RetellAI/retell-python-sdk/commit/827320564ec4a33ce33e5b30ca155dd83fb3f324)) + + +### Documentation + +* **client:** fix httpx.Timeout documentation reference ([bbbf5ce](https://github.com/RetellAI/retell-python-sdk/commit/bbbf5ceee0d1f5ea5578fe22d5a5046afd17e925)) + ## 4.36.0 (2025-06-16) Full Changelog: [v4.35.0...v4.36.0](https://github.com/RetellAI/retell-python-sdk/compare/v4.35.0...v4.36.0) diff --git a/README.md b/README.md index 72084bde..5f1821c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Retell Python API library -[![PyPI version](https://img.shields.io/pypi/v/retell-sdk.svg)](https://pypi.org/project/retell-sdk/) +[![PyPI version]()](https://pypi.org/project/retell-sdk/) The Retell Python library provides convenient access to the Retell REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -67,6 +67,43 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from PyPI +pip install retell-sdk[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import asyncio +from retell import DefaultAioHttpClient +from retell import AsyncRetell + + +async def main() -> None: + async with AsyncRetell( + api_key="YOUR_RETELL_API_KEY", + http_client=DefaultAioHttpClient(), + ) as client: + agent_response = await client.agent.create( + response_engine={ + "llm_id": "llm_234sdertfsdsfsdf", + "type": "retell-llm", + }, + voice_id="11labs-Adrian", + ) + print(agent_response.agent_id) + + +asyncio.run(main()) +``` + ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: @@ -173,7 +210,7 @@ client.with_options(max_retries=5).agent.create( ### Timeouts By default requests time out after 1 minute. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python from retell import Retell diff --git a/bin/check-release-environment b/bin/check-release-environment index 4664ca28..b845b0f4 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The RETELL_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi lenErrors=${#errors[@]} diff --git a/pyproject.toml b/pyproject.toml index 29d7bd61..8459aef7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "retell-sdk" -version = "4.36.0" +version = "4.37.0" description = "The official Python library for the retell API" dynamic = ["readme"] license = "Apache-2.0" @@ -39,6 +39,8 @@ classifiers = [ Homepage = "https://github.com/RetellAI/retell-python-sdk" Repository = "https://github.com/RetellAI/retell-python-sdk" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index e3931d8f..78a895e4 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,6 +10,13 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.13 + # via httpx-aiohttp + # via retell-sdk +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 @@ -17,6 +24,10 @@ anyio==4.4.0 # via retell-sdk argcomplete==3.1.2 # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -38,16 +49,23 @@ execnet==2.1.1 # via pytest-xdist filelock==3.12.4 # via virtualenv +frozenlist==1.7.0 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp # via respx # via retell-sdk +httpx-aiohttp==0.1.6 + # via retell-sdk idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -55,6 +73,9 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py +multidict==6.5.0 + # via aiohttp + # via yarl mypy==1.14.1 mypy-extensions==1.0.0 # via mypy @@ -69,6 +90,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest +propcache==0.3.2 + # via aiohttp + # via yarl pycparser==2.22 # via cffi pydantic==2.10.3 @@ -103,6 +127,7 @@ tomli==2.0.2 # via pytest typing-extensions==4.12.2 # via anyio + # via multidict # via mypy # via pydantic # via pydantic-core @@ -110,5 +135,7 @@ typing-extensions==4.12.2 # via retell-sdk virtualenv==20.24.5 # via nox +yarl==1.20.1 + # via aiohttp zipp==3.17.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 0a7411a7..5f96672c 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,22 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.13 + # via httpx-aiohttp + # via retell-sdk +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via retell-sdk +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -26,15 +37,28 @@ distro==1.8.0 # via retell-sdk exceptiongroup==1.2.2 # via anyio +frozenlist==1.7.0 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp + # via retell-sdk +httpx-aiohttp==0.1.6 # via retell-sdk idna==3.4 # via anyio # via httpx + # via yarl +multidict==6.5.0 + # via aiohttp + # via yarl +propcache==0.3.2 + # via aiohttp + # via yarl pycparser==2.22 # via cffi pydantic==2.10.3 @@ -46,6 +70,9 @@ sniffio==1.3.0 # via retell-sdk typing-extensions==4.12.2 # via anyio + # via multidict # via pydantic # via pydantic-core # via retell-sdk +yarl==1.20.1 + # via aiohttp diff --git a/src/retell/__init__.py b/src/retell/__init__.py index c5ddb67e..bc4b3289 100644 --- a/src/retell/__init__.py +++ b/src/retell/__init__.py @@ -26,7 +26,7 @@ UnprocessableEntityError, APIResponseValidationError, ) -from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -68,6 +68,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/retell/_base_client.py b/src/retell/_base_client.py index 6a8c6fc3..ba8385ad 100644 --- a/src/retell/_base_client.py +++ b/src/retell/_base_client.py @@ -1284,6 +1284,24 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + if TYPE_CHECKING: DefaultAsyncHttpxClient = httpx.AsyncClient """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK @@ -1292,8 +1310,12 @@ def __init__(self, **kwargs: Any) -> None: This is useful because overriding the `http_client` with your own instance of `httpx.AsyncClient` will result in httpx's defaults being used, not ours. """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" else: DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): diff --git a/src/retell/_version.py b/src/retell/_version.py index 473a22f9..1fd79d1b 100644 --- a/src/retell/_version.py +++ b/src/retell/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "retell" -__version__ = "4.36.0" # x-release-please-version +__version__ = "4.37.0" # x-release-please-version diff --git a/src/retell/resources/agent.py b/src/retell/resources/agent.py index fa4f044c..cb01223b 100644 --- a/src/retell/resources/agent.py +++ b/src/retell/resources/agent.py @@ -132,8 +132,6 @@ def create( "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] @@ -514,8 +512,6 @@ def update( "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] @@ -969,8 +965,6 @@ async def create( "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] @@ -1351,8 +1345,6 @@ async def update( "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] diff --git a/src/retell/resources/call.py b/src/retell/resources/call.py index ded37327..13881c65 100644 --- a/src/retell/resources/call.py +++ b/src/retell/resources/call.py @@ -236,6 +236,7 @@ def create_phone_call( *, from_number: str, to_number: str, + custom_sip_headers: Dict[str, str] | NotGiven = NOT_GIVEN, metadata: object | NotGiven = NOT_GIVEN, override_agent_id: str | NotGiven = NOT_GIVEN, override_agent_version: int | NotGiven = NOT_GIVEN, @@ -257,6 +258,8 @@ def create_phone_call( to_number: The number you want to call, in E.164 format. If using a number purchased from Retell, only US numbers are supported as destination. + custom_sip_headers: Add optional custom SIP headers to the call. + metadata: An arbitrary object for storage purpose only. You can put anything here like your internal customer id associated with the call. Not used for processing. You can later get this field from the call object. @@ -286,6 +289,7 @@ def create_phone_call( { "from_number": from_number, "to_number": to_number, + "custom_sip_headers": custom_sip_headers, "metadata": metadata, "override_agent_id": override_agent_id, "override_agent_version": override_agent_version, @@ -627,6 +631,7 @@ async def create_phone_call( *, from_number: str, to_number: str, + custom_sip_headers: Dict[str, str] | NotGiven = NOT_GIVEN, metadata: object | NotGiven = NOT_GIVEN, override_agent_id: str | NotGiven = NOT_GIVEN, override_agent_version: int | NotGiven = NOT_GIVEN, @@ -648,6 +653,8 @@ async def create_phone_call( to_number: The number you want to call, in E.164 format. If using a number purchased from Retell, only US numbers are supported as destination. + custom_sip_headers: Add optional custom SIP headers to the call. + metadata: An arbitrary object for storage purpose only. You can put anything here like your internal customer id associated with the call. Not used for processing. You can later get this field from the call object. @@ -677,6 +684,7 @@ async def create_phone_call( { "from_number": from_number, "to_number": to_number, + "custom_sip_headers": custom_sip_headers, "metadata": metadata, "override_agent_id": override_agent_id, "override_agent_version": override_agent_version, diff --git a/src/retell/resources/chat.py b/src/retell/resources/chat.py index 0589378a..22bf5ccd 100644 --- a/src/retell/resources/chat.py +++ b/src/retell/resources/chat.py @@ -65,7 +65,8 @@ def create( Args: agent_id: The chat agent to use for the call. - agent_version: The version of the chat agent to use for the call. + agent_version: The version of the chat agent to use for the chat. If not provided, will default + to latest version. metadata: An arbitrary object for storage purpose only. You can put anything here like your internal customer id associated with the chat. Not used for processing. You @@ -270,7 +271,8 @@ async def create( Args: agent_id: The chat agent to use for the call. - agent_version: The version of the chat agent to use for the call. + agent_version: The version of the chat agent to use for the chat. If not provided, will default + to latest version. metadata: An arbitrary object for storage purpose only. You can put anything here like your internal customer id associated with the chat. Not used for processing. You diff --git a/src/retell/types/agent_create_params.py b/src/retell/types/agent_create_params.py index 7b536200..82752478 100644 --- a/src/retell/types/agent_create_params.py +++ b/src/retell/types/agent_create_params.py @@ -306,8 +306,6 @@ class AgentCreateParams(TypedDict, total=False): "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] diff --git a/src/retell/types/agent_response.py b/src/retell/types/agent_response.py index c7f4080e..48e859f1 100644 --- a/src/retell/types/agent_response.py +++ b/src/retell/types/agent_response.py @@ -480,8 +480,6 @@ class AgentResponse(BaseModel): "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] diff --git a/src/retell/types/agent_update_params.py b/src/retell/types/agent_update_params.py index 2898ab1a..cad9dfe5 100644 --- a/src/retell/types/agent_update_params.py +++ b/src/retell/types/agent_update_params.py @@ -311,8 +311,6 @@ class AgentUpdateParams(TypedDict, total=False): "eleven_turbo_v2_5", "eleven_flash_v2_5", "eleven_multilingual_v2", - "Play3.0-mini", - "PlayDialog", "tts-1", "gpt-4o-mini-tts", ] diff --git a/src/retell/types/call_create_phone_call_params.py b/src/retell/types/call_create_phone_call_params.py index 5306274e..474bebce 100644 --- a/src/retell/types/call_create_phone_call_params.py +++ b/src/retell/types/call_create_phone_call_params.py @@ -22,6 +22,9 @@ class CallCreatePhoneCallParams(TypedDict, total=False): destination. """ + custom_sip_headers: Dict[str, str] + """Add optional custom SIP headers to the call.""" + metadata: object """An arbitrary object for storage purpose only. diff --git a/src/retell/types/chat_create_params.py b/src/retell/types/chat_create_params.py index 61a59ae9..e1c60e44 100644 --- a/src/retell/types/chat_create_params.py +++ b/src/retell/types/chat_create_params.py @@ -13,7 +13,10 @@ class ChatCreateParams(TypedDict, total=False): """The chat agent to use for the call.""" agent_version: int - """The version of the chat agent to use for the call.""" + """The version of the chat agent to use for the chat. + + If not provided, will default to latest version. + """ metadata: object """An arbitrary object for storage purpose only. diff --git a/src/retell/types/llm_create_params.py b/src/retell/types/llm_create_params.py index a045b76a..be8a6990 100644 --- a/src/retell/types/llm_create_params.py +++ b/src/retell/types/llm_create_params.py @@ -24,6 +24,12 @@ "GeneralToolPressDigitTool", "GeneralToolCustomTool", "GeneralToolCustomToolParameters", + "GeneralToolExtractDynamicVariableTool", + "GeneralToolExtractDynamicVariableToolVariable", + "GeneralToolExtractDynamicVariableToolVariableStringAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData", "State", "StateEdge", "StateEdgeParameters", @@ -44,6 +50,12 @@ "StateToolPressDigitTool", "StateToolCustomTool", "StateToolCustomToolParameters", + "StateToolExtractDynamicVariableTool", + "StateToolExtractDynamicVariableToolVariable", + "StateToolExtractDynamicVariableToolVariableStringAnalysisData", + "StateToolExtractDynamicVariableToolVariableEnumAnalysisData", + "StateToolExtractDynamicVariableToolVariableBooleanAnalysisData", + "StateToolExtractDynamicVariableToolVariableNumberAnalysisData", ] @@ -265,6 +277,9 @@ class GeneralToolTransferCallTool(TypedDict, total=False): type: Required[Literal["transfer_call"]] + custom_sip_headers: Dict[str, str] + """Custom SIP headers to be added to the call.""" + description: str """ Describes what the tool does, sometimes can also include information about when @@ -467,6 +482,85 @@ class GeneralToolCustomTool(TypedDict, total=False): """ +class GeneralToolExtractDynamicVariableToolVariableStringAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["string"]] + """Type of the variable to extract.""" + + examples: List[str] + """Examples of the variable value to teach model the style and syntax.""" + + +class GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData(TypedDict, total=False): + choices: Required[List[str]] + """The possible values of the variable, must be non empty array.""" + + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["enum"]] + """Type of the variable to extract.""" + + +class GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["boolean"]] + """Type of the variable to extract.""" + + +class GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["number"]] + """Type of the variable to extract.""" + + +GeneralToolExtractDynamicVariableToolVariable: TypeAlias = Union[ + GeneralToolExtractDynamicVariableToolVariableStringAnalysisData, + GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData, + GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData, + GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData, +] + + +class GeneralToolExtractDynamicVariableTool(TypedDict, total=False): + description: Required[str] + """ + Describes what the tool does, sometimes can also include information about when + to call the tool. + """ + + name: Required[str] + """Name of the tool. + + Must be unique within all tools available to LLM at any given time (general + tools + state tools + state edges). Must be consisted of a-z, A-Z, 0-9, or + contain underscores and dashes, with a maximum length of 64 (no space allowed). + """ + + type: Required[Literal["extract_dynamic_variable"]] + + variables: Required[Iterable[GeneralToolExtractDynamicVariableToolVariable]] + """The variables to be extracted.""" + + GeneralTool: TypeAlias = Union[ GeneralToolEndCallTool, GeneralToolTransferCallTool, @@ -474,6 +568,7 @@ class GeneralToolCustomTool(TypedDict, total=False): GeneralToolBookAppointmentCalTool, GeneralToolPressDigitTool, GeneralToolCustomTool, + GeneralToolExtractDynamicVariableTool, ] @@ -638,6 +733,9 @@ class StateToolTransferCallTool(TypedDict, total=False): type: Required[Literal["transfer_call"]] + custom_sip_headers: Dict[str, str] + """Custom SIP headers to be added to the call.""" + description: str """ Describes what the tool does, sometimes can also include information about when @@ -840,6 +938,85 @@ class StateToolCustomTool(TypedDict, total=False): """ +class StateToolExtractDynamicVariableToolVariableStringAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["string"]] + """Type of the variable to extract.""" + + examples: List[str] + """Examples of the variable value to teach model the style and syntax.""" + + +class StateToolExtractDynamicVariableToolVariableEnumAnalysisData(TypedDict, total=False): + choices: Required[List[str]] + """The possible values of the variable, must be non empty array.""" + + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["enum"]] + """Type of the variable to extract.""" + + +class StateToolExtractDynamicVariableToolVariableBooleanAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["boolean"]] + """Type of the variable to extract.""" + + +class StateToolExtractDynamicVariableToolVariableNumberAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["number"]] + """Type of the variable to extract.""" + + +StateToolExtractDynamicVariableToolVariable: TypeAlias = Union[ + StateToolExtractDynamicVariableToolVariableStringAnalysisData, + StateToolExtractDynamicVariableToolVariableEnumAnalysisData, + StateToolExtractDynamicVariableToolVariableBooleanAnalysisData, + StateToolExtractDynamicVariableToolVariableNumberAnalysisData, +] + + +class StateToolExtractDynamicVariableTool(TypedDict, total=False): + description: Required[str] + """ + Describes what the tool does, sometimes can also include information about when + to call the tool. + """ + + name: Required[str] + """Name of the tool. + + Must be unique within all tools available to LLM at any given time (general + tools + state tools + state edges). Must be consisted of a-z, A-Z, 0-9, or + contain underscores and dashes, with a maximum length of 64 (no space allowed). + """ + + type: Required[Literal["extract_dynamic_variable"]] + + variables: Required[Iterable[StateToolExtractDynamicVariableToolVariable]] + """The variables to be extracted.""" + + StateTool: TypeAlias = Union[ StateToolEndCallTool, StateToolTransferCallTool, @@ -847,6 +1024,7 @@ class StateToolCustomTool(TypedDict, total=False): StateToolBookAppointmentCalTool, StateToolPressDigitTool, StateToolCustomTool, + StateToolExtractDynamicVariableTool, ] diff --git a/src/retell/types/llm_response.py b/src/retell/types/llm_response.py index a58a9a55..877b53c7 100644 --- a/src/retell/types/llm_response.py +++ b/src/retell/types/llm_response.py @@ -26,6 +26,12 @@ "GeneralToolPressDigitTool", "GeneralToolCustomTool", "GeneralToolCustomToolParameters", + "GeneralToolExtractDynamicVariableTool", + "GeneralToolExtractDynamicVariableToolVariable", + "GeneralToolExtractDynamicVariableToolVariableStringAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData", "State", "StateEdge", "StateEdgeParameters", @@ -46,6 +52,12 @@ "StateToolPressDigitTool", "StateToolCustomTool", "StateToolCustomToolParameters", + "StateToolExtractDynamicVariableTool", + "StateToolExtractDynamicVariableToolVariable", + "StateToolExtractDynamicVariableToolVariableStringAnalysisData", + "StateToolExtractDynamicVariableToolVariableEnumAnalysisData", + "StateToolExtractDynamicVariableToolVariableBooleanAnalysisData", + "StateToolExtractDynamicVariableToolVariableNumberAnalysisData", ] @@ -168,6 +180,9 @@ class GeneralToolTransferCallTool(BaseModel): type: Literal["transfer_call"] + custom_sip_headers: Optional[Dict[str, str]] = None + """Custom SIP headers to be added to the call.""" + description: Optional[str] = None """ Describes what the tool does, sometimes can also include information about when @@ -370,6 +385,85 @@ class GeneralToolCustomTool(BaseModel): """ +class GeneralToolExtractDynamicVariableToolVariableStringAnalysisData(BaseModel): + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["string"] + """Type of the variable to extract.""" + + examples: Optional[List[str]] = None + """Examples of the variable value to teach model the style and syntax.""" + + +class GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData(BaseModel): + choices: List[str] + """The possible values of the variable, must be non empty array.""" + + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["enum"] + """Type of the variable to extract.""" + + +class GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData(BaseModel): + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["boolean"] + """Type of the variable to extract.""" + + +class GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData(BaseModel): + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["number"] + """Type of the variable to extract.""" + + +GeneralToolExtractDynamicVariableToolVariable: TypeAlias = Union[ + GeneralToolExtractDynamicVariableToolVariableStringAnalysisData, + GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData, + GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData, + GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData, +] + + +class GeneralToolExtractDynamicVariableTool(BaseModel): + description: str + """ + Describes what the tool does, sometimes can also include information about when + to call the tool. + """ + + name: str + """Name of the tool. + + Must be unique within all tools available to LLM at any given time (general + tools + state tools + state edges). Must be consisted of a-z, A-Z, 0-9, or + contain underscores and dashes, with a maximum length of 64 (no space allowed). + """ + + type: Literal["extract_dynamic_variable"] + + variables: List[GeneralToolExtractDynamicVariableToolVariable] + """The variables to be extracted.""" + + GeneralTool: TypeAlias = Union[ GeneralToolEndCallTool, GeneralToolTransferCallTool, @@ -377,6 +471,7 @@ class GeneralToolCustomTool(BaseModel): GeneralToolBookAppointmentCalTool, GeneralToolPressDigitTool, GeneralToolCustomTool, + GeneralToolExtractDynamicVariableTool, ] @@ -541,6 +636,9 @@ class StateToolTransferCallTool(BaseModel): type: Literal["transfer_call"] + custom_sip_headers: Optional[Dict[str, str]] = None + """Custom SIP headers to be added to the call.""" + description: Optional[str] = None """ Describes what the tool does, sometimes can also include information about when @@ -743,6 +841,85 @@ class StateToolCustomTool(BaseModel): """ +class StateToolExtractDynamicVariableToolVariableStringAnalysisData(BaseModel): + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["string"] + """Type of the variable to extract.""" + + examples: Optional[List[str]] = None + """Examples of the variable value to teach model the style and syntax.""" + + +class StateToolExtractDynamicVariableToolVariableEnumAnalysisData(BaseModel): + choices: List[str] + """The possible values of the variable, must be non empty array.""" + + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["enum"] + """Type of the variable to extract.""" + + +class StateToolExtractDynamicVariableToolVariableBooleanAnalysisData(BaseModel): + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["boolean"] + """Type of the variable to extract.""" + + +class StateToolExtractDynamicVariableToolVariableNumberAnalysisData(BaseModel): + description: str + """Description of the variable.""" + + name: str + """Name of the variable.""" + + type: Literal["number"] + """Type of the variable to extract.""" + + +StateToolExtractDynamicVariableToolVariable: TypeAlias = Union[ + StateToolExtractDynamicVariableToolVariableStringAnalysisData, + StateToolExtractDynamicVariableToolVariableEnumAnalysisData, + StateToolExtractDynamicVariableToolVariableBooleanAnalysisData, + StateToolExtractDynamicVariableToolVariableNumberAnalysisData, +] + + +class StateToolExtractDynamicVariableTool(BaseModel): + description: str + """ + Describes what the tool does, sometimes can also include information about when + to call the tool. + """ + + name: str + """Name of the tool. + + Must be unique within all tools available to LLM at any given time (general + tools + state tools + state edges). Must be consisted of a-z, A-Z, 0-9, or + contain underscores and dashes, with a maximum length of 64 (no space allowed). + """ + + type: Literal["extract_dynamic_variable"] + + variables: List[StateToolExtractDynamicVariableToolVariable] + """The variables to be extracted.""" + + StateTool: TypeAlias = Union[ StateToolEndCallTool, StateToolTransferCallTool, @@ -750,6 +927,7 @@ class StateToolCustomTool(BaseModel): StateToolBookAppointmentCalTool, StateToolPressDigitTool, StateToolCustomTool, + StateToolExtractDynamicVariableTool, ] diff --git a/src/retell/types/llm_update_params.py b/src/retell/types/llm_update_params.py index ef7cf682..090c1b90 100644 --- a/src/retell/types/llm_update_params.py +++ b/src/retell/types/llm_update_params.py @@ -26,6 +26,12 @@ "GeneralToolPressDigitTool", "GeneralToolCustomTool", "GeneralToolCustomToolParameters", + "GeneralToolExtractDynamicVariableTool", + "GeneralToolExtractDynamicVariableToolVariable", + "GeneralToolExtractDynamicVariableToolVariableStringAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData", + "GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData", "State", "StateEdge", "StateEdgeParameters", @@ -46,6 +52,12 @@ "StateToolPressDigitTool", "StateToolCustomTool", "StateToolCustomToolParameters", + "StateToolExtractDynamicVariableTool", + "StateToolExtractDynamicVariableToolVariable", + "StateToolExtractDynamicVariableToolVariableStringAnalysisData", + "StateToolExtractDynamicVariableToolVariableEnumAnalysisData", + "StateToolExtractDynamicVariableToolVariableBooleanAnalysisData", + "StateToolExtractDynamicVariableToolVariableNumberAnalysisData", ] @@ -270,6 +282,9 @@ class GeneralToolTransferCallTool(TypedDict, total=False): type: Required[Literal["transfer_call"]] + custom_sip_headers: Dict[str, str] + """Custom SIP headers to be added to the call.""" + description: str """ Describes what the tool does, sometimes can also include information about when @@ -472,6 +487,85 @@ class GeneralToolCustomTool(TypedDict, total=False): """ +class GeneralToolExtractDynamicVariableToolVariableStringAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["string"]] + """Type of the variable to extract.""" + + examples: List[str] + """Examples of the variable value to teach model the style and syntax.""" + + +class GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData(TypedDict, total=False): + choices: Required[List[str]] + """The possible values of the variable, must be non empty array.""" + + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["enum"]] + """Type of the variable to extract.""" + + +class GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["boolean"]] + """Type of the variable to extract.""" + + +class GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["number"]] + """Type of the variable to extract.""" + + +GeneralToolExtractDynamicVariableToolVariable: TypeAlias = Union[ + GeneralToolExtractDynamicVariableToolVariableStringAnalysisData, + GeneralToolExtractDynamicVariableToolVariableEnumAnalysisData, + GeneralToolExtractDynamicVariableToolVariableBooleanAnalysisData, + GeneralToolExtractDynamicVariableToolVariableNumberAnalysisData, +] + + +class GeneralToolExtractDynamicVariableTool(TypedDict, total=False): + description: Required[str] + """ + Describes what the tool does, sometimes can also include information about when + to call the tool. + """ + + name: Required[str] + """Name of the tool. + + Must be unique within all tools available to LLM at any given time (general + tools + state tools + state edges). Must be consisted of a-z, A-Z, 0-9, or + contain underscores and dashes, with a maximum length of 64 (no space allowed). + """ + + type: Required[Literal["extract_dynamic_variable"]] + + variables: Required[Iterable[GeneralToolExtractDynamicVariableToolVariable]] + """The variables to be extracted.""" + + GeneralTool: TypeAlias = Union[ GeneralToolEndCallTool, GeneralToolTransferCallTool, @@ -479,6 +573,7 @@ class GeneralToolCustomTool(TypedDict, total=False): GeneralToolBookAppointmentCalTool, GeneralToolPressDigitTool, GeneralToolCustomTool, + GeneralToolExtractDynamicVariableTool, ] @@ -643,6 +738,9 @@ class StateToolTransferCallTool(TypedDict, total=False): type: Required[Literal["transfer_call"]] + custom_sip_headers: Dict[str, str] + """Custom SIP headers to be added to the call.""" + description: str """ Describes what the tool does, sometimes can also include information about when @@ -845,6 +943,85 @@ class StateToolCustomTool(TypedDict, total=False): """ +class StateToolExtractDynamicVariableToolVariableStringAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["string"]] + """Type of the variable to extract.""" + + examples: List[str] + """Examples of the variable value to teach model the style and syntax.""" + + +class StateToolExtractDynamicVariableToolVariableEnumAnalysisData(TypedDict, total=False): + choices: Required[List[str]] + """The possible values of the variable, must be non empty array.""" + + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["enum"]] + """Type of the variable to extract.""" + + +class StateToolExtractDynamicVariableToolVariableBooleanAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["boolean"]] + """Type of the variable to extract.""" + + +class StateToolExtractDynamicVariableToolVariableNumberAnalysisData(TypedDict, total=False): + description: Required[str] + """Description of the variable.""" + + name: Required[str] + """Name of the variable.""" + + type: Required[Literal["number"]] + """Type of the variable to extract.""" + + +StateToolExtractDynamicVariableToolVariable: TypeAlias = Union[ + StateToolExtractDynamicVariableToolVariableStringAnalysisData, + StateToolExtractDynamicVariableToolVariableEnumAnalysisData, + StateToolExtractDynamicVariableToolVariableBooleanAnalysisData, + StateToolExtractDynamicVariableToolVariableNumberAnalysisData, +] + + +class StateToolExtractDynamicVariableTool(TypedDict, total=False): + description: Required[str] + """ + Describes what the tool does, sometimes can also include information about when + to call the tool. + """ + + name: Required[str] + """Name of the tool. + + Must be unique within all tools available to LLM at any given time (general + tools + state tools + state edges). Must be consisted of a-z, A-Z, 0-9, or + contain underscores and dashes, with a maximum length of 64 (no space allowed). + """ + + type: Required[Literal["extract_dynamic_variable"]] + + variables: Required[Iterable[StateToolExtractDynamicVariableToolVariable]] + """The variables to be extracted.""" + + StateTool: TypeAlias = Union[ StateToolEndCallTool, StateToolTransferCallTool, @@ -852,6 +1029,7 @@ class StateToolCustomTool(TypedDict, total=False): StateToolBookAppointmentCalTool, StateToolPressDigitTool, StateToolCustomTool, + StateToolExtractDynamicVariableTool, ] diff --git a/src/retell/types/phone_call_response.py b/src/retell/types/phone_call_response.py index 7c5b999c..76c70531 100644 --- a/src/retell/types/phone_call_response.py +++ b/src/retell/types/phone_call_response.py @@ -457,6 +457,9 @@ class PhoneCallResponse(BaseModel): collected_dynamic_variables: Optional[Dict[str, object]] = None """Dynamic variables collected from the call. Only available after the call ends.""" + custom_sip_headers: Optional[Dict[str, str]] = None + """Custom SIP headers to be added to the call.""" + disconnection_reason: Optional[ Literal[ "user_hangup", diff --git a/src/retell/types/web_call_response.py b/src/retell/types/web_call_response.py index bbaee969..e7243f25 100644 --- a/src/retell/types/web_call_response.py +++ b/src/retell/types/web_call_response.py @@ -448,6 +448,9 @@ class WebCallResponse(BaseModel): collected_dynamic_variables: Optional[Dict[str, object]] = None """Dynamic variables collected from the call. Only available after the call ends.""" + custom_sip_headers: Optional[Dict[str, str]] = None + """Custom SIP headers to be added to the call.""" + disconnection_reason: Optional[ Literal[ "user_hangup", diff --git a/tests/api_resources/test_agent.py b/tests/api_resources/test_agent.py index 896161a3..f5772977 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -391,7 +391,9 @@ def test_path_params_get_versions(self, client: Retell) -> None: class TestAsyncAgent: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncRetell) -> None: diff --git a/tests/api_resources/test_batch_call.py b/tests/api_resources/test_batch_call.py index 92ce0715..86dd214a 100644 --- a/tests/api_resources/test_batch_call.py +++ b/tests/api_resources/test_batch_call.py @@ -68,7 +68,9 @@ def test_streaming_response_create_batch_call(self, client: Retell) -> None: class TestAsyncBatchCall: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create_batch_call(self, async_client: AsyncRetell) -> None: diff --git a/tests/api_resources/test_call.py b/tests/api_resources/test_call.py index c5101e9b..fdd6772f 100644 --- a/tests/api_resources/test_call.py +++ b/tests/api_resources/test_call.py @@ -220,6 +220,7 @@ def test_method_create_phone_call_with_all_params(self, client: Retell) -> None: call = client.call.create_phone_call( from_number="+14157774444", to_number="+12137774445", + custom_sip_headers={"X-Custom-Header": "Custom Value"}, metadata={}, override_agent_id="oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", override_agent_version=1, @@ -340,7 +341,9 @@ def test_streaming_response_register_phone_call(self, client: Retell) -> None: class TestAsyncCall: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncRetell) -> None: @@ -540,6 +543,7 @@ async def test_method_create_phone_call_with_all_params(self, async_client: Asyn call = await async_client.call.create_phone_call( from_number="+14157774444", to_number="+12137774445", + custom_sip_headers={"X-Custom-Header": "Custom Value"}, metadata={}, override_agent_id="oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", override_agent_version=1, diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 67c52f1e..191510c7 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -199,7 +199,9 @@ def test_path_params_end(self, client: Retell) -> None: class TestAsyncChat: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncRetell) -> None: diff --git a/tests/api_resources/test_concurrency.py b/tests/api_resources/test_concurrency.py index ad2e7353..0ddda51c 100644 --- a/tests/api_resources/test_concurrency.py +++ b/tests/api_resources/test_concurrency.py @@ -44,7 +44,9 @@ def test_streaming_response_retrieve(self, client: Retell) -> None: class TestAsyncConcurrency: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncRetell) -> None: diff --git a/tests/api_resources/test_knowledge_base.py b/tests/api_resources/test_knowledge_base.py index 234e4f5c..1a50a3b0 100644 --- a/tests/api_resources/test_knowledge_base.py +++ b/tests/api_resources/test_knowledge_base.py @@ -280,7 +280,9 @@ def test_path_params_delete_source(self, client: Retell) -> None: class TestAsyncKnowledgeBase: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @pytest.mark.skip(reason="custom code") @parametrize diff --git a/tests/api_resources/test_llm.py b/tests/api_resources/test_llm.py index a2aef83a..968fa6c6 100644 --- a/tests/api_resources/test_llm.py +++ b/tests/api_resources/test_llm.py @@ -68,6 +68,7 @@ def test_method_create_with_all_params(self, client: Retell) -> None: "show_transferee_as_caller": False, }, "type": "transfer_call", + "custom_sip_headers": {"X-Custom-Header": "Custom Value"}, "description": "Transfer to the support team.", } ], @@ -224,6 +225,7 @@ def test_method_update_with_all_params(self, client: Retell) -> None: "show_transferee_as_caller": False, }, "type": "transfer_call", + "custom_sip_headers": {"X-Custom-Header": "Custom Value"}, "description": "Transfer to the support team.", } ], @@ -355,7 +357,9 @@ def test_path_params_delete(self, client: Retell) -> None: class TestAsyncLlm: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncRetell) -> None: @@ -408,6 +412,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncRetell) -> "show_transferee_as_caller": False, }, "type": "transfer_call", + "custom_sip_headers": {"X-Custom-Header": "Custom Value"}, "description": "Transfer to the support team.", } ], @@ -564,6 +569,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncRetell) -> "show_transferee_as_caller": False, }, "type": "transfer_call", + "custom_sip_headers": {"X-Custom-Header": "Custom Value"}, "description": "Transfer to the support team.", } ], diff --git a/tests/api_resources/test_phone_number.py b/tests/api_resources/test_phone_number.py index 8da86b96..d039e790 100644 --- a/tests/api_resources/test_phone_number.py +++ b/tests/api_resources/test_phone_number.py @@ -263,7 +263,9 @@ def test_streaming_response_import(self, client: Retell) -> None: class TestAsyncPhoneNumber: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncRetell) -> None: diff --git a/tests/api_resources/test_voice.py b/tests/api_resources/test_voice.py index 03ecc8be..06a6e7d1 100644 --- a/tests/api_resources/test_voice.py +++ b/tests/api_resources/test_voice.py @@ -82,7 +82,9 @@ def test_streaming_response_list(self, client: Retell) -> None: class TestAsyncVoice: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncRetell) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 360deb5e..5c1b1500 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from retell import Retell, AsyncRetell +from retell import Retell, AsyncRetell, DefaultAioHttpClient +from retell._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -25,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -43,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Retell]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncRetell]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncRetell(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncRetell( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 528a649c..01d76010 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,12 +23,16 @@ from retell import Retell, AsyncRetell, APIResponseValidationError from retell._types import Omit -from retell._utils import maybe_transform from retell._models import BaseModel, FinalRequestOptions -from retell._constants import RAW_RESPONSE_HEADER from retell._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError -from retell._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options -from retell.types.agent_create_params import AgentCreateParams +from retell._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + make_request_options, +) from .utils import update_env @@ -187,6 +191,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -697,56 +702,33 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("retell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Retell) -> None: respx_mock.post("/create-agent").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/create-agent", - body=cast( - object, - maybe_transform( - dict( - response_engine={ - "llm_id": "llm_234sdertfsdsfsdf", - "type": "retell-llm", - }, - voice_id="11labs-Adrian", - ), - AgentCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.agent.with_streaming_response.create( + response_engine={ + "llm_id": "llm_234sdertfsdsfsdf", + "type": "retell-llm", + }, + voice_id="11labs-Adrian", + ).__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("retell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Retell) -> None: respx_mock.post("/create-agent").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/create-agent", - body=cast( - object, - maybe_transform( - dict( - response_engine={ - "llm_id": "llm_234sdertfsdsfsdf", - "type": "retell-llm", - }, - voice_id="11labs-Adrian", - ), - AgentCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.agent.with_streaming_response.create( + response_engine={ + "llm_id": "llm_234sdertfsdsfsdf", + "type": "retell-llm", + }, + voice_id="11labs-Adrian", + ).__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -846,6 +828,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects @@ -1009,6 +1013,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -1535,56 +1540,33 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("retell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncRetell) -> None: respx_mock.post("/create-agent").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/create-agent", - body=cast( - object, - maybe_transform( - dict( - response_engine={ - "llm_id": "llm_234sdertfsdsfsdf", - "type": "retell-llm", - }, - voice_id="11labs-Adrian", - ), - AgentCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.agent.with_streaming_response.create( + response_engine={ + "llm_id": "llm_234sdertfsdsfsdf", + "type": "retell-llm", + }, + voice_id="11labs-Adrian", + ).__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("retell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncRetell) -> None: respx_mock.post("/create-agent").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/create-agent", - body=cast( - object, - maybe_transform( - dict( - response_engine={ - "llm_id": "llm_234sdertfsdsfsdf", - "type": "retell-llm", - }, - voice_id="11labs-Adrian", - ), - AgentCreateParams, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.agent.with_streaming_response.create( + response_engine={ + "llm_id": "llm_234sdertfsdsfsdf", + "type": "retell-llm", + }, + voice_id="11labs-Adrian", + ).__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1732,6 +1714,28 @@ async def test_main() -> None: time.sleep(0.1) + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) async def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects