# This file was auto-generated by Fern from our API Definition.

import typing
import urllib.parse
from json.decoder import JSONDecodeError

from ..calls.types.analyze_call_response import AnalyzeCallResponse
from ..commons.errors.server_error import ServerError
from ..commons.errors.unauthorized_error import UnauthorizedError
from ..commons.types.error_body import ErrorBody
from ..commons.types.phone_number import PhoneNumber
from ..commons.types.tools import Tools
from ..commons.types.voice_id import VoiceId
from ..commons.types.webhook import Webhook
from ..core.api_error import ApiError
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.jsonable_encoder import jsonable_encoder
from ..core.pydantic_utilities import pydantic_v1
from ..core.remove_none_from_dict import remove_none_from_dict
from ..core.request_options import RequestOptions
from ..types.dynamic_data import DynamicData
from ..types.interruption_threshold import InterruptionThreshold
from ..types.model_enum import ModelEnum
from ..types.prounciation_guide import ProunciationGuide
from ..types.temperature import Temperature
from ..types.transfer_list import TransferList
from ..types.voicemail_action import VoicemailAction
from .types.batch_call_data import BatchCallData
from .types.batch_details_response import BatchDetailsResponse
from .types.list_batches_response import ListBatchesResponse
from .types.retrieve_batch_analysis_response import RetrieveBatchAnalysisResponse
from .types.send_batch_response import SendBatchResponse
from .types.stop_active_batch_response import StopActiveBatchResponse

# this is used as the default value for optional parameters
OMIT = typing.cast(typing.Any, ...)


class BatchesClient:
    def __init__(self, *, client_wrapper: SyncClientWrapper):
        self._client_wrapper = client_wrapper

    def send(
        self,
        *,
        base_prompt: str,
        call_data: typing.Sequence[BatchCallData],
        phone_number: PhoneNumber,
        task: str,
        temperature: Temperature,
        tools: Tools,
        interruption_threshold: InterruptionThreshold,
        label: typing.Optional[str] = OMIT,
        campaign_id: typing.Optional[str] = OMIT,
        test_mode: typing.Optional[bool] = OMIT,
        transfer_list: typing.Optional[TransferList] = OMIT,
        model: typing.Optional[ModelEnum] = OMIT,
        pronunciation_guide: typing.Optional[ProunciationGuide] = OMIT,
        transfer_phone_number: typing.Optional[PhoneNumber] = OMIT,
        answered_by_enabled: typing.Optional[bool] = OMIT,
        from_: typing.Optional[str] = OMIT,
        reduce_latency: typing.Optional[bool] = OMIT,
        voice_id: typing.Optional[VoiceId] = OMIT,
        voice_preset_id: typing.Optional[str] = OMIT,
        start_time: typing.Optional[str] = OMIT,
        webhook: typing.Optional[Webhook] = OMIT,
        wait_for_greeting: typing.Optional[bool] = OMIT,
        first_sentence: typing.Optional[str] = OMIT,
        record: typing.Optional[bool] = OMIT,
        voice_settings: typing.Optional[bool] = OMIT,
        language: typing.Optional[str] = OMIT,
        max_duration: typing.Optional[int] = OMIT,
        voicemail_action: typing.Optional[VoicemailAction] = OMIT,
        amd: typing.Optional[bool] = OMIT,
        request_data: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        dynamic_data: typing.Optional[typing.Sequence[DynamicData]] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> SendBatchResponse:
        """
        Send large volumes of calls at once with a single API request.

        Parameters
        ----------
        base_prompt : str
            This is the prompt or task used for all the phone calls in the request.

            Information can be inserted into it surrounding variable names with double curly braces.

        call_data : typing.Sequence[BatchCallData]
            Define a list of calls to make and their properties.

            Each call in call_data **MUST** have a `phone_number` property. Properties are case-sensitive.

        phone_number : PhoneNumber
            The phone number to call. Country code defaults to `+1` (US) if not specified.

            Formatting is flexible, however for the most predictable results use the **[E.164](https://www.twilio.com/docs/glossary/what-e164#examples-of-e164-numbers)** format.

        task : str
            Provide instructions, relevant information, and examples of the ideal conversation flow.

        temperature : Temperature
            A value between 0 and 1 that controls the randomness of the LLM. 0 will cause more deterministic outputs while 1 will cause more random.

        tools : Tools

        interruption_threshold : InterruptionThreshold
            When you increase the interruption latency, you force the AI phone agent to listen longer before responding. In practice, increasing the threshold results in less interruptions and more latency.

            Try setting the threshold to `500` milliseconds. You'll encounter higher latency, but you'll be interrupted much less frequently.

        label : typing.Optional[str]
            Adds a user-friendly label to your batch to keep track of it's original intention. This can help differentiate multiple call batches that are part of the same Campaign. Shown when a batch is retreived.

        campaign_id : typing.Optional[str]
            Use `campaign_id` to organize related batches together. This can be set manually or auto-generated through Campaigns.

        test_mode : typing.Optional[bool]
            When this is set to `true`, only the first call of `call_data` will be dispatched. A common use case is to set the first `phone_number` value to your own to confirm everything's set up properly.

            Includes additional information in the response when true so that it's easier to find any issues.

            If no value is provided, `test_mode` defaults to `false`.

        transfer_list : typing.Optional[TransferList]
            Give your agent the ability to transfer calls to a set of phone numbers.

            Overrides `transfer_phone_number` if a `transfer_list.default` is specified.

            Will default to `transfer_list.default`, or the chosen phone number.

        model : typing.Optional[ModelEnum]
            Select a model to use for your call.

            In nearly all cases, `enhanced` is the best choice for now.

        pronunciation_guide : typing.Optional[ProunciationGuide]
            The pronunciation guide is an `array` of `objects` that guides the LLM on how to say specific words. This is great for situations with complicated terms or names.

        transfer_phone_number : typing.Optional[PhoneNumber]
            A phone number that the agent can transfer to under specific conditions - such as being asked to speak to a human or supervisor.

        answered_by_enabled : typing.Optional[bool]
            Enables machine detection when the call starts to determine whether the call was answered by a person or a voicemail.

            Best Practices (when enabled) -

                - Since the determination is made at the beginning of the call, use `wait_for_greeting` to try and coax a human response.

                - If combined with first_sentence, try wording it so the person answering says something back - ex. `"Hello?"`

            Price - `$0.02` per call, however there is no charge for unanswered calls or calls that failed to send.

        from_ : typing.Optional[str]
            Specify a purchased Outbound Number to call from. Country code is required, spaces or parentheses must be excluded.

            By default, calls are initiated from a separate pool of numbers owned by Bland.

        reduce_latency : typing.Optional[bool]
            Reducing latency means that the agent will generate responses more quickly and have less of a delay. This must be set to `true` when using Voice Clones.

        voice_id : typing.Optional[VoiceId]
            Determines the voice of the AI agent, in conjunction with `reduce_latency`.

            Use the `GET /v1/voices endpoint` to see a full list of your available voices.

            To create your own Voice Clone, visit our [Custom Voice Cloning page](https://app.bland.ai/home?page=voice-clone).

        voice_preset_id : typing.Optional[str]
            Use a voice preset instead of specifying individual voice settings.

            To create a voice preset, see [Create a Voice Preset](https://docs.bland.ai/api-v1/post/voices).

        start_time : typing.Optional[str]
            The time you want the call to start. If you don't specify a time (or the time is in the past), the call will send immediately.

            Set your time in the format `YYYY-MM-DD HH:MM:SS -HH:MM` (ex. `2021-01-01 12:00:00 -05:00`).

            The timezone is optional, and defaults to `UTC` if not specified.

            Note - Scheduled calls can be cancelled with the `POST /v1/calls/:call_id/stop` endpoint.

        webhook : typing.Optional[Webhook]
            The webhook should be a http / https callback url. We will send the call_id and transcript to this URL after the call completes. This can be useful if you want to have real time notifications when calls finish.

        wait_for_greeting : typing.Optional[bool]
            Should the AI speak first or wait for someone else to talk?

            Creates more realistic conversations when answered with “Hello?”, “This is {name} speaking.” and so on.

                - When `false`, The AI starts speaking shortly after the call is answered.

                - When `true`, The AI will wait for the answerer to speak.

        first_sentence : typing.Optional[str]
            A phrase that your call will start with instead of a generating one on the fly. This works both with and without `wait_for_greeting`. Can be more than one sentence, but must be less than 200 characters.

        record : typing.Optional[bool]
            To record your phone call, set `record` to true. When your call completes, you can access through the `recording_url` field in the call details or your webhook.

        voice_settings : typing.Optional[bool]
            Should the AI speak first or wait for someone else to talk? Creates more realistic conversations when answered with “Hello?”, “This is {name} speaking.” and so on. When `false` - The AI starts speaking shortly after the call is answered. When `true` - The AI will wait for the answerer to speak.

        language : typing.Optional[str]
            Select a supported language of your choice. Optimizes every part of our API for that language - transcription, speech, and other inner workings.

            Supported Languages and their codes -

            - English - `eng`
            - Spanish - `esp`
            - French - `fre`
            - Polish - `pol`

        max_duration : typing.Optional[int]
            Set the longest you want the call to possibly go in minutes. After the max_duration minutes have passed, the call will automatically end. Example Values - `20, 2`

        voicemail_action : typing.Optional[VoicemailAction]
            Voicemail action tells the AI what to do when encountering a voicemail. This has 96% accuracy. There is no such thing as a perfect VM detection, but this gets close.

            The default value is hangup to save money and keep most users in compliance.

            Note - **Leaving voicemails is strongly discouraged.**

        amd : typing.Optional[bool]
            AMD mode helps our AI navigate phone trees and IVR systems. If you know your call will hit an automated system you should switch it on.

            NOTE - AMD mode causes increased delay for the first response, even if answered by a human. Highly recommended to set to `false` in the majority of cases.

        request_data : typing.Optional[typing.Dict[str, typing.Any]]
            When you want your AI to “know” a specific fact - like the caller's name or other relevant context.

            The AI agent will be aware of both the key names as well as their corresponding values.

        dynamic_data : typing.Optional[typing.Sequence[DynamicData]]
            Make dynamic requests to external APIs and use the data in your AI's responses.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        SendBatchResponse

        Examples
        --------
        from bland import BatchCallData
        from bland.client import BlandAI

        client = BlandAI(
            api_key="YOUR_API_KEY",
        )
        client.batches.send(
            phone_number="29382721828",
            task="Would love for you to check out our AI API!",
            base_prompt="You are calling a business to renew their subscription to a service before it expires on a date.",
            call_data=[
                BatchCallData(
                    phone_number="1234567890",
                    business="ABC Corp",
                    service="Netflix",
                    date="September 4th",
                ),
                BatchCallData(
                    phone_number="32176540987",
                    business="XYZ inc.",
                    service="Window Cleaning",
                    date="December 20th",
                ),
            ],
            label="Subscription Renewal",
            campaign_id="1234",
            test_mode=True,
        )
        """
        _request: typing.Dict[str, typing.Any] = {
            "base_prompt": base_prompt,
            "call_data": call_data,
            "phone_number": phone_number,
            "task": task,
            "temperature": temperature,
            "tools": tools,
            "interruption_threshold": interruption_threshold,
        }
        if label is not OMIT:
            _request["label"] = label
        if campaign_id is not OMIT:
            _request["campaign_id"] = campaign_id
        if test_mode is not OMIT:
            _request["test_mode"] = test_mode
        if transfer_list is not OMIT:
            _request["transfer_list"] = transfer_list
        if model is not OMIT:
            _request["model"] = model
        if pronunciation_guide is not OMIT:
            _request["pronunciation_guide"] = pronunciation_guide
        if transfer_phone_number is not OMIT:
            _request["transfer_phone_number"] = transfer_phone_number
        if answered_by_enabled is not OMIT:
            _request["answered_by_enabled"] = answered_by_enabled
        if from_ is not OMIT:
            _request["from"] = from_
        if reduce_latency is not OMIT:
            _request["reduce_latency"] = reduce_latency
        if voice_id is not OMIT:
            _request["voice_id"] = voice_id
        if voice_preset_id is not OMIT:
            _request["voice_preset_id"] = voice_preset_id
        if start_time is not OMIT:
            _request["start_time"] = start_time
        if webhook is not OMIT:
            _request["webhook"] = webhook
        if wait_for_greeting is not OMIT:
            _request["wait_for_greeting"] = wait_for_greeting
        if first_sentence is not OMIT:
            _request["first_sentence"] = first_sentence
        if record is not OMIT:
            _request["record"] = record
        if voice_settings is not OMIT:
            _request["voice_settings"] = voice_settings
        if language is not OMIT:
            _request["language"] = language
        if max_duration is not OMIT:
            _request["max_duration"] = max_duration
        if voicemail_action is not OMIT:
            _request["voicemail_action"] = voicemail_action
        if amd is not OMIT:
            _request["amd"] = amd
        if request_data is not OMIT:
            _request["request_data"] = request_data
        if dynamic_data is not OMIT:
            _request["dynamic_data"] = dynamic_data
        _response = self._client_wrapper.httpx_client.request(
            method="POST",
            url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "v1/batches"),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            json=jsonable_encoder(_request)
            if request_options is None or request_options.get("additional_body_parameters") is None
            else {
                **jsonable_encoder(_request),
                **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))),
            },
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(SendBatchResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    def analyze(
        self,
        batch_id: str,
        *,
        goal: str,
        questions: typing.Sequence[typing.Sequence[str]],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> AnalyzeCallResponse:
        """
        Analyzes a batch of calls based on using questions and goals.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the batch of calls to be analyzed.

        goal : str
            This is the overall purpose of the batch of calls. Provides context for the analysis to guide how the questions/transcripts are interpreted.

        questions : typing.Sequence[typing.Sequence[str]]
            An array of questions to be analyzed for each call in the batch.

            Each question should be an array with two elements: the question text and the expected answer type (e.g., “string”, “boolean”).

            Fairly flexible in terms of the expected answer type, and unanswerable questions will default to `null`.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        AnalyzeCallResponse

        Examples
        --------
        from bland.client import BlandAI

        client = BlandAI(
            api_key="YOUR_API_KEY",
        )
        client.batches.analyze(
            batch_id="string",
            goal="Renewal Confirmation",
            questions=[
                ["Who answered the call?", "human or voicemail"],
                ["Positive feedback about the product: ", "string"],
                ["Negative feedback about the product: ", "string"],
                ["Customer confirmed they were satisfied", "boolean"],
            ],
        )
        """
        _request: typing.Dict[str, typing.Any] = {"goal": goal, "questions": questions}
        _response = self._client_wrapper.httpx_client.request(
            method="POST",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}/analyze"
            ),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            json=jsonable_encoder(_request)
            if request_options is None or request_options.get("additional_body_parameters") is None
            else {
                **jsonable_encoder(_request),
                **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))),
            },
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(AnalyzeCallResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    def stop(
        self, batch_id: str, *, request_options: typing.Optional[RequestOptions] = None
    ) -> StopActiveBatchResponse:
        """
        Stops all active calls in a batch.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the batch of calls to be cancelled.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        StopActiveBatchResponse

        Examples
        --------
        from bland.client import BlandAI

        client = BlandAI(
            api_key="YOUR_API_KEY",
        )
        client.batches.stop(
            batch_id="string",
        )
        """
        _response = self._client_wrapper.httpx_client.request(
            method="POST",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}/stop"
            ),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            json=jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))
            if request_options is not None
            else None,
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(StopActiveBatchResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListBatchesResponse:
        """
        Retrieves batch-specific data for each batch you've created.

        Parameters
        ----------
        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        ListBatchesResponse

        Examples
        --------
        from bland.client import BlandAI

        client = BlandAI(
            api_key="YOUR_API_KEY",
        )
        client.batches.list()
        """
        _response = self._client_wrapper.httpx_client.request(
            method="GET",
            url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "v1/batches"),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(ListBatchesResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    def details(
        self,
        batch_id: str,
        *,
        include_calls: typing.Optional[bool] = None,
        include_transcripts: typing.Optional[bool] = None,
        include_analysis: typing.Optional[bool] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> BatchDetailsResponse:
        """
        Retrieves calls and batch data for a specific batch_id.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the batch of calls you want to retrieve.

        include_calls : typing.Optional[bool]
            Whether or not to include individual call data in the response.

            If no value is provided, `include_calls` defaults to `true`.

        include_transcripts : typing.Optional[bool]
            If calls are included, can be set to false to exclude transcripts from the response.

            If no value is provided, `include_transcripts` defaults to `true`.

        include_analysis : typing.Optional[bool]
            If calls are included, can be set to false to exclude analysis from the response.

            If no value is provided, `include_analysis` defaults to `true`.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        BatchDetailsResponse

        Examples
        --------
        from bland.client import BlandAI

        client = BlandAI(
            api_key="YOUR_API_KEY",
        )
        client.batches.details(
            batch_id="string",
            include_calls=True,
            include_transcripts=True,
            include_analysis=True,
        )
        """
        _response = self._client_wrapper.httpx_client.request(
            method="GET",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}"
            ),
            params=jsonable_encoder(
                remove_none_from_dict(
                    {
                        "include_calls": include_calls,
                        "include_transcripts": include_transcripts,
                        "include_analysis": include_analysis,
                        **(
                            request_options.get("additional_query_parameters", {})
                            if request_options is not None
                            else {}
                        ),
                    }
                )
            ),
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(BatchDetailsResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    def retrieve(
        self, batch_id: str, *, request_options: typing.Optional[RequestOptions] = None
    ) -> RetrieveBatchAnalysisResponse:
        """
        Retrieves the analyses for a specific batch of calls, including details of each call's analysis.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the call batch. Returned in the response when creating a batch, or when listing all batches.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        RetrieveBatchAnalysisResponse

        Examples
        --------
        from bland.client import BlandAI

        client = BlandAI(
            api_key="YOUR_API_KEY",
        )
        client.batches.retrieve(
            batch_id="string",
        )
        """
        _response = self._client_wrapper.httpx_client.request(
            method="GET",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}/analysis"
            ),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(RetrieveBatchAnalysisResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)


class AsyncBatchesClient:
    def __init__(self, *, client_wrapper: AsyncClientWrapper):
        self._client_wrapper = client_wrapper

    async def send(
        self,
        *,
        base_prompt: str,
        call_data: typing.Sequence[BatchCallData],
        phone_number: PhoneNumber,
        task: str,
        temperature: Temperature,
        tools: Tools,
        interruption_threshold: InterruptionThreshold,
        label: typing.Optional[str] = OMIT,
        campaign_id: typing.Optional[str] = OMIT,
        test_mode: typing.Optional[bool] = OMIT,
        transfer_list: typing.Optional[TransferList] = OMIT,
        model: typing.Optional[ModelEnum] = OMIT,
        pronunciation_guide: typing.Optional[ProunciationGuide] = OMIT,
        transfer_phone_number: typing.Optional[PhoneNumber] = OMIT,
        answered_by_enabled: typing.Optional[bool] = OMIT,
        from_: typing.Optional[str] = OMIT,
        reduce_latency: typing.Optional[bool] = OMIT,
        voice_id: typing.Optional[VoiceId] = OMIT,
        voice_preset_id: typing.Optional[str] = OMIT,
        start_time: typing.Optional[str] = OMIT,
        webhook: typing.Optional[Webhook] = OMIT,
        wait_for_greeting: typing.Optional[bool] = OMIT,
        first_sentence: typing.Optional[str] = OMIT,
        record: typing.Optional[bool] = OMIT,
        voice_settings: typing.Optional[bool] = OMIT,
        language: typing.Optional[str] = OMIT,
        max_duration: typing.Optional[int] = OMIT,
        voicemail_action: typing.Optional[VoicemailAction] = OMIT,
        amd: typing.Optional[bool] = OMIT,
        request_data: typing.Optional[typing.Dict[str, typing.Any]] = OMIT,
        dynamic_data: typing.Optional[typing.Sequence[DynamicData]] = OMIT,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> SendBatchResponse:
        """
        Send large volumes of calls at once with a single API request.

        Parameters
        ----------
        base_prompt : str
            This is the prompt or task used for all the phone calls in the request.

            Information can be inserted into it surrounding variable names with double curly braces.

        call_data : typing.Sequence[BatchCallData]
            Define a list of calls to make and their properties.

            Each call in call_data **MUST** have a `phone_number` property. Properties are case-sensitive.

        phone_number : PhoneNumber
            The phone number to call. Country code defaults to `+1` (US) if not specified.

            Formatting is flexible, however for the most predictable results use the **[E.164](https://www.twilio.com/docs/glossary/what-e164#examples-of-e164-numbers)** format.

        task : str
            Provide instructions, relevant information, and examples of the ideal conversation flow.

        temperature : Temperature
            A value between 0 and 1 that controls the randomness of the LLM. 0 will cause more deterministic outputs while 1 will cause more random.

        tools : Tools

        interruption_threshold : InterruptionThreshold
            When you increase the interruption latency, you force the AI phone agent to listen longer before responding. In practice, increasing the threshold results in less interruptions and more latency.

            Try setting the threshold to `500` milliseconds. You'll encounter higher latency, but you'll be interrupted much less frequently.

        label : typing.Optional[str]
            Adds a user-friendly label to your batch to keep track of it's original intention. This can help differentiate multiple call batches that are part of the same Campaign. Shown when a batch is retreived.

        campaign_id : typing.Optional[str]
            Use `campaign_id` to organize related batches together. This can be set manually or auto-generated through Campaigns.

        test_mode : typing.Optional[bool]
            When this is set to `true`, only the first call of `call_data` will be dispatched. A common use case is to set the first `phone_number` value to your own to confirm everything's set up properly.

            Includes additional information in the response when true so that it's easier to find any issues.

            If no value is provided, `test_mode` defaults to `false`.

        transfer_list : typing.Optional[TransferList]
            Give your agent the ability to transfer calls to a set of phone numbers.

            Overrides `transfer_phone_number` if a `transfer_list.default` is specified.

            Will default to `transfer_list.default`, or the chosen phone number.

        model : typing.Optional[ModelEnum]
            Select a model to use for your call.

            In nearly all cases, `enhanced` is the best choice for now.

        pronunciation_guide : typing.Optional[ProunciationGuide]
            The pronunciation guide is an `array` of `objects` that guides the LLM on how to say specific words. This is great for situations with complicated terms or names.

        transfer_phone_number : typing.Optional[PhoneNumber]
            A phone number that the agent can transfer to under specific conditions - such as being asked to speak to a human or supervisor.

        answered_by_enabled : typing.Optional[bool]
            Enables machine detection when the call starts to determine whether the call was answered by a person or a voicemail.

            Best Practices (when enabled) -

                - Since the determination is made at the beginning of the call, use `wait_for_greeting` to try and coax a human response.

                - If combined with first_sentence, try wording it so the person answering says something back - ex. `"Hello?"`

            Price - `$0.02` per call, however there is no charge for unanswered calls or calls that failed to send.

        from_ : typing.Optional[str]
            Specify a purchased Outbound Number to call from. Country code is required, spaces or parentheses must be excluded.

            By default, calls are initiated from a separate pool of numbers owned by Bland.

        reduce_latency : typing.Optional[bool]
            Reducing latency means that the agent will generate responses more quickly and have less of a delay. This must be set to `true` when using Voice Clones.

        voice_id : typing.Optional[VoiceId]
            Determines the voice of the AI agent, in conjunction with `reduce_latency`.

            Use the `GET /v1/voices endpoint` to see a full list of your available voices.

            To create your own Voice Clone, visit our [Custom Voice Cloning page](https://app.bland.ai/home?page=voice-clone).

        voice_preset_id : typing.Optional[str]
            Use a voice preset instead of specifying individual voice settings.

            To create a voice preset, see [Create a Voice Preset](https://docs.bland.ai/api-v1/post/voices).

        start_time : typing.Optional[str]
            The time you want the call to start. If you don't specify a time (or the time is in the past), the call will send immediately.

            Set your time in the format `YYYY-MM-DD HH:MM:SS -HH:MM` (ex. `2021-01-01 12:00:00 -05:00`).

            The timezone is optional, and defaults to `UTC` if not specified.

            Note - Scheduled calls can be cancelled with the `POST /v1/calls/:call_id/stop` endpoint.

        webhook : typing.Optional[Webhook]
            The webhook should be a http / https callback url. We will send the call_id and transcript to this URL after the call completes. This can be useful if you want to have real time notifications when calls finish.

        wait_for_greeting : typing.Optional[bool]
            Should the AI speak first or wait for someone else to talk?

            Creates more realistic conversations when answered with “Hello?”, “This is {name} speaking.” and so on.

                - When `false`, The AI starts speaking shortly after the call is answered.

                - When `true`, The AI will wait for the answerer to speak.

        first_sentence : typing.Optional[str]
            A phrase that your call will start with instead of a generating one on the fly. This works both with and without `wait_for_greeting`. Can be more than one sentence, but must be less than 200 characters.

        record : typing.Optional[bool]
            To record your phone call, set `record` to true. When your call completes, you can access through the `recording_url` field in the call details or your webhook.

        voice_settings : typing.Optional[bool]
            Should the AI speak first or wait for someone else to talk? Creates more realistic conversations when answered with “Hello?”, “This is {name} speaking.” and so on. When `false` - The AI starts speaking shortly after the call is answered. When `true` - The AI will wait for the answerer to speak.

        language : typing.Optional[str]
            Select a supported language of your choice. Optimizes every part of our API for that language - transcription, speech, and other inner workings.

            Supported Languages and their codes -

            - English - `eng`
            - Spanish - `esp`
            - French - `fre`
            - Polish - `pol`

        max_duration : typing.Optional[int]
            Set the longest you want the call to possibly go in minutes. After the max_duration minutes have passed, the call will automatically end. Example Values - `20, 2`

        voicemail_action : typing.Optional[VoicemailAction]
            Voicemail action tells the AI what to do when encountering a voicemail. This has 96% accuracy. There is no such thing as a perfect VM detection, but this gets close.

            The default value is hangup to save money and keep most users in compliance.

            Note - **Leaving voicemails is strongly discouraged.**

        amd : typing.Optional[bool]
            AMD mode helps our AI navigate phone trees and IVR systems. If you know your call will hit an automated system you should switch it on.

            NOTE - AMD mode causes increased delay for the first response, even if answered by a human. Highly recommended to set to `false` in the majority of cases.

        request_data : typing.Optional[typing.Dict[str, typing.Any]]
            When you want your AI to “know” a specific fact - like the caller's name or other relevant context.

            The AI agent will be aware of both the key names as well as their corresponding values.

        dynamic_data : typing.Optional[typing.Sequence[DynamicData]]
            Make dynamic requests to external APIs and use the data in your AI's responses.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        SendBatchResponse

        Examples
        --------
        from bland import BatchCallData
        from bland.client import AsyncBlandAI

        client = AsyncBlandAI(
            api_key="YOUR_API_KEY",
        )
        await client.batches.send(
            phone_number="29382721828",
            task="Would love for you to check out our AI API!",
            base_prompt="You are calling a business to renew their subscription to a service before it expires on a date.",
            call_data=[
                BatchCallData(
                    phone_number="1234567890",
                    business="ABC Corp",
                    service="Netflix",
                    date="September 4th",
                ),
                BatchCallData(
                    phone_number="32176540987",
                    business="XYZ inc.",
                    service="Window Cleaning",
                    date="December 20th",
                ),
            ],
            label="Subscription Renewal",
            campaign_id="1234",
            test_mode=True,
        )
        """
        _request: typing.Dict[str, typing.Any] = {
            "base_prompt": base_prompt,
            "call_data": call_data,
            "phone_number": phone_number,
            "task": task,
            "temperature": temperature,
            "tools": tools,
            "interruption_threshold": interruption_threshold,
        }
        if label is not OMIT:
            _request["label"] = label
        if campaign_id is not OMIT:
            _request["campaign_id"] = campaign_id
        if test_mode is not OMIT:
            _request["test_mode"] = test_mode
        if transfer_list is not OMIT:
            _request["transfer_list"] = transfer_list
        if model is not OMIT:
            _request["model"] = model
        if pronunciation_guide is not OMIT:
            _request["pronunciation_guide"] = pronunciation_guide
        if transfer_phone_number is not OMIT:
            _request["transfer_phone_number"] = transfer_phone_number
        if answered_by_enabled is not OMIT:
            _request["answered_by_enabled"] = answered_by_enabled
        if from_ is not OMIT:
            _request["from"] = from_
        if reduce_latency is not OMIT:
            _request["reduce_latency"] = reduce_latency
        if voice_id is not OMIT:
            _request["voice_id"] = voice_id
        if voice_preset_id is not OMIT:
            _request["voice_preset_id"] = voice_preset_id
        if start_time is not OMIT:
            _request["start_time"] = start_time
        if webhook is not OMIT:
            _request["webhook"] = webhook
        if wait_for_greeting is not OMIT:
            _request["wait_for_greeting"] = wait_for_greeting
        if first_sentence is not OMIT:
            _request["first_sentence"] = first_sentence
        if record is not OMIT:
            _request["record"] = record
        if voice_settings is not OMIT:
            _request["voice_settings"] = voice_settings
        if language is not OMIT:
            _request["language"] = language
        if max_duration is not OMIT:
            _request["max_duration"] = max_duration
        if voicemail_action is not OMIT:
            _request["voicemail_action"] = voicemail_action
        if amd is not OMIT:
            _request["amd"] = amd
        if request_data is not OMIT:
            _request["request_data"] = request_data
        if dynamic_data is not OMIT:
            _request["dynamic_data"] = dynamic_data
        _response = await self._client_wrapper.httpx_client.request(
            method="POST",
            url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "v1/batches"),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            json=jsonable_encoder(_request)
            if request_options is None or request_options.get("additional_body_parameters") is None
            else {
                **jsonable_encoder(_request),
                **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))),
            },
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(SendBatchResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    async def analyze(
        self,
        batch_id: str,
        *,
        goal: str,
        questions: typing.Sequence[typing.Sequence[str]],
        request_options: typing.Optional[RequestOptions] = None,
    ) -> AnalyzeCallResponse:
        """
        Analyzes a batch of calls based on using questions and goals.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the batch of calls to be analyzed.

        goal : str
            This is the overall purpose of the batch of calls. Provides context for the analysis to guide how the questions/transcripts are interpreted.

        questions : typing.Sequence[typing.Sequence[str]]
            An array of questions to be analyzed for each call in the batch.

            Each question should be an array with two elements: the question text and the expected answer type (e.g., “string”, “boolean”).

            Fairly flexible in terms of the expected answer type, and unanswerable questions will default to `null`.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        AnalyzeCallResponse

        Examples
        --------
        from bland.client import AsyncBlandAI

        client = AsyncBlandAI(
            api_key="YOUR_API_KEY",
        )
        await client.batches.analyze(
            batch_id="string",
            goal="Renewal Confirmation",
            questions=[
                ["Who answered the call?", "human or voicemail"],
                ["Positive feedback about the product: ", "string"],
                ["Negative feedback about the product: ", "string"],
                ["Customer confirmed they were satisfied", "boolean"],
            ],
        )
        """
        _request: typing.Dict[str, typing.Any] = {"goal": goal, "questions": questions}
        _response = await self._client_wrapper.httpx_client.request(
            method="POST",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}/analyze"
            ),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            json=jsonable_encoder(_request)
            if request_options is None or request_options.get("additional_body_parameters") is None
            else {
                **jsonable_encoder(_request),
                **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))),
            },
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(AnalyzeCallResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    async def stop(
        self, batch_id: str, *, request_options: typing.Optional[RequestOptions] = None
    ) -> StopActiveBatchResponse:
        """
        Stops all active calls in a batch.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the batch of calls to be cancelled.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        StopActiveBatchResponse

        Examples
        --------
        from bland.client import AsyncBlandAI

        client = AsyncBlandAI(
            api_key="YOUR_API_KEY",
        )
        await client.batches.stop(
            batch_id="string",
        )
        """
        _response = await self._client_wrapper.httpx_client.request(
            method="POST",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}/stop"
            ),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            json=jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))
            if request_options is not None
            else None,
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(StopActiveBatchResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListBatchesResponse:
        """
        Retrieves batch-specific data for each batch you've created.

        Parameters
        ----------
        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        ListBatchesResponse

        Examples
        --------
        from bland.client import AsyncBlandAI

        client = AsyncBlandAI(
            api_key="YOUR_API_KEY",
        )
        await client.batches.list()
        """
        _response = await self._client_wrapper.httpx_client.request(
            method="GET",
            url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "v1/batches"),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(ListBatchesResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    async def details(
        self,
        batch_id: str,
        *,
        include_calls: typing.Optional[bool] = None,
        include_transcripts: typing.Optional[bool] = None,
        include_analysis: typing.Optional[bool] = None,
        request_options: typing.Optional[RequestOptions] = None,
    ) -> BatchDetailsResponse:
        """
        Retrieves calls and batch data for a specific batch_id.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the batch of calls you want to retrieve.

        include_calls : typing.Optional[bool]
            Whether or not to include individual call data in the response.

            If no value is provided, `include_calls` defaults to `true`.

        include_transcripts : typing.Optional[bool]
            If calls are included, can be set to false to exclude transcripts from the response.

            If no value is provided, `include_transcripts` defaults to `true`.

        include_analysis : typing.Optional[bool]
            If calls are included, can be set to false to exclude analysis from the response.

            If no value is provided, `include_analysis` defaults to `true`.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        BatchDetailsResponse

        Examples
        --------
        from bland.client import AsyncBlandAI

        client = AsyncBlandAI(
            api_key="YOUR_API_KEY",
        )
        await client.batches.details(
            batch_id="string",
            include_calls=True,
            include_transcripts=True,
            include_analysis=True,
        )
        """
        _response = await self._client_wrapper.httpx_client.request(
            method="GET",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}"
            ),
            params=jsonable_encoder(
                remove_none_from_dict(
                    {
                        "include_calls": include_calls,
                        "include_transcripts": include_transcripts,
                        "include_analysis": include_analysis,
                        **(
                            request_options.get("additional_query_parameters", {})
                            if request_options is not None
                            else {}
                        ),
                    }
                )
            ),
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(BatchDetailsResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)

    async def retrieve(
        self, batch_id: str, *, request_options: typing.Optional[RequestOptions] = None
    ) -> RetrieveBatchAnalysisResponse:
        """
        Retrieves the analyses for a specific batch of calls, including details of each call's analysis.

        Parameters
        ----------
        batch_id : str
            The unique identifier for the call batch. Returned in the response when creating a batch, or when listing all batches.

        request_options : typing.Optional[RequestOptions]
            Request-specific configuration.

        Returns
        -------
        RetrieveBatchAnalysisResponse

        Examples
        --------
        from bland.client import AsyncBlandAI

        client = AsyncBlandAI(
            api_key="YOUR_API_KEY",
        )
        await client.batches.retrieve(
            batch_id="string",
        )
        """
        _response = await self._client_wrapper.httpx_client.request(
            method="GET",
            url=urllib.parse.urljoin(
                f"{self._client_wrapper.get_base_url()}/", f"v1/batches/{jsonable_encoder(batch_id)}/analysis"
            ),
            params=jsonable_encoder(
                request_options.get("additional_query_parameters") if request_options is not None else None
            ),
            headers=jsonable_encoder(
                remove_none_from_dict(
                    {
                        **self._client_wrapper.get_headers(),
                        **(request_options.get("additional_headers", {}) if request_options is not None else {}),
                    }
                )
            ),
            timeout=request_options.get("timeout_in_seconds")
            if request_options is not None and request_options.get("timeout_in_seconds") is not None
            else self._client_wrapper.get_timeout(),
            retries=0,
            max_retries=request_options.get("max_retries") if request_options is not None else 0,  # type: ignore
        )
        if 200 <= _response.status_code < 300:
            return pydantic_v1.parse_obj_as(RetrieveBatchAnalysisResponse, _response.json())  # type: ignore
        if _response.status_code == 500:
            raise ServerError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        if _response.status_code == 401:
            raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorBody, _response.json()))  # type: ignore
        try:
            _response_json = _response.json()
        except JSONDecodeError:
            raise ApiError(status_code=_response.status_code, body=_response.text)
        raise ApiError(status_code=_response.status_code, body=_response_json)
