diff --git a/.fernignore b/.fernignore index 3d06feb..673a106 100644 --- a/.fernignore +++ b/.fernignore @@ -31,3 +31,16 @@ requirements.txt .fern/replay.lock .fern/replay.yml .gitattributes + +# Additive v3 preview adapter and typed staging contracts are maintained manually. +src/agora_agent/v3/ + +# Public token-only policy and transport enforcement are maintained manually. +src/agora_agent/client.py +src/agora_agent/core/public_auth.py +src/agora_agent/core/http_client.py +src/agora_agent/core/client_wrapper.py + +# Shared corrected release contract, local preview generation input/handoff. +openapi.json +openapi.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd1bbf0..620d5ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,30 +102,12 @@ jobs: - name: Verify package versions match release tag run: | - TAG_VERSION="${GITHUB_REF_NAME#v}" - ROOT_VERSION="$(poetry version -s | sed 's/^v//')" - COMPAT_VERSION="$(cd compat/agora-agent-server-sdk && poetry version -s | sed 's/^v//')" - COMPAT_DEP_VERSION="$(python -c "import re, sys; from pathlib import Path; text = Path('compat/agora-agent-server-sdk/pyproject.toml').read_text(); match = re.search(r'^agora-agents\s*=\s*\"([^\"]+)\"', text, re.M); sys.exit('agora-agents dependency not found in compat pyproject.toml') if not match else None; print(match.group(1))")" - - if [ "$ROOT_VERSION" != "$TAG_VERSION" ]; then - echo "Root package version ($ROOT_VERSION) does not match tag version ($TAG_VERSION)." - exit 1 - fi - - if [ "$COMPAT_VERSION" != "$TAG_VERSION" ]; then - echo "Compat package version ($COMPAT_VERSION) does not match tag version ($TAG_VERSION)." - exit 1 - fi - - if [ "$COMPAT_DEP_VERSION" != ">=${TAG_VERSION},<3.0.0" ]; then - echo "Compat package dependency on agora-agents ($COMPAT_DEP_VERSION) does not match >=${TAG_VERSION},<3.0.0." - exit 1 - fi + poetry run python scripts/check_release_workflow.py --tag "$GITHUB_REF_NAME" - name: Publish primary package to pypi run: | PACKAGE_NAME="agora-agents" - PACKAGE_VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION="$(poetry version -s | sed 's/^v//')" STATUS="$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PACKAGE_NAME}/${PACKAGE_VERSION}/json")" case "$STATUS" in 200) @@ -146,7 +128,7 @@ jobs: - name: Wait for primary package on PyPI run: | - TAG_VERSION="${GITHUB_REF_NAME#v}" + TAG_VERSION="$(poetry version -s | sed 's/^v//')" PACKAGE="agora-agents" for attempt in $(seq 1 12); do if curl -fsS "https://pypi.org/pypi/${PACKAGE}/${TAG_VERSION}/json" >/dev/null; then @@ -162,7 +144,7 @@ jobs: - name: Publish compatibility package to pypi run: | PACKAGE_NAME="agora-agent-server-sdk" - PACKAGE_VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION="$(poetry version -s | sed 's/^v//')" STATUS="$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PACKAGE_NAME}/${PACKAGE_VERSION}/json")" case "$STATUS" in 200) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b50f010..34ec973 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,8 +45,10 @@ jobs: GH_TOKEN: ${{ github.token }} run: | TAG="${RELEASE_TAG}" + PRERELEASE=false + if [[ "$TAG" == *-* ]]; then PRERELEASE=true; fi if gh release view "$TAG" >/dev/null 2>&1; then - gh release edit "$TAG" --title "$TAG" --notes-file release_notes.md + gh release edit "$TAG" --title "$TAG" --notes-file release_notes.md --prerelease="$PRERELEASE" else - gh release create "$TAG" --title "$TAG" --notes-file release_notes.md + gh release create "$TAG" --title "$TAG" --notes-file release_notes.md --prerelease="$PRERELEASE" fi diff --git a/README.md b/README.md index 67c1110..3f6ecf3 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,26 @@ The Agora Conversational AI SDK provides convenient access to the Agora Conversa enabling you to build voice-powered AI agents with support for both cascading flows (ASR -> LLM -> TTS) and multimodal flows (MLLM) for real-time audio processing. +## Run or save the same agent configuration + +Use one `Agent` builder to configure providers, then call `create_ephemeral_session(...).start()` for an explicit Platform session or `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `create_async_ephemeral_session()`, `create_async_session()` and `save_async()`. A saved handle owns `update(config)`, deletion, number assignment/unassignment, and client-side `list_numbers()` scans. Use `client.secrets` and `client.numbers` for provisioning, with explicit `secret_ref()` credentials for saved BYOK agents (inline keys remain valid for ephemeral sessions) and immutable `with_variable()` definitions for saved configurations. + +Saving and project provisioning are part of SDK preview `3.0.0a3`. Existing session behavior is preserved. Saved agents and preview ephemeral sessions support cascaded ASR/LLM/TTS only; MLLM remains available for existing inline sessions. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 32-operation reference](docs/reference/v3.md) for ETags, cursor pagination, session lifecycle, limited token/messages/provider-params updates, and project queries. The shared release contract is available as [openapi.yaml](openapi.yaml). `deploy` / `DeployedAgent` remain compatibility names. Saved handles also create sessions by ID; configurations with secret references or variable definitions start ephemeral sessions. Use `client.campaigns` for typed campaign create/list/get/update/delete/pause/resume/cancel operations. REST authentication supports `auth_token`, tokens signed from App ID/App Certificate, or Basic authentication through `customer_id` and `customer_secret`. Internal gateway headers remain unsupported. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. + ## Install ```sh -pip install agora-agents +pip install agora-agents==3.0.0a3 +``` + +For source checkouts on Windows, use a short path and enable Git long paths before checkout: + +```sh +git clone -c core.longpaths=true https://github.com/AgoraIO/agora-agents-python.git C:/src/agora-python ``` +The generated legacy modules retain their public import names, including long filenames. A default Windows checkout in a deeply nested folder can still exceed OS/tool limits; installing the wheel avoids Git checkout limits. + ## Requirements - Python 3.8+ @@ -20,7 +34,7 @@ pip install agora-agents ## Quick Start Start with the `Agent` builder: create a client with app credentials, pass it to `Agent(client=client, ...)`, choose your ASR, LLM, and TTS providers, then start a session. Omit vendor API keys for supported Agora-managed global models, or provide keys when you want BYOK. -Set Agora interaction language with `turn_detection.language`; provider-specific STT language values remain under `asr.params`. Ares uses only the REST `asr.language` value sourced from `turn_detection.language`. +For Platform sessions, vendor STT language maps to `pipeline.asr.language`; explicit `turn_detection.language` overrides it. Ares defaults to `en-US`. Existing Voice Agent sessions retain their language mapping. ```python import os @@ -167,7 +181,7 @@ agent = Agent(client=client, turn_detection={"language": "en-US"}).with_stt( ) ``` -Migrating from `agora-agent-server-sdk` on PyPI? Use `pip install agora-agents`; imports stay `agora_agent` — see [changelog migration notes](./changelog.md#migration-notes) or [installation guide](./docs/getting-started/installation.md#migrating-from-a-previous-package-name). +Migrating from `agora-agent-server-sdk` on PyPI? Use `pip install agora-agents==3.0.0a3`; imports stay `agora_agent` — see [changelog migration notes](./changelog.md#migration-notes) or [installation guide](./docs/getting-started/installation.md#migrating-from-a-previous-package-name). ## BYOK @@ -207,6 +221,21 @@ session = agent.create_session( session.start() ``` +For Gemini 3.8 Live Extended Thinking, use the same single `GeminiLive` class as the regular Live model: + +```python +from agora_agent import GeminiLive + +gemini_agent = Agent(client=client).with_mllm(GeminiLive( + api_key=os.environ["GOOGLE_API_KEY"], + model="models/gemini-3.8-live-extended-thinking", + thinking_level="medium", + greeting_message="Hello! Ready to chat.", +)) +``` + +Use `models/gemini-3.8-live` without `thinking_level` for the lower-latency model. Gemini sessions use the preview gateway and `agora-feature: gemini-live`; the Google key is sent as `mllm.api_key`. See the [Preview Endpoint guide](./docs/guides/preview-endpoint.md). + See the [MLLM Flow guide](./docs/guides/mllm-flow.md) for full examples with Gemini Live and Vertex AI. ## Documentation diff --git a/changelog.md b/changelog.md index 2dfa618..bfdea28 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## [v3.0.0-alpha.3] — 2026-10-30 + +- Support Basic REST authentication with `customer_id`/`customer_secret` alongside Token007 authentication in synchronous and asynchronous clients. Optional App Certificate signing remains available for RTC transport tokens. + +- Include the ConvoAI service in SDK-generated authorization tokens by integrating the latest production token builder fix. Regression tests verify service grants, signatures and sync/async Platform session request headers. +- Preserve ASR provider language parameters for AssemblyAI, Google, Microsoft, Sarvam and Speechmatics, alongside the interaction language. +- Default Sarvam TTS to `bulbul:v3`, preserving explicit model overrides. +- Reject malformed App ID/certificate values, including whitespace. + +## [v3.0.0-alpha.2] — 2026-10-29 + +- Correct Platform provider IDs, Gemini and Bedrock credentials, ASR language/keywords/endpoints, and HeyGen mapping. Managed OpenAI model availability is validated by the service. +- Add explicit ephemeral-session and outbound-call helpers, full inbound call policies, RTC token generation for fixed numeric UIDs, Azure resource names, optional session analysis and secret-key metadata. +- Fix Pydantic import configuration warnings and document Windows source checkout requirements. +- Preserve Gemini credentials during endpoint normalization and capture RTC signing identity per session. Retain generated RTC tokens across failed-start retries, refresh them for new unkeyed session lifecycles, and leave unsupported UIDs to server provisioning. + +## [v3.0.0-alpha.1] — 2026-10-28 + +- Shared root OpenAPI JSON/YAML, local reproducible wire generator, sync/async/raw session PATCH, and cascaded-only preview contracts. +- Realtime/MLLM configurations and runtime updates are rejected by preview APIs before network access; existing inline MLLM sessions remain supported. +- BYOK `credential.fields` preserves vendor credential names; saved writes require explicit secret references, ephemeral sessions permit inline values. +- Optional RTC UID, inbound/voicemail end-call policies, SIP IPv4 allowlists, campaign create retention, 50,000-contact / 10 MiB limits. Removed campaign retry fields are rejected. +- Six idempotent POST operations use optional 1–256 visible ASCII keys, 24-hour replay and replay response headers. No automatic write retries or region availability gates. + +### Preview rollout + +The current stored-agent, session and campaign preview rollout is available in the US only (PM confirmation, 2026-10-27). Availability may expand as rollout progresses. + ## [v2.13.0] — 2026-10-29 ### Added diff --git a/compat/agora-agent-server-sdk/README.md b/compat/agora-agent-server-sdk/README.md index 1da36aa..fd9bdb3 100644 --- a/compat/agora-agent-server-sdk/README.md +++ b/compat/agora-agent-server-sdk/README.md @@ -5,7 +5,7 @@ This package has been renamed to `agora-agents`. New projects should install: ```sh -pip install agora-agents +pip install agora-agents==3.0.0a3 ``` This compatibility package re-exports the public API from `agora-agents` to support existing installs during the migration window. The primary import path remains `agora_agent`; you can also import from `agora_agent_server_sdk_compat`: @@ -16,3 +16,5 @@ from agora_agent_server_sdk_compat import Agora, Area ``` Maintainers: dual-publish steps live in the repository release workflow, not in the root README. + +The shim exposes features from the installed `agora-agents` version; it does not independently add API support. The [v3 stored-agent preview](../../docs/guides/v3-api-preview.md) is included in preview `3.0.0a3`. Existing v2 imports remain available. diff --git a/compat/agora-agent-server-sdk/pyproject.toml b/compat/agora-agent-server-sdk/pyproject.toml index d6b10bd..8369b27 100644 --- a/compat/agora-agent-server-sdk/pyproject.toml +++ b/compat/agora-agent-server-sdk/pyproject.toml @@ -3,7 +3,7 @@ name = "agora-agent-server-sdk" [tool.poetry] name = "agora-agent-server-sdk" -version = "v2.13.0" +version = "3.0.0a3" description = "Compatibility shim for the renamed agora-agents package." readme = "README.md" authors = [] @@ -35,7 +35,7 @@ Repository = 'https://github.com/AgoraIO/agora-agents-python' [tool.poetry.dependencies] python = "^3.8" -agora-agents = ">=2.13.0,<3.0.0" +agora-agents = ">=3.0.0a3,<4.0.0" [build-system] requires = ["poetry-core"] diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index 6b47972..e84124b 100644 --- a/docs/concepts/agent.md +++ b/docs/concepts/agent.md @@ -8,6 +8,8 @@ description: The Agent builder — configure an AI agent with LLM, TTS, STT, and The `Agent` class is a fluent builder for configuring AI agent properties. Pass a bound `Agora` or `AsyncAgora` client with `client=...` — it is required for `create_session()` and `create_async_session()`. The builder collects vendor settings (LLM, TTS, STT, MLLM, avatar) and produces a fully configured `AgentSession` when you call `create_session()`. The agent instance name is set on `create_session(name=...)`, not on the `Agent` constructor. +In the [v3 preview](../guides/v3-api-preview.md), the same local builder can save a stored configuration with `save()` / `save_async()`. Its stored name is passed to deploy, and the returned handle manages inbound-number assignments. This does not start a session or change the v2 `create_session()` lifecycle. Secret references require deployment; they cannot be sent to v2 sessions. + ## Constructor diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md index 98e41db..2390c70 100644 --- a/docs/concepts/architecture.md +++ b/docs/concepts/architecture.md @@ -41,7 +41,11 @@ This is the primary developer-facing API. It provides: ### Fern-Generated Layer -The auto-generated core provides typed HTTP methods for every Agora API endpoint. You rarely need this directly, but it is accessible via `session.raw` for advanced use cases or new endpoints that the agentkit layer does not yet cover. +The auto-generated core provides typed HTTP methods for the existing v2 API endpoints. You rarely need this directly, but it is accessible via `session.raw` for advanced use cases or new endpoints that the agentkit layer does not yet cover. + +### v3 Preview Adapter + +`client.v3` adds typed wire dictionaries and agents/secrets/numbers/session-read resources alongside the v2 generated clients. It reuses the pool client's HTTP transport and authentication, with separate staging origin/header overrides. This is protected custom/local-generated code rather than canonical Fern output. AgentKit adds `SavedAgent` / `AsyncSavedAgent` for persisted configurations; existing session handles remain v2. See [v3 workflows](../guides/v3-api-preview.md) and [reference](../reference/v3.md). ## Sync vs. Async @@ -51,6 +55,7 @@ The SDK provides two parallel client hierarchies: |---|---|---| | `Agora` | `AsyncAgora` | `httpx.Client` / `httpx.AsyncClient` | | `AgentSession` | `AsyncAgentSession` | Blocking calls / Coroutines | +| `SavedAgent` (preview) | `AsyncSavedAgent` (preview) | Stored configuration and number assignment calls | ### When to Use Each diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md index a15f43a..fc5454e 100644 --- a/docs/getting-started/authentication.md +++ b/docs/getting-started/authentication.md @@ -6,7 +6,7 @@ description: Configure the Python SDK with app credentials and understand other # Authentication -Create `Agora` or `AsyncAgora` with `app_id` and `app_certificate` only. The SDK mints a fresh ConvoAI REST token for each API call and generates the RTC join token when the session starts. +By default, create `Agora` or `AsyncAgora` with `app_id` and `app_certificate`. The SDK mints a fresh ConvoAI REST token for each API call and generates the RTC join token when the session starts. ## App credentials @@ -50,35 +50,40 @@ session = agent.create_session( print(client.auth_mode) # "app-credentials" ``` -## Legacy auth modes +## Pre-built REST tokens -The generated client still supports pre-minted REST tokens and HTTP Basic Auth for legacy integrations. Do not use those modes for new session integrations. Use app credentials so AgentKit can mint short-lived ConvoAI REST auth and RTC join tokens for each session. +The SDK supports Token007 and Basic REST authentication. Pass a raw `auth_token`, let the SDK sign tokens from `app_id` and `app_certificate`, or configure a Customer ID/Secret pair. REST authentication is separate from the optional RTC transport token. -### HTTP Basic Auth +### Pre-built REST token + +Pass the raw token string. The SDK sets `Authorization: agora token=` automatically. ```python client = Agora( area=Area.US, app_id="your-app-id", - app_certificate="your-app-certificate", - customer_id="your-customer-id", - customer_secret="your-customer-secret", + auth_token="your-raw-token", # No certificate needed for prebuilt REST auth. ) -print(client.auth_mode) # "basic" +print(client.auth_mode) # "token" ``` -### Pre-built REST token +New resources use `https://preview.ai.agora.io/conversational-ai/v1/projects/{appid}`. Do not send internal gateway headers or `X-Studio-Region`. -Pass the raw token string. The SDK sets `Authorization: agora token=` automatically. +A client with only a REST `auth_token` or Basic credentials cannot generate RTC join tokens. Supply `token=` when starting an ordinary legacy RTC session, or also configure the App Certificate for RTC signing. Saved/ephemeral RTC sessions accept their optional token through `transport.token`. + +## Basic REST authentication ```python client = Agora( area=Area.US, app_id="your-app-id", - app_certificate="your-app-certificate", - auth_token="your-raw-token", + customer_id="your-customer-id", + customer_secret="your-customer-secret", + # app_certificate="your-app-certificate", # Optional RTC token signing. ) -print(client.auth_mode) # "token" +print(client.auth_mode) # "basic" ``` -`auth_token` is mutually exclusive with `customer_id` / `customer_secret`. +The same options work with `AsyncAgora`. Both credentials must be nonempty, and Customer ID must not contain a colon. Do not combine them with `auth_token`. The SDK sends `Authorization: Basic ` on REST requests, including saved-agent and ephemeral sessions. An optional App Certificate signs RTC tokens without replacing Basic REST authentication. + +For low-level overrides, a valid Basic `Authorization` header is accepted in request options. Configure credentials on the SDK client instead of attaching `httpx.BasicAuth` to the underlying HTTP client, which would silently override SDK authentication. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index cca2853..549e233 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -13,13 +13,13 @@ description: Install the Agora Conversational AI Python SDK. ## Install with pip ```sh -pip install agora-agents +pip install agora-agents==3.0.0a3 ``` ## Install with Poetry ```sh -poetry add agora-agents +poetry add agora-agents@3.0.0a3 ``` ## Imports @@ -59,7 +59,7 @@ from agora_agent import AsyncAgora, AsyncAgentSession, Area | Package | Purpose | | ------------------------------ | ------------------------------------------------------ | | `httpx` (>= 0.21.2) | HTTP client for sync and async requests | -| `pydantic` (>= 1.9.2) | Data validation for vendor configuration and API types | +| `pydantic` (>= 2.7.1, < 3) | Data validation for vendor configuration and API types | | `typing_extensions` (>= 4.0.0) | Backported type hints for Python 3.8+ | ## Next steps diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md new file mode 100644 index 0000000..ecbd6b6 --- /dev/null +++ b/docs/guides/agent-workflow.md @@ -0,0 +1,134 @@ +# One agent configuration, two explicit outcomes + +Configure an immutable `Agent` with providers once. Create a session when you want a running conversation, or save the configuration when you want a persistent agent and inbound-number assignments. Async clients follow the same flow with awaitable network methods. + +Saving and project secrets/numbers are part of SDK preview `3.0.0a3`, installed with `python -m pip install agora-agents==3.0.0a3`. Use token authentication with the public preview origin as described in the [transport setup](./v3-api-preview.md#quick-start-with-staging). + +```python +import os +from agora_agent import Agent, DeepgramSTT, OpenAI, OpenAITTS, SavedAgent, secret_ref + +# `client` is the Agora instance configured once in the linked transport setup. +# Provision once; future runs reuse the stored secret set. +client.secrets.create(name="support", secrets={"deepgram": os.environ["DEEPGRAM_API_KEY"], "openai": os.environ["OPENAI_API_KEY"]}) +agent = ( + Agent(client=client) + .with_stt(DeepgramSTT(api_key=secret_ref("support", "deepgram"), model="nova-3")) + .with_llm(OpenAI( + api_key=secret_ref("support", "openai"), model="gpt-4o-mini", + base_url="https://api.openai.com/v1/chat/completions", + system_messages=[{"role": "system", "content": "Help callers briefly."}], + )) + .with_tts(OpenAITTS( + api_key=secret_ref("support", "openai"), model="tts-1", voice="alloy", + base_url="https://api.openai.com/v1/audio/speech", + )) +) + +# Choose a running conversation. Constructing the handle is local; start() calls the service. +session = agent.create_session(channel="room", agent_uid="1", remote_uids=["2"]) +session_id = session.start() +# session.stop() + +# Or explicitly persist the same configuration. Saving does not start a session. +saved = agent.save(name="Support") +print(saved.id) # Store the ID so another process can load the handle. +saved = saved.update(agent.with_greeting("Hello. How can I help?")) +loaded = SavedAgent.get(client, saved.id) +# loaded.delete() +``` + +`save()` creates a configuration. `saved.update(config)` changes that saved ID and preserves its name; `name="New name"` optionally renames it. The configuration must use the same client object as the handle. Both paths return a fresh saved handle and leave the local builder unchanged. `save(agent_id=...)` remains an advanced alternative when working directly with persisted IDs. + +## Provision secrets and numbers + +Use `client.secrets` for project secret sets and `client.numbers` to register a SIP number. Both have synchronous and async methods with the same arguments. + +```python +from agora_agent import secret_ref + +client.secrets.create(name="support", secrets={"openai": os.environ["OPENAI_API_KEY"]}) +number = client.numbers.create( + number="+12025550123", + sip_trunk={"hostname": "YOUR_APPROVED_SIP_HOST", "transport": "tls", "port": 5061}, +) +saved.assign_number(number, max_call_duration_seconds=900) +for assigned in saved.list_numbers(): + print(assigned["number_id"], assigned["number"]) +saved.unassign_number(number) + +# Reference the stored secret when saving provider configuration. +referenced = agent.with_llm(OpenAI( + api_key=secret_ref("support", "openai"), model="gpt-4o-mini", + base_url="https://api.openai.com/v1/chat/completions", +)).with_variable("resolved", type="bool", extract=True, description="Was the issue resolved?") +saved = saved.update(referenced) +``` + +Saving BYOK configuration requires explicit secret references for every provider credential. Ephemeral sessions can also use inline credentials; saving those configurations fails locally and never creates secrets automatically. Omit `agent_uid` on the new transport to request server assignment; one nonzero subscriber is still required. + +Secret creation and number registration are provisioning operations, not steps to repeat for every conversation. Save IDs and use conditional updates for existing resources. `list_numbers()` scans all project pages and filters assignments locally; it costs one request per page, with no individual number GETs. + +The same builder can start directly with secret references and variable definitions: the SDK sends an ephemeral inline agent, without saving it or reading secret values back. Saved handles start by their stored ID. Neither flow implicitly caches or reverses write-only credentials. + +```python +session = saved.create_session( + channel="room", agent_uid="1", remote_uids=["2"], + idle_timeout=60, max_duration=120, idempotency_key="support-call", + labels={"owner": "support"}, +) +session_id = session.start() +info = session.get_info() +session.stop() + +# Secret references and variables use the same session interface. +ephemeral = referenced.create_session(channel="room", agent_uid="1", remote_uids=["2"]) +ephemeral.start() +ephemeral.stop() +``` + +`idle_timeout` and `max_duration` are seconds, converted to integer milliseconds. `values`, `labels`, `token` (RTC only), `encryption`, `audio_scenario`, `graceful_stop`, `data_policy`, `geofence` and `idempotency_key` work for both saved and ephemeral sessions. Exactly one nonzero subscriber is required; wildcard subscriptions are unsupported. Agent UID `0` can request automatic assignment. Agent/subscriber UID types must match. The agreed contract permits string UIDs; numeric-only validation may remain during backend rollout. + +`name` stays a local label for these sessions; it is never sent as `client_reference`. The obsolete `client_reference` option raises an error. Legacy inline sessions retain their existing name/transport behavior. Preset, pipeline-ID, expiry and explicit string-UID switches are rejected on saved/ephemeral sessions. + +```python +session = saved.create_session( + channel="room", agent_uid="1002", remote_uids=["1001"], + values={"customer_name": "Ada"}, labels={"team": "support"}, + graceful_stop={"enabled": True, "timeout_ms": 10000}, + data_policy={"retention": "none"}, idempotency_key="support-call-4821", +) +``` + +Retention `none` disables retained conversation content and post-call extraction; the session resource and operational metadata can remain visible independently. Builder `parameters.opt_out` maps to session retention; `farewell_config.graceful_enabled` / `graceful_timeout_seconds` map to graceful stop (seconds × 1000). These are session options, not persisted agent fields; pass them when starting a saved agent. Graceful stop does not create a farewell message. + +Start, stop, `get_info` and a limited `update` are available for new session IDs; say/interrupt/think/history/turn helpers fail locally. `session.update({"token": "renewed", "llm": {"params": {"temperature": 0.2}}})` updates token and replaces active provider params. `messages` (or `llm.system_messages`) updates conversation messages on either transport; supplying both spellings is rejected. Realtime uses `mllm.params`. Credentials, URLs and other fields are rejected. Start accepts corrected 200/`create_ts`/uppercase statuses and rollout 201/`created_at`/lowercase; low-level data normalizes to uppercase and `create_ts`. Handle states remain lowercase (`FAILED` becomes `error`). STARTING sessions can be stopped or read. Stop failures propagate: `404 TaskNotFound` is not success; `409 TaskNotRunning` records stopped and `409 TaskConflict` records stopping, while preserving the error for the caller. + +## Async applications + +Bind the builder to `AsyncAgora`, keep the same provider methods, and use: + +```python +async def use_async_agent(agent): + session = agent.create_async_session(channel="room", agent_uid="1", remote_uids=["2"]) + await session.start() + saved = await agent.save_async(name="Support") + saved = await saved.update(agent.with_greeting("Hello")) + numbers = await saved.list_numbers() + return session, saved, numbers +``` + +Use `await AsyncSavedAgent.get(client, saved_id)` to load another handle, and await secret/number provisioning, assignment and deletion. `with_variable` stays a local synchronous builder operation. + +## Advanced transport and compatibility + +Application code does not select a REST version. Ordinary inline configurations retain the established session transport. Configurations with secret references or variable definitions, or new session options such as `values`, `labels` or `max_duration`, use the documented ephemeral session transport. Saved handles use the documented start-by-ID transport. See [transport behavior and limits](./v3-api-preview.md) and the [complete low-level reference](../reference/v3.md) for ETags, raw headers, cursor pagination, strict mapping, project session reads, staging overrides and deferred features. + +`deploy()` / `deploy_async()` remain compatibility wrappers for `save()` / `save_async()`. `DeployedAgent` is exactly `SavedAgent`; the async handle names are likewise identical aliases. Advanced `client.v3` resources remain available, including session start/stop and project queries; the existing `client.agents` running-session resource retains its behavior. + + +## Campaign provisioning + +`client.campaigns` (or async `client.campaigns`) exposes the eight documented operations. Campaign creation and pause/resume/cancel accept `idempotency_key`; update requires `if_match`. Use raw GET to obtain the current ETag. Contact lists contain 1–5,000 entries and cannot be edited after creation. Retry pacing fields remain unconfirmed; the SDK implements no calling/retry algorithm. No contact-result, upload, event or webhook endpoints are invented. See the [campaign reference](../reference/v3.md#campaigns-eight-operations). + +RTC geofence uses `area` (`global`, `north_america`, `europe`, `asia`, `india`, `japan`) and optional `exclude_area`, allowed only with global area. An existing builder geofence maps into new-session transport (uppercase legacy area names normalize to lowercase). This agreed correction still depends on backend rollout. Geofence is not saved-agent configuration. diff --git a/docs/guides/low-level-api.md b/docs/guides/low-level-api.md index 47397b7..7bc437a 100644 --- a/docs/guides/low-level-api.md +++ b/docs/guides/low-level-api.md @@ -10,6 +10,8 @@ Use the `Agent` builder and `AgentSession` for conversational agent starts. That Generated clients are still available for API surface that AgentKit does not wrap yet, such as telephony and phone-number management. +For stored-agent configuration, project secrets, SIP numbers and read-only sessions, the additive [v3 preview adapter](./v3-api-preview.md) provides 20 operations under `client.v3`. It is separate from the Fern-generated v2 clients described below. See the [v3 reference](../reference/v3.md) for raw ETags, conditional writes and cursor pagination. + ## Client setup ```python diff --git a/docs/guides/pagination.md b/docs/guides/pagination.md index a80cb23..3bacaae 100644 --- a/docs/guides/pagination.md +++ b/docs/guides/pagination.md @@ -6,7 +6,7 @@ description: Iterate over paginated list endpoints. # Pagination -Paginated requests return a `SyncPager` or `AsyncPager` that you can iterate over. +Existing v2 paginated requests return a `SyncPager` or `AsyncPager` that you can iterate over. The [v3 preview](../reference/v3.md#pagination-errors-and-transport) instead returns wire dictionaries from `list()`, complete cursor pages from `iter_pages()`, and items from `iter_all()`; async clients use `async for` for the iterators. ```python from agora_agent import Agora, Area diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md new file mode 100644 index 0000000..d7864bb --- /dev/null +++ b/docs/guides/v3-api-preview.md @@ -0,0 +1,209 @@ +# Saving agent configurations (preview transport guide) + +For the everyday builder → session or saved-agent flow, start with the [agent workflow guide](./agent-workflow.md). Use `Agent.save()` to save a reusable configuration and assign it to an existing inbound number. The additive `client.v3` namespace covers 32 documented operations, including eight campaign operations. See the [complete v3 reference](../reference/v3.md) for method arguments and required headers. + +| Object or API | Meaning | Available now | +|---|---|---| +| `Agent` | Immutable local builder | Existing vendor configuration plus `with_variable` and deploy | +| v3 stored agent / `SavedAgent` | Persisted configuration identified by `agent_id` | Create, read, conditional update, delete, inbound-number assignment and number listing | +| v2 `AgentSession` | Running RTC instance, also historically called an agent | Existing `create_session(...).start()` lifecycle | +| v3 session | Running instance identified by `agent_session_id` | Start saved/ephemeral, list, get and stop | + +Saving stores configuration; it does not itself start a session. Call `saved.create_session(...).start()` to start by ID. Use `agent.call(...)` or `saved.call(...)` for outbound sessions and `client.campaigns` for campaigns. Session information includes optional typed `analysis` during rollout. The SDK package version continues to describe package compatibility independently of the REST API version. + +## Quick start with staging + +Install SDK preview `3.0.0a3`: + +```sh +python -m pip install agora-agents==3.0.0a3 +``` + +See [installation](../getting-started/installation.md) for prerequisites. The default public preview origin is `https://preview.ai.agora.io`. Configure App ID and App Certificate (or supply `auth_token`), then set: + +```sh +export AGORA_APP_ID='YOUR_APP_ID' +export AGORA_APP_CERTIFICATE='YOUR_APP_CERTIFICATE' +export OPENAI_API_KEY='YOUR_OPENAI_KEY' +export DEEPGRAM_API_KEY='YOUR_DEEPGRAM_KEY' +``` + +The default base is `https://preview.ai.agora.io`; the SDK appends `/conversational-ai/v1/projects/{appid}`. Optional `V3Options(base_url=...)` or `AGORA_AGENTS_V3_BASE_URL` overrides accept an origin without path/query/fragment. Authentication supports a prebuilt `auth_token`, App ID/App Certificate signing, or Basic authentication through `customer_id` and `customer_secret`. Basic credentials and `auth_token` cannot be combined. An App Certificate can accompany Basic credentials to generate RTC transport tokens without changing REST authentication. Internal gateway headers and `X-Studio-Region` remain unsupported. Current rollout availability is described in the [release notes](../../changelog.md#preview-rollout); the SDK does not enforce region availability. + +This complete cascaded configuration stores one project secret set, then saves a reusable agent. Set creation is a one-time provisioning step; subsequent runs can update secrets separately using the [secret methods](../reference/v3.md#secrets-eight-operations). + +```python +import os +from agora_agent import ( + Agent, Agora, Area, DeepgramSTT, OpenAI, OpenAITTS, V3Options, secret_ref, +) + +client = Agora( + area=Area.US, + app_id=os.environ["AGORA_APP_ID"], + app_certificate=os.environ["AGORA_APP_CERTIFICATE"], +) +client.secrets.create( + name="support", + secrets={"openai": os.environ["OPENAI_API_KEY"], "deepgram": os.environ["DEEPGRAM_API_KEY"]}, +) +agent = ( + Agent(client=client) + .with_stt(DeepgramSTT(api_key=secret_ref("support", "deepgram"), model="nova-3")) + .with_llm(OpenAI( + api_key=secret_ref("support", "openai"), model="gpt-4o-mini", + base_url="https://api.openai.com/v1/chat/completions", + system_messages=[{"role": "system", "content": "Help callers briefly."}], + greeting_message="Hello. How can I help?", + )) + .with_tts(OpenAITTS( + api_key=secret_ref("support", "openai"), model="tts-1", voice="alloy", + base_url="https://api.openai.com/v1/audio/speech", + )) + .with_variable("customer_name", type="str", required=True) + .with_variable("resolved", type="bool", extract=True, description="Was the issue resolved?") +) +support = agent.save(name="Support") +print(support.id, support.etag) # Persist the ID for the next update. +``` + +The example endpoints are existing vendor configuration, not additional v3 defaults. Use backend-approved provider endpoints/models for your staging deployment. Managed modules omit top-level and parameter URLs; the server selects the endpoint. BYOK modules retain their configured URLs. + +## Update, load and delete + +Use `saved.update(config)` to update the same stored agent while preserving its ID and name. Pass `name="New name"` to rename it. The configuration must use the exact same client object, preventing accidental writes to another project. `agent.save()` creates another configuration; matching names do not provide upsert or deduplication. The advanced `agent.save(agent_id=...)` form remains available. + +```python +from agora_agent import SavedAgent + +support = support.update(agent) # Preserve the saved ID and name. +loaded = SavedAgent.get(client, support.id) +print(loaded.id, loaded.name, loaded.etag, loaded.data) +# loaded.delete() # Delete the stored configuration when it is no longer needed. +``` + +Updating GETs the latest ETag, then sends one conditional PATCH. It recursively nulls stale readable mutable fields, including nested provider params; IDs, timestamps and resolved credential metadata are excluded. Credentials come from the builder. Switching an existing module to managed mode explicitly nulls `credential.fields`. Updates null omitted known credential-field names of the previous vendor before writing the desired fields, including alternate credential forms for the same vendor. + +Agent PATCH uses JSON Merge Patch with If-Match. The SDK clears readable stale fields and known provider credential fields, and does not assume name-based upsert. Custom credential-field names and write-only headers cannot be reconstructed from GET; use a deliberate targeted [low-level update](../reference/v3.md#agents-five-operations) when retaining existing hidden credentials matters. Saved-agent update conflicts are surfaced without retry. + +## Assign and discover inbound numbers + +Create the SIP number in the same project before assignment, either through your provisioning workflow or `client.numbers.create(number=..., sip_trunk={"hostname": ...})`. Number registration details and unsupported staging options are in the [reference](../reference/v3.md#numbers-five-operations). + +```python +support.assign_number("+12025550123", max_call_duration_seconds=900, + max_silence_duration_ms=30000) +assigned = support.list_numbers() +for number in assigned: + print(number["number_id"], number["number"]) +support.unassign_number("+12025550123") +``` + +Assignment accepts an E.164 string, a number ID (without assuming UUID format), or a v3 number dictionary with `number_id`. A leading `+` selects E.164 lookup. The lookup scans project number pages with `limit=100` and normalizes spaces, parentheses, periods and hyphens. Unknown numbers raise `NumberNotFoundError` naming the project. + +An ID assignment costs one GET for its ETag and one PATCH of `inbound.agent`; E.164 resolution adds list requests. A 412 triggers one fresh GET and one retry, adding two requests. Policy options map directly to `inbound.call_policy` and must be positive integers. Unassignment sets `inbound.agent` to JSON null. + +`list_numbers()` scans **every project number page** at 100 items per request, filters `inbound.agent == support.id`, and returns a list of matching wire dictionaries. It performs no per-number GETs. A scan costs one request per page and is not an atomic snapshot of concurrent assignment changes. + +There is no reliable stored-agent session filter: the current session summary lacks `agent_id`, and detail provides only a potentially nonunique `agent_name`. Use project-wide `client.v3.sessions.list(...)` / `.get(...)`; no guessed name-based `list_sessions()` helper is provided. + +## Raw responses, pagination and conflicts + +Normal methods return typed wire dictionaries from `agora_agent.v3.types`. The same methods under `.with_raw_response` return `.data`, `.headers`, `.etag`, `.status_code` and the underlying httpx `.response`. + +```python +from agora_agent import PreconditionFailedError + +response = client.v3.agents.with_raw_response.get(agent_id=support.id) +if response.etag is None: + raise RuntimeError("Server omitted ETag; refusing an unguarded update") +try: + client.v3.agents.update(agent_id=support.id, if_match=response.etag, + instructions="Updated support instructions") +except PreconditionFailedError as error: + latest = error.latest() # GET current data; does not repeat the write. + print(error.status_code, error.headers.get("agora-request-id"), latest) + +page = client.v3.sessions.list(limit=20, status="RUNNING", channel="room") +if page["has_more"]: + next_page = client.v3.sessions.list(limit=20, status="RUNNING", channel="room", + cursor=page["next_cursor"]) +for number in client.numbers.iter_all(limit=100): + print(number["number_id"]) +``` + +`iter_pages()` yields complete pages with `data.count`, `data.list`, `has_more` and `next_cursor`; `iter_all()` yields their items. Both reject missing or repeated continuation cursors. Cursor state is opaque: keep the same filters on subsequent requests. `list()` itself fetches one page. Session reads default to running sessions at the backend; request a historical filter such as `status="STOPPED,FAILED"` when needed. Iteration preserves the chosen filter rather than implicitly scanning every status. + +A 412 raises `PreconditionFailedError`, a subclass of `ApiError`; other HTTP failures raise `ApiError`. Both retain status, response headers and body (including server `detail` / `reason`). `latest()` reloads the affected resource; a secret-key conflict reloads its set. It preserves authentication/context headers while stripping all SDK, per-request and HTTP-client default `If-Match`, `If-None-Match` and `Idempotency-Key` headers from the reload. It returns normal parsed data rather than a raw response. To obtain the new ETag for a deliberate retry, GET through `.with_raw_response` again. + +## Async applications + +Use `AsyncAgora`, the same low-level keyword arguments, and `await agent.save_async(...)`. The saved handle is `AsyncSavedAgent`; `get`, `update`, `delete`, `assign_number`, `unassign_number` and `list_numbers` are awaitable. `with_variable` and the vendor builders stay local and synchronous. + +```python +import os +from agora_agent import Agent, AsyncAgora, AsyncSavedAgent, Area, V3Options + +async def inspect_stored_agent(agent_id: str): + client = AsyncAgora( + area=Area.US, app_id=os.environ["AGORA_APP_ID"], + app_certificate=os.environ["AGORA_APP_CERTIFICATE"], + ) + support = await AsyncSavedAgent.get(client, agent_id) + numbers = await support.list_numbers() + async for page in client.v3.sessions.iter_pages(limit=20, status="RUNNING"): + print(page["data"]["count"]) + return support, numbers + +# With a fully configured Agent bound to AsyncAgora: +# support = await agent.save_async(name="Support") +# support = await support.update(agent) +# await support.assign_number("+12025550123") +# await support.unassign_number("+12025550123") +# await support.delete() +``` + +Use `await error.latest()` after an async 412. `save()` requires an `Agora` client; `save_async()` requires `AsyncAgora`. Passing the other client kind raises `TypeError` before saving. + +## Compatibility and backend limits + +The preview supports cascaded ASR/LLM/TTS pipelines only. MLLM configurations cannot be saved or used with preview ephemeral-session options, and preview session updates reject MLLM parameters before network access. Existing inline MLLM sessions remain supported with an explicit `agent_uid` and the established session options. + +Existing resources and ordinary inline `Agent.create_session()` behavior remain available. Secret references and variable definitions select ephemeral session start automatically; a saved handle selects start by stored ID. `to_properties()` still refuses secret references because it explicitly serializes the older session contract. Session starts accept `values` overrides; session info exposes optional typed `analysis` for extraction results during rollout. See the [daily workflow](./agent-workflow.md) for session options and unsupported controls. + +`with_variable` returns a new builder. Names match `^[a-z][a-z0-9_]{0,63}$`; supported types are `str`, `int`, `float`, `bool`, `date`, `datetime`; `extract=True` requires a nonempty description (at most 2,000 characters). `options`, when supplied, contains 1–100 unique primitive values: equivalent numbers such as `1` and `1.0` count as duplicates, while `True`, `1`, and `"1"` remain distinct JSON types. `secret_ref(set_name, key)` validates 1–64 letters, digits, underscores per component and returns `$secrets..`. It creates a reference, not a secret value. + +The central mapper reuses existing vendor configs. LLM instructions and greetings take precedence over deprecated Agent fields; supported conversation messages move to top-level `messages`. Provider keys become `{mode: "byok", fields: {: ...}}`; for example OpenAI uses `api_key`, Microsoft ASR uses `key`, and Amazon ASR uses `access_key_id` / `secret_access_key`. Multipart Amazon and Google credentials retain their vendor field names. Saving BYOK configuration requires explicit `$secrets..` references; ephemeral session start also accepts inline values. The SDK never auto-stores keys. Absent keys select `managed`. Cascaded turn detection, supported text/audio greetings, silence settings and filler words are converted to preview shapes. Greeting playback modes `single_every` and `single_first` map to `each_join` and `first_join`; other v2 modes without a v3 equivalent are rejected. A managed MiniMax TTS preset model is retained in `tts.params.model`. + +For cascaded deployment, `with_turn_detection({"language": "de-DE"})` sets `pipeline.asr.language`; otherwise the configured vendor language is used. AssemblyAI, Google, Microsoft, Sarvam and Speechmatics retain the provider language separately in `params.language`. Ares defaults to `en-US`. An explicit interaction language without an STT vendor uses the existing managed `ares` fallback, without inventing a URL; managed modules omit URLs and let the server select the endpoint. A partial builder with neither STT nor an interaction language keeps ASR absent. + +| Constraint | Current behavior | +|---|---| +| Builder fields without stored-agent equivalents | One `UnsupportedFieldError.fields` list before network access: SAL, advanced features, geofence, labels, RTC, pipeline IDs, parameters other than `silence_config` | +| Value-valued `template_variables` | Rejected; use `with_variable` definitions | +| Unmapped multipart credentials, numeric v2 TTS `skip_patterns`, audio-only settings on text greetings, unmappable deprecated turn/greeting settings | Rejected rather than guessed or silently omitted | +| Tools/MCP/custom provider destinations | Deferred backend support; the server may reject otherwise typed fields | +| Number policies | Inbound `end_call` (including `on_voicemail`) and IPv4/CIDR/`*` `allowed_ips` are supported. Inbound transfer remains unsupported; SIP password references are not supported | +| Writes and retry options | All low-level writes disable automatic retries, even when `max_retries` is requested; assignment has only its explicit one-conflict retry | +| Agent and number creation | Agent, secret and number creation do not support idempotency keys and reject explicit key arguments/headers. An uncertain create outcome requires inspection before manual retry | +| Routing | Public preview host and v1 project path are confirmed; availability follows rollout release notes; no preview region allowlist is hardcoded | +| Session controls and analysis helpers | PATCH supports token, messages and LLM params only. Say/interrupt/think/history/turn and extraction-result helpers are unavailable on new session IDs | + +The adapter shares the existing client's HTTP transport, authentication. New resources use the confirmed public preview origin; the origin override applies only to these resources. These docs and offline tests do not establish live backend availability. + +## Maintainer provenance + +The shared corrected [OpenAPI JSON](../../openapi.json) is the generation input; [openapi.yaml](../../openapi.yaml) is the equivalent release handoff. Run `python3 scripts/generate-v3-preview.py` to regenerate TypedDict wire models, sync/async resources and the packaged `src/agora_agent/v3/openapi.json` snapshot, then run Homebrew Ruff on generated Python. Transport, validation and AgentKit mapping are handwritten. All are protected by `.fernignore`; this is a local preview generator, **not Fern output**. Campaign retry fields have been removed from the current contract; no retry algorithm is implemented. + +See [the v3 source](../../src/agora_agent/v3/client.py), [wire schemas](../../src/agora_agent/v3/types.py), [AgentKit mapping](../../src/agora_agent/agentkit/v3.py), and [offline contract tests](../../tests/custom/test_v3_preview.py). Preserve `.fernignore` protection when integrating a future canonical Fern namespace. + +## Compatibility names + +`deploy()` / `deploy_async()` remain compatibility wrappers for `save()` / `save_async()`. `DeployedAgent` is exactly `SavedAgent`, and `AsyncDeployedAgent` is exactly `AsyncSavedAgent`. `client.secrets` / `client.numbers` share the same resource objects as their advanced `client.v3` counterparts. These aliases do not duplicate requests or configuration. Saved handles create sessions by ID; the original builder also starts directly, using ephemeral start when secret references or variables require it. Use `agent.create_ephemeral_session(...)` (or `create_async_ephemeral_session`) to select it explicitly. + + +## Session idempotency + +Session start and ephemeral start accept `idempotency_key`, sent only as `Idempotency-Key`. The server retains keys for 24 hours and marks replays `Idempotent-Replayed: true`. Same-key/different-body or in-flight reuse returns `409 IdempotencyKeyConflict`. After an accepted or uncertain outcome, keep the same key; a new key may create a second session. The SDK performs no automatic write retries. Agent, secret and number creation reject idempotency keys. Campaign create/pause/resume/cancel accept them. + +After an idempotent start succeeds, use a new session handle and a new key for a genuinely new session. Restarting the same successful handle with its old explicit key is rejected locally because it would replay the ended session. An initial failed/unknown request without a returned ID may retry on the same handle with the original key. diff --git a/docs/index.md b/docs/index.md index c1f8da3..63d8a32 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,8 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on ## Start here +- Use the [Agent workflow](./guides/agent-workflow.md) for one configuration builder that creates a running session or explicitly saves a reusable configuration. + - Start with [Quick Start](./getting-started/quick-start.md). It shows the baseline app-credentials setup and starts a cascading ASR -> LLM -> TTS agent. - Use [MLLM Flow](./guides/mllm-flow.md) when your agent uses one supported realtime multimodal model. - Use [Cascading Flow](./guides/cascading-flow.md) for more examples of the default ASR -> LLM -> TTS flow, including provider-specific configuration. @@ -30,6 +32,7 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on | Layer | What it does | When to use | |---|---|---| | **AgentKit** (`Agent`, `AgentSession`, vendor classes) | High-level builder pattern, lifecycle, typed vendors | Most use cases | +| **v3 preview adapter** (`client.v3`) | Stored agents, secrets, SIP numbers and project session reads | Staging v3 integrations; [workflow guide](./guides/v3-api-preview.md) | | **Generated REST clients** (`client.agents`, `client.telephony`) | Typed access to REST APIs not covered by AgentKit | Advanced use cases | ## Documentation @@ -60,3 +63,7 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on | [Session Reference](./reference/session.md) | Full `AgentSession` / `AsyncAgentSession` API | | [Vendor Reference](./reference/vendors.md) | Constructor options for all vendor classes | | [Error Reference](./reference/errors.md) | v2.7 status codes and error reason values | + +## v3 stored-agent preview + +The additive v3 adapter stores reusable agent configurations and starts saved or ephemeral sessions, while existing inline sessions keep their current lifecycle. Start with the [v3 workflow guide](./guides/v3-api-preview.md), or consult the [complete 32-operation v3 reference](./reference/v3.md). The preview uses protected custom/local-generated code rather than the canonical Fern pipeline. diff --git a/docs/reference/agent.md b/docs/reference/agent.md index f4e8aed..cec79c0 100644 --- a/docs/reference/agent.md +++ b/docs/reference/agent.md @@ -231,6 +231,12 @@ agent = agent.with_filler_words(filler_words) `generated_config` is optional. Its `llm_provider`, `prompt`, and `fallback_strategy` fields are optional as well; when omitted, the service uses its defaults. Set `context_message_limit` to cap the number of recent messages and `history_character_limit` to cap the number of conversation-history characters passed to the filler-word model. +## Save configurations (preview checkout) + +`with_variable(name, definition=None, **fields)` returns a new builder with a typed variable definition. `save(name=None, agent_id=None)` stores the configuration with an `Agora` client; `await save_async(...)` does so with `AsyncAgora`. Deploy arguments are keyword-only. After saving, use `saved.update(config)` to reuse its ID/name with a fresh ETag; an advanced `agent_id` argument is also supported. Updates require the same bound client object. + +The returned `SavedAgent` / `AsyncSavedAgent` supports loading, `update(config)`, deletion, number assignment/unassignment, and `list_numbers()` project-page scans. Saving does not start a session. `deploy()` / `deploy_async()` and the `DeployedAgent` handle names remain compatibility aliases. Existing v2 session methods below retain their behavior and reject v3 secret references. See the [v3 workflow guide](../guides/v3-api-preview.md) and [complete reference](./v3.md) for strict mapping, language precedence, secrets, pagination, conflicts and preview limits. + ## `create_session()` @@ -257,7 +263,7 @@ Creates an `AgentSession` using the client already bound to `Agent(client=...)`. | `agent_uid` | `str` | Yes | UID for the agent | | `remote_uids` | `List[str]` | Yes | UIDs of remote participants | | `name` | `Optional[str]` | No | Session name sent to the Start Agent API (defaults to `agent-{timestamp}` if omitted) | -| `token` | `Optional[str]` | No | Pre-built RTC+RTM token | +| `token` | `Optional[str]` | No | Pre-built RTC + RTM + ConvoAI token | | `expires_in` | `Optional[int]` | No | Token lifetime in seconds (default: `86400` = 24 h, Agora max). Only applies when the token is auto-generated. Use `expires_in_hours()` or `expires_in_minutes()` for clarity. Valid range: 1–86400. | | `idle_timeout` | `Optional[int]` | No | Idle timeout in seconds | | `enable_string_uid` | `Optional[bool]` | No | Enable string UIDs | diff --git a/docs/reference/client.md b/docs/reference/client.md index 475fc24..a06e7fc 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -26,18 +26,19 @@ client = Agora( | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `area` | `Area` | Yes | — | Region for API routing (`Area.US`, `Area.EU`, `Area.AP`, `Area.CN`) | -| `app_id` | `str` | Yes* | — | Agora App ID (app-credentials mode) | -| `app_certificate` | `str` | Yes* | — | Agora App Certificate (app-credentials mode) | -| `customer_id` | `str` | Yes* | — | Customer ID (Basic Auth mode) | -| `customer_secret` | `str` | Yes* | — | Customer Secret (Basic Auth mode) | +| `app_id` | `str` | Yes | — | Agora App ID identifying the project | +| `app_certificate` | `str` | Unless token or Basic auth supplied | `None` | Agora App Certificate for signing | | `auth_token` | `str` | No | — | Pre-built raw REST token; SDK sets `Authorization: agora token=` | +| `customer_id` | `str` | With `customer_secret` | `None` | Customer ID for Basic REST authentication | +| `customer_secret` | `str` | With `customer_id` | `None` | Customer Secret for Basic REST authentication | | `headers` | `Dict[str, str]` | No | `None` | Additional headers sent with every request | | `timeout` | `float` | No | `60` | Request timeout in seconds | | `follow_redirects` | `bool` | No | `True` | Whether to follow HTTP redirects | | `httpx_client` | `httpx.Client` | No | `None` | Custom httpx client instance | | `debug` | `bool` | No | `False` | Log HTTP requests and responses when `True` | +| `v3` | `V3Options` | No | `None` | Preview-only origin and headers; see the [v3 reference](./v3.md) | -*Provide either `app_id` + `app_certificate`, or `customer_id` + `customer_secret`. `auth_token` is mutually exclusive with `customer_id` / `customer_secret`. +`app_id` is always required. Configure either a raw `auth_token`, both `customer_id` and `customer_secret` for Basic auth, or `app_certificate` for automatic Token007 signing. Basic credentials and `auth_token` are mutually exclusive. With token or Basic REST auth, `app_certificate` is optional and can still generate RTC transport tokens. Customer ID must not contain a colon. When `area=Area.CN`, the constructor returns a `CNAgora` instance; global areas return `GlobalAgoraClient`. Both are subclasses of `Agora` and behave the same at runtime. @@ -144,3 +145,11 @@ Both `Agora` and `AsyncAgora` expose Fern-generated sub-clients as properties: | `client.phone_numbers` | `PhoneNumbersClient` / `AsyncPhoneNumbersClient` | Phone number management | These are lazily initialized on first access. For most use cases, prefer the [AgentSession](./session.md) API instead of calling `client.agents` directly. + +## v3 preview sub-client + +`client.v3` / `async_client.v3` provide stored agents, secrets, numbers and project-wide session reads with the owning client's transport/auth. This protected custom adapter is separate from the Fern-generated v2 resources above. See [v3 workflows](../guides/v3-api-preview.md) for staging configuration and [all 20 operations](./v3.md) for conditional headers, typed dictionary responses and cursor iterators. Preview availability requires this checkout, not an assumed PyPI release. + +## Project provisioning + +Use `client.secrets` and `client.numbers` for project provisioning (await methods with `AsyncAgora`). They share the underlying resources and transport with the advanced namespace. The [agent workflow](../guides/agent-workflow.md) combines these with `Agent.save()` and saved-handle updates. Existing `client.agents` continues to manage running sessions, so stored-agent CRUD stays available through `SavedAgent` or advanced `client.v3.agents`. diff --git a/docs/reference/v3.md b/docs/reference/v3.md new file mode 100644 index 0000000..5eee43e --- /dev/null +++ b/docs/reference/v3.md @@ -0,0 +1,213 @@ +# v3 preview reference + +This reference describes the 32 implemented operations under `Agora.v3` and `AsyncAgora.v3`. For setup, complete examples, redeploy semantics and limitations, see the [v3 workflow guide](../guides/v3-api-preview.md). This custom low-level adapter is maintained from the supplied OpenAPI and reply, not Fern-generated code; see [maintainer provenance](../guides/v3-api-preview.md#maintainer-provenance). + +## Shared calling conventions + +All arguments are keyword-only. **Every method below also accepts `appid=None` and `request_options=None`.** `appid` defaults to the owning client's `app_id`; supplying another value targets that project for the individual call. Empty path identifiers are rejected and path segments are URL-encoded. + +Compact signatures below list every resource-specific argument. A bare name is required. `=None` means an optional query/header argument; `=OMIT` means an optional body field that is absent unless supplied. `OMIT` is documentation notation, not a value to import or pass. An explicit `None` in a body becomes JSON null, which is significant for merge patch and secret-key deletion. The response names link to the [TypedDict wire schemas](../../src/agora_agent/v3/types.py); return values are dictionaries, not Pydantic instances. + +Each method is available in three forms: + +```python +# Given a configured sync client and async_client: +data = client.v3.agents.get(agent_id="STORED_AGENT_ID") +raw = client.v3.agents.with_raw_response.get(agent_id="STORED_AGENT_ID") +# AsyncAgora methods use the identical arguments: +# data = await async_client.v3.agents.get(agent_id="STORED_AGENT_ID") +# raw = await async_client.v3.agents.with_raw_response.get(agent_id="STORED_AGENT_ID") +``` + +The raw form returns `RawResponse`: `.data` is the normal result; `.headers` is `httpx.Headers`; `.etag` is an optional string; `.status_code` is the HTTP status; `.response` is the original `httpx.Response`. Keep ETag quotes exactly as received. A GET raw response is the way to obtain `If-Match` for conditional writes. + +`request_options` uses the existing transport options, including `timeout_in_seconds`, `additional_headers`, and GET `max_retries`. Write retry limits are always forced to zero. Headers added through request options follow the existing transport's precedence. PATCH requests send `application/json`, as declared by staging. + +## Agents: five operations + +Prefix: `/conversational-ai/v1/projects/{appid}/agents`. A stored configuration's identifier is `agent_id`; it is not a v2 running-session ID. + +| Method and resource-specific signature | HTTP | Result | +|---|---|---| +| `create(agent_name, instructions=OMIT, messages=OMIT, greeting=OMIT, variables=OMIT, pipeline=OMIT, filler_words=OMIT)` | POST collection | `AgentResponse` | +| `list(limit=None, cursor=None, order=None)` | GET collection | `AgentListResponse` | +| `get(agent_id)` | GET `/{agent_id}` | `AgentResponse` | +| `update(agent_id, if_match, agent_name=OMIT, instructions=OMIT, messages=OMIT, greeting=OMIT, variables=OMIT, pipeline=OMIT, filler_words=OMIT)` | PATCH `/{agent_id}` | `AgentResponse` | +| `delete(agent_id)` | DELETE `/{agent_id}` | `AgentDeleteResponse` | + +`if_match: str` is required by the SDK signature and becomes `If-Match`. Agent create has no `idempotency_key` parameter. `order` accepts `create_ts.asc` or `create_ts.desc`. Body structures are documented by `AgentCreateModel`, `AgentPatchRequest` and the contract schemas. Preview pipelines use `mode="cascaded"` with ASR/LLM/TTS only. Realtime/MLLM pipelines are not supported in this preview; existing inline MLLM sessions remain available through the established session API. Provider credentials use `mode="managed"` or `mode="byok", fields={"api_key": "$secrets.support.openai"}`. Field names are vendor-specific; saved agents require secret references while ephemeral agents also accept inline values. + +`AgentResponse` includes `agent_id`, `agent_name`, creation/update timestamps, and configured fields. Keys, custom headers and other write-only credentials are not recoverable from GET. The low-level update sends only supplied fields; unlike AgentKit redeploy it does not construct a replacement-style patch. + +## Secrets: eight operations + +Prefix: `/conversational-ai/v1/projects/{appid}/secrets`. Set names and keys are distinct path components. Secret values are write-only. + +| Method and resource-specific signature | HTTP | Result | +|---|---|---| +| `create(name, secrets)` | POST collection | `SecretSet` | +| `list(limit=None, cursor=None, order=None)` | GET collection | `SecretList` | +| `get(name)` | GET `/{name}` | `SecretSet` | +| `replace(name, if_match, secrets)` | PUT `/{name}` | `SecretSet` | +| `update(name, secrets, if_match=None)` | PATCH `/{name}` | `SecretSet` | +| `delete(name, if_match)` | DELETE `/{name}` | `SecretDeleted` | +| `set_key(name, key, value, if_match=None, if_none_match=None)` | PUT `/{name}/keys/{key}` | `SecretKey` | +| `delete_key(name, key, if_match=None)` | DELETE `/{name}/keys/{key}` | `SecretDeleted` | + +`secrets` is a map of key names to string values for create/replace. Update also accepts null values to remove individual entries. `replace` replaces the set; `update` changes supplied entries. `value` writes a single string. Names and keys follow the staging contract's 1–64 letter/digit/underscore syntax; `secret_ref` validates those components locally. + +| Argument | Header | Requirement | +|---|---|---| +| Replace/delete `if_match` | `If-Match` | Required by their SDK signatures | +| Update/delete-key `if_match` | `If-Match` | Optional in the published staging signatures | +| Set-key `if_match` | `If-Match` | Optional conditional write | +| Set-key `if_none_match` | `If-None-Match` | Optional; `"*"` expresses create-if-absent intent | + +Use the condition appropriate to the write; the SDK does not resolve conflicting conditions locally. Retain server errors rather than assuming an optional signature guarantees an unconditional write is accepted. Set `order` to `create_ts.asc` or `create_ts.desc` for list. + +```python +# Given client, a configured Agora instance: +client.v3.secrets.set_key(name="support", key="new_key", value="NEW_VALUE", if_none_match="*") +raw = client.v3.secrets.with_raw_response.get(name="support") +if raw.etag is None: + raise RuntimeError("Missing secret-set ETag") +client.v3.secrets.update(name="support", if_match=raw.etag, secrets={"new_key": "ROTATED_VALUE"}) +``` + +## Numbers: five operations + +Prefix: `/conversational-ai/v1/projects/{appid}/numbers`. A number resource has `number_id` and an E.164 `number` string. Registration and stored-agent assignment are scoped to the app/project. + +| Method and resource-specific signature | HTTP | Result | +|---|---|---| +| `create(number, sip_trunk, description=OMIT, inbound=OMIT)` | POST collection | `NumberResponse` | +| `list(limit=None, cursor=None, order=None)` | GET collection | `NumberListResponse` | +| `get(number_id)` | GET `/{number_id}` | `NumberResponse` | +| `update(number_id, if_match, number=OMIT, description=OMIT, sip_trunk=OMIT, inbound=OMIT)` | PATCH `/{number_id}` | `NumberResponse` | +| `delete(number_id)` | DELETE `/{number_id}` | `NumberDeleteResponse` | + +Update requires `if_match: str` and sends `If-Match`. Create rejects `idempotency_key` and explicit idempotency headers. `order` accepts `create_ts.asc` or `create_ts.desc`. + +`SipTrunkInput` requires `hostname`. Optional fields are `name`, `port`, `transport` (`udp`, `tcp`, `tls`), `auth` (`username`, `password`), and the currently unsupported `allowed_ips`. `InboundInput` requires `agent`, which is a stored agent ID or null, and optionally `call_policy`. Supported policy fields are `max_call_duration_seconds` and `max_silence_duration_ms`; `end_call` and `transfer` are not implemented on staging. SIP-password secret references are also unsupported there. + +```python +# Given client and support, a deployed handle: +number = client.v3.numbers.create( + number="+12025550123", + sip_trunk={"hostname": "YOUR_APPROVED_SIP_HOST", "transport": "tls", "port": 5061}, + inbound={"agent": support.id}, +) +# Store number["number_id"] for subsequent conditional updates or assignment. +``` + +## Sessions: six operations + +Prefix: `/conversational-ai/v1/projects/{appid}/sessions`. + +| Method and resource-specific signature | HTTP | Result | +|---|---|---| +| `list(channel=None, status=None, started_after=None, started_before=None, limit=None, cursor=None)` | GET collection | `SessionList` | +| `get(agent_session_id)` | GET `/{agent_session_id}` | `SessionResponse` | +| `start(agent_id, transport, values=OMIT, labels=OMIT, lifecycle=OMIT, data_policy=OMIT, idempotency_key=None)` | POST collection | `StartSessionResponse` | +| `start_ephemeral(agent, transport, values=OMIT, labels=OMIT, lifecycle=OMIT, data_policy=OMIT, idempotency_key=None)` | POST `/ephemeral` | `StartSessionResponse` | +| `update(agent_session_id, transport=OMIT, agent=OMIT)` | PATCH `/{agent_session_id}` | `SessionUpdateResponse` | +| `stop(agent_session_id)` | POST `/{agent_session_id}/stop`, no request body | empty dictionary | + +Session requests follow the supplied OpenAPI plus the engineering reply corrections. `RtcSessionTransport` requires `type="rtc"`, `channel` and exactly one non-wildcard, nonzero `subscribe_uids` entry; omitting `uid` or setting agent UID `0` requests automatic assignment. Optional fields: `token`, `encryption` (`mode`, `key`, optional base64 `salt`), `audio_scenario` (`aiserver` default or `chorus`). String UIDs are agreed but may require backend rollout; agent/subscriber types must match. Low-level `SessionTransport` also accepts `TelephonyTransport`: `type="telephony"`, `from` (registered project number UUID), `to` (E.164), and optional `CallPolicy`. All six outbound policy fields are typed: ring/call/silence limits, voicemail, end-call and transfer. `Agent.call(from_number=..., to=..., call_policy=...)` and `SavedAgent.call(...)` configure outbound sessions. Call `.start()` to dial; `.stop()` ends ringing or connected calls. Async configured agents use `call_async`; async saved handles use `call`. + +`SessionLifecycle` uses integer milliseconds: idle timeout 0–3,600,000; max duration 1,000–3,600,000; optional `graceful_stop` with boolean `enabled` and timeout 0–120,000 (default 30,000). `values` maps variable names to primitive values; `labels` maps strings to strings and works for ephemeral sessions too. `DataPolicy.retention` is `30_days` (default) or `none`. + +Start normalizes corrected 200/`create_ts`/uppercase and rollout 201/`created_at`/lowercase data to `agent_session_id`, `create_ts`, uppercase status. Raw HTTP status/body remain available in `.response`. Start/ephemeral accept `Idempotency-Key`; no `client_reference` parameter remains. Session PATCH updates token, messages and LLM params; no speculative action endpoints are exposed. + +List and retrieve are project-wide reads. There is no `agent_id` filter and, more fundamentally, the published responses do not carry a reliable stored-agent association. Summaries contain `agent_session_id`, `start_ts` and `status`. Detail may include `agent_name`, `stop_ts`, `message`; optional `analysis` exposes extraction status, skip reason, completion timestamp, result and error; a nonunique name does not establish ownership by a stored agent. + +`status` accepts comma-separated statuses, for example `"STOPPED,FAILED"` for historical sessions; returned status values are `IDLE`, `STARTING`, `RUNNING`, `STOPPING`, `STOPPED`, `FAILED`. The SDK omits the filter when `status=None`; the published staging server default is `RUNNING`. `started_after` / `started_before` are integer timestamp filters documented as Unix seconds in the staging guide. + +`SessionResponse.analysis` is optional during rollout. Read `info.get("analysis")` after `session.get_info()`; an absent value is not an empty extraction result. Secret-set `keys` accepts both name strings and metadata objects; key responses preserve optional `create_ts`/`update_ts`. + +## Pagination, errors and transport + +| Resource | Server default limit | Published range | Server default order/filter | +|---|---|---|---| +| Agents | 50 | 1–200 | `create_ts.desc` | +| Secrets | 50 | 1–200 | `create_ts.desc` | +| Numbers | 50 | 1–100 | `create_ts.desc` | +| Sessions | 20 | Minimum 1; no maximum declared in OpenAPI | `status=RUNNING`; no order parameter | + +Omitted arguments use backend defaults, not client-side substitutions. In particular, an unfiltered session iterator still inherits the server's running-session default; pass historical status filters explicitly. + +All four resources offer `list()`, `iter_pages(**list_arguments)` and `iter_all(**list_arguments)`. `list()` returns one wire page: `{data: {count, list}, has_more, next_cursor}`. Iterators follow continuation cursors and reject missing or repeated cursors. `AsyncAgora` uses `await list(...)` and `async for` for the two iterators. These helpers are used on the normal resource, not its raw-response facade. + +`PreconditionFailedError` is raised for HTTP 412 and retains `.status_code`, `.headers`, `.body`; it inherits `ApiError`. Its `latest()` GETs the current parsed resource without retrying the write. On an individual key conflict it reloads the parent secret set. Reloads preserve authentication/context headers and remove all inherited and per-request `If-Match`, `If-None-Match`, and `Idempotency-Key` headers. With `AsyncAgora`, await `.latest()`. It does not return a new ETag: use another raw GET for a conditional retry. Other non-2xx responses raise `ApiError` with response status/headers/body. + +All low-level writes disable automatic retries. AgentKit number assignment is the exception only in the sense that it explicitly reloads and retries once after a 412. Agent deploy, secret writes, and failed agent/number creates are never silently repeated. GET retry options use the existing shared transport. + +`V3Options(base_url=None, headers={})` configures the preview origin and headers. Origin precedence is explicit option, `AGORA_AGENTS_V3_BASE_URL`, then `https://preview.ai.agora.io`. Only HTTP(S) origins without credentials, path beyond `/`, query or fragment are accepted. Legacy regional resources retain their routing. The adapter shares HTTP clients, token authentication and timeouts. + +## AgentKit surface + +| Operation | Synchronous | Async client | +|---|---|---| +| Store/update a builder | `agent.save(name=None, agent_id=None)` → `SavedAgent` | `await agent.save_async(...)` → `AsyncSavedAgent` | +| Load a stored handle | `SavedAgent.get(client, agent_id)` | `await AsyncSavedAgent.get(client, agent_id)` | +| Update saved config | `saved.update(config, name=None)` | `await saved.update(config, name=None)` | +| Delete | `saved.delete()` | `await saved.delete()` | +| Assign | `saved.assign_number(number, max_call_duration_seconds=None, max_silence_duration_ms=None, call_policy=None)` | `await saved.assign_number(...)` | +| Unassign | `saved.unassign_number(number)` | `await saved.unassign_number(number)` | +| Discover assigned numbers | `saved.list_numbers()` → `List[NumberResponse]` | `await saved.list_numbers()` | +| Add a variable | `agent.with_variable(name, definition=None, **fields)` → new `Agent` | Same synchronous local builder method | +| Build a secret reference | `secret_ref(secret_set, key)` → string subclass `SecretRef` | Same local function | + +The `max_*` assignment options, save `name` / `agent_id`, and update `name` are keyword-only. `SavedAgent.update(config)` preserves the handle ID/name and requires the configuration to be bound to the same client object. Handles expose `id`, `name`, `etag`, `data` and `raw` (the `client.v3.agents` resource). `list_numbers()` scans all project number pages with `limit=100`, filters `inbound.agent` locally, and makes no detail GETs; cost is one request per page. A concurrent project change can affect results during a scan. No `list_sessions()` association helper is provided without a reliable agent identity in session responses. + +`with_variable` validates names, types, extraction descriptions and 1–100 unique primitive options. Numeric choices use JSON-semantic equality (`1` and `1.0` duplicate); boolean and string choices remain distinct from numbers. `UnsupportedFieldError.fields` lists unmappable builder fields. `NumberNotFoundError` reports unsuccessful E.164 lookup; `SecretRefNotSupportedError` protects v2 session paths. See the [guide's compatibility table](../guides/v3-api-preview.md#compatibility-and-backend-limits) for strict mapping, merge-patch and staging limits. + +`deploy()` / `deploy_async()` are compatibility wrappers; `DeployedAgent` / `AsyncDeployedAgent` are exact class aliases for `SavedAgent` / `AsyncSavedAgent`. Prefer the version-neutral [daily workflow](../guides/agent-workflow.md) for application code. `client.secrets`, `client.numbers` and `client.campaigns` are the same resource objects as their `client.v3` counterparts. The advanced namespace remains available for raw transport and project session reads. + + +### Session handles + +`SavedAgent.create_session(*, channel, agent_uid=None, remote_uids=None, name=None, idle_timeout=None, max_duration=None, labels=None, values=None, idempotency_key=None, token=None, encryption=None, audio_scenario=None, graceful_stop=None, data_policy=None, geofence=None, debug=None, warn=None)` returns `AgentSession`. The async handle provides `create_session` and `create_async_session`, returning `AsyncAgentSession`. Creation is local. Start/read/stop use only the new session ID. Durations on handles are seconds; graceful-stop timeout remains explicitly milliseconds. Obsolete `client_reference` raises a clear error. `name` is local only. `.raw` exposes the session resource; `.raw_agent_management` is `None`. + +## Campaigns: eight operations + +`client.campaigns` and `client.v3.campaigns` are identical resources; async clients use the same arguments with `await`. Prefix: `/conversational-ai/v1/projects/{appid}/campaigns`. Every method also accepts `appid=None, request_options=None`, and has `.with_raw_response`. + +| Method | HTTP | Result | +|---|---|---| +| `create(name, agent_id, transport, contacts, schedule, lifecycle=OMIT, pacing=OMIT, data_policy=OMIT, idempotency_key=None)` | POST collection | `CampaignResponse` | +| `list(status=None, limit=None, cursor=None, order=None)` | GET collection | `CampaignListResponse` | +| `get(campaign_id)` | GET `/{campaign_id}` | `CampaignResponse` | +| `update(campaign_id, if_match, lifecycle=OMIT, pacing=OMIT, schedule=OMIT, transport=OMIT)` | PATCH `/{campaign_id}` | `CampaignResponse` | +| `delete(campaign_id)` | DELETE `/{campaign_id}` | `CampaignDeleteResponse` | +| `pause(campaign_id, idempotency_key=None)` | POST `/{campaign_id}:pause`, no body | `CampaignResponse` | +| `resume(campaign_id, idempotency_key=None)` | POST `/{campaign_id}:resume`, no body | `CampaignResponse` | +| `cancel(campaign_id, idempotency_key=None)` | POST `/{campaign_id}:cancel`, no body | `CampaignResponse` | + +Create/action keys become `Idempotency-Key`; update requires `If-Match`. Writes never automatically retry. Pagination uses `iter_pages` / `iter_all`, default 50, limits 1–200, order `create_ts.desc` or `create_ts.asc`. Status is one of scheduled/running/paused/completed/canceled/failed. + +Contacts contain E.164 `phone` and optional primitive `values` (≤100 keys), 1–50,000 contacts per campaign, with a 10 MiB create-body limit. Transport uses `type="telephony"`, `from` project number UUID or UUID list, optional call policy. Schedule is immediate or scheduled (IANA timezone and local `YYYY-MM-DDTHH:MM` start/end). Pacing supports `max_concurrent` and `calls_per_second`; removed retry fields are rejected. No SDK retry algorithm is implemented. + +After calls begin, only pacing changes are supported; schedule, lifecycle and call-policy changes return `409 CampaignNotEditable`. Pacing merges; schedule and call policy replace (`{}` clears policy). Pause/cancel/end time stop new calls but leave active calls running. Invalid actions return `409 CampaignInvalidState`; deleting active campaigns returns `409 CampaignActive`. Contact results/events/webhooks are deferred, and campaign/session association is still planned. No unsupported endpoints are synthesized. + +### Corrected release contract + +Campaign create accepts 1–50,000 contacts and at most 10 MiB of JSON. `data_policy` is create-only and defaults to `30_days`; pacing contains only `max_concurrent` (1–10,000, request default 10) and optional `calls_per_second` (1–1,000). Removed retry fields are rejected. These schema ranges are not operational project quotas. Campaign silence has a 30,000 ms minimum; lifecycle limits extend to 259,200,000 ms. Session lifecycle limits remain 3,600,000 ms. + +Session PATCH accepts `transport={"token": "..."}` and/or `agent={"overrides": {"messages": [...], "pipeline": {"llm": {"params": {...}}}}}`. Realtime/MLLM updates are not supported in this preview. Params replace the complete active module params; endpoint aliases, credentials, vendor and other configuration are not mutable. No ETag is required. AgentKit `session.update({"token": "...", "messages": [...], "llm": {"params": {...}}})` maps to this shape; `llm.system_messages` remains a compatibility spelling for messages. Existing legacy update fields retain their behavior; the new `messages` shortcut also works on legacy sessions by mapping to `llm.system_messages` without mutating the input. Supplying both spellings is rejected. + +Session start/ephemeral and campaign create/pause/resume/cancel accept optional 1–256 visible ASCII `Idempotency-Key` values. Replays are retained for 24 hours and may return `Idempotent-Replayed: true`; conflicting/in-flight reuse returns `409 IdempotencyKeyConflict`. No automatic write retry occurs. + +### Explicit session creation and tokens + +Use `agent.create_ephemeral_session(channel="room", agent_uid="1", remote_uids=["2"])` to select an unsaved Platform session without relying on configuration inference. Async clients use `create_async_ephemeral_session`. Both return local session handles; call `start()` afterward. Existing `create_session` routing is retained for compatibility. + +For Platform RTC sessions, an explicit numeric `agent_uid` from 1 through 4294967295 and App Certificate enable token generation at first start using the existing signer. The token is kept unchanged when retrying the same session/key. Explicit tokens are preserved. With an omitted/zero UID, string UID, numeric UID outside that range, or no certificate, the SDK leaves provisioning to the service; supply your own RTC token when required. This signer supports numeric UIDs only. + +```python +session = agent.create_ephemeral_session(channel="support", agent_uid="1", remote_uids=["2"]) +session.start() +call = saved.call(from_number="12345678-1234-1234-1234-123456789abc", to="+12025550123") +call.start() +saved.assign_number("number-id", call_policy={"end_call": {"on_user_request": True}}) +``` diff --git a/docs/reference/vendors.md b/docs/reference/vendors.md index 2e9b221..073074d 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -110,7 +110,8 @@ llm = OpenAI(api_key='your-key', base_url='https://api.openai.com/v1/chat/comple |---|---|---|---|---| | `api_key` | `str` | Yes | — | Azure OpenAI API key | | `model` | `str` | Yes | — | Deployment's base model name. Emitted as `params.model`. | -| `endpoint` | `str` | Yes | — | Azure endpoint URL | +| `endpoint` | `str` | Unless `resource_name` is provided | `None` | Azure endpoint URL; takes precedence when both are set | +| `resource_name` | `str` | Unless `endpoint` is provided | `None` | Azure resource name used to build the endpoint | | `deployment_name` | `str` | Yes | — | Azure deployment name | | `api_version` | `str` | No | `2024-08-01-preview` | Azure API version | | `temperature` | `float` | No | `None` | Sampling temperature (0.0–2.0) | @@ -210,7 +211,7 @@ The SDK also includes named helpers for the remaining Agora-supported LLM provid |---|---|---| | `Groq` | Groq | `api_key`, `model`, `base_url` | | `VertexAILLM` | Google Vertex AI | `api_key`, `model`, `project_id`, `location`, `url?` | -| `AmazonBedrock` | Amazon Bedrock | `access_key`, `secret_key`, `region`, `model` | +| `AmazonBedrock` | Amazon Bedrock | `region`, `model`, and either `api_key` or both `access_key` and `secret_key` | | `Dify` | Dify | `api_key`, `url`, `model`, `user?`, `conversation_id?` | | `CustomLLM` | OpenAI-compatible LLM | `api_key`, `model`, `base_url` | @@ -1205,3 +1206,9 @@ and use the provider default `2x3`. Options and caller maps are not mutated. LemonSlice recommends the fixed `avatar_id="lemonslice"`; explicit overrides remain supported for backwards compatibility. Branded providers are no longer exact aliases, and LemonSlice selector validation is now mandatory. + +### Platform provider mapping + +Provider classes supply their vendor identifier automatically. Managed OpenAI model availability is validated by the service rather than a local model allowlist. Gemini's API key maps to `credential.fields.api_key`, including when a custom URL is supplied; Platform URLs never include the key query parameter. AzureOpenAI accepts either `endpoint` or `resource_name`. AmazonBedrock accepts `api_key` or the `access_key`/`secret_key` pair, exclusively, and uses vendor `amazon`. + +For Platform ASR, explicit interaction language takes precedence over the vendor language at module level. AssemblyAI, Google, Microsoft, Sarvam and Speechmatics also retain the vendor language in `params.language`, as required by those providers. Ares keywords map to `params.keywords`, and Ares defaults to `en-US`. AssemblyAI `ws_url` and Speechmatics `uri` map to module `url`. Legacy session serialization remains unchanged. HeyGen avatars map to `liveavatar` for Platform requests; prefer `LiveAvatarAvatar` for new code. diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..188b0e5 --- /dev/null +++ b/openapi.json @@ -0,0 +1,14634 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Conversational AI V3 API", + "version": "3.0.0", + "description": "Public Conversational AI preview contract used by the Agent SDKs. Authenticate with Agora token007 (Authorization: agora token=) or HTTP Basic authentication using Customer ID and Customer Secret." + }, + "paths": { + "/conversational-ai/v1/projects/{appid}/agents": { + "post": { + "tags": [ + "agents" + ], + "summary": "Create Agent", + "operationId": "create_agent_conversational_ai_v3_projects__appid__agents_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentCreateInput" + } + } + } + } + }, + "get": { + "tags": [ + "agents" + ], + "summary": "List Agents", + "operationId": "list_agents_conversational_ai_v3_projects__appid__agents_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.desc", + "create_ts.asc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentListResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/agents/{id}": { + "get": { + "tags": [ + "agents" + ], + "summary": "Retrieve Agent", + "operationId": "retrieve_agent_conversational_ai_v3_projects__appid__agents__id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "patch": { + "tags": [ + "agents" + ], + "summary": "Update Agent", + "operationId": "update_agent_conversational_ai_v3_projects__appid__agents__id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentPatch" + } + } + } + } + }, + "delete": { + "tags": [ + "agents" + ], + "summary": "Delete Agent", + "operationId": "delete_agent_conversational_ai_v3_projects__appid__agents__id__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentDeleteResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Create Campaign", + "description": "Create a campaign with up to 50,000 contacts. The complete JSON request body is limited to 10 MiB (10,485,760 bytes).", + "operationId": "create_campaign_conversational_ai_v3_projects__appid__campaigns_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "get": { + "tags": [ + "campaigns" + ], + "summary": "List Campaigns", + "operationId": "list_campaigns_conversational_ai_v3_projects__appid__campaigns_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "scheduled", + "running", + "paused", + "completed", + "canceled", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.desc", + "create_ts.asc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignListResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:pause": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Pause Campaign", + "operationId": "pause_campaign_conversational_ai_v3_projects__appid__campaigns__id__pause_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:resume": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Resume Campaign", + "operationId": "resume_campaign_conversational_ai_v3_projects__appid__campaigns__id__resume_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:cancel": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Cancel Campaign", + "operationId": "cancel_campaign_conversational_ai_v3_projects__appid__campaigns__id__cancel_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}": { + "get": { + "tags": [ + "campaigns" + ], + "summary": "Get Campaign", + "operationId": "get_campaign_conversational_ai_v3_projects__appid__campaigns__id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "patch": { + "tags": [ + "campaigns" + ], + "summary": "Update Campaign", + "operationId": "update_campaign_conversational_ai_v3_projects__appid__campaigns__id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "delete": { + "tags": [ + "campaigns" + ], + "summary": "Delete Campaign", + "operationId": "delete_campaign_conversational_ai_v3_projects__appid__campaigns__id__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignDeleteResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/secrets": { + "post": { + "tags": [ + "secrets" + ], + "summary": "Create", + "operationId": "create_conversational_ai_v3_projects__appid__secrets_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "get": { + "tags": [ + "secrets" + ], + "summary": "List Sets", + "operationId": "list_sets_conversational_ai_v3_projects__appid__secrets_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.asc", + "create_ts.desc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretList" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/secrets/{name}": { + "get": { + "tags": [ + "secrets" + ], + "summary": "Retrieve", + "operationId": "retrieve_conversational_ai_v3_projects__appid__secrets__name__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "put": { + "tags": [ + "secrets" + ], + "summary": "Replace", + "operationId": "replace_conversational_ai_v3_projects__appid__secrets__name__put", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretReplace" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "patch": { + "tags": [ + "secrets" + ], + "summary": "Update", + "operationId": "update_conversational_ai_v3_projects__appid__secrets__name__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "If-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ], + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "delete": { + "tags": [ + "secrets" + ], + "summary": "Delete", + "operationId": "delete_conversational_ai_v3_projects__appid__secrets__name__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretDeleted" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/secrets/{name}/keys/{key}": { + "put": { + "tags": [ + "secrets" + ], + "summary": "Set Key", + "operationId": "set_key_conversational_ai_v3_projects__appid__secrets__name__keys__key__put", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Key" + } + }, + { + "name": "If-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ], + "title": "If-Match" + } + }, + { + "name": "If-None-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "const": "*", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "If-None-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretKeyWrite" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretKey" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretKey" + } + } + }, + "description": "Created" + } + } + }, + "delete": { + "tags": [ + "secrets" + ], + "summary": "Delete Key", + "operationId": "delete_key_conversational_ai_v3_projects__appid__secrets__name__keys__key__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Key" + } + }, + { + "name": "If-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ], + "title": "If-Match" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretDeleted" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/numbers": { + "post": { + "tags": [ + "numbers" + ], + "summary": "Create Number", + "operationId": "create_number_conversational_ai_v3_projects__appid__numbers_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "get": { + "tags": [ + "numbers" + ], + "summary": "List Numbers", + "operationId": "list_numbers_conversational_ai_v3_projects__appid__numbers_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.desc", + "create_ts.asc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberListResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/numbers/{id}": { + "get": { + "tags": [ + "numbers" + ], + "summary": "Get Number", + "operationId": "get_number_conversational_ai_v3_projects__appid__numbers__id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "patch": { + "tags": [ + "numbers" + ], + "summary": "Update Number", + "operationId": "update_number_conversational_ai_v3_projects__appid__numbers__id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "delete": { + "tags": [ + "numbers" + ], + "summary": "Delete Number", + "operationId": "delete_number_conversational_ai_v3_projects__appid__numbers__id__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberDeleteResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions": { + "post": { + "tags": [ + "Sessions" + ], + "summary": "Create Session", + "operationId": "create_session_conversational_ai_v3_projects__appid__sessions_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStartResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "409": { + "description": "TaskConflict: an agent already runs with the same RTC channel and UID. IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "TaskConflict": { + "value": { + "detail": "An agent is already running with the same channel and UID.", + "reason": "TaskConflict" + } + }, + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "get": { + "tags": [ + "Sessions" + ], + "summary": "List Sessions", + "operationId": "list_sessions_conversational_ai_v3_projects__appid__sessions_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "channel", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Channel" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "RUNNING", + "title": "Status" + } + }, + { + "name": "started_after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Started After" + } + }, + { + "name": "started_before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Started Before" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 20, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionList" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions/ephemeral": { + "post": { + "tags": [ + "Sessions" + ], + "summary": "Create Ephemeral Session", + "operationId": "create_ephemeral_session_conversational_ai_v3_projects__appid__sessions_ephemeral_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicEphemeralSessionCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStartResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "409": { + "description": "TaskConflict: an agent already runs with the same RTC channel and UID. IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "TaskConflict": { + "value": { + "detail": "An agent is already running with the same channel and UID.", + "reason": "TaskConflict" + } + }, + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions/{agent_session_id}": { + "get": { + "tags": [ + "Sessions" + ], + "summary": "Retrieve Session", + "operationId": "retrieve_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "agent_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "patch": { + "tags": [ + "Sessions" + ], + "summary": "Update Session", + "operationId": "update_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "agent_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionUpdateResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions/{agent_session_id}/stop": { + "post": { + "tags": [ + "Sessions" + ], + "summary": "Stop Session", + "operationId": "stop_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__stop_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "agent_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStopResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + } + }, + "components": { + "schemas": { + "AgentDeleteResponse": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "deleted" + ], + "title": "AgentDeleteResponse" + }, + "AgentListData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/AgentListItem" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "AgentListData" + }, + "AgentListItem": { + "properties": { + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "update_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Update Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_id", + "agent_name", + "create_ts", + "update_ts" + ], + "title": "AgentListItem" + }, + "AgentListResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/AgentListData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "AgentListResponse" + }, + "AgentResponse": { + "properties": { + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "update_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Update Ts" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextGreeting" + }, + { + "$ref": "#/components/schemas/AudioGreeting" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "audio": "#/components/schemas/AudioGreeting", + "text": "#/components/schemas/TextGreeting" + } + } + }, + { + "type": "null" + } + ], + "title": "Greeting" + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "$ref": "#/components/schemas/VariableDefinition" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCascadedPipeline" + }, + { + "type": "null" + } + ] + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/FillerWords" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_id", + "create_ts", + "update_ts", + "agent_name" + ], + "title": "AgentResponse" + }, + "AsrInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "access_key_id": { + "type": "string", + "minLength": 1 + }, + "secret_access_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "access_key_id", + "secret_access_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "assemblyai", + "deepgram", + "gemini", + "openai", + "sarvam", + "smallestai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "ares", + "assemblyai", + "deepgram", + "gemini", + "google", + "microsoft", + "openai", + "sarvam", + "smallestai", + "speechmatics", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "adc_credentials_string": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "adc_credentials_string" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "microsoft", + "speechmatics" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + } + }, + "additionalProperties": false, + "type": "object", + "title": "AsrInput" + }, + "AudioGreeting": { + "properties": { + "mode": { + "type": "string", + "const": "audio", + "title": "Mode" + }, + "on": { + "type": "string", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "default": "each_join" + }, + "delay_ms": { + "type": "integer", + "title": "Delay Ms", + "default": 0, + "minimum": 0, + "maximum": 5000 + }, + "url": { + "type": "string", + "title": "Url", + "maxLength": 2048 + }, + "text": { + "type": "string", + "title": "Text", + "minLength": 1 + }, + "download_timeout_ms": { + "type": "integer", + "title": "Download Timeout Ms", + "default": 1000, + "minimum": 200, + "maximum": 10000 + }, + "pcm_sample_rate": { + "type": "integer", + "title": "Pcm Sample Rate", + "default": 16000 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "url", + "text" + ], + "title": "AudioGreeting" + }, + "AvatarInput": { + "properties": { + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarVendor" + }, + { + "type": "null" + } + ] + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + } + }, + "additionalProperties": false, + "type": "object", + "title": "AvatarInput" + }, + "AvatarVendor": { + "type": "string", + "enum": [ + "akool", + "liveavatar", + "anam", + "sensetime", + "spatius", + "generic" + ] + }, + "CallPolicy": { + "properties": { + "max_ring_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Ring Duration Ms" + }, + "max_call_duration_seconds": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Call Duration Seconds" + }, + "voicemail": { + "anyOf": [ + { + "$ref": "#/components/schemas/VoicemailPolicy" + }, + { + "type": "null" + } + ] + }, + "max_silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Silence Duration Ms" + }, + "end_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionEndCallPolicy" + }, + { + "type": "null" + } + ] + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransferPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CallPolicy" + }, + "CallingDay": { + "properties": { + "weekday": { + "type": "string", + "enum": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ], + "title": "Weekday" + }, + "ranges": { + "items": { + "$ref": "#/components/schemas/TimeRange" + }, + "type": "array", + "maxItems": 10, + "minItems": 1, + "title": "Ranges" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "weekday", + "ranges" + ], + "title": "CallingDay" + }, + "CampaignCallPolicy": { + "properties": { + "max_ring_duration_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 259200000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Ring Duration Ms" + }, + "max_call_duration_seconds": { + "anyOf": [ + { + "type": "integer", + "maximum": 259200.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Call Duration Seconds" + }, + "voicemail": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignVoicemailPolicy" + }, + { + "type": "null" + } + ] + }, + "max_silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 259200000.0, + "minimum": 30000.0 + }, + { + "type": "null" + } + ], + "title": "Max Silence Duration Ms" + }, + "end_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignEndCallPolicy" + }, + { + "type": "null" + } + ] + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignTransferPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignCallPolicy" + }, + "CampaignCounts": { + "properties": { + "contacts": { + "type": "integer", + "minimum": 0.0, + "title": "Contacts" + }, + "pending": { + "type": "integer", + "minimum": 0.0, + "title": "Pending" + }, + "calling": { + "type": "integer", + "minimum": 0.0, + "title": "Calling" + }, + "completed": { + "type": "integer", + "minimum": 0.0, + "title": "Completed" + }, + "failed": { + "type": "integer", + "minimum": 0.0, + "title": "Failed" + }, + "canceled": { + "type": "integer", + "minimum": 0.0, + "title": "Canceled" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "contacts", + "pending", + "calling", + "completed", + "failed", + "canceled" + ], + "title": "CampaignCounts" + }, + "CampaignCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Name" + }, + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "transport": { + "$ref": "#/components/schemas/CampaignTransport" + }, + "contacts": { + "items": { + "$ref": "#/components/schemas/ContactInput" + }, + "type": "array", + "maxItems": 50000, + "minItems": 1, + "title": "Contacts" + }, + "lifecycle": { + "$ref": "#/components/schemas/CampaignLifecycle" + }, + "pacing": { + "$ref": "#/components/schemas/Pacing" + }, + "schedule": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImmediateSchedule" + }, + { + "$ref": "#/components/schemas/ScheduledSchedule" + } + ], + "title": "Schedule", + "discriminator": { + "propertyName": "mode", + "mapping": { + "immediate": "#/components/schemas/ImmediateSchedule", + "scheduled": "#/components/schemas/ScheduledSchedule" + } + } + }, + "data_policy": { + "$ref": "#/components/schemas/DataPolicy", + "description": "Campaign data-retention policy. Defaults to 30-day retention.", + "default": { + "retention": "30_days" + } + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "agent_id", + "transport", + "contacts", + "schedule" + ], + "title": "CampaignCreate" + }, + "CampaignDeleteResponse": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignDeleteResponse" + }, + "CampaignEndCallPolicy": { + "properties": { + "on_conversation_complete": { + "type": "boolean", + "title": "On Conversation Complete", + "default": false + }, + "on_user_request": { + "type": "boolean", + "title": "On User Request", + "default": false + }, + "on_fax": { + "type": "boolean", + "title": "On Fax", + "default": false + }, + "on_ai_assistant": { + "type": "boolean", + "title": "On Ai Assistant", + "default": false + }, + "on_voicemail": { + "type": "boolean", + "title": "On Voicemail", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignEndCallPolicy" + }, + "CampaignFailure": { + "properties": { + "code": { + "type": "string", + "title": "Code" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + }, + "message": { + "type": "string", + "title": "Message" + }, + "ts": { + "type": "integer", + "minimum": 0.0, + "title": "Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "code", + "message", + "ts" + ], + "title": "CampaignFailure" + }, + "CampaignLifecycle": { + "properties": { + "idle_timeout_ms": { + "type": "integer", + "maximum": 259200000.0, + "minimum": 0.0, + "title": "Idle Timeout Ms", + "default": 30000 + }, + "max_duration_ms": { + "type": "integer", + "maximum": 259200000.0, + "minimum": 1000.0, + "title": "Max Duration Ms", + "default": 259200000 + }, + "graceful_stop": { + "$ref": "#/components/schemas/GracefulStop" + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignLifecycle" + }, + "CampaignListData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/CampaignResponse" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "CampaignListData" + }, + "CampaignListResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/CampaignListData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "CampaignListResponse" + }, + "CampaignPatch": { + "properties": { + "lifecycle": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignLifecycle" + }, + { + "type": "null" + } + ] + }, + "pacing": { + "anyOf": [ + { + "$ref": "#/components/schemas/PacingPatch" + }, + { + "type": "null" + } + ] + }, + "schedule": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ImmediateSchedule" + }, + { + "$ref": "#/components/schemas/ScheduledSchedule" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "immediate": "#/components/schemas/ImmediateSchedule", + "scheduled": "#/components/schemas/ScheduledSchedule" + } + } + }, + { + "type": "null" + } + ], + "title": "Schedule" + }, + "transport": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignTransportPatch" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignPatch" + }, + "CampaignResponse": { + "properties": { + "campaign_id": { + "type": "string", + "format": "uuid", + "title": "Campaign Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "status": { + "type": "string", + "enum": [ + "scheduled", + "running", + "paused", + "completed", + "canceled", + "failed" + ], + "title": "Status" + }, + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "transport": { + "$ref": "#/components/schemas/CampaignTransportResponse" + }, + "lifecycle": { + "$ref": "#/components/schemas/CampaignLifecycle" + }, + "schedule": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImmediateSchedule" + }, + { + "$ref": "#/components/schemas/ScheduledSchedule" + } + ], + "title": "Schedule", + "discriminator": { + "propertyName": "mode", + "mapping": { + "immediate": "#/components/schemas/ImmediateSchedule", + "scheduled": "#/components/schemas/ScheduledSchedule" + } + } + }, + "counts": { + "$ref": "#/components/schemas/CampaignCounts" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "start_ts": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Start Ts" + }, + "complete_ts": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Complete Ts" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignFailure" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "campaign_id", + "name", + "status", + "agent_id", + "transport", + "lifecycle", + "schedule", + "counts", + "create_ts", + "start_ts", + "complete_ts", + "failure" + ], + "title": "CampaignResponse" + }, + "CampaignTransferPolicy": { + "properties": { + "phone_number": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "Phone Number" + }, + "description": { + "type": "string", + "maxLength": 1000, + "minLength": 1, + "title": "Description" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "phone_number", + "description" + ], + "title": "CampaignTransferPolicy" + }, + "CampaignTransport": { + "properties": { + "type": { + "type": "string", + "const": "telephony", + "title": "Type" + }, + "from": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array" + } + ], + "title": "From" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "from" + ], + "title": "CampaignTransport" + }, + "CampaignTransportPatch": { + "properties": { + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignTransportPatch" + }, + "CampaignTransportResponse": { + "properties": { + "type": { + "type": "string", + "const": "telephony", + "title": "Type" + }, + "from": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "From" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "from" + ], + "title": "CampaignTransportResponse" + }, + "CampaignVoicemailPolicy": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "continue", + "hangup" + ], + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "CampaignVoicemailPolicy" + }, + "CascadedPipelineInput": { + "properties": { + "mode": { + "type": "string", + "const": "cascaded", + "title": "Mode" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarInput" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/TurnDetection" + }, + { + "type": "null" + } + ] + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/AsrInput" + }, + { + "type": "null" + } + ] + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/TtsInput" + }, + { + "type": "null" + } + ] + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/LlmInput" + }, + { + "type": "null" + } + ] + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SilenceConfig" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "CascadedPipelineInput" + }, + "ContactInput": { + "properties": { + "phone": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "Phone" + }, + "values": { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object", + "maxProperties": 100, + "title": "Values" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "phone" + ], + "title": "ContactInput" + }, + "DataPolicy": { + "properties": { + "retention": { + "type": "string", + "enum": [ + "30_days", + "none" + ], + "title": "Retention", + "default": "30_days" + } + }, + "additionalProperties": false, + "type": "object", + "title": "DataPolicy" + }, + "DisabledInterruption": { + "properties": { + "mode": { + "type": "string", + "const": "off", + "title": "Mode" + }, + "when_off": { + "type": "string", + "enum": [ + "append", + "ignore" + ], + "title": "When Off", + "default": "append" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "DisabledInterruption" + }, + "ErrorReason": { + "type": "string", + "enum": [ + "InvalidRequestBody", + "MissingRequiredField", + "InvalidFieldValue", + "InvalidModuleParameter", + "UnknownSecretReference", + "NotImplemented", + "InvalidPermission", + "ServiceNotEnabled", + "AccountSuspended", + "TaskNotFound", + "ResourceNotFound", + "AgentNotFound", + "NumberNotFound", + "NumberAlreadyExists", + "ResourceConflict", + "MethodNotAllowed", + "PayloadTooLarge", + "UnsupportedMediaType", + "TaskConflict", + "CampaignActive", + "CampaignInvalidState", + "CampaignNotEditable", + "IdempotencyKeyConflict", + "SecretSetAlreadyExists", + "SecretSetInUse", + "SecretSetEmpty", + "TaskNotRunning", + "RateLimitExceeded", + "PreconditionFailed", + "PreconditionRequired", + "ResourceQuotaLimitExceeded", + "ConcurrencyLimitExceeded", + "InternalError", + "ServiceUnavailable", + "ResourceAllocationFailed", + "TaskOperationTimeout" + ], + "title": "ErrorReason" + }, + "ErrorResponse": { + "properties": { + "detail": { + "type": "string", + "minLength": 1, + "title": "Detail" + }, + "reason": { + "$ref": "#/components/schemas/ErrorReason" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "detail", + "reason" + ], + "title": "ErrorResponse" + }, + "FillerTrigger": { + "properties": { + "mode": { + "type": "string", + "const": "fixed_time", + "title": "Mode" + }, + "response_wait_ms": { + "type": "integer", + "title": "Response Wait Ms" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "response_wait_ms" + ], + "title": "FillerTrigger" + }, + "FillerWords": { + "properties": { + "enable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable" + }, + "trigger": { + "anyOf": [ + { + "$ref": "#/components/schemas/FillerTrigger" + }, + { + "type": "null" + } + ] + }, + "content": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/StaticFillerContent" + }, + { + "$ref": "#/components/schemas/GeneratedFillerContent" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "generated": "#/components/schemas/GeneratedFillerContent", + "static": "#/components/schemas/StaticFillerContent" + } + } + }, + { + "type": "null" + } + ], + "title": "Content" + } + }, + "additionalProperties": false, + "type": "object", + "title": "FillerWords" + }, + "FunctionTool": { + "properties": { + "type": { + "type": "string", + "const": "function", + "title": "Type" + }, + "name": { + "type": "string", + "pattern": "^(?![mM][cC][pP])[A-Za-z][A-Za-z0-9]{0,63}$", + "title": "Name" + }, + "description": { + "type": "string", + "maxLength": 1024, + "minLength": 1, + "title": "Description" + }, + "parameters": { + "$ref": "#/components/schemas/JSONObject", + "description": "JSON Schema (draft 2020-12) describing the tool arguments." + }, + "execution": { + "$ref": "#/components/schemas/ToolExecution" + }, + "server": { + "$ref": "#/components/schemas/ToolServer" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "name", + "description", + "parameters", + "execution", + "server" + ], + "title": "FunctionTool" + }, + "GeneratedFillerContent": { + "properties": { + "mode": { + "type": "string", + "const": "generated", + "title": "Mode" + }, + "instructions": { + "type": "string", + "title": "Instructions" + }, + "fallback_strategy": { + "anyOf": [ + { + "type": "string", + "const": "static" + }, + { + "type": "null" + } + ], + "title": "Fallback Strategy" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "instructions" + ], + "title": "GeneratedFillerContent" + }, + "GracefulStop": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": false + }, + "timeout_ms": { + "type": "integer", + "maximum": 120000.0, + "minimum": 0.0, + "title": "Timeout Ms", + "default": 30000 + } + }, + "additionalProperties": false, + "type": "object", + "title": "GracefulStop" + }, + "ImmediateSchedule": { + "properties": { + "mode": { + "type": "string", + "const": "immediate", + "title": "Mode" + }, + "timezone": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Timezone" + }, + "days": { + "items": { + "$ref": "#/components/schemas/CallingDay" + }, + "type": "array", + "maxItems": 7, + "minItems": 1, + "title": "Days" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ImmediateSchedule" + }, + "InboundCallPolicy": { + "properties": { + "max_call_duration_seconds": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Call Duration Seconds" + }, + "max_silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Silence Duration Ms" + }, + "end_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundEndCallPolicy" + }, + { + "type": "null" + } + ], + "description": "Inbound agent end-call controls. Requires an agent; omitted flags default to false. An empty/all-false object disables the owned policy." + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransferPolicy" + }, + { + "type": "null" + } + ], + "description": "Reserved for runtime support; requests containing this field are rejected." + } + }, + "additionalProperties": false, + "type": "object", + "title": "InboundCallPolicy" + }, + "InboundInput": { + "properties": { + "agent": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Agent" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent" + ], + "title": "InboundInput" + }, + "InboundOutput": { + "properties": { + "agent": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Agent" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent" + ], + "title": "InboundOutput" + }, + "JSONObject": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "JsonValue": {}, + "KeywordInterruption": { + "properties": { + "mode": { + "type": "string", + "const": "keywords", + "title": "Mode" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Keywords", + "maxItems": 128 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "keywords" + ], + "title": "KeywordInterruption" + }, + "Lifecycle": { + "properties": { + "idle_timeout_ms": { + "type": "integer", + "maximum": 3600000.0, + "minimum": 0.0, + "title": "Idle Timeout Ms", + "default": 30000 + }, + "max_duration_ms": { + "type": "integer", + "maximum": 3600000.0, + "minimum": 1000.0, + "title": "Max Duration Ms", + "default": 3600000 + }, + "graceful_stop": { + "anyOf": [ + { + "$ref": "#/components/schemas/GracefulStop" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "Lifecycle" + }, + "LlmInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + }, + { + "properties": { + "fields": { + "properties": { + "access_key": { + "type": "string", + "minLength": 1 + }, + "secret_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "access_key", + "secret_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/FunctionTool" + }, + "type": "array", + "maxItems": 32 + }, + { + "type": "null" + } + ], + "title": "Tools" + }, + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "type": "integer", + "maximum": 1024.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Failure Message" + } + }, + "additionalProperties": false, + "type": "object", + "title": "LlmInput" + }, + "ManagedCredential": { + "properties": { + "mode": { + "type": "string", + "const": "managed", + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ManagedCredential" + }, + "ManualTurnBoundary": { + "properties": { + "type": { + "type": "string", + "const": "manual", + "title": "Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "ManualTurnBoundary" + }, + "McpServer": { + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Za-z0-9]{1,48}$", + "title": "Name" + }, + "endpoint": { + "type": "string", + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Endpoint" + }, + "transport": { + "type": "string", + "const": "streamable_http", + "title": "Transport" + }, + "headers": { + "anyOf": [ + { + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object", + "maxProperties": 32 + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "allowed_tools": { + "anyOf": [ + { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Tools" + }, + "timeout_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 100000.0, + "minimum": 1000.0 + }, + { + "type": "null" + } + ], + "title": "Timeout Ms" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "endpoint", + "transport" + ], + "title": "McpServer" + }, + "Message": { + "properties": { + "role": { + "type": "string", + "enum": [ + "system", + "user", + "assistant" + ], + "title": "Role" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "role", + "content" + ], + "title": "Message" + }, + "NumberCreate": { + "properties": { + "number": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "title": "Number", + "description": "International inputs normalize to E.164; national inputs normalize to national significant digits." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "sip_trunk": { + "$ref": "#/components/schemas/SipTrunkInput" + }, + "inbound": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundInput" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "number", + "sip_trunk" + ], + "title": "NumberCreate" + }, + "NumberDeleteResponse": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "NumberDeleteResponse" + }, + "NumberListData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/NumberResponse" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "NumberListData" + }, + "NumberListResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/NumberListData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "NumberListResponse" + }, + "NumberPatch": { + "properties": { + "number": { + "anyOf": [ + { + "type": "string", + "maxLength": 128, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Number", + "description": "May reformat the existing number but must not change its normalized identity." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "sip_trunk": { + "anyOf": [ + { + "$ref": "#/components/schemas/SipTrunkInput" + }, + { + "type": "null" + } + ] + }, + "inbound": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundInput" + }, + { + "type": "null" + } + ], + "description": "Explicit null unbinds inbound; omission leaves inbound unchanged." + } + }, + "additionalProperties": false, + "type": "object", + "title": "NumberPatch" + }, + "NumberResponse": { + "properties": { + "number_id": { + "type": "string", + "format": "uuid", + "title": "Number Id" + }, + "number": { + "type": "string", + "title": "Number" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "sip_trunk": { + "anyOf": [ + { + "$ref": "#/components/schemas/SipTrunkOutput" + }, + { + "type": "null" + } + ] + }, + "inbound": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundOutput" + }, + { + "type": "null" + } + ] + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "number_id", + "number", + "create_ts" + ], + "title": "NumberResponse" + }, + "Pacing": { + "properties": { + "max_concurrent": { + "type": "integer", + "maximum": 10000.0, + "minimum": 1.0, + "title": "Max Concurrent", + "default": 10 + }, + "max_calls_per_second": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Calls Per Second" + } + }, + "additionalProperties": false, + "type": "object", + "title": "Pacing" + }, + "PacingPatch": { + "properties": { + "max_concurrent": { + "anyOf": [ + { + "type": "integer", + "maximum": 10000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Concurrent" + }, + "max_calls_per_second": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Calls Per Second" + } + }, + "additionalProperties": false, + "type": "object", + "title": "PacingPatch" + }, + "PublicAgentInput": { + "properties": { + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextGreeting" + }, + { + "$ref": "#/components/schemas/AudioGreeting" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "audio": "#/components/schemas/AudioGreeting", + "text": "#/components/schemas/TextGreeting" + } + } + }, + { + "type": "null" + } + ], + "title": "Greeting" + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "$ref": "#/components/schemas/VariableDefinition" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/CascadedPipelineInput" + }, + { + "type": "null" + } + ] + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/FillerWords" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "PublicAgentInput" + }, + "PublicByokCredential": { + "properties": { + "mode": { + "type": "string", + "const": "byok", + "title": "Mode" + }, + "fields": { + "additionalProperties": { + "type": "string", + "minLength": 1 + }, + "type": "object", + "maxProperties": 8, + "minProperties": 1, + "description": "Provider credential fields for the selected vendor, for example {\"api_key\": \"$secrets..\"}. Agent writes require $secrets references; ephemeral sessions also accept direct values." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "fields", + "mode" + ], + "title": "PublicByokCredential" + }, + "PublicEphemeralSessionCreate": { + "properties": { + "agent": { + "$ref": "#/components/schemas/PublicAgentInput" + }, + "values": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Values" + }, + "labels": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Labels" + }, + "transport": { + "oneOf": [ + { + "$ref": "#/components/schemas/RtcTransport" + }, + { + "$ref": "#/components/schemas/TelephonyTransport" + } + ], + "title": "Transport", + "discriminator": { + "propertyName": "type", + "mapping": { + "rtc": "#/components/schemas/RtcTransport", + "telephony": "#/components/schemas/TelephonyTransport" + } + } + }, + "lifecycle": { + "anyOf": [ + { + "$ref": "#/components/schemas/Lifecycle" + }, + { + "type": "null" + } + ] + }, + "data_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/DataPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent", + "transport" + ], + "title": "PublicEphemeralSessionCreate" + }, + "ResolvedAsr": { + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedAsr" + }, + "ResolvedAvatar": { + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedAvatar" + }, + "ResolvedCascadedPipeline": { + "properties": { + "mode": { + "type": "string", + "const": "cascaded", + "title": "Mode" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedAvatar" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/TurnDetection" + }, + { + "type": "null" + } + ] + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedAsr" + }, + { + "type": "null" + } + ] + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedTts" + }, + { + "type": "null" + } + ] + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedLlm" + }, + { + "type": "null" + } + ] + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SilenceConfig" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ResolvedCascadedPipeline" + }, + "ResolvedCredential": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "managed", + "byok" + ], + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ResolvedCredential" + }, + "ResolvedLlm": { + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Failure Message" + }, + "mcp_servers": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/McpServer" + } + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/FunctionTool" + } + }, + { + "type": "null" + } + ], + "title": "Tools" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedLlm" + }, + "ResolvedTts": { + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + }, + "skip_patterns": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Skip Patterns" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedTts" + }, + "RtcEncryption": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "aes_128_xts", + "aes_128_ecb", + "aes_256_xts", + "sm4_128_ecb", + "aes_128_gcm", + "aes_256_gcm", + "aes_128_gcm2", + "aes_256_gcm2" + ], + "title": "Mode" + }, + "key": { + "type": "string", + "minLength": 1, + "format": "password", + "title": "Key", + "writeOnly": true + }, + "salt": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "pattern": "^[A-Za-z0-9+/]{43}=$", + "title": "Salt" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "key" + ], + "title": "RtcEncryption" + }, + "RtcTransport": { + "properties": { + "type": { + "type": "string", + "const": "rtc", + "title": "Type" + }, + "channel": { + "type": "string", + "maxLength": 256, + "minLength": 1, + "title": "Channel" + }, + "token": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "Token" + }, + "uid": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "title": "Uid", + "description": "UID the agent uses to join the channel. Omit it to let the server assign one." + }, + "subscribe_uids": { + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "type": "array", + "maxItems": 1, + "minItems": 1, + "title": "Subscribe Uids", + "uniqueItems": true + }, + "encryption": { + "anyOf": [ + { + "$ref": "#/components/schemas/RtcEncryption" + }, + { + "type": "null" + } + ] + }, + "audio_scenario": { + "type": "string", + "enum": [ + "aiserver", + "chorus" + ], + "title": "Audio Scenario", + "default": "aiserver" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "channel", + "subscribe_uids" + ], + "title": "RtcTransport" + }, + "ScheduledSchedule": { + "properties": { + "mode": { + "type": "string", + "const": "scheduled", + "title": "Mode" + }, + "timezone": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Timezone" + }, + "start": { + "type": "string", + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]$", + "title": "Start" + }, + "end": { + "type": "string", + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]$", + "title": "End" + }, + "days": { + "items": { + "$ref": "#/components/schemas/CallingDay" + }, + "type": "array", + "maxItems": 7, + "minItems": 1, + "title": "Days" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "timezone", + "start", + "end" + ], + "title": "ScheduledSchedule" + }, + "SecretCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + }, + "secrets": { + "patternProperties": { + "^[A-Za-z0-9_]+$": { + "type": "string", + "format": "password", + "writeOnly": true + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1 + }, + "type": "object", + "minProperties": 1, + "title": "Secrets", + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "secrets" + ], + "title": "SecretCreate" + }, + "SecretDeleted": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "deleted" + ], + "title": "SecretDeleted" + }, + "SecretKey": { + "properties": { + "key": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Key" + }, + "create_ts": { + "type": "integer", + "minimum": 0, + "description": "Key timestamp when returned by the service." + }, + "update_ts": { + "type": "integer", + "minimum": 0, + "description": "Key timestamp when returned by the service." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ], + "title": "SecretKey" + }, + "SecretKeyWrite": { + "properties": { + "value": { + "type": "string", + "minLength": 1, + "format": "password", + "title": "Value", + "writeOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "value" + ], + "title": "SecretKeyWrite" + }, + "SecretList": { + "properties": { + "data": { + "$ref": "#/components/schemas/SecretPage" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "SecretList" + }, + "SecretPage": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/SecretSet" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "SecretPage" + }, + "SecretReplace": { + "properties": { + "secrets": { + "patternProperties": { + "^[A-Za-z0-9_]+$": { + "type": "string", + "format": "password", + "writeOnly": true + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1 + }, + "type": "object", + "minProperties": 1, + "title": "Secrets", + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "secrets" + ], + "title": "SecretReplace" + }, + "SecretSet": { + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + }, + "keys": { + "items": { + "anyOf": [ + { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$" + }, + { + "$ref": "#/components/schemas/SecretKey" + } + ] + }, + "type": "array", + "title": "Keys", + "description": "Accepts legacy key names and launch key metadata objects during rollout." + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "update_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Update Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "keys", + "create_ts", + "update_ts" + ], + "title": "SecretSet" + }, + "SecretUpdate": { + "properties": { + "secrets": { + "patternProperties": { + "^[A-Za-z0-9_]+$": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ] + }, + "^[A-Za-z0-9_]*-[A-Za-z0-9_-]*$": { + "type": "null", + "description": "An existing legacy hyphenated key may only be deleted, not written." + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1 + }, + "type": "object", + "minProperties": 1, + "title": "Secrets", + "description": "Non-null writes require ASCII letters, digits or underscores; existing hyphenated keys may only be removed with null.", + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "secrets" + ], + "title": "SecretUpdate" + }, + "SemanticTurnEnd": { + "properties": { + "type": { + "type": "string", + "const": "semantic", + "title": "Type", + "default": "semantic" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "title": "Silence Duration Ms", + "default": 320 + }, + "max_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 500, + "maximum": 10000 + }, + { + "type": "integer", + "const": -1 + }, + { + "type": "null" + } + ], + "title": "Max Wait Ms", + "default": 3000, + "description": "500-10000 ms, or -1 to wait without a timeout." + }, + "pause_state_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Pause State Enabled", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "SemanticTurnEnd" + }, + "SemanticTurnStart": { + "properties": { + "type": { + "type": "string", + "const": "semantic", + "title": "Type", + "default": "semantic" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Speaking Interrupt Duration Ms", + "default": 160 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "SemanticTurnStart" + }, + "SessionAgentOverridesPatch": { + "properties": { + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "title": "Messages" + }, + "pipeline": { + "$ref": "#/components/schemas/SessionPipelinePatch", + "title": "Pipeline" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionAgentOverridesPatch" + }, + "SessionAgentPatch": { + "properties": { + "overrides": { + "$ref": "#/components/schemas/SessionAgentOverridesPatch", + "title": "Overrides" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionAgentPatch" + }, + "SessionCreate": { + "properties": { + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "values": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Values" + }, + "labels": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Labels" + }, + "transport": { + "oneOf": [ + { + "$ref": "#/components/schemas/RtcTransport" + }, + { + "$ref": "#/components/schemas/TelephonyTransport" + } + ], + "title": "Transport", + "discriminator": { + "propertyName": "type", + "mapping": { + "rtc": "#/components/schemas/RtcTransport", + "telephony": "#/components/schemas/TelephonyTransport" + } + } + }, + "lifecycle": { + "anyOf": [ + { + "$ref": "#/components/schemas/Lifecycle" + }, + { + "type": "null" + } + ] + }, + "data_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/DataPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_id", + "transport" + ], + "title": "SessionCreate" + }, + "SessionData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "SessionData" + }, + "SessionList": { + "properties": { + "data": { + "$ref": "#/components/schemas/SessionData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "SessionList" + }, + "SessionModulePatch": { + "properties": { + "params": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object", + "title": "Params", + "description": "Complete provider params replacement. Runtime endpoint aliases are not mutable." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionModulePatch" + }, + "SessionPatch": { + "properties": { + "transport": { + "$ref": "#/components/schemas/SessionTransportPatch", + "title": "Transport" + }, + "agent": { + "$ref": "#/components/schemas/SessionAgentPatch", + "title": "Agent" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionPatch" + }, + "SessionPipelinePatch": { + "properties": { + "llm": { + "$ref": "#/components/schemas/SessionModulePatch", + "title": "Llm" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionPipelinePatch" + }, + "SessionResponse": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name", + "description": "Name of the agent the session runs." + }, + "start_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Start Ts" + }, + "status": { + "type": "string", + "enum": [ + "IDLE", + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "FAILED" + ], + "title": "Status" + }, + "stop_ts": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Stop Ts" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message" + }, + "analysis": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionAnalysis" + }, + { + "type": "null" + } + ], + "description": "Optional extraction analysis. May be absent while analysis support is rolling out." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "start_ts", + "status" + ], + "title": "SessionResponse" + }, + "SessionStartResponse": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "create_ts": { + "type": "integer", + "minimum": 0, + "title": "Create Ts", + "description": "Unix timestamp in seconds when the session was created." + }, + "status": { + "type": "string", + "enum": [ + "STARTING", + "RUNNING" + ], + "title": "Status" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "create_ts", + "status" + ], + "title": "SessionStartResponse" + }, + "SessionStopResponse": { + "properties": {}, + "additionalProperties": false, + "type": "object", + "title": "SessionStopResponse" + }, + "SessionSummary": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "start_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Start Ts" + }, + "status": { + "type": "string", + "enum": [ + "IDLE", + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "FAILED" + ], + "title": "Status" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "start_ts", + "status" + ], + "title": "SessionSummary" + }, + "SessionTransportPatch": { + "properties": { + "token": { + "type": "string", + "minLength": 1, + "format": "password", + "title": "Token", + "writeOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "token" + ], + "title": "SessionTransportPatch" + }, + "SessionUpdateResponse": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "status": { + "type": "string", + "enum": [ + "IDLE", + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "FAILED" + ], + "title": "Status" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "create_ts", + "status" + ], + "title": "SessionUpdateResponse" + }, + "SilenceConfig": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled" + }, + "timeout_ms": { + "type": "integer", + "title": "Timeout Ms" + }, + "action": { + "type": "string", + "enum": [ + "speak", + "think" + ], + "title": "Action" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "enabled", + "timeout_ms", + "action", + "content" + ], + "title": "SilenceConfig" + }, + "SipAuth": { + "properties": { + "username": { + "type": "string", + "title": "Username", + "writeOnly": true + }, + "password": { + "type": "string", + "format": "password", + "title": "Password", + "description": "Literal SIP password; secret references are not supported.", + "writeOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "username", + "password" + ], + "title": "SipAuth" + }, + "SipTrunkInput": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "hostname": { + "type": "string", + "pattern": "^[^:/\\s]+$", + "title": "Hostname" + }, + "port": { + "anyOf": [ + { + "type": "integer", + "maximum": 65535.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Port" + }, + "transport": { + "type": "string", + "enum": [ + "udp", + "tcp", + "tls" + ], + "title": "Transport", + "default": "tls" + }, + "auth": { + "anyOf": [ + { + "$ref": "#/components/schemas/SipAuth" + }, + { + "type": "null" + } + ] + }, + "allowed_ips": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Ips", + "description": "Inbound source IPv4 addresses, IPv4 CIDRs (decimal prefix length 0-32 without leading zeros), or '*'. Host bits are accepted; entries are preserved unchanged. IPv6, dotted masks and hostnames unsupported. Omission defaults runtime inbound allowed_addresses to ['*']; [] explicitly clears it." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "hostname" + ], + "title": "SipTrunkInput" + }, + "SipTrunkOutput": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "hostname": { + "type": "string", + "title": "Hostname" + }, + "port": { + "type": "integer", + "title": "Port" + }, + "transport": { + "type": "string", + "enum": [ + "udp", + "tcp", + "tls" + ], + "title": "Transport" + }, + "allowed_ips": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Ips", + "description": "Inbound source IPv4 addresses, IPv4 CIDRs (decimal prefix length 0-32 without leading zeros), or '*'. Host bits are accepted; entries are preserved unchanged. IPv6, dotted masks and hostnames unsupported. Omission defaults runtime inbound allowed_addresses to ['*']; [] explicitly clears it." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "hostname", + "port", + "transport" + ], + "title": "SipTrunkOutput" + }, + "SkipPattern": { + "type": "string", + "enum": [ + "fullwidth_parentheses", + "lenticular_brackets", + "parentheses", + "square_brackets", + "curly_braces" + ] + }, + "SpeechInterruption": { + "properties": { + "mode": { + "type": "string", + "const": "start_of_speech", + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "SpeechInterruption" + }, + "StaticFillerContent": { + "properties": { + "mode": { + "type": "string", + "const": "static", + "title": "Mode" + }, + "phrases": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array", + "title": "Phrases", + "minItems": 1, + "maxItems": 100 + }, + "selection_rule": { + "type": "string", + "enum": [ + "shuffle", + "round_robin" + ], + "title": "Selection Rule", + "default": "shuffle" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "phrases" + ], + "title": "StaticFillerContent" + }, + "TelephonyTransport": { + "properties": { + "type": { + "type": "string", + "const": "telephony", + "title": "Type" + }, + "from": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "title": "From", + "description": "ID of a Number in the project. Phone numbers are not accepted." + }, + "to": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "To" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "from", + "to" + ], + "title": "TelephonyTransport" + }, + "TextGreeting": { + "properties": { + "mode": { + "type": "string", + "const": "text", + "title": "Mode" + }, + "on": { + "type": "string", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "default": "each_join" + }, + "delay_ms": { + "type": "integer", + "title": "Delay Ms", + "default": 0, + "minimum": 0, + "maximum": 5000 + }, + "text": { + "type": "string", + "title": "Text" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "text" + ], + "title": "TextGreeting" + }, + "TimeRange": { + "properties": { + "start": { + "type": "string", + "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", + "title": "Start" + }, + "end": { + "type": "string", + "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$", + "title": "End" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "start", + "end" + ], + "title": "TimeRange" + }, + "ToolExecution": { + "properties": { + "mode": { + "type": "string", + "const": "sync", + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ToolExecution" + }, + "ToolServer": { + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST" + ], + "title": "Method" + }, + "url": { + "type": "string", + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object", + "maxProperties": 32 + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "body": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "timeout_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 100000.0, + "minimum": 1000.0 + }, + { + "type": "null" + } + ], + "title": "Timeout Ms", + "default": 10000 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "method", + "url" + ], + "title": "ToolServer" + }, + "TransferPolicy": { + "properties": { + "phone_number": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "Phone Number" + }, + "description": { + "type": "string", + "maxLength": 1000, + "minLength": 1, + "title": "Description" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "phone_number", + "description" + ], + "title": "TransferPolicy" + }, + "TtsInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "aws_access_key_id": { + "type": "string", + "minLength": 1 + }, + "aws_secret_access_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "aws_access_key_id", + "aws_secret_access_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "cartesia", + "deepgram", + "fishaudio", + "gemini", + "generic_http", + "gradium", + "mistral", + "murf", + "openai", + "rime", + "smallestai", + "typecast", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "cartesia", + "deepgram", + "elevenlabs", + "fishaudio", + "gemini", + "generic_http", + "google", + "gradium", + "humeai", + "microsoft", + "minimax", + "mistral", + "murf", + "openai", + "rime", + "sarvam", + "smallestai", + "typecast", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "elevenlabs", + "humeai", + "microsoft", + "minimax" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "credentials": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "credentials" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "sarvam" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_subscription_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_subscription_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "skip_patterns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/SkipPattern" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "uniqueItems": true, + "title": "Skip Patterns" + }, + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers" + } + }, + "additionalProperties": false, + "type": "object", + "title": "TtsInput" + }, + "TurnDetection": { + "properties": { + "speech_threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "title": "Speech Threshold", + "default": 0.5 + }, + "start_of_speech": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/SemanticTurnStart" + }, + { + "$ref": "#/components/schemas/VadTurnStart" + }, + { + "$ref": "#/components/schemas/ManualTurnBoundary" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "manual": "#/components/schemas/ManualTurnBoundary", + "semantic": "#/components/schemas/SemanticTurnStart", + "vad": "#/components/schemas/VadTurnStart" + } + } + }, + { + "type": "null" + } + ], + "title": "Start Of Speech" + }, + "end_of_speech": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/SemanticTurnEnd" + }, + { + "$ref": "#/components/schemas/VadTurnEnd" + }, + { + "$ref": "#/components/schemas/ManualTurnBoundary" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "manual": "#/components/schemas/ManualTurnBoundary", + "semantic": "#/components/schemas/SemanticTurnEnd", + "vad": "#/components/schemas/VadTurnEnd" + } + } + }, + { + "type": "null" + } + ], + "title": "End Of Speech" + }, + "interruption": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/SpeechInterruption" + }, + { + "$ref": "#/components/schemas/KeywordInterruption" + }, + { + "$ref": "#/components/schemas/DisabledInterruption" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "keywords": "#/components/schemas/KeywordInterruption", + "off": "#/components/schemas/DisabledInterruption", + "start_of_speech": "#/components/schemas/SpeechInterruption" + } + } + }, + { + "type": "null" + } + ], + "title": "Interruption", + "default": { + "mode": "start_of_speech" + } + } + }, + "additionalProperties": false, + "type": "object", + "title": "TurnDetection" + }, + "VadTurnEnd": { + "properties": { + "type": { + "type": "string", + "const": "vad", + "title": "Type" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "title": "Silence Duration Ms", + "default": 640 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "VadTurnEnd" + }, + "VadTurnStart": { + "properties": { + "type": { + "type": "string", + "const": "vad", + "title": "Type" + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "title": "Interrupt Duration Ms", + "default": 160 + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "title": "Speaking Interrupt Duration Ms", + "default": 160 + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "title": "Prefix Padding Ms", + "default": 800 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "VadTurnStart" + }, + "VariableDefinition": { + "properties": { + "type": { + "type": "string", + "enum": [ + "str", + "int", + "float", + "bool", + "date", + "datetime" + ], + "title": "Type" + }, + "required": { + "type": "boolean", + "title": "Required", + "default": false + }, + "options": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "type": "array", + "maxItems": 100, + "minItems": 1 + }, + { + "type": "null" + } + ], + "title": "Options" + }, + "extract": { + "type": "boolean", + "title": "Extract", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "VariableDefinition" + }, + "VoicemailPolicy": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "continue", + "hangup" + ], + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "VoicemailPolicy" + }, + "AgentRequest_AgentCreateInput": { + "additionalProperties": false, + "properties": { + "agent_name": { + "maxLength": 255, + "minLength": 1, + "title": "Agent Name", + "type": "string" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "audio": "#/components/schemas/AgentRequest_AudioGreeting", + "text": "#/components/schemas/AgentRequest_TextGreeting" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TextGreeting" + }, + { + "$ref": "#/components/schemas/AgentRequest_AudioGreeting" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Greeting" + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "$ref": "#/components/schemas/AgentRequest_VariableDefinition" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedPipelineInput" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Omission, null, or an empty object selects the managed ARES, OpenAI gpt-4.1-mini, and MiniMax speech-2.8-turbo default pipeline." + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerWords" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "agent_name" + ], + "title": "AgentCreateInput", + "type": "object" + }, + "AgentRequest_AgentPatch": { + "additionalProperties": false, + "properties": { + "agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Name" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_GreetingPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_VariableDefinitionPatch" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedPipelinePatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerWordsPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "AgentPatch", + "type": "object" + }, + "AgentRequest_AsrInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "access_key_id": { + "minLength": 1, + "type": "string" + }, + "secret_access_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "access_key_id", + "secret_access_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "assemblyai", + "deepgram", + "gemini", + "openai", + "sarvam", + "smallestai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "ares", + "assemblyai", + "deepgram", + "gemini", + "google", + "microsoft", + "openai", + "sarvam", + "smallestai", + "speechmatics", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "adc_credentials_string": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "adc_credentials_string" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "microsoft", + "speechmatics" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + } + }, + "title": "AsrInput", + "type": "object" + }, + "AgentRequest_AsrPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + }, + "then": { + "properties": { + "url": { + "type": "null" + } + } + } + } + ], + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Non-null URLs require effective BYOK mode; null clears a saved override.", + "title": "Url" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + } + }, + "title": "AsrPatch", + "type": "object" + }, + "AgentRequest_AudioGreeting": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "audio", + "title": "Mode", + "type": "string" + }, + "on": { + "default": "each_join", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "type": "string" + }, + "delay_ms": { + "default": 0, + "title": "Delay Ms", + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + "url": { + "title": "Url", + "type": "string", + "maxLength": 2048 + }, + "text": { + "title": "Text", + "type": "string", + "minLength": 1 + }, + "download_timeout_ms": { + "default": 1000, + "title": "Download Timeout Ms", + "type": "integer", + "minimum": 200, + "maximum": 10000 + }, + "pcm_sample_rate": { + "default": 16000, + "title": "Pcm Sample Rate", + "type": "integer" + } + }, + "required": [ + "mode", + "url", + "text" + ], + "title": "AudioGreeting", + "type": "object" + }, + "AgentRequest_AvatarInput": { + "additionalProperties": false, + "properties": { + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarVendor" + }, + { + "type": "null" + } + ], + "default": null + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + } + }, + "title": "AvatarInput", + "type": "object" + }, + "AgentRequest_AvatarPatch": { + "additionalProperties": false, + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarVendor" + }, + { + "type": "null" + } + ], + "default": null + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + } + }, + "title": "AvatarPatch", + "type": "object" + }, + "AgentRequest_AvatarVendor": { + "enum": [ + "akool", + "liveavatar", + "anam", + "sensetime", + "spatius", + "generic" + ], + "type": "string" + }, + "AgentRequest_CascadedPipelineInput": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "cascaded", + "title": "Mode", + "type": "string" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TurnDetection" + }, + { + "type": "null" + } + ], + "default": null + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AsrInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TtsInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_LlmInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SilenceConfig" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "mode" + ], + "title": "CascadedPipelineInput", + "type": "object" + }, + "AgentRequest_CascadedPipelinePatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "const": "cascaded", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AsrPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TtsPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_LlmPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SilenceConfigPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "CascadedPipelinePatch", + "type": "object" + }, + "AgentRequest_CascadedTurnDetectionPatch": { + "additionalProperties": false, + "properties": { + "speech_threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Speech Threshold" + }, + "start_of_speech": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TurnBoundaryPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "end_of_speech": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TurnBoundaryPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "interruption": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_InterruptionPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "CascadedTurnDetectionPatch", + "type": "object" + }, + "AgentRequest_DisabledInterruption": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "off", + "title": "Mode", + "type": "string" + }, + "when_off": { + "default": "append", + "enum": [ + "append", + "ignore" + ], + "title": "When Off", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "DisabledInterruption", + "type": "object" + }, + "AgentRequest_FillerContentPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "static", + "generated" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "phrases": { + "anyOf": [ + { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array", + "minItems": 1, + "maxItems": 100 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Phrases" + }, + "selection_rule": { + "anyOf": [ + { + "enum": [ + "shuffle", + "round_robin" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Selection Rule" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Instructions" + }, + "fallback_strategy": { + "anyOf": [ + { + "const": "static", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fallback Strategy" + } + }, + "title": "FillerContentPatch", + "type": "object" + }, + "AgentRequest_FillerTrigger": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "fixed_time", + "title": "Mode", + "type": "string" + }, + "response_wait_ms": { + "title": "Response Wait Ms", + "type": "integer" + } + }, + "required": [ + "mode", + "response_wait_ms" + ], + "title": "FillerTrigger", + "type": "object" + }, + "AgentRequest_FillerTriggerPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "const": "fixed_time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "response_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 100, + "maximum": 10000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Response Wait Ms" + } + }, + "title": "FillerTriggerPatch", + "type": "object" + }, + "AgentRequest_FillerWords": { + "additionalProperties": false, + "properties": { + "enable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enable" + }, + "trigger": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerTrigger" + }, + { + "type": "null" + } + ], + "default": null + }, + "content": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "generated": "#/components/schemas/AgentRequest_GeneratedFillerContent", + "static": "#/components/schemas/AgentRequest_StaticFillerContent" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_StaticFillerContent" + }, + { + "$ref": "#/components/schemas/AgentRequest_GeneratedFillerContent" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Content" + } + }, + "title": "FillerWords", + "type": "object" + }, + "AgentRequest_FillerWordsPatch": { + "additionalProperties": false, + "properties": { + "enable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enable" + }, + "trigger": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerTriggerPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "content": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerContentPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "FillerWordsPatch", + "type": "object" + }, + "AgentRequest_FunctionTool": { + "additionalProperties": false, + "properties": { + "type": { + "const": "function", + "title": "Type", + "type": "string" + }, + "name": { + "pattern": "^(?![mM][cC][pP])[A-Za-z][A-Za-z0-9]{0,63}$", + "title": "Name", + "type": "string" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "title": "Description", + "type": "string" + }, + "parameters": { + "$ref": "#/components/schemas/AgentRequest_JSONObject", + "description": "JSON Schema (draft 2020-12) describing the tool arguments." + }, + "execution": { + "$ref": "#/components/schemas/AgentRequest_ToolExecution" + }, + "server": { + "$ref": "#/components/schemas/AgentRequest_ToolServer" + } + }, + "required": [ + "type", + "name", + "description", + "parameters", + "execution", + "server" + ], + "title": "FunctionTool", + "type": "object" + }, + "AgentRequest_GeneratedFillerContent": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "generated", + "title": "Mode", + "type": "string" + }, + "instructions": { + "title": "Instructions", + "type": "string" + }, + "fallback_strategy": { + "anyOf": [ + { + "const": "static", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fallback Strategy" + } + }, + "required": [ + "mode", + "instructions" + ], + "title": "GeneratedFillerContent", + "type": "object" + }, + "AgentRequest_GreetingPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "text", + "audio" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "on": { + "anyOf": [ + { + "enum": [ + "each_join", + "first_join" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "On" + }, + "delay_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Delay Ms" + }, + "url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + }, + "download_timeout_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 200, + "maximum": 10000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Download Timeout Ms" + }, + "pcm_sample_rate": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pcm Sample Rate" + } + }, + "title": "GreetingPatch", + "type": "object" + }, + "AgentRequest_InterruptionPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "start_of_speech", + "keywords", + "off" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "keywords": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "maxItems": 128 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Keywords" + }, + "when_off": { + "anyOf": [ + { + "enum": [ + "append", + "ignore" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "When Off" + } + }, + "title": "InterruptionPatch", + "type": "object" + }, + "AgentRequest_JSONObject": { + "additionalProperties": { + "$ref": "#/components/schemas/AgentRequest_JsonValue" + }, + "type": "object" + }, + "AgentRequest_JsonValue": {}, + "AgentRequest_KeywordInterruption": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "keywords", + "title": "Mode", + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "title": "Keywords", + "type": "array", + "maxItems": 128 + } + }, + "required": [ + "mode", + "keywords" + ], + "title": "KeywordInterruption", + "type": "object" + }, + "AgentRequest_LlmInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + }, + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "access_key": { + "minLength": 1, + "type": "string" + }, + "secret_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "access_key", + "secret_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_FunctionTool" + }, + "maxItems": 32, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tools" + }, + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "maximum": 1024, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Failure Message" + } + }, + "title": "LlmInput", + "type": "object" + }, + "AgentRequest_LlmPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + }, + "then": { + "properties": { + "url": { + "type": "null" + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_FunctionTool" + }, + "maxItems": 32, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tools" + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Non-null URLs require effective BYOK mode; null clears a saved override.", + "title": "Url" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "maximum": 1024, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Failure Message" + } + }, + "title": "LlmPatch", + "type": "object" + }, + "AgentRequest_ManagedCredential": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "managed", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "ManagedCredential", + "type": "object" + }, + "AgentRequest_ManualTurnBoundary": { + "additionalProperties": false, + "properties": { + "type": { + "const": "manual", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ManualTurnBoundary", + "type": "object" + }, + "AgentRequest_McpServer": { + "additionalProperties": false, + "properties": { + "name": { + "pattern": "^[A-Za-z0-9]{1,48}$", + "title": "Name", + "type": "string" + }, + "endpoint": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Endpoint", + "type": "string" + }, + "transport": { + "const": "streamable_http", + "title": "Transport", + "type": "string" + }, + "headers": { + "anyOf": [ + { + "maxProperties": 32, + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "allowed_tools": { + "anyOf": [ + { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Allowed Tools" + }, + "timeout_ms": { + "anyOf": [ + { + "maximum": 100000, + "minimum": 1000, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Ms" + } + }, + "required": [ + "name", + "endpoint", + "transport" + ], + "title": "McpServer", + "type": "object" + }, + "AgentRequest_Message": { + "additionalProperties": false, + "properties": { + "role": { + "enum": [ + "system", + "user", + "assistant" + ], + "title": "Role", + "type": "string" + }, + "content": { + "title": "Content", + "type": "string" + } + }, + "required": [ + "role", + "content" + ], + "title": "Message", + "type": "object" + }, + "AgentRequest_PublicByokCredential": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "byok", + "title": "Mode", + "type": "string" + }, + "fields": { + "additionalProperties": { + "minLength": 1, + "type": "string", + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + }, + "maxProperties": 8, + "minProperties": 1, + "type": "object", + "description": "Provider credential fields for the selected vendor, for example {\"api_key\": \"$secrets..\"}. Agent writes require $secrets references; ephemeral sessions also accept direct values." + } + }, + "required": [ + "fields", + "mode" + ], + "title": "PublicByokCredential", + "type": "object" + }, + "AgentRequest_PublicCredentialPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "properties": { + "fields": { + "type": "null" + } + } + } + } + ], + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "managed", + "byok" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "fields": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + }, + { + "type": "null" + } + ] + }, + "maxProperties": 8, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fields" + } + }, + "title": "PublicCredentialPatch", + "type": "object", + "description": "JSON Merge Patch of provider credentials. Non-null field values on saved-agent writes must be $secrets references. Set fields to null when switching to managed credentials; individual null map entries remove stored fields. The merged result must satisfy the credential schema for the selected provider." + }, + "AgentRequest_SemanticTurnEnd": { + "additionalProperties": false, + "properties": { + "type": { + "const": "semantic", + "title": "Type", + "type": "string", + "default": "semantic" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "default": 320, + "title": "Silence Duration Ms" + }, + "max_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 500, + "maximum": 10000 + }, + { + "type": "integer", + "const": -1 + }, + { + "type": "null" + } + ], + "default": 3000, + "title": "Max Wait Ms", + "description": "500-10000 ms, or -1 to wait without a timeout." + }, + "pause_state_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "title": "Pause State Enabled" + } + }, + "required": [ + "type" + ], + "title": "SemanticTurnEnd", + "type": "object" + }, + "AgentRequest_SemanticTurnStart": { + "additionalProperties": false, + "properties": { + "type": { + "const": "semantic", + "title": "Type", + "type": "string", + "default": "semantic" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 160, + "title": "Speaking Interrupt Duration Ms" + } + }, + "required": [ + "type" + ], + "title": "SemanticTurnStart", + "type": "object" + }, + "AgentRequest_SilenceConfig": { + "additionalProperties": false, + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "timeout_ms": { + "title": "Timeout Ms", + "type": "integer" + }, + "action": { + "enum": [ + "speak", + "think" + ], + "title": "Action", + "type": "string" + }, + "content": { + "title": "Content", + "type": "string" + } + }, + "required": [ + "enabled", + "timeout_ms", + "action", + "content" + ], + "title": "SilenceConfig", + "type": "object" + }, + "AgentRequest_SilenceConfigPatch": { + "additionalProperties": false, + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "timeout_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Ms" + }, + "action": { + "anyOf": [ + { + "enum": [ + "speak", + "think" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Content" + } + }, + "title": "SilenceConfigPatch", + "type": "object" + }, + "AgentRequest_SkipPattern": { + "enum": [ + "fullwidth_parentheses", + "lenticular_brackets", + "parentheses", + "square_brackets", + "curly_braces" + ], + "type": "string" + }, + "AgentRequest_SpeechInterruption": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "start_of_speech", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "SpeechInterruption", + "type": "object" + }, + "AgentRequest_StaticFillerContent": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "static", + "title": "Mode", + "type": "string" + }, + "phrases": { + "items": { + "type": "string", + "minLength": 1 + }, + "title": "Phrases", + "type": "array", + "minItems": 1, + "maxItems": 100 + }, + "selection_rule": { + "default": "shuffle", + "enum": [ + "shuffle", + "round_robin" + ], + "title": "Selection Rule", + "type": "string" + } + }, + "required": [ + "mode", + "phrases" + ], + "title": "StaticFillerContent", + "type": "object" + }, + "AgentRequest_TextGreeting": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "text", + "title": "Mode", + "type": "string" + }, + "on": { + "default": "each_join", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "type": "string" + }, + "delay_ms": { + "default": 0, + "title": "Delay Ms", + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + "text": { + "title": "Text", + "type": "string" + } + }, + "required": [ + "mode", + "text" + ], + "title": "TextGreeting", + "type": "object" + }, + "AgentRequest_ToolExecution": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "sync", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "ToolExecution", + "type": "object" + }, + "AgentRequest_ToolServer": { + "additionalProperties": false, + "properties": { + "method": { + "enum": [ + "GET", + "POST" + ], + "title": "Method", + "type": "string" + }, + "url": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Url", + "type": "string" + }, + "headers": { + "anyOf": [ + { + "maxProperties": 32, + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "body": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "timeout_ms": { + "anyOf": [ + { + "maximum": 100000, + "minimum": 1000, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 10000, + "title": "Timeout Ms" + } + }, + "required": [ + "method", + "url" + ], + "title": "ToolServer", + "type": "object" + }, + "AgentRequest_TtsInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "aws_access_key_id": { + "minLength": 1, + "type": "string" + }, + "aws_secret_access_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "aws_access_key_id", + "aws_secret_access_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "cartesia", + "deepgram", + "fishaudio", + "gemini", + "generic_http", + "gradium", + "mistral", + "murf", + "openai", + "rime", + "smallestai", + "typecast", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "cartesia", + "deepgram", + "elevenlabs", + "fishaudio", + "gemini", + "generic_http", + "google", + "gradium", + "humeai", + "microsoft", + "minimax", + "mistral", + "murf", + "openai", + "rime", + "sarvam", + "smallestai", + "typecast", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "elevenlabs", + "humeai", + "microsoft", + "minimax" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "credentials": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "credentials" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "sarvam" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_subscription_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_subscription_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "skip_patterns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_SkipPattern" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Skip Patterns", + "uniqueItems": true + }, + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + } + }, + "title": "TtsInput", + "type": "object" + }, + "AgentRequest_TtsPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + }, + "then": { + "properties": { + "url": { + "type": "null" + } + } + } + } + ], + "properties": { + "skip_patterns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_SkipPattern" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Skip Patterns", + "uniqueItems": true + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Non-null URLs require effective BYOK mode; null clears a saved override.", + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + } + }, + "title": "TtsPatch", + "type": "object" + }, + "AgentRequest_TurnBoundaryPatch": { + "additionalProperties": false, + "properties": { + "type": { + "anyOf": [ + { + "enum": [ + "semantic", + "vad", + "manual" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Type" + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Interrupt Duration Ms" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Speaking Interrupt Duration Ms" + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Prefix Padding Ms" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Silence Duration Ms" + }, + "max_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 500, + "maximum": 10000 + }, + { + "type": "integer", + "const": -1 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Wait Ms", + "description": "500-10000 ms, or -1 to wait without a timeout." + }, + "pause_state_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pause State Enabled" + } + }, + "title": "TurnBoundaryPatch", + "type": "object" + }, + "AgentRequest_TurnDetection": { + "additionalProperties": false, + "properties": { + "speech_threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "default": 0.5, + "title": "Speech Threshold" + }, + "start_of_speech": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "manual": "#/components/schemas/AgentRequest_ManualTurnBoundary", + "semantic": "#/components/schemas/AgentRequest_SemanticTurnStart", + "vad": "#/components/schemas/AgentRequest_VadTurnStart" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SemanticTurnStart" + }, + { + "$ref": "#/components/schemas/AgentRequest_VadTurnStart" + }, + { + "$ref": "#/components/schemas/AgentRequest_ManualTurnBoundary" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Of Speech" + }, + "end_of_speech": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "manual": "#/components/schemas/AgentRequest_ManualTurnBoundary", + "semantic": "#/components/schemas/AgentRequest_SemanticTurnEnd", + "vad": "#/components/schemas/AgentRequest_VadTurnEnd" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SemanticTurnEnd" + }, + { + "$ref": "#/components/schemas/AgentRequest_VadTurnEnd" + }, + { + "$ref": "#/components/schemas/AgentRequest_ManualTurnBoundary" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "End Of Speech" + }, + "interruption": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "keywords": "#/components/schemas/AgentRequest_KeywordInterruption", + "off": "#/components/schemas/AgentRequest_DisabledInterruption", + "start_of_speech": "#/components/schemas/AgentRequest_SpeechInterruption" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SpeechInterruption" + }, + { + "$ref": "#/components/schemas/AgentRequest_KeywordInterruption" + }, + { + "$ref": "#/components/schemas/AgentRequest_DisabledInterruption" + } + ] + }, + { + "type": "null" + } + ], + "default": { + "mode": "start_of_speech" + }, + "title": "Interruption" + } + }, + "title": "TurnDetection", + "type": "object" + }, + "AgentRequest_VadTurnEnd": { + "additionalProperties": false, + "properties": { + "type": { + "const": "vad", + "title": "Type", + "type": "string" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "default": 640, + "title": "Silence Duration Ms" + } + }, + "required": [ + "type" + ], + "title": "VadTurnEnd", + "type": "object" + }, + "AgentRequest_VadTurnStart": { + "additionalProperties": false, + "properties": { + "type": { + "const": "vad", + "title": "Type", + "type": "string" + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": 160, + "title": "Interrupt Duration Ms" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": 160, + "title": "Speaking Interrupt Duration Ms" + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "default": 800, + "title": "Prefix Padding Ms" + } + }, + "required": [ + "type" + ], + "title": "VadTurnStart", + "type": "object" + }, + "AgentRequest_VariableDefinition": { + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "str", + "int", + "float", + "bool", + "date", + "datetime" + ], + "title": "Type", + "type": "string" + }, + "required": { + "default": false, + "title": "Required", + "type": "boolean" + }, + "options": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "maxItems": 100, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Options" + }, + "extract": { + "default": false, + "title": "Extract", + "type": "boolean" + }, + "description": { + "anyOf": [ + { + "maxLength": 2000, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + } + }, + "required": [ + "type" + ], + "title": "VariableDefinition", + "type": "object" + }, + "AgentRequest_VariableDefinitionPatch": { + "additionalProperties": false, + "properties": { + "type": { + "anyOf": [ + { + "enum": [ + "str", + "int", + "float", + "bool", + "date", + "datetime" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Type" + }, + "required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Required" + }, + "options": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "maxItems": 100, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Options" + }, + "extract": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Extract" + }, + "description": { + "anyOf": [ + { + "maxLength": 2000, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + } + }, + "title": "VariableDefinitionPatch", + "type": "object" + }, + "AgentCreateInput": { + "$ref": "#/components/schemas/AgentRequest_AgentCreateInput" + }, + "AgentPatch": { + "$ref": "#/components/schemas/AgentRequest_AgentPatch" + }, + "SessionEndCallPolicy": { + "properties": { + "on_conversation_complete": { + "type": "boolean", + "title": "On Conversation Complete", + "default": false + }, + "on_user_request": { + "type": "boolean", + "title": "On User Request", + "default": false + }, + "on_fax": { + "type": "boolean", + "title": "On Fax", + "default": false + }, + "on_ai_assistant": { + "type": "boolean", + "title": "On Ai Assistant", + "default": false + }, + "on_voicemail": { + "type": "boolean", + "title": "On Voicemail", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "EndCallPolicy" + }, + "InboundEndCallPolicy": { + "properties": { + "on_conversation_complete": { + "type": "boolean", + "title": "On Conversation Complete", + "default": false + }, + "on_user_request": { + "type": "boolean", + "title": "On User Request", + "default": false + }, + "on_voicemail": { + "type": "boolean", + "title": "On Voicemail", + "default": false + }, + "on_ai_assistant": { + "type": "boolean", + "title": "On Ai Assistant", + "default": false + }, + "on_fax": { + "type": "boolean", + "title": "On Fax", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "EndCallPolicy" + }, + "SessionAnalysis": { + "properties": { + "status": { + "type": "string", + "enum": [ + "pending", + "completed", + "failed", + "skipped" + ], + "title": "Status" + }, + "skip_reason": { + "anyOf": [ + { + "type": "string", + "enum": [ + "no_user_turns", + "retention_none" + ] + }, + { + "type": "null" + } + ], + "title": "Skip Reason" + }, + "complete_ts": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Complete Ts" + }, + "result": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Result" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "status", + "skip_reason", + "complete_ts", + "result", + "error" + ], + "title": "SessionAnalysis" + } + }, + "securitySchemes": { + "AgoraTokenAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "An Agora AccessToken2 token. Platform session routes require the ConvoAI service (service type 9); an RTC + RTM token alone is insufficient. Use a dedicated ConvoAI token builder. Send the complete header value: agora token=. SDKs accept a supplied token or generate one from App ID and App Certificate. HTTP Basic authentication is also supported as an alternative." + }, + "BasicAuth": { + "type": "http", + "scheme": "basic", + "description": "HTTP Basic authentication using an Agora Customer ID and Customer Secret. Send Authorization: Basic . This is an alternative to AgoraTokenAuth; supply one authentication method." + } + }, + "parameters": { + "IdempotencyKey": { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[\\x21-\\x7E]+$" + }, + "description": "Optional idempotency key. The server retains each key for 24 hours. Repeating the same request with the same key and body returns the first response without performing the operation again, with Idempotent-Replayed: true. Reusing a key with a different body, or while the original request is still in progress, returns 409 IdempotencyKeyConflict. If validation rejects a request before the operation starts, correct the request and reuse the same key. If the operation was accepted or its outcome is unknown, keep the same key; using a new key can start a second operation. This header is supported only for session starts and campaign creation/pause/resume/cancel, not agent, secret, or number creation.", + "example": "6f1c2b0e-support-4821" + } + }, + "headers": { + "IdempotentReplayed": { + "description": "Present with value true when this response replays the original response for the same idempotency key and body within the 24-hour retention period.", + "schema": { + "type": "boolean", + "const": true + }, + "example": true + } + } + }, + "servers": [ + { + "url": "https://preview.ai.agora.io", + "description": "Agora Conversational AI API" + } + ], + "security": [ + { + "AgoraTokenAuth": [] + }, + { + "BasicAuth": [] + } + ] +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..9955807 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,8925 @@ +--- +openapi: 3.1.0 +info: + title: Conversational AI V3 API + version: 3.0.0 + description: 'Public Conversational AI preview contract used by the Agent SDKs. + Authenticate with Agora token007 (Authorization: agora token=) or HTTP Basic authentication using Customer ID and Customer Secret.' +paths: + "/conversational-ai/v1/projects/{appid}/agents": + post: + tags: + - agents + summary: Create Agent + operationId: create_agent_conversational_ai_v3_projects__appid__agents_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + responses: + '201': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentCreateInput" + get: + tags: + - agents + summary: List Agents + operationId: list_agents_conversational_ai_v3_projects__appid__agents_get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: limit + in: query + required: false + schema: + type: integer + maximum: 200 + minimum: 1 + default: 50 + title: Limit + - name: cursor + in: query + required: false + schema: + anyOf: + - type: string + maxLength: 2048 + - type: 'null' + title: Cursor + - name: order + in: query + required: false + schema: + enum: + - create_ts.desc + - create_ts.asc + type: string + default: create_ts.desc + title: Order + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentListResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/agents/{id}": + get: + tags: + - agents + summary: Retrieve Agent + operationId: retrieve_agent_conversational_ai_v3_projects__appid__agents__id__get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + patch: + tags: + - agents + summary: Update Agent + operationId: update_agent_conversational_ai_v3_projects__appid__agents__id__patch + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + - name: If-Match + in: header + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + title: If-Match + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentPatch" + delete: + tags: + - agents + summary: Delete Agent + operationId: delete_agent_conversational_ai_v3_projects__appid__agents__id__delete + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/AgentDeleteResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/campaigns": + post: + tags: + - campaigns + summary: Create Campaign + description: Create a campaign with up to 50,000 contacts. The complete JSON + request body is limited to 10 MiB (10,485,760 bytes). + operationId: create_campaign_conversational_ai_v3_projects__appid__campaigns_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - "$ref": "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignCreate" + responses: + '201': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignResponse" + headers: + Idempotent-Replayed: + "$ref": "#/components/headers/IdempotentReplayed" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + examples: + IdempotencyKeyConflict: + summary: Key reused with a different body + value: + detail: The Idempotency-Key is bound to a request with a different + body. + reason: IdempotencyKeyConflict + IdempotencyRequestInProgress: + summary: Original request is still in progress + value: + detail: A request with this Idempotency-Key is still in progress. + reason: IdempotencyKeyConflict + description: 'Conflict IdempotencyKeyConflict: the key was used with a different + body or its original request is still in progress. A completed request + repeated with the same key and body replays its original response for + 24 hours.' + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + get: + tags: + - campaigns + summary: List Campaigns + operationId: list_campaigns_conversational_ai_v3_projects__appid__campaigns_get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: status + in: query + required: false + schema: + anyOf: + - enum: + - scheduled + - running + - paused + - completed + - canceled + - failed + type: string + - type: 'null' + title: Status + - name: limit + in: query + required: false + schema: + type: integer + maximum: 200 + minimum: 1 + default: 50 + title: Limit + - name: cursor + in: query + required: false + schema: + anyOf: + - type: string + maxLength: 2048 + - type: 'null' + title: Cursor + - name: order + in: query + required: false + schema: + enum: + - create_ts.desc + - create_ts.asc + type: string + default: create_ts.desc + title: Order + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignListResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:pause": + post: + tags: + - campaigns + summary: Pause Campaign + operationId: pause_campaign_conversational_ai_v3_projects__appid__campaigns__id__pause_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + - "$ref": "#/components/parameters/IdempotencyKey" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignResponse" + headers: + Idempotent-Replayed: + "$ref": "#/components/headers/IdempotentReplayed" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + examples: + IdempotencyKeyConflict: + summary: Key reused with a different body + value: + detail: The Idempotency-Key is bound to a request with a different + body. + reason: IdempotencyKeyConflict + IdempotencyRequestInProgress: + summary: Original request is still in progress + value: + detail: A request with this Idempotency-Key is still in progress. + reason: IdempotencyKeyConflict + description: 'Conflict IdempotencyKeyConflict: the key was used with a different + body or its original request is still in progress. A completed request + repeated with the same key and body replays its original response for + 24 hours.' + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:resume": + post: + tags: + - campaigns + summary: Resume Campaign + operationId: resume_campaign_conversational_ai_v3_projects__appid__campaigns__id__resume_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + - "$ref": "#/components/parameters/IdempotencyKey" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignResponse" + headers: + Idempotent-Replayed: + "$ref": "#/components/headers/IdempotentReplayed" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + examples: + IdempotencyKeyConflict: + summary: Key reused with a different body + value: + detail: The Idempotency-Key is bound to a request with a different + body. + reason: IdempotencyKeyConflict + IdempotencyRequestInProgress: + summary: Original request is still in progress + value: + detail: A request with this Idempotency-Key is still in progress. + reason: IdempotencyKeyConflict + description: 'Conflict IdempotencyKeyConflict: the key was used with a different + body or its original request is still in progress. A completed request + repeated with the same key and body replays its original response for + 24 hours.' + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:cancel": + post: + tags: + - campaigns + summary: Cancel Campaign + operationId: cancel_campaign_conversational_ai_v3_projects__appid__campaigns__id__cancel_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + - "$ref": "#/components/parameters/IdempotencyKey" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignResponse" + headers: + Idempotent-Replayed: + "$ref": "#/components/headers/IdempotentReplayed" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + examples: + IdempotencyKeyConflict: + summary: Key reused with a different body + value: + detail: The Idempotency-Key is bound to a request with a different + body. + reason: IdempotencyKeyConflict + IdempotencyRequestInProgress: + summary: Original request is still in progress + value: + detail: A request with this Idempotency-Key is still in progress. + reason: IdempotencyKeyConflict + description: 'Conflict IdempotencyKeyConflict: the key was used with a different + body or its original request is still in progress. A completed request + repeated with the same key and body replays its original response for + 24 hours.' + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/campaigns/{id}": + get: + tags: + - campaigns + summary: Get Campaign + operationId: get_campaign_conversational_ai_v3_projects__appid__campaigns__id__get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + patch: + tags: + - campaigns + summary: Update Campaign + operationId: update_campaign_conversational_ai_v3_projects__appid__campaigns__id__patch + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + - name: If-Match + in: header + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + title: If-Match + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignPatch" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + delete: + tags: + - campaigns + summary: Delete Campaign + operationId: delete_campaign_conversational_ai_v3_projects__appid__campaigns__id__delete + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/CampaignDeleteResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/secrets": + post: + tags: + - secrets + summary: Create + operationId: create_conversational_ai_v3_projects__appid__secrets_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretCreate" + responses: + '201': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretSet" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + get: + tags: + - secrets + summary: List Sets + operationId: list_sets_conversational_ai_v3_projects__appid__secrets_get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: limit + in: query + required: false + schema: + type: integer + maximum: 200 + minimum: 1 + default: 50 + title: Limit + - name: cursor + in: query + required: false + schema: + anyOf: + - type: string + maxLength: 2048 + - type: 'null' + title: Cursor + - name: order + in: query + required: false + schema: + enum: + - create_ts.asc + - create_ts.desc + type: string + default: create_ts.desc + title: Order + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretList" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/secrets/{name}": + get: + tags: + - secrets + summary: Retrieve + operationId: retrieve_conversational_ai_v3_projects__appid__secrets__name__get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: name + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Name + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretSet" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + put: + tags: + - secrets + summary: Replace + operationId: replace_conversational_ai_v3_projects__appid__secrets__name__put + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: name + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Name + - name: If-Match + in: header + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + title: If-Match + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretReplace" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretSet" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + patch: + tags: + - secrets + summary: Update + operationId: update_conversational_ai_v3_projects__appid__secrets__name__patch + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: name + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Name + - name: If-Match + in: header + required: false + schema: + anyOf: + - type: string + minLength: 1 + maxLength: 256 + - type: 'null' + title: If-Match + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretUpdate" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretSet" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + delete: + tags: + - secrets + summary: Delete + operationId: delete_conversational_ai_v3_projects__appid__secrets__name__delete + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: name + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Name + - name: If-Match + in: header + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + title: If-Match + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretDeleted" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/secrets/{name}/keys/{key}": + put: + tags: + - secrets + summary: Set Key + operationId: set_key_conversational_ai_v3_projects__appid__secrets__name__keys__key__put + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: name + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Name + - name: key + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Key + - name: If-Match + in: header + required: false + schema: + anyOf: + - type: string + minLength: 1 + maxLength: 256 + - type: 'null' + title: If-Match + - name: If-None-Match + in: header + required: false + schema: + anyOf: + - const: "*" + type: string + - type: 'null' + title: If-None-Match + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretKeyWrite" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretKey" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '201': + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretKey" + description: Created + delete: + tags: + - secrets + summary: Delete Key + operationId: delete_key_conversational_ai_v3_projects__appid__secrets__name__keys__key__delete + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: name + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Name + - name: key + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 64 + pattern: "^[A-Za-z0-9_]+$" + title: Key + - name: If-Match + in: header + required: false + schema: + anyOf: + - type: string + minLength: 1 + maxLength: 256 + - type: 'null' + title: If-Match + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SecretDeleted" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/numbers": + post: + tags: + - numbers + summary: Create Number + operationId: create_number_conversational_ai_v3_projects__appid__numbers_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberCreate" + responses: + '201': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + get: + tags: + - numbers + summary: List Numbers + operationId: list_numbers_conversational_ai_v3_projects__appid__numbers_get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: limit + in: query + required: false + schema: + type: integer + maximum: 100 + minimum: 1 + default: 50 + title: Limit + - name: cursor + in: query + required: false + schema: + anyOf: + - type: string + maxLength: 2048 + - type: 'null' + title: Cursor + - name: order + in: query + required: false + schema: + enum: + - create_ts.desc + - create_ts.asc + type: string + default: create_ts.desc + title: Order + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberListResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/numbers/{id}": + get: + tags: + - numbers + summary: Get Number + operationId: get_number_conversational_ai_v3_projects__appid__numbers__id__get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + patch: + tags: + - numbers + summary: Update Number + operationId: update_number_conversational_ai_v3_projects__appid__numbers__id__patch + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + - name: If-Match + in: header + required: true + schema: + type: string + minLength: 1 + maxLength: 256 + title: If-Match + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberPatch" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + delete: + tags: + - numbers + summary: Delete Number + operationId: delete_number_conversational_ai_v3_projects__appid__numbers__id__delete + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: id + in: path + required: true + schema: + type: string + format: uuid + title: Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/NumberDeleteResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + '400': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Request + '401': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unauthorized + '403': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Forbidden + '404': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Not Found + '405': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Method Not Allowed + '409': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Conflict + '412': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Precondition Failed + '413': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Content Too Large + '415': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unsupported Media Type + '422': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Unprocessable Content + '429': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Too Many Requests + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Internal Server Error + '502': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Bad Gateway + '503': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Service Unavailable + '504': + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Gateway Timeout + "/conversational-ai/v1/projects/{appid}/sessions": + post: + tags: + - Sessions + summary: Create Session + operationId: create_session_conversational_ai_v3_projects__appid__sessions_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - "$ref": "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionCreate" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionStartResponse" + headers: + Idempotent-Replayed: + "$ref": "#/components/headers/IdempotentReplayed" + '409': + description: 'TaskConflict: an agent already runs with the same RTC channel + and UID. IdempotencyKeyConflict: the key was used with a different body + or its original request is still in progress. A completed request repeated + with the same key and body replays its original response for 24 hours.' + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + examples: + TaskConflict: + value: + detail: An agent is already running with the same channel and + UID. + reason: TaskConflict + IdempotencyKeyConflict: + summary: Key reused with a different body + value: + detail: The Idempotency-Key is bound to a request with a different + body. + reason: IdempotencyKeyConflict + IdempotencyRequestInProgress: + summary: Original request is still in progress + value: + detail: A request with this Idempotency-Key is still in progress. + reason: IdempotencyKeyConflict + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + get: + tags: + - Sessions + summary: List Sessions + operationId: list_sessions_conversational_ai_v3_projects__appid__sessions_get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: channel + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Channel + - name: status + in: query + required: false + schema: + type: string + default: RUNNING + title: Status + - name: started_after + in: query + required: false + schema: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + title: Started After + - name: started_before + in: query + required: false + schema: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + title: Started Before + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + default: 20 + title: Limit + - name: cursor + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Cursor + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionList" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/sessions/ephemeral": + post: + tags: + - Sessions + summary: Create Ephemeral Session + operationId: create_ephemeral_session_conversational_ai_v3_projects__appid__sessions_ephemeral_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - "$ref": "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/PublicEphemeralSessionCreate" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionStartResponse" + headers: + Idempotent-Replayed: + "$ref": "#/components/headers/IdempotentReplayed" + '409': + description: 'TaskConflict: an agent already runs with the same RTC channel + and UID. IdempotencyKeyConflict: the key was used with a different body + or its original request is still in progress. A completed request repeated + with the same key and body replays its original response for 24 hours.' + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + examples: + TaskConflict: + value: + detail: An agent is already running with the same channel and + UID. + reason: TaskConflict + IdempotencyKeyConflict: + summary: Key reused with a different body + value: + detail: The Idempotency-Key is bound to a request with a different + body. + reason: IdempotencyKeyConflict + IdempotencyRequestInProgress: + summary: Original request is still in progress + value: + detail: A request with this Idempotency-Key is still in progress. + reason: IdempotencyKeyConflict + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/sessions/{agent_session_id}": + get: + tags: + - Sessions + summary: Retrieve Session + operationId: retrieve_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__get + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: agent_session_id + in: path + required: true + schema: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + patch: + tags: + - Sessions + summary: Update Session + operationId: update_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__patch + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: agent_session_id + in: path + required: true + schema: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionPatch" + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionUpdateResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error + "/conversational-ai/v1/projects/{appid}/sessions/{agent_session_id}/stop": + post: + tags: + - Sessions + summary: Stop Session + operationId: stop_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__stop_post + parameters: + - name: appid + in: path + required: true + schema: + type: string + minLength: 1 + maxLength: 100 + title: Appid + - name: agent_session_id + in: path + required: true + schema: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionStopResponse" + 4XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Client Error + 5XX: + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + description: Server Error +components: + schemas: + AgentDeleteResponse: + properties: + deleted: + type: boolean + const: true + title: Deleted + additionalProperties: false + type: object + required: + - deleted + title: AgentDeleteResponse + AgentListData: + properties: + count: + type: integer + minimum: 0.0 + title: Count + list: + items: + "$ref": "#/components/schemas/AgentListItem" + type: array + title: List + additionalProperties: false + type: object + required: + - count + - list + title: AgentListData + AgentListItem: + properties: + agent_id: + type: string + format: uuid + title: Agent Id + agent_name: + type: string + title: Agent Name + create_ts: + type: integer + minimum: 0.0 + title: Create Ts + update_ts: + type: integer + minimum: 0.0 + title: Update Ts + additionalProperties: false + type: object + required: + - agent_id + - agent_name + - create_ts + - update_ts + title: AgentListItem + AgentListResponse: + properties: + data: + "$ref": "#/components/schemas/AgentListData" + has_more: + type: boolean + title: Has More + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + additionalProperties: false + type: object + required: + - data + - has_more + - next_cursor + title: AgentListResponse + AgentResponse: + properties: + agent_id: + type: string + format: uuid + title: Agent Id + create_ts: + type: integer + minimum: 0.0 + title: Create Ts + update_ts: + type: integer + minimum: 0.0 + title: Update Ts + agent_name: + type: string + title: Agent Name + instructions: + anyOf: + - type: string + - type: 'null' + title: Instructions + messages: + anyOf: + - items: + "$ref": "#/components/schemas/Message" + type: array + - type: 'null' + title: Messages + greeting: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/TextGreeting" + - "$ref": "#/components/schemas/AudioGreeting" + discriminator: + propertyName: mode + mapping: + audio: "#/components/schemas/AudioGreeting" + text: "#/components/schemas/TextGreeting" + - type: 'null' + title: Greeting + variables: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + "$ref": "#/components/schemas/VariableDefinition" + type: object + - type: 'null' + title: Variables + pipeline: + anyOf: + - "$ref": "#/components/schemas/ResolvedCascadedPipeline" + - type: 'null' + filler_words: + anyOf: + - "$ref": "#/components/schemas/FillerWords" + - type: 'null' + additionalProperties: false + type: object + required: + - agent_id + - create_ts + - update_ts + - agent_name + title: AgentResponse + AsrInput: + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + - if: + allOf: + - properties: + vendor: + enum: + - amazon + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + access_key_id: + type: string + minLength: 1 + secret_access_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - access_key_id + - secret_access_key + required: + - fields + - if: + allOf: + - anyOf: + - properties: + vendor: + enum: + - assemblyai + - deepgram + - gemini + - openai + - sarvam + - smallestai + - xai + required: + - vendor + - not: + required: + - vendor + - properties: + vendor: + not: + enum: + - amazon + - ares + - assemblyai + - deepgram + - gemini + - google + - microsoft + - openai + - sarvam + - smallestai + - speechmatics + - xai + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + api_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - api_key + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - google + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + adc_credentials_string: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - adc_credentials_string + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - microsoft + - speechmatics + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - key + required: + - fields + properties: + credential: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/ManagedCredential" + - "$ref": "#/components/schemas/PublicByokCredential" + discriminator: + propertyName: mode + mapping: + byok: "#/components/schemas/PublicByokCredential" + managed: "#/components/schemas/ManagedCredential" + - type: 'null' + title: Credential + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/JSONObject" + - type: 'null' + url: + anyOf: + - type: string + format: uri + - type: 'null' + title: Url + model: + anyOf: + - type: string + - type: 'null' + title: Model + language: + anyOf: + - type: string + - type: 'null' + title: Language + additionalProperties: false + type: object + title: AsrInput + AudioGreeting: + properties: + mode: + type: string + const: audio + title: Mode + 'on': + type: string + enum: + - each_join + - first_join + title: 'On' + default: each_join + delay_ms: + type: integer + title: Delay Ms + default: 0 + minimum: 0 + maximum: 5000 + url: + type: string + title: Url + maxLength: 2048 + text: + type: string + title: Text + minLength: 1 + download_timeout_ms: + type: integer + title: Download Timeout Ms + default: 1000 + minimum: 200 + maximum: 10000 + pcm_sample_rate: + type: integer + title: Pcm Sample Rate + default: 16000 + additionalProperties: false + type: object + required: + - mode + - url + - text + title: AudioGreeting + AvatarInput: + properties: + credential: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/ManagedCredential" + - "$ref": "#/components/schemas/PublicByokCredential" + discriminator: + propertyName: mode + mapping: + byok: "#/components/schemas/PublicByokCredential" + managed: "#/components/schemas/ManagedCredential" + - type: 'null' + title: Credential + vendor: + anyOf: + - "$ref": "#/components/schemas/AvatarVendor" + - type: 'null' + params: + anyOf: + - "$ref": "#/components/schemas/JSONObject" + - type: 'null' + enabled: + anyOf: + - type: boolean + - type: 'null' + title: Enabled + additionalProperties: false + type: object + title: AvatarInput + AvatarVendor: + type: string + enum: + - akool + - liveavatar + - anam + - sensetime + - spatius + - generic + CallPolicy: + properties: + max_ring_duration_ms: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + title: Max Ring Duration Ms + max_call_duration_seconds: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + title: Max Call Duration Seconds + voicemail: + anyOf: + - "$ref": "#/components/schemas/VoicemailPolicy" + - type: 'null' + max_silence_duration_ms: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + title: Max Silence Duration Ms + end_call: + anyOf: + - "$ref": "#/components/schemas/SessionEndCallPolicy" + - type: 'null' + transfer: + anyOf: + - "$ref": "#/components/schemas/TransferPolicy" + - type: 'null' + additionalProperties: false + type: object + title: CallPolicy + CallingDay: + properties: + weekday: + type: string + enum: + - mon + - tue + - wed + - thu + - fri + - sat + - sun + title: Weekday + ranges: + items: + "$ref": "#/components/schemas/TimeRange" + type: array + maxItems: 10 + minItems: 1 + title: Ranges + additionalProperties: false + type: object + required: + - weekday + - ranges + title: CallingDay + CampaignCallPolicy: + properties: + max_ring_duration_ms: + anyOf: + - type: integer + maximum: 259200000.0 + minimum: 1.0 + - type: 'null' + title: Max Ring Duration Ms + max_call_duration_seconds: + anyOf: + - type: integer + maximum: 259200.0 + minimum: 1.0 + - type: 'null' + title: Max Call Duration Seconds + voicemail: + anyOf: + - "$ref": "#/components/schemas/CampaignVoicemailPolicy" + - type: 'null' + max_silence_duration_ms: + anyOf: + - type: integer + maximum: 259200000.0 + minimum: 30000.0 + - type: 'null' + title: Max Silence Duration Ms + end_call: + anyOf: + - "$ref": "#/components/schemas/CampaignEndCallPolicy" + - type: 'null' + transfer: + anyOf: + - "$ref": "#/components/schemas/CampaignTransferPolicy" + - type: 'null' + additionalProperties: false + type: object + title: CampaignCallPolicy + CampaignCounts: + properties: + contacts: + type: integer + minimum: 0.0 + title: Contacts + pending: + type: integer + minimum: 0.0 + title: Pending + calling: + type: integer + minimum: 0.0 + title: Calling + completed: + type: integer + minimum: 0.0 + title: Completed + failed: + type: integer + minimum: 0.0 + title: Failed + canceled: + type: integer + minimum: 0.0 + title: Canceled + additionalProperties: false + type: object + required: + - contacts + - pending + - calling + - completed + - failed + - canceled + title: CampaignCounts + CampaignCreate: + properties: + name: + type: string + maxLength: 255 + minLength: 1 + title: Name + agent_id: + type: string + format: uuid + title: Agent Id + transport: + "$ref": "#/components/schemas/CampaignTransport" + contacts: + items: + "$ref": "#/components/schemas/ContactInput" + type: array + maxItems: 50000 + minItems: 1 + title: Contacts + lifecycle: + "$ref": "#/components/schemas/CampaignLifecycle" + pacing: + "$ref": "#/components/schemas/Pacing" + schedule: + oneOf: + - "$ref": "#/components/schemas/ImmediateSchedule" + - "$ref": "#/components/schemas/ScheduledSchedule" + title: Schedule + discriminator: + propertyName: mode + mapping: + immediate: "#/components/schemas/ImmediateSchedule" + scheduled: "#/components/schemas/ScheduledSchedule" + data_policy: + "$ref": "#/components/schemas/DataPolicy" + description: Campaign data-retention policy. Defaults to 30-day retention. + default: + retention: 30_days + additionalProperties: false + type: object + required: + - name + - agent_id + - transport + - contacts + - schedule + title: CampaignCreate + CampaignDeleteResponse: + properties: + deleted: + type: boolean + const: true + title: Deleted + default: true + additionalProperties: false + type: object + title: CampaignDeleteResponse + CampaignEndCallPolicy: + properties: + on_conversation_complete: + type: boolean + title: On Conversation Complete + default: false + on_user_request: + type: boolean + title: On User Request + default: false + on_fax: + type: boolean + title: On Fax + default: false + on_ai_assistant: + type: boolean + title: On Ai Assistant + default: false + on_voicemail: + type: boolean + title: On Voicemail + default: false + additionalProperties: false + type: object + title: CampaignEndCallPolicy + CampaignFailure: + properties: + code: + type: string + title: Code + provider: + anyOf: + - type: string + - type: 'null' + title: Provider + message: + type: string + title: Message + ts: + type: integer + minimum: 0.0 + title: Ts + additionalProperties: false + type: object + required: + - code + - message + - ts + title: CampaignFailure + CampaignLifecycle: + properties: + idle_timeout_ms: + type: integer + maximum: 259200000.0 + minimum: 0.0 + title: Idle Timeout Ms + default: 30000 + max_duration_ms: + type: integer + maximum: 259200000.0 + minimum: 1000.0 + title: Max Duration Ms + default: 259200000 + graceful_stop: + "$ref": "#/components/schemas/GracefulStop" + additionalProperties: false + type: object + title: CampaignLifecycle + CampaignListData: + properties: + count: + type: integer + minimum: 0.0 + title: Count + list: + items: + "$ref": "#/components/schemas/CampaignResponse" + type: array + title: List + additionalProperties: false + type: object + required: + - count + - list + title: CampaignListData + CampaignListResponse: + properties: + data: + "$ref": "#/components/schemas/CampaignListData" + has_more: + type: boolean + title: Has More + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + additionalProperties: false + type: object + required: + - data + - has_more + - next_cursor + title: CampaignListResponse + CampaignPatch: + properties: + lifecycle: + anyOf: + - "$ref": "#/components/schemas/CampaignLifecycle" + - type: 'null' + pacing: + anyOf: + - "$ref": "#/components/schemas/PacingPatch" + - type: 'null' + schedule: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/ImmediateSchedule" + - "$ref": "#/components/schemas/ScheduledSchedule" + discriminator: + propertyName: mode + mapping: + immediate: "#/components/schemas/ImmediateSchedule" + scheduled: "#/components/schemas/ScheduledSchedule" + - type: 'null' + title: Schedule + transport: + anyOf: + - "$ref": "#/components/schemas/CampaignTransportPatch" + - type: 'null' + additionalProperties: false + type: object + title: CampaignPatch + CampaignResponse: + properties: + campaign_id: + type: string + format: uuid + title: Campaign Id + name: + type: string + title: Name + status: + type: string + enum: + - scheduled + - running + - paused + - completed + - canceled + - failed + title: Status + agent_id: + type: string + format: uuid + title: Agent Id + transport: + "$ref": "#/components/schemas/CampaignTransportResponse" + lifecycle: + "$ref": "#/components/schemas/CampaignLifecycle" + schedule: + oneOf: + - "$ref": "#/components/schemas/ImmediateSchedule" + - "$ref": "#/components/schemas/ScheduledSchedule" + title: Schedule + discriminator: + propertyName: mode + mapping: + immediate: "#/components/schemas/ImmediateSchedule" + scheduled: "#/components/schemas/ScheduledSchedule" + counts: + "$ref": "#/components/schemas/CampaignCounts" + create_ts: + type: integer + minimum: 0.0 + title: Create Ts + start_ts: + anyOf: + - type: integer + - type: 'null' + title: Start Ts + complete_ts: + anyOf: + - type: integer + - type: 'null' + title: Complete Ts + failure: + anyOf: + - "$ref": "#/components/schemas/CampaignFailure" + - type: 'null' + additionalProperties: false + type: object + required: + - campaign_id + - name + - status + - agent_id + - transport + - lifecycle + - schedule + - counts + - create_ts + - start_ts + - complete_ts + - failure + title: CampaignResponse + CampaignTransferPolicy: + properties: + phone_number: + type: string + pattern: "^\\+[1-9][0-9]{1,14}$" + title: Phone Number + description: + type: string + maxLength: 1000 + minLength: 1 + title: Description + additionalProperties: false + type: object + required: + - phone_number + - description + title: CampaignTransferPolicy + CampaignTransport: + properties: + type: + type: string + const: telephony + title: Type + from: + anyOf: + - type: string + format: uuid + - items: + type: string + format: uuid + type: array + title: From + call_policy: + anyOf: + - "$ref": "#/components/schemas/CampaignCallPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - type + - from + title: CampaignTransport + CampaignTransportPatch: + properties: + call_policy: + anyOf: + - "$ref": "#/components/schemas/CampaignCallPolicy" + - type: 'null' + additionalProperties: false + type: object + title: CampaignTransportPatch + CampaignTransportResponse: + properties: + type: + type: string + const: telephony + title: Type + from: + items: + type: string + format: uuid + type: array + title: From + call_policy: + anyOf: + - "$ref": "#/components/schemas/CampaignCallPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - type + - from + title: CampaignTransportResponse + CampaignVoicemailPolicy: + properties: + mode: + type: string + enum: + - continue + - hangup + title: Mode + additionalProperties: false + type: object + required: + - mode + title: CampaignVoicemailPolicy + CascadedPipelineInput: + properties: + mode: + type: string + const: cascaded + title: Mode + avatar: + anyOf: + - "$ref": "#/components/schemas/AvatarInput" + - type: 'null' + turn_detection: + anyOf: + - "$ref": "#/components/schemas/TurnDetection" + - type: 'null' + asr: + anyOf: + - "$ref": "#/components/schemas/AsrInput" + - type: 'null' + tts: + anyOf: + - "$ref": "#/components/schemas/TtsInput" + - type: 'null' + llm: + anyOf: + - "$ref": "#/components/schemas/LlmInput" + - type: 'null' + silence_config: + anyOf: + - "$ref": "#/components/schemas/SilenceConfig" + - type: 'null' + additionalProperties: false + type: object + required: + - mode + title: CascadedPipelineInput + ContactInput: + properties: + phone: + type: string + pattern: "^\\+[1-9][0-9]{1,14}$" + title: Phone + values: + patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + type: object + maxProperties: 100 + title: Values + additionalProperties: false + type: object + required: + - phone + title: ContactInput + DataPolicy: + properties: + retention: + type: string + enum: + - 30_days + - none + title: Retention + default: 30_days + additionalProperties: false + type: object + title: DataPolicy + DisabledInterruption: + properties: + mode: + type: string + const: 'off' + title: Mode + when_off: + type: string + enum: + - append + - ignore + title: When Off + default: append + additionalProperties: false + type: object + required: + - mode + title: DisabledInterruption + ErrorReason: + type: string + enum: + - InvalidRequestBody + - MissingRequiredField + - InvalidFieldValue + - InvalidModuleParameter + - UnknownSecretReference + - NotImplemented + - InvalidPermission + - ServiceNotEnabled + - AccountSuspended + - TaskNotFound + - ResourceNotFound + - AgentNotFound + - NumberNotFound + - NumberAlreadyExists + - ResourceConflict + - MethodNotAllowed + - PayloadTooLarge + - UnsupportedMediaType + - TaskConflict + - CampaignActive + - CampaignInvalidState + - CampaignNotEditable + - IdempotencyKeyConflict + - SecretSetAlreadyExists + - SecretSetInUse + - SecretSetEmpty + - TaskNotRunning + - RateLimitExceeded + - PreconditionFailed + - PreconditionRequired + - ResourceQuotaLimitExceeded + - ConcurrencyLimitExceeded + - InternalError + - ServiceUnavailable + - ResourceAllocationFailed + - TaskOperationTimeout + title: ErrorReason + ErrorResponse: + properties: + detail: + type: string + minLength: 1 + title: Detail + reason: + "$ref": "#/components/schemas/ErrorReason" + additionalProperties: false + type: object + required: + - detail + - reason + title: ErrorResponse + FillerTrigger: + properties: + mode: + type: string + const: fixed_time + title: Mode + response_wait_ms: + type: integer + title: Response Wait Ms + additionalProperties: false + type: object + required: + - mode + - response_wait_ms + title: FillerTrigger + FillerWords: + properties: + enable: + anyOf: + - type: boolean + - type: 'null' + title: Enable + trigger: + anyOf: + - "$ref": "#/components/schemas/FillerTrigger" + - type: 'null' + content: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/StaticFillerContent" + - "$ref": "#/components/schemas/GeneratedFillerContent" + discriminator: + propertyName: mode + mapping: + generated: "#/components/schemas/GeneratedFillerContent" + static: "#/components/schemas/StaticFillerContent" + - type: 'null' + title: Content + additionalProperties: false + type: object + title: FillerWords + FunctionTool: + properties: + type: + type: string + const: function + title: Type + name: + type: string + pattern: "^(?![mM][cC][pP])[A-Za-z][A-Za-z0-9]{0,63}$" + title: Name + description: + type: string + maxLength: 1024 + minLength: 1 + title: Description + parameters: + "$ref": "#/components/schemas/JSONObject" + description: JSON Schema (draft 2020-12) describing the tool arguments. + execution: + "$ref": "#/components/schemas/ToolExecution" + server: + "$ref": "#/components/schemas/ToolServer" + additionalProperties: false + type: object + required: + - type + - name + - description + - parameters + - execution + - server + title: FunctionTool + GeneratedFillerContent: + properties: + mode: + type: string + const: generated + title: Mode + instructions: + type: string + title: Instructions + fallback_strategy: + anyOf: + - type: string + const: static + - type: 'null' + title: Fallback Strategy + additionalProperties: false + type: object + required: + - mode + - instructions + title: GeneratedFillerContent + GracefulStop: + properties: + enabled: + type: boolean + title: Enabled + default: false + timeout_ms: + type: integer + maximum: 120000.0 + minimum: 0.0 + title: Timeout Ms + default: 30000 + additionalProperties: false + type: object + title: GracefulStop + ImmediateSchedule: + properties: + mode: + type: string + const: immediate + title: Mode + timezone: + type: string + maxLength: 255 + minLength: 1 + title: Timezone + days: + items: + "$ref": "#/components/schemas/CallingDay" + type: array + maxItems: 7 + minItems: 1 + title: Days + additionalProperties: false + type: object + required: + - mode + title: ImmediateSchedule + InboundCallPolicy: + properties: + max_call_duration_seconds: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + title: Max Call Duration Seconds + max_silence_duration_ms: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + title: Max Silence Duration Ms + end_call: + anyOf: + - "$ref": "#/components/schemas/InboundEndCallPolicy" + - type: 'null' + description: Inbound agent end-call controls. Requires an agent; omitted + flags default to false. An empty/all-false object disables the owned policy. + transfer: + anyOf: + - "$ref": "#/components/schemas/TransferPolicy" + - type: 'null' + description: Reserved for runtime support; requests containing this field + are rejected. + additionalProperties: false + type: object + title: InboundCallPolicy + InboundInput: + properties: + agent: + anyOf: + - type: string + format: uuid + - type: 'null' + title: Agent + call_policy: + anyOf: + - "$ref": "#/components/schemas/InboundCallPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - agent + title: InboundInput + InboundOutput: + properties: + agent: + anyOf: + - type: string + format: uuid + - type: 'null' + title: Agent + call_policy: + anyOf: + - "$ref": "#/components/schemas/InboundCallPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - agent + title: InboundOutput + JSONObject: + additionalProperties: + "$ref": "#/components/schemas/JsonValue" + type: object + JsonValue: {} + KeywordInterruption: + properties: + mode: + type: string + const: keywords + title: Mode + keywords: + items: + type: string + type: array + title: Keywords + maxItems: 128 + additionalProperties: false + type: object + required: + - mode + - keywords + title: KeywordInterruption + Lifecycle: + properties: + idle_timeout_ms: + type: integer + maximum: 3600000.0 + minimum: 0.0 + title: Idle Timeout Ms + default: 30000 + max_duration_ms: + type: integer + maximum: 3600000.0 + minimum: 1000.0 + title: Max Duration Ms + default: 3600000 + graceful_stop: + anyOf: + - "$ref": "#/components/schemas/GracefulStop" + - type: 'null' + additionalProperties: false + type: object + title: Lifecycle + LlmInput: + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + - if: + allOf: + - properties: + vendor: + enum: + - amazon + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + api_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - api_key + required: + - fields + - properties: + fields: + properties: + access_key: + type: string + minLength: 1 + secret_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - access_key + - secret_key + required: + - fields + - if: + allOf: + - anyOf: + - properties: + vendor: + enum: + - azure + - anthropic + - dify + - gemini + - vertexai + - groq + - openai + - xai + required: + - vendor + - not: + required: + - vendor + - properties: + vendor: + not: + enum: + - amazon + - azure + - anthropic + - dify + - gemini + - vertexai + - groq + - openai + - xai + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + api_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - api_key + required: + - fields + properties: + mcp_servers: + anyOf: + - items: + "$ref": "#/components/schemas/McpServer" + type: array + - type: 'null' + title: Mcp Servers + tools: + anyOf: + - items: + "$ref": "#/components/schemas/FunctionTool" + type: array + maxItems: 32 + - type: 'null' + title: Tools + credential: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/ManagedCredential" + - "$ref": "#/components/schemas/PublicByokCredential" + discriminator: + propertyName: mode + mapping: + byok: "#/components/schemas/PublicByokCredential" + managed: "#/components/schemas/ManagedCredential" + - type: 'null' + title: Credential + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/JSONObject" + - type: 'null' + url: + anyOf: + - type: string + format: uri + - type: 'null' + title: Url + style: + anyOf: + - type: string + - type: 'null' + title: Style + model: + anyOf: + - type: string + - type: 'null' + title: Model + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + title: Headers + input_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Input Modalities + output_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Output Modalities + max_history: + anyOf: + - type: integer + maximum: 1024.0 + minimum: 1.0 + - type: 'null' + title: Max History + failure_message: + anyOf: + - type: string + - type: 'null' + title: Failure Message + additionalProperties: false + type: object + title: LlmInput + ManagedCredential: + properties: + mode: + type: string + const: managed + title: Mode + additionalProperties: false + type: object + required: + - mode + title: ManagedCredential + ManualTurnBoundary: + properties: + type: + type: string + const: manual + title: Type + additionalProperties: false + type: object + required: + - type + title: ManualTurnBoundary + McpServer: + properties: + name: + type: string + pattern: "^[A-Za-z0-9]{1,48}$" + title: Name + endpoint: + type: string + maxLength: 2048 + minLength: 1 + pattern: "^https://" + title: Endpoint + transport: + type: string + const: streamable_http + title: Transport + headers: + anyOf: + - patternProperties: + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": + type: string + propertyNames: + maxLength: 128 + minLength: 1 + type: object + maxProperties: 32 + - type: 'null' + title: Headers + allowed_tools: + anyOf: + - items: + type: string + minLength: 1 + type: array + - type: 'null' + title: Allowed Tools + timeout_ms: + anyOf: + - type: integer + maximum: 100000.0 + minimum: 1000.0 + - type: 'null' + title: Timeout Ms + additionalProperties: false + type: object + required: + - name + - endpoint + - transport + title: McpServer + Message: + properties: + role: + type: string + enum: + - system + - user + - assistant + title: Role + content: + type: string + title: Content + additionalProperties: false + type: object + required: + - role + - content + title: Message + NumberCreate: + properties: + number: + type: string + maxLength: 128 + minLength: 1 + title: Number + description: International inputs normalize to E.164; national inputs normalize + to national significant digits. + description: + anyOf: + - type: string + - type: 'null' + title: Description + sip_trunk: + "$ref": "#/components/schemas/SipTrunkInput" + inbound: + anyOf: + - "$ref": "#/components/schemas/InboundInput" + - type: 'null' + additionalProperties: false + type: object + required: + - number + - sip_trunk + title: NumberCreate + NumberDeleteResponse: + properties: + deleted: + type: boolean + const: true + title: Deleted + default: true + additionalProperties: false + type: object + title: NumberDeleteResponse + NumberListData: + properties: + count: + type: integer + minimum: 0.0 + title: Count + list: + items: + "$ref": "#/components/schemas/NumberResponse" + type: array + title: List + additionalProperties: false + type: object + required: + - count + - list + title: NumberListData + NumberListResponse: + properties: + data: + "$ref": "#/components/schemas/NumberListData" + has_more: + type: boolean + title: Has More + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + additionalProperties: false + type: object + required: + - data + - has_more + - next_cursor + title: NumberListResponse + NumberPatch: + properties: + number: + anyOf: + - type: string + maxLength: 128 + minLength: 1 + - type: 'null' + title: Number + description: May reformat the existing number but must not change its normalized + identity. + description: + anyOf: + - type: string + - type: 'null' + title: Description + sip_trunk: + anyOf: + - "$ref": "#/components/schemas/SipTrunkInput" + - type: 'null' + inbound: + anyOf: + - "$ref": "#/components/schemas/InboundInput" + - type: 'null' + description: Explicit null unbinds inbound; omission leaves inbound unchanged. + additionalProperties: false + type: object + title: NumberPatch + NumberResponse: + properties: + number_id: + type: string + format: uuid + title: Number Id + number: + type: string + title: Number + description: + anyOf: + - type: string + - type: 'null' + title: Description + sip_trunk: + anyOf: + - "$ref": "#/components/schemas/SipTrunkOutput" + - type: 'null' + inbound: + anyOf: + - "$ref": "#/components/schemas/InboundOutput" + - type: 'null' + create_ts: + type: integer + minimum: 0.0 + title: Create Ts + additionalProperties: false + type: object + required: + - number_id + - number + - create_ts + title: NumberResponse + Pacing: + properties: + max_concurrent: + type: integer + maximum: 10000.0 + minimum: 1.0 + title: Max Concurrent + default: 10 + max_calls_per_second: + anyOf: + - type: integer + maximum: 1000.0 + minimum: 1.0 + - type: 'null' + title: Max Calls Per Second + additionalProperties: false + type: object + title: Pacing + PacingPatch: + properties: + max_concurrent: + anyOf: + - type: integer + maximum: 10000.0 + minimum: 1.0 + - type: 'null' + title: Max Concurrent + max_calls_per_second: + anyOf: + - type: integer + maximum: 1000.0 + minimum: 1.0 + - type: 'null' + title: Max Calls Per Second + additionalProperties: false + type: object + title: PacingPatch + PublicAgentInput: + properties: + instructions: + anyOf: + - type: string + - type: 'null' + title: Instructions + messages: + anyOf: + - items: + "$ref": "#/components/schemas/Message" + type: array + - type: 'null' + title: Messages + greeting: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/TextGreeting" + - "$ref": "#/components/schemas/AudioGreeting" + discriminator: + propertyName: mode + mapping: + audio: "#/components/schemas/AudioGreeting" + text: "#/components/schemas/TextGreeting" + - type: 'null' + title: Greeting + variables: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + "$ref": "#/components/schemas/VariableDefinition" + type: object + - type: 'null' + title: Variables + pipeline: + anyOf: + - "$ref": "#/components/schemas/CascadedPipelineInput" + - type: 'null' + filler_words: + anyOf: + - "$ref": "#/components/schemas/FillerWords" + - type: 'null' + additionalProperties: false + type: object + title: PublicAgentInput + PublicByokCredential: + properties: + mode: + type: string + const: byok + title: Mode + fields: + additionalProperties: + type: string + minLength: 1 + type: object + maxProperties: 8 + minProperties: 1 + description: 'Provider credential fields for the selected vendor, for example + {"api_key": "$secrets.."}. Agent writes require $secrets references; + ephemeral sessions also accept direct values.' + additionalProperties: false + type: object + required: + - fields + - mode + title: PublicByokCredential + PublicEphemeralSessionCreate: + properties: + agent: + "$ref": "#/components/schemas/PublicAgentInput" + values: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + type: object + - type: 'null' + title: Values + labels: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + title: Labels + transport: + oneOf: + - "$ref": "#/components/schemas/RtcTransport" + - "$ref": "#/components/schemas/TelephonyTransport" + title: Transport + discriminator: + propertyName: type + mapping: + rtc: "#/components/schemas/RtcTransport" + telephony: "#/components/schemas/TelephonyTransport" + lifecycle: + anyOf: + - "$ref": "#/components/schemas/Lifecycle" + - type: 'null' + data_policy: + anyOf: + - "$ref": "#/components/schemas/DataPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - agent + - transport + title: PublicEphemeralSessionCreate + ResolvedAsr: + properties: + credential: + anyOf: + - "$ref": "#/components/schemas/ResolvedCredential" + - type: 'null' + url: + anyOf: + - type: string + - type: 'null' + title: Url + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + model: + anyOf: + - type: string + - type: 'null' + title: Model + language: + anyOf: + - type: string + - type: 'null' + title: Language + params: + anyOf: + - additionalProperties: + "$ref": "#/components/schemas/JsonValue" + type: object + - type: 'null' + title: Params + additionalProperties: false + type: object + title: ResolvedAsr + ResolvedAvatar: + properties: + enabled: + anyOf: + - type: boolean + - type: 'null' + title: Enabled + credential: + anyOf: + - "$ref": "#/components/schemas/ResolvedCredential" + - type: 'null' + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + params: + anyOf: + - additionalProperties: + "$ref": "#/components/schemas/JsonValue" + type: object + - type: 'null' + title: Params + additionalProperties: false + type: object + title: ResolvedAvatar + ResolvedCascadedPipeline: + properties: + mode: + type: string + const: cascaded + title: Mode + avatar: + anyOf: + - "$ref": "#/components/schemas/ResolvedAvatar" + - type: 'null' + turn_detection: + anyOf: + - "$ref": "#/components/schemas/TurnDetection" + - type: 'null' + asr: + anyOf: + - "$ref": "#/components/schemas/ResolvedAsr" + - type: 'null' + tts: + anyOf: + - "$ref": "#/components/schemas/ResolvedTts" + - type: 'null' + llm: + anyOf: + - "$ref": "#/components/schemas/ResolvedLlm" + - type: 'null' + silence_config: + anyOf: + - "$ref": "#/components/schemas/SilenceConfig" + - type: 'null' + additionalProperties: false + type: object + required: + - mode + title: ResolvedCascadedPipeline + ResolvedCredential: + properties: + mode: + type: string + enum: + - managed + - byok + title: Mode + additionalProperties: false + type: object + required: + - mode + title: ResolvedCredential + ResolvedLlm: + properties: + credential: + anyOf: + - "$ref": "#/components/schemas/ResolvedCredential" + - type: 'null' + url: + anyOf: + - type: string + - type: 'null' + title: Url + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + style: + anyOf: + - type: string + - type: 'null' + title: Style + model: + anyOf: + - type: string + - type: 'null' + title: Model + params: + anyOf: + - additionalProperties: + "$ref": "#/components/schemas/JsonValue" + type: object + - type: 'null' + title: Params + input_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Input Modalities + output_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Output Modalities + max_history: + anyOf: + - type: integer + - type: 'null' + title: Max History + failure_message: + anyOf: + - type: string + - type: 'null' + title: Failure Message + mcp_servers: + anyOf: + - type: array + items: + "$ref": "#/components/schemas/McpServer" + - type: 'null' + title: Mcp Servers + tools: + anyOf: + - type: array + items: + "$ref": "#/components/schemas/FunctionTool" + - type: 'null' + title: Tools + additionalProperties: false + type: object + title: ResolvedLlm + ResolvedTts: + properties: + credential: + anyOf: + - "$ref": "#/components/schemas/ResolvedCredential" + - type: 'null' + url: + anyOf: + - type: string + - type: 'null' + title: Url + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + model: + anyOf: + - type: string + - type: 'null' + title: Model + params: + anyOf: + - additionalProperties: + "$ref": "#/components/schemas/JsonValue" + type: object + - type: 'null' + title: Params + skip_patterns: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Skip Patterns + additionalProperties: false + type: object + title: ResolvedTts + RtcEncryption: + properties: + mode: + type: string + enum: + - aes_128_xts + - aes_128_ecb + - aes_256_xts + - sm4_128_ecb + - aes_128_gcm + - aes_256_gcm + - aes_128_gcm2 + - aes_256_gcm2 + title: Mode + key: + type: string + minLength: 1 + format: password + title: Key + writeOnly: true + salt: + anyOf: + - type: string + format: password + writeOnly: true + - type: 'null' + pattern: "^[A-Za-z0-9+/]{43}=$" + title: Salt + additionalProperties: false + type: object + required: + - mode + - key + title: RtcEncryption + RtcTransport: + properties: + type: + type: string + const: rtc + title: Type + channel: + type: string + maxLength: 256 + minLength: 1 + title: Channel + token: + anyOf: + - type: string + format: password + writeOnly: true + - type: 'null' + title: Token + uid: + type: string + minLength: 1 + maxLength: 255 + title: Uid + description: UID the agent uses to join the channel. Omit it to let the + server assign one. + subscribe_uids: + items: + type: string + minLength: 1 + maxLength: 255 + type: array + maxItems: 1 + minItems: 1 + title: Subscribe Uids + uniqueItems: true + encryption: + anyOf: + - "$ref": "#/components/schemas/RtcEncryption" + - type: 'null' + audio_scenario: + type: string + enum: + - aiserver + - chorus + title: Audio Scenario + default: aiserver + additionalProperties: false + type: object + required: + - type + - channel + - subscribe_uids + title: RtcTransport + ScheduledSchedule: + properties: + mode: + type: string + const: scheduled + title: Mode + timezone: + type: string + maxLength: 255 + minLength: 1 + title: Timezone + start: + type: string + pattern: "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]$" + title: Start + end: + type: string + pattern: "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]$" + title: End + days: + items: + "$ref": "#/components/schemas/CallingDay" + type: array + maxItems: 7 + minItems: 1 + title: Days + additionalProperties: false + type: object + required: + - mode + - timezone + - start + - end + title: ScheduledSchedule + SecretCreate: + properties: + name: + type: string + maxLength: 64 + minLength: 1 + pattern: "^[A-Za-z0-9_]+$" + title: Name + secrets: + patternProperties: + "^[A-Za-z0-9_]+$": + type: string + format: password + writeOnly: true + propertyNames: + maxLength: 64 + minLength: 1 + type: object + minProperties: 1 + title: Secrets + additionalProperties: false + additionalProperties: false + type: object + required: + - name + - secrets + title: SecretCreate + SecretDeleted: + properties: + deleted: + type: boolean + const: true + title: Deleted + additionalProperties: false + type: object + required: + - deleted + title: SecretDeleted + SecretKey: + properties: + key: + type: string + maxLength: 64 + minLength: 1 + pattern: "^[A-Za-z0-9_]+$" + title: Key + create_ts: + type: integer + minimum: 0 + description: Key timestamp when returned by the service. + update_ts: + type: integer + minimum: 0 + description: Key timestamp when returned by the service. + additionalProperties: false + type: object + required: + - key + title: SecretKey + SecretKeyWrite: + properties: + value: + type: string + minLength: 1 + format: password + title: Value + writeOnly: true + additionalProperties: false + type: object + required: + - value + title: SecretKeyWrite + SecretList: + properties: + data: + "$ref": "#/components/schemas/SecretPage" + has_more: + type: boolean + title: Has More + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + additionalProperties: false + type: object + required: + - data + - has_more + - next_cursor + title: SecretList + SecretPage: + properties: + count: + type: integer + minimum: 0.0 + title: Count + list: + items: + "$ref": "#/components/schemas/SecretSet" + type: array + title: List + additionalProperties: false + type: object + required: + - count + - list + title: SecretPage + SecretReplace: + properties: + secrets: + patternProperties: + "^[A-Za-z0-9_]+$": + type: string + format: password + writeOnly: true + propertyNames: + maxLength: 64 + minLength: 1 + type: object + minProperties: 1 + title: Secrets + additionalProperties: false + additionalProperties: false + type: object + required: + - secrets + title: SecretReplace + SecretSet: + properties: + name: + type: string + maxLength: 64 + minLength: 1 + pattern: "^[A-Za-z0-9_]+$" + title: Name + keys: + items: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + pattern: "^[A-Za-z0-9_]+$" + - "$ref": "#/components/schemas/SecretKey" + type: array + title: Keys + description: Accepts legacy key names and launch key metadata objects during + rollout. + create_ts: + type: integer + minimum: 0.0 + title: Create Ts + update_ts: + type: integer + minimum: 0.0 + title: Update Ts + additionalProperties: false + type: object + required: + - name + - keys + - create_ts + - update_ts + title: SecretSet + SecretUpdate: + properties: + secrets: + patternProperties: + "^[A-Za-z0-9_]+$": + anyOf: + - type: string + format: password + writeOnly: true + - type: 'null' + "^[A-Za-z0-9_]*-[A-Za-z0-9_-]*$": + type: 'null' + description: An existing legacy hyphenated key may only be deleted, + not written. + propertyNames: + maxLength: 64 + minLength: 1 + type: object + minProperties: 1 + title: Secrets + description: Non-null writes require ASCII letters, digits or underscores; + existing hyphenated keys may only be removed with null. + additionalProperties: false + additionalProperties: false + type: object + required: + - secrets + title: SecretUpdate + SemanticTurnEnd: + properties: + type: + type: string + const: semantic + title: Type + default: semantic + silence_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 2000 + - type: 'null' + title: Silence Duration Ms + default: 320 + max_wait_ms: + anyOf: + - type: integer + minimum: 500 + maximum: 10000 + - type: integer + const: -1 + - type: 'null' + title: Max Wait Ms + default: 3000 + description: 500-10000 ms, or -1 to wait without a timeout. + pause_state_enabled: + anyOf: + - type: boolean + - type: 'null' + title: Pause State Enabled + default: true + additionalProperties: false + type: object + required: + - type + title: SemanticTurnEnd + SemanticTurnStart: + properties: + type: + type: string + const: semantic + title: Type + default: semantic + speaking_interrupt_duration_ms: + anyOf: + - type: integer + - type: 'null' + title: Speaking Interrupt Duration Ms + default: 160 + additionalProperties: false + type: object + required: + - type + title: SemanticTurnStart + SessionAgentOverridesPatch: + properties: + messages: + items: + "$ref": "#/components/schemas/Message" + type: array + title: Messages + pipeline: + "$ref": "#/components/schemas/SessionPipelinePatch" + title: Pipeline + additionalProperties: false + type: object + title: SessionAgentOverridesPatch + SessionAgentPatch: + properties: + overrides: + "$ref": "#/components/schemas/SessionAgentOverridesPatch" + title: Overrides + additionalProperties: false + type: object + title: SessionAgentPatch + SessionCreate: + properties: + agent_id: + type: string + format: uuid + title: Agent Id + values: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + type: object + - type: 'null' + title: Values + labels: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + title: Labels + transport: + oneOf: + - "$ref": "#/components/schemas/RtcTransport" + - "$ref": "#/components/schemas/TelephonyTransport" + title: Transport + discriminator: + propertyName: type + mapping: + rtc: "#/components/schemas/RtcTransport" + telephony: "#/components/schemas/TelephonyTransport" + lifecycle: + anyOf: + - "$ref": "#/components/schemas/Lifecycle" + - type: 'null' + data_policy: + anyOf: + - "$ref": "#/components/schemas/DataPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - agent_id + - transport + title: SessionCreate + SessionData: + properties: + count: + type: integer + minimum: 0.0 + title: Count + list: + items: + "$ref": "#/components/schemas/SessionSummary" + type: array + title: List + additionalProperties: false + type: object + required: + - count + - list + title: SessionData + SessionList: + properties: + data: + "$ref": "#/components/schemas/SessionData" + has_more: + type: boolean + title: Has More + next_cursor: + anyOf: + - type: string + - type: 'null' + title: Next Cursor + additionalProperties: false + type: object + required: + - data + - has_more + - next_cursor + title: SessionList + SessionModulePatch: + properties: + params: + additionalProperties: + "$ref": "#/components/schemas/JsonValue" + type: object + title: Params + description: Complete provider params replacement. Runtime endpoint aliases + are not mutable. + additionalProperties: false + type: object + title: SessionModulePatch + SessionPatch: + properties: + transport: + "$ref": "#/components/schemas/SessionTransportPatch" + title: Transport + agent: + "$ref": "#/components/schemas/SessionAgentPatch" + title: Agent + additionalProperties: false + type: object + title: SessionPatch + SessionPipelinePatch: + properties: + llm: + "$ref": "#/components/schemas/SessionModulePatch" + title: Llm + additionalProperties: false + type: object + title: SessionPipelinePatch + SessionResponse: + properties: + agent_session_id: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + agent_name: + type: string + title: Agent Name + description: Name of the agent the session runs. + start_ts: + type: integer + minimum: 0.0 + title: Start Ts + status: + type: string + enum: + - IDLE + - STARTING + - RUNNING + - STOPPING + - STOPPED + - FAILED + title: Status + stop_ts: + anyOf: + - type: integer + minimum: 0.0 + - type: 'null' + title: Stop Ts + message: + anyOf: + - type: string + - type: 'null' + title: Message + analysis: + anyOf: + - "$ref": "#/components/schemas/SessionAnalysis" + - type: 'null' + description: Optional extraction analysis. May be absent while analysis + support is rolling out. + additionalProperties: false + type: object + required: + - agent_session_id + - start_ts + - status + title: SessionResponse + SessionStartResponse: + properties: + agent_session_id: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + create_ts: + type: integer + minimum: 0 + title: Create Ts + description: Unix timestamp in seconds when the session was created. + status: + type: string + enum: + - STARTING + - RUNNING + title: Status + additionalProperties: false + type: object + required: + - agent_session_id + - create_ts + - status + title: SessionStartResponse + SessionStopResponse: + properties: {} + additionalProperties: false + type: object + title: SessionStopResponse + SessionSummary: + properties: + agent_session_id: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + start_ts: + type: integer + minimum: 0.0 + title: Start Ts + status: + type: string + enum: + - IDLE + - STARTING + - RUNNING + - STOPPING + - STOPPED + - FAILED + title: Status + additionalProperties: false + type: object + required: + - agent_session_id + - start_ts + - status + title: SessionSummary + SessionTransportPatch: + properties: + token: + type: string + minLength: 1 + format: password + title: Token + writeOnly: true + additionalProperties: false + type: object + required: + - token + title: SessionTransportPatch + SessionUpdateResponse: + properties: + agent_session_id: + type: string + pattern: "^[a-z0-9]{32}$" + title: Agent Session Id + create_ts: + type: integer + minimum: 0.0 + title: Create Ts + status: + type: string + enum: + - IDLE + - STARTING + - RUNNING + - STOPPING + - STOPPED + - FAILED + title: Status + additionalProperties: false + type: object + required: + - agent_session_id + - create_ts + - status + title: SessionUpdateResponse + SilenceConfig: + properties: + enabled: + type: boolean + title: Enabled + timeout_ms: + type: integer + title: Timeout Ms + action: + type: string + enum: + - speak + - think + title: Action + content: + type: string + title: Content + additionalProperties: false + type: object + required: + - enabled + - timeout_ms + - action + - content + title: SilenceConfig + SipAuth: + properties: + username: + type: string + title: Username + writeOnly: true + password: + type: string + format: password + title: Password + description: Literal SIP password; secret references are not supported. + writeOnly: true + additionalProperties: false + type: object + required: + - username + - password + title: SipAuth + SipTrunkInput: + properties: + name: + anyOf: + - type: string + - type: 'null' + title: Name + hostname: + type: string + pattern: "^[^:/\\s]+$" + title: Hostname + port: + anyOf: + - type: integer + maximum: 65535.0 + minimum: 1.0 + - type: 'null' + title: Port + transport: + type: string + enum: + - udp + - tcp + - tls + title: Transport + default: tls + auth: + anyOf: + - "$ref": "#/components/schemas/SipAuth" + - type: 'null' + allowed_ips: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Allowed Ips + description: Inbound source IPv4 addresses, IPv4 CIDRs (decimal prefix length + 0-32 without leading zeros), or '*'. Host bits are accepted; entries are + preserved unchanged. IPv6, dotted masks and hostnames unsupported. Omission + defaults runtime inbound allowed_addresses to ['*']; [] explicitly clears + it. + additionalProperties: false + type: object + required: + - hostname + title: SipTrunkInput + SipTrunkOutput: + properties: + name: + anyOf: + - type: string + - type: 'null' + title: Name + hostname: + type: string + title: Hostname + port: + type: integer + title: Port + transport: + type: string + enum: + - udp + - tcp + - tls + title: Transport + allowed_ips: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Allowed Ips + description: Inbound source IPv4 addresses, IPv4 CIDRs (decimal prefix length + 0-32 without leading zeros), or '*'. Host bits are accepted; entries are + preserved unchanged. IPv6, dotted masks and hostnames unsupported. Omission + defaults runtime inbound allowed_addresses to ['*']; [] explicitly clears + it. + additionalProperties: false + type: object + required: + - hostname + - port + - transport + title: SipTrunkOutput + SkipPattern: + type: string + enum: + - fullwidth_parentheses + - lenticular_brackets + - parentheses + - square_brackets + - curly_braces + SpeechInterruption: + properties: + mode: + type: string + const: start_of_speech + title: Mode + additionalProperties: false + type: object + required: + - mode + title: SpeechInterruption + StaticFillerContent: + properties: + mode: + type: string + const: static + title: Mode + phrases: + items: + type: string + minLength: 1 + type: array + title: Phrases + minItems: 1 + maxItems: 100 + selection_rule: + type: string + enum: + - shuffle + - round_robin + title: Selection Rule + default: shuffle + additionalProperties: false + type: object + required: + - mode + - phrases + title: StaticFillerContent + TelephonyTransport: + properties: + type: + type: string + const: telephony + title: Type + from: + type: string + format: uuid + pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + title: From + description: ID of a Number in the project. Phone numbers are not accepted. + to: + type: string + pattern: "^\\+[1-9][0-9]{1,14}$" + title: To + call_policy: + anyOf: + - "$ref": "#/components/schemas/CallPolicy" + - type: 'null' + additionalProperties: false + type: object + required: + - type + - from + - to + title: TelephonyTransport + TextGreeting: + properties: + mode: + type: string + const: text + title: Mode + 'on': + type: string + enum: + - each_join + - first_join + title: 'On' + default: each_join + delay_ms: + type: integer + title: Delay Ms + default: 0 + minimum: 0 + maximum: 5000 + text: + type: string + title: Text + additionalProperties: false + type: object + required: + - mode + - text + title: TextGreeting + TimeRange: + properties: + start: + type: string + pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$" + title: Start + end: + type: string + pattern: "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$" + title: End + additionalProperties: false + type: object + required: + - start + - end + title: TimeRange + ToolExecution: + properties: + mode: + type: string + const: sync + title: Mode + additionalProperties: false + type: object + required: + - mode + title: ToolExecution + ToolServer: + properties: + method: + type: string + enum: + - GET + - POST + title: Method + url: + type: string + maxLength: 2048 + minLength: 1 + pattern: "^https://" + title: Url + headers: + anyOf: + - patternProperties: + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": + type: string + propertyNames: + maxLength: 128 + minLength: 1 + type: object + maxProperties: 32 + - type: 'null' + title: Headers + body: + anyOf: + - "$ref": "#/components/schemas/JSONObject" + - type: 'null' + timeout_ms: + anyOf: + - type: integer + maximum: 100000.0 + minimum: 1000.0 + - type: 'null' + title: Timeout Ms + default: 10000 + additionalProperties: false + type: object + required: + - method + - url + title: ToolServer + TransferPolicy: + properties: + phone_number: + type: string + pattern: "^\\+[1-9][0-9]{1,14}$" + title: Phone Number + description: + type: string + maxLength: 1000 + minLength: 1 + title: Description + additionalProperties: false + type: object + required: + - phone_number + - description + title: TransferPolicy + TtsInput: + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + - if: + allOf: + - properties: + vendor: + enum: + - amazon + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + aws_access_key_id: + type: string + minLength: 1 + aws_secret_access_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - aws_access_key_id + - aws_secret_access_key + required: + - fields + - if: + allOf: + - anyOf: + - properties: + vendor: + enum: + - cartesia + - deepgram + - fishaudio + - gemini + - generic_http + - gradium + - mistral + - murf + - openai + - rime + - smallestai + - typecast + - xai + required: + - vendor + - not: + required: + - vendor + - properties: + vendor: + not: + enum: + - amazon + - cartesia + - deepgram + - elevenlabs + - fishaudio + - gemini + - generic_http + - google + - gradium + - humeai + - microsoft + - minimax + - mistral + - murf + - openai + - rime + - sarvam + - smallestai + - typecast + - xai + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + api_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - api_key + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - elevenlabs + - humeai + - microsoft + - minimax + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - key + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - google + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + credentials: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - credentials + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - sarvam + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + properties: + api_subscription_key: + type: string + minLength: 1 + additionalProperties: false + type: object + required: + - api_subscription_key + required: + - fields + properties: + skip_patterns: + anyOf: + - items: + "$ref": "#/components/schemas/SkipPattern" + type: array + - type: 'null' + uniqueItems: true + title: Skip Patterns + credential: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/ManagedCredential" + - "$ref": "#/components/schemas/PublicByokCredential" + discriminator: + propertyName: mode + mapping: + byok: "#/components/schemas/PublicByokCredential" + managed: "#/components/schemas/ManagedCredential" + - type: 'null' + title: Credential + vendor: + anyOf: + - type: string + - type: 'null' + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/JSONObject" + - type: 'null' + url: + anyOf: + - type: string + format: uri + - type: 'null' + title: Url + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + title: Headers + additionalProperties: false + type: object + title: TtsInput + TurnDetection: + properties: + speech_threshold: + anyOf: + - type: number + minimum: 0 + maximum: 1 + - type: integer + minimum: 0 + maximum: 1 + - type: 'null' + title: Speech Threshold + default: 0.5 + start_of_speech: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/SemanticTurnStart" + - "$ref": "#/components/schemas/VadTurnStart" + - "$ref": "#/components/schemas/ManualTurnBoundary" + discriminator: + propertyName: type + mapping: + manual: "#/components/schemas/ManualTurnBoundary" + semantic: "#/components/schemas/SemanticTurnStart" + vad: "#/components/schemas/VadTurnStart" + - type: 'null' + title: Start Of Speech + end_of_speech: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/SemanticTurnEnd" + - "$ref": "#/components/schemas/VadTurnEnd" + - "$ref": "#/components/schemas/ManualTurnBoundary" + discriminator: + propertyName: type + mapping: + manual: "#/components/schemas/ManualTurnBoundary" + semantic: "#/components/schemas/SemanticTurnEnd" + vad: "#/components/schemas/VadTurnEnd" + - type: 'null' + title: End Of Speech + interruption: + anyOf: + - oneOf: + - "$ref": "#/components/schemas/SpeechInterruption" + - "$ref": "#/components/schemas/KeywordInterruption" + - "$ref": "#/components/schemas/DisabledInterruption" + discriminator: + propertyName: mode + mapping: + keywords: "#/components/schemas/KeywordInterruption" + 'off': "#/components/schemas/DisabledInterruption" + start_of_speech: "#/components/schemas/SpeechInterruption" + - type: 'null' + title: Interruption + default: + mode: start_of_speech + additionalProperties: false + type: object + title: TurnDetection + VadTurnEnd: + properties: + type: + type: string + const: vad + title: Type + silence_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 2000 + - type: 'null' + title: Silence Duration Ms + default: 640 + additionalProperties: false + type: object + required: + - type + title: VadTurnEnd + VadTurnStart: + properties: + type: + type: string + const: vad + title: Type + interrupt_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 1200 + - type: 'null' + title: Interrupt Duration Ms + default: 160 + speaking_interrupt_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 1200 + - type: 'null' + title: Speaking Interrupt Duration Ms + default: 160 + prefix_padding_ms: + anyOf: + - type: integer + minimum: 0 + maximum: 5000 + - type: 'null' + title: Prefix Padding Ms + default: 800 + additionalProperties: false + type: object + required: + - type + title: VadTurnStart + VariableDefinition: + properties: + type: + type: string + enum: + - str + - int + - float + - bool + - date + - datetime + title: Type + required: + type: boolean + title: Required + default: false + options: + anyOf: + - items: + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + type: array + maxItems: 100 + minItems: 1 + - type: 'null' + title: Options + extract: + type: boolean + title: Extract + default: false + description: + anyOf: + - type: string + maxLength: 2000 + - type: 'null' + title: Description + additionalProperties: false + type: object + required: + - type + title: VariableDefinition + VoicemailPolicy: + properties: + mode: + type: string + enum: + - continue + - hangup + title: Mode + additionalProperties: false + type: object + required: + - mode + title: VoicemailPolicy + AgentRequest_AgentCreateInput: + additionalProperties: false + properties: + agent_name: + maxLength: 255 + minLength: 1 + title: Agent Name + type: string + instructions: + anyOf: + - type: string + - type: 'null' + default: + title: Instructions + messages: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_Message" + type: array + - type: 'null' + default: + title: Messages + greeting: + anyOf: + - discriminator: + mapping: + audio: "#/components/schemas/AgentRequest_AudioGreeting" + text: "#/components/schemas/AgentRequest_TextGreeting" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_TextGreeting" + - "$ref": "#/components/schemas/AgentRequest_AudioGreeting" + - type: 'null' + default: + title: Greeting + variables: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + "$ref": "#/components/schemas/AgentRequest_VariableDefinition" + type: object + - type: 'null' + default: + title: Variables + pipeline: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_CascadedPipelineInput" + - type: 'null' + default: + description: Omission, null, or an empty object selects the managed ARES, + OpenAI gpt-4.1-mini, and MiniMax speech-2.8-turbo default pipeline. + filler_words: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_FillerWords" + - type: 'null' + default: + required: + - agent_name + title: AgentCreateInput + type: object + AgentRequest_AgentPatch: + additionalProperties: false + properties: + agent_name: + anyOf: + - type: string + - type: 'null' + default: + title: Agent Name + instructions: + anyOf: + - type: string + - type: 'null' + default: + title: Instructions + messages: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_Message" + type: array + - type: 'null' + default: + title: Messages + greeting: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_GreetingPatch" + - type: 'null' + default: + variables: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + anyOf: + - "$ref": "#/components/schemas/AgentRequest_VariableDefinitionPatch" + - type: 'null' + type: object + - type: 'null' + default: + title: Variables + pipeline: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_CascadedPipelinePatch" + - type: 'null' + default: + filler_words: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_FillerWordsPatch" + - type: 'null' + default: + title: AgentPatch + type: object + AgentRequest_AsrInput: + additionalProperties: false + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + - if: + allOf: + - properties: + vendor: + enum: + - amazon + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + access_key_id: + minLength: 1 + type: string + secret_access_key: + minLength: 1 + type: string + required: + - access_key_id + - secret_access_key + type: object + required: + - fields + - if: + allOf: + - anyOf: + - properties: + vendor: + enum: + - assemblyai + - deepgram + - gemini + - openai + - sarvam + - smallestai + - xai + required: + - vendor + - not: + required: + - vendor + - properties: + vendor: + not: + enum: + - amazon + - ares + - assemblyai + - deepgram + - gemini + - google + - microsoft + - openai + - sarvam + - smallestai + - speechmatics + - xai + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + api_key: + minLength: 1 + type: string + required: + - api_key + type: object + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - google + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + adc_credentials_string: + minLength: 1 + type: string + required: + - adc_credentials_string + type: object + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - microsoft + - speechmatics + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + key: + minLength: 1 + type: string + required: + - key + type: object + required: + - fields + properties: + credential: + anyOf: + - discriminator: + mapping: + byok: "#/components/schemas/AgentRequest_PublicByokCredential" + managed: "#/components/schemas/AgentRequest_ManagedCredential" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + - "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + - type: 'null' + default: + title: Credential + vendor: + anyOf: + - type: string + - type: 'null' + default: + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + url: + anyOf: + - format: uri + type: string + - type: 'null' + default: + title: Url + model: + anyOf: + - type: string + - type: 'null' + default: + title: Model + language: + anyOf: + - type: string + - type: 'null' + default: + title: Language + title: AsrInput + type: object + AgentRequest_AsrPatch: + additionalProperties: false + allOf: + - if: + properties: + credential: + properties: + mode: + const: managed + required: + - mode + type: object + required: + - credential + then: + properties: + url: + type: 'null' + properties: + credential: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + - type: 'null' + default: + vendor: + anyOf: + - type: string + - type: 'null' + default: + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + url: + anyOf: + - format: uri + type: string + - type: 'null' + default: + description: Non-null URLs require effective BYOK mode; null clears a saved + override. + title: Url + model: + anyOf: + - type: string + - type: 'null' + default: + title: Model + language: + anyOf: + - type: string + - type: 'null' + default: + title: Language + title: AsrPatch + type: object + AgentRequest_AudioGreeting: + additionalProperties: false + properties: + mode: + const: audio + title: Mode + type: string + 'on': + default: each_join + enum: + - each_join + - first_join + title: 'On' + type: string + delay_ms: + default: 0 + title: Delay Ms + type: integer + minimum: 0 + maximum: 5000 + url: + title: Url + type: string + maxLength: 2048 + text: + title: Text + type: string + minLength: 1 + download_timeout_ms: + default: 1000 + title: Download Timeout Ms + type: integer + minimum: 200 + maximum: 10000 + pcm_sample_rate: + default: 16000 + title: Pcm Sample Rate + type: integer + required: + - mode + - url + - text + title: AudioGreeting + type: object + AgentRequest_AvatarInput: + additionalProperties: false + properties: + credential: + anyOf: + - discriminator: + mapping: + byok: "#/components/schemas/AgentRequest_PublicByokCredential" + managed: "#/components/schemas/AgentRequest_ManagedCredential" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + - "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + - type: 'null' + default: + title: Credential + vendor: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarVendor" + - type: 'null' + default: + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + enabled: + anyOf: + - type: boolean + - type: 'null' + default: + title: Enabled + title: AvatarInput + type: object + AgentRequest_AvatarPatch: + additionalProperties: false + properties: + credential: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + - type: 'null' + default: + vendor: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarVendor" + - type: 'null' + default: + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + enabled: + anyOf: + - type: boolean + - type: 'null' + default: + title: Enabled + title: AvatarPatch + type: object + AgentRequest_AvatarVendor: + enum: + - akool + - liveavatar + - anam + - sensetime + - spatius + - generic + type: string + AgentRequest_CascadedPipelineInput: + additionalProperties: false + properties: + mode: + const: cascaded + title: Mode + type: string + avatar: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarInput" + - type: 'null' + default: + turn_detection: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_TurnDetection" + - type: 'null' + default: + asr: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AsrInput" + - type: 'null' + default: + tts: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_TtsInput" + - type: 'null' + default: + llm: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_LlmInput" + - type: 'null' + default: + silence_config: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_SilenceConfig" + - type: 'null' + default: + required: + - mode + title: CascadedPipelineInput + type: object + AgentRequest_CascadedPipelinePatch: + additionalProperties: false + properties: + mode: + anyOf: + - const: cascaded + type: string + - type: 'null' + default: + title: Mode + avatar: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + - type: 'null' + default: + turn_detection: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + - type: 'null' + default: + asr: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AsrPatch" + - type: 'null' + default: + tts: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_TtsPatch" + - type: 'null' + default: + llm: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_LlmPatch" + - type: 'null' + default: + silence_config: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_SilenceConfigPatch" + - type: 'null' + default: + title: CascadedPipelinePatch + type: object + AgentRequest_CascadedTurnDetectionPatch: + additionalProperties: false + properties: + speech_threshold: + anyOf: + - type: number + minimum: 0 + maximum: 1 + - type: integer + minimum: 0 + maximum: 1 + - type: 'null' + default: + title: Speech Threshold + start_of_speech: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_TurnBoundaryPatch" + - type: 'null' + default: + end_of_speech: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_TurnBoundaryPatch" + - type: 'null' + default: + interruption: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_InterruptionPatch" + - type: 'null' + default: + title: CascadedTurnDetectionPatch + type: object + AgentRequest_DisabledInterruption: + additionalProperties: false + properties: + mode: + const: 'off' + title: Mode + type: string + when_off: + default: append + enum: + - append + - ignore + title: When Off + type: string + required: + - mode + title: DisabledInterruption + type: object + AgentRequest_FillerContentPatch: + additionalProperties: false + properties: + mode: + anyOf: + - enum: + - static + - generated + type: string + - type: 'null' + default: + title: Mode + phrases: + anyOf: + - items: + type: string + minLength: 1 + type: array + minItems: 1 + maxItems: 100 + - type: 'null' + default: + title: Phrases + selection_rule: + anyOf: + - enum: + - shuffle + - round_robin + type: string + - type: 'null' + default: + title: Selection Rule + instructions: + anyOf: + - type: string + - type: 'null' + default: + title: Instructions + fallback_strategy: + anyOf: + - const: static + type: string + - type: 'null' + default: + title: Fallback Strategy + title: FillerContentPatch + type: object + AgentRequest_FillerTrigger: + additionalProperties: false + properties: + mode: + const: fixed_time + title: Mode + type: string + response_wait_ms: + title: Response Wait Ms + type: integer + required: + - mode + - response_wait_ms + title: FillerTrigger + type: object + AgentRequest_FillerTriggerPatch: + additionalProperties: false + properties: + mode: + anyOf: + - const: fixed_time + type: string + - type: 'null' + default: + title: Mode + response_wait_ms: + anyOf: + - type: integer + minimum: 100 + maximum: 10000 + - type: 'null' + default: + title: Response Wait Ms + title: FillerTriggerPatch + type: object + AgentRequest_FillerWords: + additionalProperties: false + properties: + enable: + anyOf: + - type: boolean + - type: 'null' + default: + title: Enable + trigger: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_FillerTrigger" + - type: 'null' + default: + content: + anyOf: + - discriminator: + mapping: + generated: "#/components/schemas/AgentRequest_GeneratedFillerContent" + static: "#/components/schemas/AgentRequest_StaticFillerContent" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_StaticFillerContent" + - "$ref": "#/components/schemas/AgentRequest_GeneratedFillerContent" + - type: 'null' + default: + title: Content + title: FillerWords + type: object + AgentRequest_FillerWordsPatch: + additionalProperties: false + properties: + enable: + anyOf: + - type: boolean + - type: 'null' + default: + title: Enable + trigger: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_FillerTriggerPatch" + - type: 'null' + default: + content: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_FillerContentPatch" + - type: 'null' + default: + title: FillerWordsPatch + type: object + AgentRequest_FunctionTool: + additionalProperties: false + properties: + type: + const: function + title: Type + type: string + name: + pattern: "^(?![mM][cC][pP])[A-Za-z][A-Za-z0-9]{0,63}$" + title: Name + type: string + description: + maxLength: 1024 + minLength: 1 + title: Description + type: string + parameters: + "$ref": "#/components/schemas/AgentRequest_JSONObject" + description: JSON Schema (draft 2020-12) describing the tool arguments. + execution: + "$ref": "#/components/schemas/AgentRequest_ToolExecution" + server: + "$ref": "#/components/schemas/AgentRequest_ToolServer" + required: + - type + - name + - description + - parameters + - execution + - server + title: FunctionTool + type: object + AgentRequest_GeneratedFillerContent: + additionalProperties: false + properties: + mode: + const: generated + title: Mode + type: string + instructions: + title: Instructions + type: string + fallback_strategy: + anyOf: + - const: static + type: string + - type: 'null' + default: + title: Fallback Strategy + required: + - mode + - instructions + title: GeneratedFillerContent + type: object + AgentRequest_GreetingPatch: + additionalProperties: false + properties: + mode: + anyOf: + - enum: + - text + - audio + type: string + - type: 'null' + default: + title: Mode + 'on': + anyOf: + - enum: + - each_join + - first_join + type: string + - type: 'null' + default: + title: 'On' + delay_ms: + anyOf: + - type: integer + minimum: 0 + maximum: 5000 + - type: 'null' + default: + title: Delay Ms + url: + anyOf: + - type: string + maxLength: 2048 + - type: 'null' + default: + title: Url + text: + anyOf: + - type: string + - type: 'null' + default: + title: Text + download_timeout_ms: + anyOf: + - type: integer + minimum: 200 + maximum: 10000 + - type: 'null' + default: + title: Download Timeout Ms + pcm_sample_rate: + anyOf: + - type: integer + - type: 'null' + default: + title: Pcm Sample Rate + title: GreetingPatch + type: object + AgentRequest_InterruptionPatch: + additionalProperties: false + properties: + mode: + anyOf: + - enum: + - start_of_speech + - keywords + - 'off' + type: string + - type: 'null' + default: + title: Mode + keywords: + anyOf: + - items: + type: string + type: array + maxItems: 128 + - type: 'null' + default: + title: Keywords + when_off: + anyOf: + - enum: + - append + - ignore + type: string + - type: 'null' + default: + title: When Off + title: InterruptionPatch + type: object + AgentRequest_JSONObject: + additionalProperties: + "$ref": "#/components/schemas/AgentRequest_JsonValue" + type: object + AgentRequest_JsonValue: {} + AgentRequest_KeywordInterruption: + additionalProperties: false + properties: + mode: + const: keywords + title: Mode + type: string + keywords: + items: + type: string + title: Keywords + type: array + maxItems: 128 + required: + - mode + - keywords + title: KeywordInterruption + type: object + AgentRequest_LlmInput: + additionalProperties: false + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + - if: + allOf: + - properties: + vendor: + enum: + - amazon + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + api_key: + minLength: 1 + type: string + required: + - api_key + type: object + required: + - fields + - properties: + fields: + additionalProperties: false + properties: + access_key: + minLength: 1 + type: string + secret_key: + minLength: 1 + type: string + required: + - access_key + - secret_key + type: object + required: + - fields + - if: + allOf: + - anyOf: + - properties: + vendor: + enum: + - azure + - anthropic + - dify + - gemini + - vertexai + - groq + - openai + - xai + required: + - vendor + - not: + required: + - vendor + - properties: + vendor: + not: + enum: + - amazon + - azure + - anthropic + - dify + - gemini + - vertexai + - groq + - openai + - xai + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + api_key: + minLength: 1 + type: string + required: + - api_key + type: object + required: + - fields + properties: + mcp_servers: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_McpServer" + type: array + - type: 'null' + default: + title: Mcp Servers + tools: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_FunctionTool" + maxItems: 32 + type: array + - type: 'null' + default: + title: Tools + credential: + anyOf: + - discriminator: + mapping: + byok: "#/components/schemas/AgentRequest_PublicByokCredential" + managed: "#/components/schemas/AgentRequest_ManagedCredential" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + - "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + - type: 'null' + default: + title: Credential + vendor: + anyOf: + - type: string + - type: 'null' + default: + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + url: + anyOf: + - format: uri + type: string + - type: 'null' + default: + title: Url + style: + anyOf: + - type: string + - type: 'null' + default: + title: Style + model: + anyOf: + - type: string + - type: 'null' + default: + title: Model + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + default: + title: Headers + input_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + default: + title: Input Modalities + output_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + default: + title: Output Modalities + max_history: + anyOf: + - maximum: 1024 + minimum: 1 + type: integer + - type: 'null' + default: + title: Max History + failure_message: + anyOf: + - type: string + - type: 'null' + default: + title: Failure Message + title: LlmInput + type: object + AgentRequest_LlmPatch: + additionalProperties: false + allOf: + - if: + properties: + credential: + properties: + mode: + const: managed + required: + - mode + type: object + required: + - credential + then: + properties: + url: + type: 'null' + properties: + mcp_servers: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_McpServer" + type: array + - type: 'null' + default: + title: Mcp Servers + tools: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_FunctionTool" + maxItems: 32 + type: array + - type: 'null' + default: + title: Tools + credential: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + - type: 'null' + default: + vendor: + anyOf: + - type: string + - type: 'null' + default: + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + url: + anyOf: + - format: uri + type: string + - type: 'null' + default: + description: Non-null URLs require effective BYOK mode; null clears a saved + override. + title: Url + style: + anyOf: + - type: string + - type: 'null' + default: + title: Style + model: + anyOf: + - type: string + - type: 'null' + default: + title: Model + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + default: + title: Headers + input_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + default: + title: Input Modalities + output_modalities: + anyOf: + - items: + type: string + type: array + - type: 'null' + default: + title: Output Modalities + max_history: + anyOf: + - maximum: 1024 + minimum: 1 + type: integer + - type: 'null' + default: + title: Max History + failure_message: + anyOf: + - type: string + - type: 'null' + default: + title: Failure Message + title: LlmPatch + type: object + AgentRequest_ManagedCredential: + additionalProperties: false + properties: + mode: + const: managed + title: Mode + type: string + required: + - mode + title: ManagedCredential + type: object + AgentRequest_ManualTurnBoundary: + additionalProperties: false + properties: + type: + const: manual + title: Type + type: string + required: + - type + title: ManualTurnBoundary + type: object + AgentRequest_McpServer: + additionalProperties: false + properties: + name: + pattern: "^[A-Za-z0-9]{1,48}$" + title: Name + type: string + endpoint: + maxLength: 2048 + minLength: 1 + pattern: "^https://" + title: Endpoint + type: string + transport: + const: streamable_http + title: Transport + type: string + headers: + anyOf: + - maxProperties: 32 + patternProperties: + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": + type: string + propertyNames: + maxLength: 128 + minLength: 1 + type: object + - type: 'null' + default: + title: Headers + allowed_tools: + anyOf: + - items: + minLength: 1 + type: string + type: array + - type: 'null' + default: + title: Allowed Tools + timeout_ms: + anyOf: + - maximum: 100000 + minimum: 1000 + type: integer + - type: 'null' + default: + title: Timeout Ms + required: + - name + - endpoint + - transport + title: McpServer + type: object + AgentRequest_Message: + additionalProperties: false + properties: + role: + enum: + - system + - user + - assistant + title: Role + type: string + content: + title: Content + type: string + required: + - role + - content + title: Message + type: object + AgentRequest_PublicByokCredential: + additionalProperties: false + properties: + mode: + const: byok + title: Mode + type: string + fields: + additionalProperties: + minLength: 1 + type: string + pattern: "^\\$secrets\\.[^.]+\\.[^.]+$" + maxProperties: 8 + minProperties: 1 + type: object + description: 'Provider credential fields for the selected vendor, for example + {"api_key": "$secrets.."}. Agent writes require $secrets references; + ephemeral sessions also accept direct values.' + required: + - fields + - mode + title: PublicByokCredential + type: object + AgentRequest_PublicCredentialPatch: + additionalProperties: false + allOf: + - if: + properties: + mode: + const: managed + required: + - mode + then: + properties: + fields: + type: 'null' + properties: + mode: + anyOf: + - enum: + - managed + - byok + type: string + - type: 'null' + default: + title: Mode + fields: + anyOf: + - additionalProperties: + anyOf: + - type: string + minLength: 1 + pattern: "^\\$secrets\\.[^.]+\\.[^.]+$" + - type: 'null' + maxProperties: 8 + type: object + - type: 'null' + default: + title: Fields + title: PublicCredentialPatch + type: object + description: JSON Merge Patch of provider credentials. Non-null field values + on saved-agent writes must be $secrets references. Set fields to null when + switching to managed credentials; individual null map entries remove stored + fields. The merged result must satisfy the credential schema for the selected + provider. + AgentRequest_SemanticTurnEnd: + additionalProperties: false + properties: + type: + const: semantic + title: Type + type: string + default: semantic + silence_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 2000 + - type: 'null' + default: 320 + title: Silence Duration Ms + max_wait_ms: + anyOf: + - type: integer + minimum: 500 + maximum: 10000 + - type: integer + const: -1 + - type: 'null' + default: 3000 + title: Max Wait Ms + description: 500-10000 ms, or -1 to wait without a timeout. + pause_state_enabled: + anyOf: + - type: boolean + - type: 'null' + default: true + title: Pause State Enabled + required: + - type + title: SemanticTurnEnd + type: object + AgentRequest_SemanticTurnStart: + additionalProperties: false + properties: + type: + const: semantic + title: Type + type: string + default: semantic + speaking_interrupt_duration_ms: + anyOf: + - type: integer + - type: 'null' + default: 160 + title: Speaking Interrupt Duration Ms + required: + - type + title: SemanticTurnStart + type: object + AgentRequest_SilenceConfig: + additionalProperties: false + properties: + enabled: + title: Enabled + type: boolean + timeout_ms: + title: Timeout Ms + type: integer + action: + enum: + - speak + - think + title: Action + type: string + content: + title: Content + type: string + required: + - enabled + - timeout_ms + - action + - content + title: SilenceConfig + type: object + AgentRequest_SilenceConfigPatch: + additionalProperties: false + properties: + enabled: + anyOf: + - type: boolean + - type: 'null' + default: + title: Enabled + timeout_ms: + anyOf: + - type: integer + - type: 'null' + default: + title: Timeout Ms + action: + anyOf: + - enum: + - speak + - think + type: string + - type: 'null' + default: + title: Action + content: + anyOf: + - type: string + - type: 'null' + default: + title: Content + title: SilenceConfigPatch + type: object + AgentRequest_SkipPattern: + enum: + - fullwidth_parentheses + - lenticular_brackets + - parentheses + - square_brackets + - curly_braces + type: string + AgentRequest_SpeechInterruption: + additionalProperties: false + properties: + mode: + const: start_of_speech + title: Mode + type: string + required: + - mode + title: SpeechInterruption + type: object + AgentRequest_StaticFillerContent: + additionalProperties: false + properties: + mode: + const: static + title: Mode + type: string + phrases: + items: + type: string + minLength: 1 + title: Phrases + type: array + minItems: 1 + maxItems: 100 + selection_rule: + default: shuffle + enum: + - shuffle + - round_robin + title: Selection Rule + type: string + required: + - mode + - phrases + title: StaticFillerContent + type: object + AgentRequest_TextGreeting: + additionalProperties: false + properties: + mode: + const: text + title: Mode + type: string + 'on': + default: each_join + enum: + - each_join + - first_join + title: 'On' + type: string + delay_ms: + default: 0 + title: Delay Ms + type: integer + minimum: 0 + maximum: 5000 + text: + title: Text + type: string + required: + - mode + - text + title: TextGreeting + type: object + AgentRequest_ToolExecution: + additionalProperties: false + properties: + mode: + const: sync + title: Mode + type: string + required: + - mode + title: ToolExecution + type: object + AgentRequest_ToolServer: + additionalProperties: false + properties: + method: + enum: + - GET + - POST + title: Method + type: string + url: + maxLength: 2048 + minLength: 1 + pattern: "^https://" + title: Url + type: string + headers: + anyOf: + - maxProperties: 32 + patternProperties: + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": + type: string + propertyNames: + maxLength: 128 + minLength: 1 + type: object + - type: 'null' + default: + title: Headers + body: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + timeout_ms: + anyOf: + - maximum: 100000 + minimum: 1000 + type: integer + - type: 'null' + default: 10000 + title: Timeout Ms + required: + - method + - url + title: ToolServer + type: object + AgentRequest_TtsInput: + additionalProperties: false + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + - if: + allOf: + - properties: + vendor: + enum: + - amazon + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + aws_access_key_id: + minLength: 1 + type: string + aws_secret_access_key: + minLength: 1 + type: string + required: + - aws_access_key_id + - aws_secret_access_key + type: object + required: + - fields + - if: + allOf: + - anyOf: + - properties: + vendor: + enum: + - cartesia + - deepgram + - fishaudio + - gemini + - generic_http + - gradium + - mistral + - murf + - openai + - rime + - smallestai + - typecast + - xai + required: + - vendor + - not: + required: + - vendor + - properties: + vendor: + not: + enum: + - amazon + - cartesia + - deepgram + - elevenlabs + - fishaudio + - gemini + - generic_http + - google + - gradium + - humeai + - microsoft + - minimax + - mistral + - murf + - openai + - rime + - sarvam + - smallestai + - typecast + - xai + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + api_key: + minLength: 1 + type: string + required: + - api_key + type: object + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - elevenlabs + - humeai + - microsoft + - minimax + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + key: + minLength: 1 + type: string + required: + - key + type: object + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - google + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + credentials: + minLength: 1 + type: string + required: + - credentials + type: object + required: + - fields + - if: + allOf: + - properties: + vendor: + enum: + - sarvam + required: + - vendor + - properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + then: + properties: + credential: + anyOf: + - properties: + fields: + additionalProperties: false + properties: + api_subscription_key: + minLength: 1 + type: string + required: + - api_subscription_key + type: object + required: + - fields + properties: + skip_patterns: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_SkipPattern" + type: array + - type: 'null' + default: + title: Skip Patterns + uniqueItems: true + credential: + anyOf: + - discriminator: + mapping: + byok: "#/components/schemas/AgentRequest_PublicByokCredential" + managed: "#/components/schemas/AgentRequest_ManagedCredential" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + - "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + - type: 'null' + default: + title: Credential + vendor: + anyOf: + - type: string + - type: 'null' + default: + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + url: + anyOf: + - format: uri + type: string + - type: 'null' + default: + title: Url + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + default: + title: Headers + title: TtsInput + type: object + AgentRequest_TtsPatch: + additionalProperties: false + allOf: + - if: + properties: + credential: + properties: + mode: + const: managed + required: + - mode + type: object + required: + - credential + then: + properties: + url: + type: 'null' + properties: + skip_patterns: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_SkipPattern" + type: array + - type: 'null' + default: + title: Skip Patterns + uniqueItems: true + credential: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + - type: 'null' + default: + vendor: + anyOf: + - type: string + - type: 'null' + default: + title: Vendor + params: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_JSONObject" + - type: 'null' + default: + url: + anyOf: + - format: uri + type: string + - type: 'null' + default: + description: Non-null URLs require effective BYOK mode; null clears a saved + override. + title: Url + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + default: + title: Headers + title: TtsPatch + type: object + AgentRequest_TurnBoundaryPatch: + additionalProperties: false + properties: + type: + anyOf: + - enum: + - semantic + - vad + - manual + type: string + - type: 'null' + default: + title: Type + interrupt_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 1200 + - type: 'null' + default: + title: Interrupt Duration Ms + speaking_interrupt_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 1200 + - type: 'null' + default: + title: Speaking Interrupt Duration Ms + prefix_padding_ms: + anyOf: + - type: integer + minimum: 0 + maximum: 5000 + - type: 'null' + default: + title: Prefix Padding Ms + silence_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 2000 + - type: 'null' + default: + title: Silence Duration Ms + max_wait_ms: + anyOf: + - type: integer + minimum: 500 + maximum: 10000 + - type: integer + const: -1 + - type: 'null' + default: + title: Max Wait Ms + description: 500-10000 ms, or -1 to wait without a timeout. + pause_state_enabled: + anyOf: + - type: boolean + - type: 'null' + default: + title: Pause State Enabled + title: TurnBoundaryPatch + type: object + AgentRequest_TurnDetection: + additionalProperties: false + properties: + speech_threshold: + anyOf: + - type: number + minimum: 0 + maximum: 1 + - type: integer + minimum: 0 + maximum: 1 + - type: 'null' + default: 0.5 + title: Speech Threshold + start_of_speech: + anyOf: + - discriminator: + mapping: + manual: "#/components/schemas/AgentRequest_ManualTurnBoundary" + semantic: "#/components/schemas/AgentRequest_SemanticTurnStart" + vad: "#/components/schemas/AgentRequest_VadTurnStart" + propertyName: type + oneOf: + - "$ref": "#/components/schemas/AgentRequest_SemanticTurnStart" + - "$ref": "#/components/schemas/AgentRequest_VadTurnStart" + - "$ref": "#/components/schemas/AgentRequest_ManualTurnBoundary" + - type: 'null' + default: + title: Start Of Speech + end_of_speech: + anyOf: + - discriminator: + mapping: + manual: "#/components/schemas/AgentRequest_ManualTurnBoundary" + semantic: "#/components/schemas/AgentRequest_SemanticTurnEnd" + vad: "#/components/schemas/AgentRequest_VadTurnEnd" + propertyName: type + oneOf: + - "$ref": "#/components/schemas/AgentRequest_SemanticTurnEnd" + - "$ref": "#/components/schemas/AgentRequest_VadTurnEnd" + - "$ref": "#/components/schemas/AgentRequest_ManualTurnBoundary" + - type: 'null' + default: + title: End Of Speech + interruption: + anyOf: + - discriminator: + mapping: + keywords: "#/components/schemas/AgentRequest_KeywordInterruption" + 'off': "#/components/schemas/AgentRequest_DisabledInterruption" + start_of_speech: "#/components/schemas/AgentRequest_SpeechInterruption" + propertyName: mode + oneOf: + - "$ref": "#/components/schemas/AgentRequest_SpeechInterruption" + - "$ref": "#/components/schemas/AgentRequest_KeywordInterruption" + - "$ref": "#/components/schemas/AgentRequest_DisabledInterruption" + - type: 'null' + default: + mode: start_of_speech + title: Interruption + title: TurnDetection + type: object + AgentRequest_VadTurnEnd: + additionalProperties: false + properties: + type: + const: vad + title: Type + type: string + silence_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 2000 + - type: 'null' + default: 640 + title: Silence Duration Ms + required: + - type + title: VadTurnEnd + type: object + AgentRequest_VadTurnStart: + additionalProperties: false + properties: + type: + const: vad + title: Type + type: string + interrupt_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 1200 + - type: 'null' + default: 160 + title: Interrupt Duration Ms + speaking_interrupt_duration_ms: + anyOf: + - type: integer + minimum: 120 + maximum: 1200 + - type: 'null' + default: 160 + title: Speaking Interrupt Duration Ms + prefix_padding_ms: + anyOf: + - type: integer + minimum: 0 + maximum: 5000 + - type: 'null' + default: 800 + title: Prefix Padding Ms + required: + - type + title: VadTurnStart + type: object + AgentRequest_VariableDefinition: + additionalProperties: false + properties: + type: + enum: + - str + - int + - float + - bool + - date + - datetime + title: Type + type: string + required: + default: false + title: Required + type: boolean + options: + anyOf: + - items: + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + maxItems: 100 + minItems: 1 + type: array + - type: 'null' + default: + title: Options + extract: + default: false + title: Extract + type: boolean + description: + anyOf: + - maxLength: 2000 + type: string + - type: 'null' + default: + title: Description + required: + - type + title: VariableDefinition + type: object + AgentRequest_VariableDefinitionPatch: + additionalProperties: false + properties: + type: + anyOf: + - enum: + - str + - int + - float + - bool + - date + - datetime + type: string + - type: 'null' + default: + title: Type + required: + anyOf: + - type: boolean + - type: 'null' + default: + title: Required + options: + anyOf: + - items: + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + maxItems: 100 + minItems: 1 + type: array + - type: 'null' + default: + title: Options + extract: + anyOf: + - type: boolean + - type: 'null' + default: + title: Extract + description: + anyOf: + - maxLength: 2000 + type: string + - type: 'null' + default: + title: Description + title: VariableDefinitionPatch + type: object + AgentCreateInput: + "$ref": "#/components/schemas/AgentRequest_AgentCreateInput" + AgentPatch: + "$ref": "#/components/schemas/AgentRequest_AgentPatch" + SessionEndCallPolicy: + properties: + on_conversation_complete: + type: boolean + title: On Conversation Complete + default: false + on_user_request: + type: boolean + title: On User Request + default: false + on_fax: + type: boolean + title: On Fax + default: false + on_ai_assistant: + type: boolean + title: On Ai Assistant + default: false + on_voicemail: + type: boolean + title: On Voicemail + default: false + additionalProperties: false + type: object + title: EndCallPolicy + InboundEndCallPolicy: + properties: + on_conversation_complete: + type: boolean + title: On Conversation Complete + default: false + on_user_request: + type: boolean + title: On User Request + default: false + on_voicemail: + type: boolean + title: On Voicemail + default: false + on_ai_assistant: + type: boolean + title: On Ai Assistant + default: false + on_fax: + type: boolean + title: On Fax + default: false + additionalProperties: false + type: object + title: EndCallPolicy + SessionAnalysis: + properties: + status: + type: string + enum: + - pending + - completed + - failed + - skipped + title: Status + skip_reason: + anyOf: + - type: string + enum: + - no_user_turns + - retention_none + - type: 'null' + title: Skip Reason + complete_ts: + anyOf: + - type: integer + minimum: 0.0 + - type: 'null' + title: Complete Ts + result: + anyOf: + - patternProperties: + "^[a-z][a-z0-9_]{0,63}$": + anyOf: + - type: string + - type: integer + - type: number + - type: boolean + - type: 'null' + type: object + - type: 'null' + title: Result + error: + anyOf: + - type: string + - type: 'null' + title: Error + additionalProperties: false + type: object + required: + - status + - skip_reason + - complete_ts + - result + - error + title: SessionAnalysis + securitySchemes: + BasicAuth: + type: http + scheme: basic + description: 'HTTP Basic authentication using an Agora Customer ID and Customer + Secret. Send Authorization: Basic . + This is an alternative to AgoraTokenAuth; supply one authentication method.' + AgoraTokenAuth: + type: apiKey + in: header + name: Authorization + description: 'An Agora AccessToken2 token. Platform session routes require the + ConvoAI service (service type 9); an RTC + RTM token alone is insufficient. + Use a dedicated ConvoAI token builder. Send the complete header value: agora + token=. SDKs accept a supplied token or generate one from App ID and + App Certificate. HTTP Basic authentication is also supported as an alternative.' + parameters: + IdempotencyKey: + name: Idempotency-Key + in: header + required: false + schema: + type: string + minLength: 1 + maxLength: 256 + pattern: "^[\\x21-\\x7E]+$" + description: 'Optional idempotency key. The server retains each key for 24 hours. + Repeating the same request with the same key and body returns the first response + without performing the operation again, with Idempotent-Replayed: true. Reusing + a key with a different body, or while the original request is still in progress, + returns 409 IdempotencyKeyConflict. If validation rejects a request before + the operation starts, correct the request and reuse the same key. If the operation + was accepted or its outcome is unknown, keep the same key; using a new key + can start a second operation. This header is supported only for session starts + and campaign creation/pause/resume/cancel, not agent, secret, or number creation.' + example: 6f1c2b0e-support-4821 + headers: + IdempotentReplayed: + description: Present with value true when this response replays the original + response for the same idempotency key and body within the 24-hour retention + period. + schema: + type: boolean + const: true + example: true +servers: +- url: https://preview.ai.agora.io + description: Agora Conversational AI API +security: +- AgoraTokenAuth: [] +- BasicAuth: [] diff --git a/pyproject.toml b/pyproject.toml index 760701d..f85d0fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,17 @@ dynamic = ["version"] [tool.poetry] name = "agora-agents" -version = "v2.13.0" +version = "3.0.0a3" description = "" readme = "README.md" authors = [] keywords = [] +include = [ + { path = "src/agora_agent/v3/openapi.json", format = ["wheel", "sdist"] }, + { path = "openapi.json", format = "sdist" }, + { path = "openapi.yaml", format = "sdist" }, + { path = "scripts/generate-v3-preview.py", format = "sdist" }, +] classifiers = [ "Intended Audience :: Developers", diff --git a/reference.md b/reference.md index 55a516e..241f6af 100644 --- a/reference.md +++ b/reference.md @@ -1,4 +1,6 @@ # Reference + +For the additive stored-agent preview, see the [v3 API reference](docs/reference/v3.md) and [v3 workflows](docs/guides/v3-api-preview.md). The v3 adapter covers 31 documented operations alongside the existing v2 resources documented below. ## Agents
client.agents.start(...)
@@ -27,7 +29,7 @@ Create and start a Conversational AI agent instance.
```python -from agora_agent import Agora, MicrosoftTtsParams, Tts_Microsoft +from agora_agent import Area, Agora, MicrosoftTtsParams, Tts_Microsoft from agora_agent.agents import ( StartAgentsRequestProperties, StartAgentsRequestPropertiesAsr, @@ -38,9 +40,10 @@ from agora_agent.agents import ( ) client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.start( appid="appid", @@ -182,12 +185,13 @@ Retrieve a list of agents that meet the specified conditions.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) response = client.agents.list( appid="appid", @@ -315,12 +319,13 @@ Get the current state information of the specified agent instance.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.get( appid="appid", @@ -398,12 +403,13 @@ Call this endpoint while the agent is running to retrieve the conversation histo
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.get_history( appid="appid", @@ -483,12 +489,13 @@ You can query sessions within the last 7 days.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.get_turns( appid="appid", @@ -580,12 +587,13 @@ Stop the specified conversational agent instance. The API responds after request
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.stop( appid="appid", @@ -661,16 +669,17 @@ Adjust Conversation AI Engine parameters at runtime.
```python -from agora_agent import Agora +from agora_agent import Area, Agora from agora_agent.agents import ( UpdateAgentsRequestProperties, UpdateAgentsRequestPropertiesLlm, ) client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.update( appid="appid", @@ -774,12 +783,13 @@ Note: The speak API is not supported when using `mllm` configuration.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.speak( appid="appid", @@ -891,12 +901,13 @@ Interrupt the specified agent while speaking or thinking.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agents.interrupt( appid="appid", @@ -980,12 +991,13 @@ Use this endpoint for the following scenarios:
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.agent_management.agent_think( appid="appid", @@ -1133,12 +1145,13 @@ Query historical call records for a specified appid based on the filter criteria
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) response = client.telephony.list( appid="appid", @@ -1266,16 +1279,17 @@ Use this endpoint to initiate an outbound call to the specified number and creat
```python -from agora_agent import Agora +from agora_agent import Area, Agora from agora_agent.telephony import ( CallTelephonyRequestProperties, CallTelephonyRequestSip, ) client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.telephony.call( appid="appid", @@ -1391,12 +1405,13 @@ Retrieve the call status and related information of a specified agent.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.telephony.get( appid="appid", @@ -1472,12 +1487,13 @@ Instruct the agent to proactively hang up the ongoing call and leave the RTC cha
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.telephony.hangup( appid="appid", @@ -1554,12 +1570,13 @@ Retrieve a list of all imported phone numbers under the current account.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.phone_numbers.list() @@ -1616,16 +1633,17 @@ Import a pre-configured phone number that can be used for inbound or outbound ca
```python -from agora_agent import Agora +from agora_agent import Area, Agora from agora_agent.phone_numbers import ( AddPhoneNumbersRequestInboundConfig, AddPhoneNumbersRequestOutboundConfig, ) client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.phone_numbers.add( provider="byo", @@ -1755,12 +1773,13 @@ Retrieve detailed information for a specific phone number.
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.phone_numbers.get( phone_number="phone_number", @@ -1830,12 +1849,13 @@ After calling this endpoint, the number stops receiving calls routed through thi
```python -from agora_agent import Agora +from agora_agent import Area, Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.phone_numbers.delete( phone_number="phone_number", @@ -1902,16 +1922,17 @@ Update the configuration for a phone number.
```python -from agora_agent import Agora +from agora_agent import Area, Agora from agora_agent.phone_numbers import ( UpdatePhoneNumbersRequestInboundConfig, UpdatePhoneNumbersRequestOutboundConfig, ) client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + area=Area.US, + app_id="YOUR_APP_ID", + app_certificate="YOUR_APP_CERTIFICATE", + auth_token="YOUR_RAW_TOKEN", ) client.phone_numbers.update( phone_number="phone_number", diff --git a/scripts/check_release_workflow.py b/scripts/check_release_workflow.py index a7229a2..159e410 100644 --- a/scripts/check_release_workflow.py +++ b/scripts/check_release_workflow.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 +import argparse import re import sys from pathlib import Path from typing import NoReturn +from packaging.version import InvalidVersion, Version + def fail(message: str) -> NoReturn: print(message, file=sys.stderr) @@ -27,15 +30,25 @@ def read_compat_dependency(path: str) -> str: return match.group(1) +parser = argparse.ArgumentParser() +parser.add_argument("--tag", help="Git release tag, such as v3.0.0-alpha.3") +args = parser.parse_args() + root_version = read_version("pyproject.toml") compat_pyproject = "compat/agora-agent-server-sdk/pyproject.toml" compat_version = read_version(compat_pyproject) compat_dependency = read_compat_dependency(compat_pyproject) +try: + if args.tag and str(Version(args.tag)) != root_version: + fail(f"Release tag ({args.tag}) does not match package version ({root_version}).") +except InvalidVersion as error: + fail(str(error)) + if compat_version != root_version: fail(f"Compat package version ({compat_version}) must match root package version ({root_version}).") -expected_dependency = f">={root_version},<3.0.0" +expected_dependency = f">={root_version},<{Version(root_version).major + 1}.0.0" if compat_dependency != expected_dependency: fail(f"Compat package dependency on agora-agents ({compat_dependency}) must be {expected_dependency}.") diff --git a/scripts/generate-v3-preview.py b/scripts/generate-v3-preview.py new file mode 100644 index 0000000..42d54cb --- /dev/null +++ b/scripts/generate-v3-preview.py @@ -0,0 +1,179 @@ +"""Generate Python wire TypedDicts/resources from root openapi.json (not Fern). + +Transport, validators and AgentKit adapters are handwritten. The packaged JSON is +an identical runtime copy of the input; root openapi.yaml is the shared handoff. +""" +import json +import keyword +import re +from pathlib import Path +from typing import Dict, List + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "src/agora_agent/v3" +spec = json.loads((ROOT / "openapi.json").read_text()) +schemas = spec["components"]["schemas"] + +def ref(node): + while "$ref" in node: + obj = spec + for part in node["$ref"][2:].split("/"): + obj = obj[part.replace("~1", "/").replace("~0", "~")] + node = obj + return node + +def typ(node): + if "$ref" in node: + return node["$ref"].split("/")[-1] + if "const" in node: + return f"Literal[{node['const']!r}]" + if "enum" in node: + return "Literal[" + ", ".join(repr(x) for x in node["enum"]) + "]" + for key in ("anyOf", "oneOf"): + if key in node: + return "Union[" + ", ".join(typ(x) for x in node[key]) + "]" + kind = node.get("type") + if kind == "array": + return "List[" + typ(node.get("items", {})) + "]" + if kind == "object": + value = node.get("additionalProperties") + if not isinstance(value, dict): + value = next(iter(node.get("patternProperties", {}).values()), {}) + return "Dict[str, " + typ(value) + "]" + return {"string": "str", "integer": "int", "number": "float", "boolean": "bool", "null": "None"}.get(kind, "Any") + +lines = ['"""Generated wire types from root openapi.json; constraints live in the shared contract."""', + 'from __future__ import annotations', 'from typing import Any, Dict, List, Union', + 'from typing_extensions import Literal, Required, TypedDict', ''] +aliases = [] +for name, node in schemas.items(): + if "$ref" in node: + aliases.append(f"{name} = {typ(node)}") + continue + if "properties" not in node: + aliases.append(f"{name} = {typ(node)}") + continue + props = node["properties"] + fields = [(key, ("Required[" + typ(value) + "]") if key in node.get("required", []) else typ(value)) for key, value in props.items()] + if any(keyword.iskeyword(key) or not key.isidentifier() for key, _ in fields): + # Only telephony transports contain reserved Python keys. Quoted refs + # retain forward annotations while preserving required scalar fields. + parts = [] + for key, value in fields: + expr = value + if value.startswith("Required["): + expr = "Required[" + repr(value[9:-1]) + "]" + else: + expr = repr(value) + parts.append(repr(key) + ": " + expr) + lines.append(f"{name} = TypedDict({name!r}, {{{', '.join(parts)}}}, total=False)") + else: + lines += [f"class {name}(TypedDict, total=False):"] + ([f" {key}: {value}" for key, value in fields] or [" pass"]) + lines.append("") +lines += [re.sub(r"(? {ret}:"] + if raw: + expression = lambda value: "{" + ", ".join(repr(k) + ": " + v for k, v in value.items()) + "}" + body = "{" + ", ".join(repr(k) + ": " + k for k in request_fields) + "}" if request_fields else "None" + text += [f" return {'await ' if asynchronous else ''}self._request({verb!r}, f{path_expr!r}, body={body}, headers={expression(headers)}, params={expression(query)}, request_options=request_options)"] + else: + call = f"self.with_raw_response.{method}(" + ", ".join(k + "=" + k for k in forwarding + ['appid', 'request_options']) + ")" + text += [f" return {'(await ' + call + ').data' if asynchronous else call + '.data'}"] + text += [""] + text += [f"class {prefix}V3Client:", ' def __init__(self, owner, options):'] + for resource in classes: + text += [f" self.{resource} = {prefix}{resource.title()}Client(owner, options)"] + text += [""] +(OUT / "client.py").write_text("\n".join(text)) +(OUT / "campaigns.py").write_text('"""Compatibility re-exports of generated campaign resources."""\nfrom .client import CampaignsClient, AsyncCampaignsClient\n__all__ = ["CampaignsClient", "AsyncCampaignsClient"]\n') +(OUT / "openapi.json").write_bytes((ROOT / "openapi.json").read_bytes()) diff --git a/src/agora_agent/__init__.py b/src/agora_agent/__init__.py index 1a29ed4..448046d 100644 --- a/src/agora_agent/__init__.py +++ b/src/agora_agent/__init__.py @@ -157,3 +157,8 @@ def __getattr__(attr_name: str) -> typing.Any: def __dir__() -> typing.List[str]: return list(__all__) + +from .agentkit.v3 import (SavedAgent, AsyncSavedAgent, DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) +from .v3 import V3Options, PreconditionFailedError + +__all__ = sorted(set(__all__) | {"SavedAgent", "AsyncSavedAgent", "DeployedAgent", "AsyncDeployedAgent", "SecretRef", "secret_ref", "UnsupportedFieldError", "NumberNotFoundError", "SecretRefNotSupportedError"} | {"V3Options", "PreconditionFailedError"}) diff --git a/src/agora_agent/agentkit/__init__.py b/src/agora_agent/agentkit/__init__.py index 0e658e8..5af031e 100644 --- a/src/agora_agent/agentkit/__init__.py +++ b/src/agora_agent/agentkit/__init__.py @@ -502,3 +502,7 @@ "validate_avatar_config", "validate_tts_sample_rate", ] + +from .v3 import (SavedAgent, AsyncSavedAgent, DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) + +__all__ = sorted(set(__all__) | {"SavedAgent", "AsyncSavedAgent", "DeployedAgent", "AsyncDeployedAgent", "SecretRef", "secret_ref", "UnsupportedFieldError", "NumberNotFoundError", "SecretRefNotSupportedError"}) diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index 34be68c..52ad6a6 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -2,92 +2,150 @@ import time import typing -import typing_extensions import warnings +import typing_extensions + if typing.TYPE_CHECKING: - from .agent_session import AgentSession, AsyncAgentSession from ..core.domain import Area from ..pool_client import Agora, AsyncAgora + from .agent_session import AgentSession, AsyncAgentSession + from .v3 import AsyncDeployedAgent, AsyncSavedAgent, DeployedAgent, SavedAgent -from ..agents.types.start_agents_request_properties import StartAgentsRequestProperties -from ..agents.types.start_agents_request_properties_avatar import StartAgentsRequestPropertiesAvatar -from ..agents.types.start_agents_request_properties_avatar_vendor import StartAgentsRequestPropertiesAvatarVendor -from ..agents.types.update_agents_request_properties import UpdateAgentsRequestProperties +from ..agent_management.types.agent_think_agent_management_request_on_listening_action import ( + AgentThinkAgentManagementRequestOnListeningAction, +) +from ..agent_management.types.agent_think_agent_management_request_on_speaking_action import ( + AgentThinkAgentManagementRequestOnSpeakingAction, +) +from ..agent_management.types.agent_think_agent_management_request_on_thinking_action import ( + AgentThinkAgentManagementRequestOnThinkingAction, +) +from ..agent_management.types.agent_think_agent_management_response import ( + AgentThinkAgentManagementResponse, +) from ..agents.types.get_agents_response import GetAgentsResponse -from ..agents.types.list_agents_response import ListAgentsResponse -from ..agents.types.list_agents_response_data_list_item import ListAgentsResponseDataListItem -from ..agents.types.list_agents_response_data_list_item_status import ListAgentsResponseDataListItemStatus from ..agents.types.get_history_agents_response import GetHistoryAgentsResponse from ..agents.types.get_history_agents_response_contents_item import GetHistoryAgentsResponseContentsItem from ..agents.types.get_history_agents_response_contents_item_role import GetHistoryAgentsResponseContentsItemRole from ..agents.types.get_turns_agents_response import GetTurnsAgentsResponse from ..agents.types.get_turns_agents_response_turns_item import GetTurnsAgentsResponseTurnsItem +from ..agents.types.list_agents_response import ListAgentsResponse +from ..agents.types.list_agents_response_data_list_item import ListAgentsResponseDataListItem +from ..agents.types.list_agents_response_data_list_item_status import ListAgentsResponseDataListItemStatus from ..agents.types.speak_agents_request_priority import SpeakAgentsRequestPriority -from ..agents.types.start_agents_request_properties_turn_detection import StartAgentsRequestPropertiesTurnDetection -from ..agents.types.start_agents_request_properties_turn_detection_config import StartAgentsRequestPropertiesTurnDetectionConfig -from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeech -from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_mode import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechMode -from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_vad_config import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechVadConfig -from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_keywords_config import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechKeywordsConfig -from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_disabled_config import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechDisabledConfig -from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_disabled_config_strategy import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechDisabledConfigStrategy -from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech import StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeech -from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech_mode import StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeechMode -from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech_vad_config import StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeechVadConfig -from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech_semantic_config import StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeechSemanticConfig -from ..agents.types.start_agents_request_properties_turn_detection_type import StartAgentsRequestPropertiesTurnDetectionType -from ..agents.types.start_agents_request_properties_turn_detection_interrupt_mode import StartAgentsRequestPropertiesTurnDetectionInterruptMode -from ..agents.types.start_agents_request_properties_turn_detection_eagerness import StartAgentsRequestPropertiesTurnDetectionEagerness +from ..agents.types.start_agents_request_properties import StartAgentsRequestProperties +from ..agents.types.start_agents_request_properties_advanced_features import ( + StartAgentsRequestPropertiesAdvancedFeatures, +) +from ..agents.types.start_agents_request_properties_avatar import StartAgentsRequestPropertiesAvatar +from ..agents.types.start_agents_request_properties_avatar_vendor import StartAgentsRequestPropertiesAvatarVendor +from ..agents.types.start_agents_request_properties_filler_words import StartAgentsRequestPropertiesFillerWords +from ..agents.types.start_agents_request_properties_filler_words_content import ( + StartAgentsRequestPropertiesFillerWordsContent, +) +from ..agents.types.start_agents_request_properties_filler_words_content_generated_config import ( + StartAgentsRequestPropertiesFillerWordsContentGeneratedConfig, +) +from ..agents.types.start_agents_request_properties_filler_words_content_generated_config_llm_provider import ( + StartAgentsRequestPropertiesFillerWordsContentGeneratedConfigLlmProvider, +) +from ..agents.types.start_agents_request_properties_filler_words_content_static_config import ( + StartAgentsRequestPropertiesFillerWordsContentStaticConfig, +) +from ..agents.types.start_agents_request_properties_filler_words_content_static_config_selection_rule import ( + StartAgentsRequestPropertiesFillerWordsContentStaticConfigSelectionRule, +) +from ..agents.types.start_agents_request_properties_filler_words_trigger import ( + StartAgentsRequestPropertiesFillerWordsTrigger, +) +from ..agents.types.start_agents_request_properties_filler_words_trigger_fixed_time_config import ( + StartAgentsRequestPropertiesFillerWordsTriggerFixedTimeConfig, +) +from ..agents.types.start_agents_request_properties_geofence import StartAgentsRequestPropertiesGeofence +from ..agents.types.start_agents_request_properties_interruption import StartAgentsRequestPropertiesInterruption +from ..agents.types.start_agents_request_properties_interruption_mode import ( + StartAgentsRequestPropertiesInterruptionMode, +) +from ..agents.types.start_agents_request_properties_parameters import StartAgentsRequestPropertiesParameters +from ..agents.types.start_agents_request_properties_parameters_audio_scenario import ( + StartAgentsRequestPropertiesParametersAudioScenario, +) +from ..agents.types.start_agents_request_properties_parameters_data_channel import ( + StartAgentsRequestPropertiesParametersDataChannel, +) +from ..agents.types.start_agents_request_properties_parameters_farewell_config import ( + StartAgentsRequestPropertiesParametersFarewellConfig, +) +from ..agents.types.start_agents_request_properties_parameters_silence_config import ( + StartAgentsRequestPropertiesParametersSilenceConfig, +) +from ..agents.types.start_agents_request_properties_parameters_silence_config_action import ( + StartAgentsRequestPropertiesParametersSilenceConfigAction, +) +from ..agents.types.start_agents_request_properties_rtc import StartAgentsRequestPropertiesRtc from ..agents.types.start_agents_request_properties_sal import StartAgentsRequestPropertiesSal from ..agents.types.start_agents_request_properties_sal_sal_mode import StartAgentsRequestPropertiesSalSalMode -from ..agents.types.start_agents_request_properties_parameters import StartAgentsRequestPropertiesParameters -from ..agents.types.start_agents_request_properties_parameters_silence_config import StartAgentsRequestPropertiesParametersSilenceConfig -from ..agents.types.start_agents_request_properties_parameters_silence_config_action import StartAgentsRequestPropertiesParametersSilenceConfigAction -from ..agents.types.start_agents_request_properties_parameters_farewell_config import StartAgentsRequestPropertiesParametersFarewellConfig -from ..agents.types.start_agents_request_properties_parameters_data_channel import StartAgentsRequestPropertiesParametersDataChannel -from ..agents.types.start_agents_request_properties_parameters_audio_scenario import StartAgentsRequestPropertiesParametersAudioScenario +from ..agents.types.start_agents_request_properties_turn_detection import StartAgentsRequestPropertiesTurnDetection +from ..agents.types.start_agents_request_properties_turn_detection_config import ( + StartAgentsRequestPropertiesTurnDetectionConfig, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech import ( + StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeech, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech_mode import ( + StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeechMode, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech_semantic_config import ( + StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeechSemanticConfig, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_end_of_speech_vad_config import ( + StartAgentsRequestPropertiesTurnDetectionConfigEndOfSpeechVadConfig, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech import ( + StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeech, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_disabled_config import ( + StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechDisabledConfig, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_disabled_config_strategy import ( + StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechDisabledConfigStrategy, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_keywords_config import ( + StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechKeywordsConfig, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_mode import ( + StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechMode, +) +from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech_vad_config import ( + StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeechVadConfig, +) +from ..agents.types.start_agents_request_properties_turn_detection_eagerness import ( + StartAgentsRequestPropertiesTurnDetectionEagerness, +) +from ..agents.types.start_agents_request_properties_turn_detection_interrupt_mode import ( + StartAgentsRequestPropertiesTurnDetectionInterruptMode, +) +from ..agents.types.start_agents_request_properties_turn_detection_type import ( + StartAgentsRequestPropertiesTurnDetectionType, +) +from ..agents.types.update_agents_request_properties import UpdateAgentsRequestProperties +from ..core.pydantic_utilities import parse_obj_as from ..agents.types.start_agents_request_properties_parameters_speak import StartAgentsRequestPropertiesParametersSpeak -from ..agents.types.start_agents_request_properties_interruption import StartAgentsRequestPropertiesInterruption -from ..agents.types.start_agents_request_properties_interruption_mode import StartAgentsRequestPropertiesInterruptionMode -from ..agents.types.start_agents_request_properties_geofence import StartAgentsRequestPropertiesGeofence -from ..agents.types.start_agents_request_properties_rtc import StartAgentsRequestPropertiesRtc -from ..agents.types.start_agents_request_properties_advanced_features import StartAgentsRequestPropertiesAdvancedFeatures -from ..agents.types.start_agents_request_properties_filler_words import StartAgentsRequestPropertiesFillerWords -from ..agents.types.start_agents_request_properties_filler_words_trigger import StartAgentsRequestPropertiesFillerWordsTrigger -from ..agents.types.start_agents_request_properties_filler_words_trigger_fixed_time_config import StartAgentsRequestPropertiesFillerWordsTriggerFixedTimeConfig -from ..agents.types.start_agents_request_properties_filler_words_content import StartAgentsRequestPropertiesFillerWordsContent -from ..agents.types.start_agents_request_properties_filler_words_content_static_config import StartAgentsRequestPropertiesFillerWordsContentStaticConfig -from ..agents.types.start_agents_request_properties_filler_words_content_static_config_selection_rule import StartAgentsRequestPropertiesFillerWordsContentStaticConfigSelectionRule -from ..agents.types.start_agents_request_properties_filler_words_content_generated_config import StartAgentsRequestPropertiesFillerWordsContentGeneratedConfig -from ..agents.types.start_agents_request_properties_filler_words_content_generated_config_llm_provider import StartAgentsRequestPropertiesFillerWordsContentGeneratedConfigLlmProvider -from ..types.tts import Tts from ..types.asr import Asr from ..types.llm import Llm from ..types.llm_style import LlmStyle as GeneratedLlmStyle -from ..types.mllm import Mllm -from ..types.mllm_turn_detection import MllmTurnDetection -from ..types.mllm_turn_detection_mode import MllmTurnDetectionMode as GeneratedMllmTurnDetectionMode -from ..types.mllm_vendor import MllmVendor as GeneratedMllmVendor from ..types.llm_tool import LlmTool from ..types.llm_tool_execution import LlmToolExecution from ..types.llm_tool_function import LlmToolFunction from ..types.llm_tool_function_parameters import LlmToolFunctionParameters from ..types.llm_tool_server import LlmToolServer from ..types.mcp_server import McpServer -from ..agent_management.types.agent_think_agent_management_request_on_listening_action import ( - AgentThinkAgentManagementRequestOnListeningAction, -) -from ..agent_management.types.agent_think_agent_management_request_on_thinking_action import ( - AgentThinkAgentManagementRequestOnThinkingAction, -) -from ..agent_management.types.agent_think_agent_management_request_on_speaking_action import ( - AgentThinkAgentManagementRequestOnSpeakingAction, -) -from ..agent_management.types.agent_think_agent_management_response import ( - AgentThinkAgentManagementResponse, -) -from ..core.pydantic_utilities import parse_obj_as +from ..types.mllm import Mllm +from ..types.mllm_turn_detection import MllmTurnDetection +from ..types.mllm_turn_detection_mode import MllmTurnDetectionMode as GeneratedMllmTurnDetectionMode +from ..types.mllm_vendor import MllmVendor as GeneratedMllmVendor +from ..types.tts import Tts from .vendors.base import BaseAvatar, BaseLLM, BaseMLLM, BaseSTT, BaseTTS # Top-level aliases @@ -261,7 +319,7 @@ def _start_properties_from_mapping( ThinkOnSpeakingAction = AgentThinkAgentManagementRequestOnSpeakingAction ThinkResponse = AgentThinkAgentManagementResponse -from .token import generate_convo_ai_token, _parse_numeric_uid, _validate_expires_in +from .token import _parse_numeric_uid, _validate_expires_in, generate_convo_ai_token TurnDetectionLanguage = typing_extensions.Literal[ "ar-EG", @@ -395,6 +453,8 @@ class Agent: _GlobalArea = typing_extensions.Literal[Area.US, Area.EU, Area.AP] + _v3_variables: typing.Dict[str, typing.Dict[str, typing.Any]] + @typing.overload def __new__( cls, @@ -864,11 +924,58 @@ def config(self) -> typing.Dict[str, typing.Any]: "greeting_configs": self._greeting_configs, } + def with_variable(self, name: str, definition: typing.Optional[typing.Dict[str, typing.Any]] = None, **fields: typing.Any) -> "Agent": + """Return a new builder with a typed v3 template/extraction variable.""" + from .v3 import variable_definition + new_agent = self._clone() + new_agent._v3_variables = {**getattr(self, "_v3_variables", {}), name: variable_definition(name, definition, **fields)} + return new_agent + + def save(self, *, name: typing.Optional[str] = None, agent_id: typing.Optional[str] = None) -> "SavedAgent": + """Save this configuration and return its persistent handle.""" + from .v3 import save + return save(self, name=name, agent_id=agent_id) + + async def save_async(self, *, name: typing.Optional[str] = None, agent_id: typing.Optional[str] = None) -> "AsyncSavedAgent": + """Save using a bound AsyncAgora client and return its persistent handle.""" + from .v3 import save_async + return await save_async(self, name=name, agent_id=agent_id) + + def deploy(self, *, name: typing.Optional[str] = None, agent_id: typing.Optional[str] = None) -> "DeployedAgent": + """Compatibility alias for save().""" + return self.save(name=name, agent_id=agent_id) + + async def deploy_async(self, *, name: typing.Optional[str] = None, agent_id: typing.Optional[str] = None) -> "AsyncDeployedAgent": + """Compatibility alias for save_async().""" + return await self.save_async(name=name, agent_id=agent_id) + + def create_ephemeral_session(self, channel: str, agent_uid: typing.Optional[str] = None, + remote_uids: typing.Optional[typing.List[str]] = None, **options) -> "AgentSession": + """Configure an unsaved Platform session explicitly; call start() to run it.""" + from .session_backend import create_session + return create_session(self, channel=channel, agent_uid=agent_uid, remote_uids=remote_uids, **options) + + def create_async_ephemeral_session(self, channel: str, agent_uid: typing.Optional[str] = None, + remote_uids: typing.Optional[typing.List[str]] = None, **options) -> "AsyncAgentSession": + """Async-client equivalent of create_ephemeral_session().""" + from .session_backend import create_session + return create_session(self, asynchronous=True, channel=channel, agent_uid=agent_uid, remote_uids=remote_uids, **options) + + def call(self, *, from_number: str, to: str, call_policy=None, **options) -> "AgentSession": + """Configure an outbound phone session; call start() to place the call.""" + from .session_backend import create_call + return create_call(self, from_number=from_number, to=to, call_policy=call_policy, **options) + + def call_async(self, *, from_number: str, to: str, call_policy=None, **options) -> "AsyncAgentSession": + """Configure an outbound call using an AsyncAgora client.""" + from .session_backend import create_call + return create_call(self, asynchronous=True, from_number=from_number, to=to, call_policy=call_policy, **options) + def create_session( self, channel: str, - agent_uid: str, - remote_uids: typing.List[str], + agent_uid: typing.Optional[str] = None, + remote_uids: typing.Optional[typing.List[str]] = None, name: typing.Optional[str] = None, token: typing.Optional[str] = None, idle_timeout: typing.Optional[int] = None, @@ -878,6 +985,16 @@ def create_session( expires_in: typing.Optional[int] = None, debug: typing.Optional[bool] = None, warn: typing.Optional[typing.Callable[[str], None]] = None, + max_duration: typing.Optional[float] = None, + client_reference: typing.Optional[str] = None, + labels: typing.Optional[typing.Dict[str, str]] = None, + values: typing.Optional[typing.Dict[str, typing.Union[str, int, float, bool]]] = None, + idempotency_key: typing.Optional[str] = None, + encryption: typing.Optional[typing.Dict[str, typing.Any]] = None, + audio_scenario: typing.Optional[str] = None, + graceful_stop: typing.Optional[typing.Dict[str, typing.Any]] = None, + data_policy: typing.Optional[typing.Dict[str, str]] = None, + geofence: typing.Optional[typing.Dict[str, str]] = None, ) -> "AgentSession": """Create a synchronous session bound to this agent configuration. @@ -892,7 +1009,18 @@ def create_session( Optional session overrides documented on :class:`AgentSession`. """ from .agent_session import AgentSession - + from .session_backend import create_session, needs_session_backend + if remote_uids is None: + raise ValueError("remote_uids is required") + if agent_uid is None or needs_session_backend(self) or max_duration is not None or client_reference is not None or any(value is not None for value in (labels, values, idempotency_key, encryption, audio_scenario, graceful_stop, data_policy, geofence)): + return create_session(self, asynchronous=False, channel=channel, + agent_uid=agent_uid, remote_uids=remote_uids, name=name, + client_reference=client_reference, idle_timeout=idle_timeout, + max_duration=max_duration, labels=labels, token=token, values=values, + idempotency_key=idempotency_key, encryption=encryption, audio_scenario=audio_scenario, + graceful_stop=graceful_stop, data_policy=data_policy, geofence=geofence, + enable_string_uid=enable_string_uid, preset=preset, + pipeline_id=pipeline_id, expires_in=expires_in, debug=debug, warn=warn) resolved_client = self._client if resolved_client is None: raise ValueError("client is required. Pass client=... to Agent(...).") @@ -920,8 +1048,8 @@ def create_session( def create_async_session( self, channel: str, - agent_uid: str, - remote_uids: typing.List[str], + agent_uid: typing.Optional[str] = None, + remote_uids: typing.Optional[typing.List[str]] = None, name: typing.Optional[str] = None, token: typing.Optional[str] = None, idle_timeout: typing.Optional[int] = None, @@ -931,6 +1059,16 @@ def create_async_session( expires_in: typing.Optional[int] = None, debug: typing.Optional[bool] = None, warn: typing.Optional[typing.Callable[[str], None]] = None, + max_duration: typing.Optional[float] = None, + client_reference: typing.Optional[str] = None, + labels: typing.Optional[typing.Dict[str, str]] = None, + values: typing.Optional[typing.Dict[str, typing.Union[str, int, float, bool]]] = None, + idempotency_key: typing.Optional[str] = None, + encryption: typing.Optional[typing.Dict[str, typing.Any]] = None, + audio_scenario: typing.Optional[str] = None, + graceful_stop: typing.Optional[typing.Dict[str, typing.Any]] = None, + data_policy: typing.Optional[typing.Dict[str, str]] = None, + geofence: typing.Optional[typing.Dict[str, str]] = None, ) -> "AsyncAgentSession": """Create an async session for use with :class:`~agora_agent.AsyncAgora`. @@ -939,7 +1077,18 @@ def create_async_session( name via ``name=...``; when omitted, the SDK generates ``agent-{timestamp}``. """ from .agent_session import AsyncAgentSession - + from .session_backend import create_session, needs_session_backend + if remote_uids is None: + raise ValueError("remote_uids is required") + if agent_uid is None or needs_session_backend(self) or max_duration is not None or client_reference is not None or any(value is not None for value in (labels, values, idempotency_key, encryption, audio_scenario, graceful_stop, data_policy, geofence)): + return create_session(self, asynchronous=True, channel=channel, + agent_uid=agent_uid, remote_uids=remote_uids, name=name, + client_reference=client_reference, idle_timeout=idle_timeout, + max_duration=max_duration, labels=labels, token=token, values=values, + idempotency_key=idempotency_key, encryption=encryption, audio_scenario=audio_scenario, + graceful_stop=graceful_stop, data_policy=data_policy, geofence=geofence, + enable_string_uid=enable_string_uid, preset=preset, + pipeline_id=pipeline_id, expires_in=expires_in, debug=debug, warn=warn) resolved_client = self._client if resolved_client is None: raise ValueError("client is required. Pass client=... to Agent(...).") @@ -979,6 +1128,8 @@ def to_properties( skip_vendor_validation_categories: typing.Optional[typing.AbstractSet[str]] = None, allow_missing_vendor_categories: typing.Optional[typing.AbstractSet[str]] = None, ) -> StartAgentsRequestProperties: + from .v3 import guard_v2 + guard_v2(self.config) # Validate the MLLM + enabled-avatar combination BEFORE generating the # RTC token so callers get a clear, actionable error first (matches the # TypeScript and Go SDKs' fail-fast contract). @@ -997,7 +1148,7 @@ def to_properties( if app_id is None or app_certificate is None: raise ValueError("Either token or app_id+app_certificate must be provided") validated_expires_in = _validate_expires_in(expires_in) if expires_in is not None else None - # Use generate_convo_ai_token (RTC + RTM) so the token works whether or + # Use generate_convo_ai_token (RTC + RTM + ConvoAI) so the token works whether or # not the caller enables advanced_features.enable_rtm. token_kwargs: typing.Dict[str, typing.Any] = {} if validated_expires_in is not None: @@ -1107,8 +1258,10 @@ def to_properties( return _start_properties_from_mapping(base_kwargs) - def _resolve_llm_config(self) -> typing.Dict[str, typing.Any]: + def _resolve_llm_config(self, *, platform: bool = False) -> typing.Dict[str, typing.Any]: llm_config = dict(self._llm or {}) + if not platform and llm_config.get("style") == "gemini" and "key=" in llm_config.get("url", ""): + llm_config.pop("api_key", None) if self._instructions is not None and "system_messages" not in llm_config: llm_config["system_messages"] = [{"role": "system", "content": self._instructions}] if self._greeting is not None and "greeting_message" not in llm_config: @@ -1145,6 +1298,7 @@ def _resolve_turn_detection_config(self) -> TurnDetectionConfig: def _clone(self) -> "Agent": new_agent = self.__class__.__new__(self.__class__, self._client) + new_agent._v3_variables = getattr(self, "_v3_variables", {}) new_agent._client = self._client new_agent._pipeline_id = self._pipeline_id new_agent._llm = self._llm diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index b1171db..2514e5c 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -43,7 +43,7 @@ normalize_preset_input, resolve_session_presets, ) -from .preview.client import create_preview_session_clients, required_preview_features +from .preview.client import apply_preview_shape, create_preview_session_clients, required_preview_features from .token import _parse_numeric_uid, generate_convo_ai_token @@ -112,7 +112,9 @@ def __init__( expires_in: typing.Optional[int] = None, debug: typing.Optional[bool] = None, warn: typing.Optional[typing.Callable[[str], None]] = None, + _session_backend: typing.Optional[typing.Dict[str, typing.Any]] = None, ): + self._session_backend = _session_backend self._client = client self._agent = agent self._app_id = app_id @@ -131,6 +133,11 @@ def __init__( self._warn = warn or warnings.warn self._agent_id: typing.Optional[str] = None self._status: str = "idle" + # New session lifecycle requests can overlap a GET. This revision keeps + # a late response from an older session state from overwriting a later + # start/stop transition. + self._v3_state_revision = 0 + self._v3_transition_pending = False self._event_handlers: typing.Dict[ str, typing.List[typing.Callable[..., None]] ] = {} @@ -167,12 +174,12 @@ def raw(self) -> typing.Any: Use this to access any new endpoints that Fern generates without waiting for agentkit method updates. """ - return self._agents + return self._client.v3.sessions if self._session_backend is not None else self._agents @property def raw_agent_management(self) -> typing.Any: """Direct access to the underlying Fern-generated AgentManagement client.""" - return self._agent_management + return None if self._session_backend is not None else self._agent_management def _require_agent_management(self) -> typing.Any: """Return the management client when the configured client supports it.""" @@ -394,23 +401,7 @@ def _build_start_properties( if self._agent.tts is not None: properties["tts"] = self._dump_model(self._agent.tts) if self._agent.llm is not None: - llm = dict(self._agent.llm) - if self._agent.instructions is not None and "system_messages" not in llm: - llm["system_messages"] = [ - {"role": "system", "content": self._agent.instructions} - ] - if self._agent.greeting is not None and "greeting_message" not in llm: - llm["greeting_message"] = self._agent.greeting - if ( - self._agent.greeting_configs is not None - and "greeting_configs" not in llm - ): - llm["greeting_configs"] = self._dump_model(self._agent.greeting_configs) - if self._agent.failure_message is not None and "failure_message" not in llm: - llm["failure_message"] = self._agent.failure_message - if self._agent.max_history is not None and "max_history" not in llm: - llm["max_history"] = self._agent.max_history - properties["llm"] = llm + properties["llm"] = self._agent._resolve_llm_config() if self._agent.stt is not None: turn_detection = properties.get("turn_detection") or {} properties["asr"] = self._dump_model( @@ -599,6 +590,36 @@ def start(self) -> str: ValueError If avatar/TTS configuration is invalid. """ + if self._session_backend is not None: + if self._agent_id is not None and self._session_backend["body"].get("idempotency_key") is not None: + raise RuntimeError("Create a new session handle and key after a successful idempotent start") + if self._status not in ("idle", "stopped", "error"): + raise RuntimeError(f"Cannot start session in {self._status} state") + self._v3_state_revision += 1 + self._v3_transition_pending = True + self._agent_id = None + self._status = "starting" + try: + resource = self._client.v3.sessions + method = resource.start if self._session_backend["stored"] else resource.start_ephemeral + from .session_backend import prepare_start_body + response = method(**prepare_start_body(self)) + identifier = response.get("agent_session_id") + if not isinstance(identifier, str) or not identifier: + raise RuntimeError("Session response is missing agent_session_id") + self._agent_id = identifier + self._session_backend["successfully_started"] = True + self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("started", {"agent_id": identifier, "agent_session_id": identifier}) + return identifier + except BaseException as error: + self._status = "error" + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("error", error) + raise if self._status not in ("idle", "stopped", "error"): raise RuntimeError(f"Cannot start session in {self._status} state") @@ -633,6 +654,7 @@ def start(self) -> str: properties, ) + apply_preview_shape(resolved_properties) self._bind_session_clients(required_preview_features(resolved_properties)) if self._debug: @@ -685,6 +707,35 @@ def stop(self) -> None: server returns 404, which this method treats as a successful stop rather than raising an error. """ + if self._session_backend is not None: + if self._status not in ("starting", "running") or not self._agent_id: + raise RuntimeError(f"Cannot stop session in {self._status} state") + self._v3_state_revision += 1 + self._v3_transition_pending = True + self._status = "stopping" + try: + self._client.v3.sessions.stop(agent_session_id=self._agent_id) + except BaseException as error: + if getattr(error, "status_code", None) == 409: + error_body = getattr(error, "body", None) + reason = error_body.get("reason") if isinstance(error_body, dict) else None + if reason == "TaskNotRunning": + self._status = "stopped" + elif reason == "TaskConflict": + self._status = "stopping" + else: + self._status = "error" + else: + self._status = "error" + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("error", error) + raise + self._status = "stopped" + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("stopped", {"agent_id": self._agent_id, "agent_session_id": self._agent_id}) + return if self._status != "running": raise RuntimeError(f"Cannot stop session in {self._status} state") if not self._agent_id: @@ -730,6 +781,8 @@ def say( interruptable : bool, optional Whether the message can be interrupted by the user. """ + if self._session_backend is not None: + raise NotImplementedError("say is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot say in {self._status} state") if not self._agent_id: @@ -752,6 +805,8 @@ def say( def interrupt(self) -> None: """Interrupt the agent while it is speaking or thinking.""" + if self._session_backend is not None: + raise NotImplementedError("interrupt is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot interrupt in {self._status} state") if not self._agent_id: @@ -778,6 +833,8 @@ def think( ``"interrupt"``. Pass ``on_listening_action="inject"`` explicitly to preserve the pre-v2.7 behavior. """ + if self._session_backend is not None: + raise NotImplementedError("think is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot think in {self._status} state") if not self._agent_id: @@ -812,20 +869,29 @@ def update(self, properties: typing.Any) -> None: properties : UpdateAgentsRequestProperties Partial configuration to update. """ + if self._session_backend is not None: + if self._status != "running" or not self._agent_id: + raise RuntimeError(f"Cannot update in {self._status} state") + from .session_backend import update_body + self._client.v3.sessions.update(agent_session_id=self._agent_id, **update_body(properties)) + return if self._status != "running": raise RuntimeError(f"Cannot update in {self._status} state") if not self._agent_id: raise RuntimeError("No agent ID available") + from .session_backend import legacy_update_body self._agents.update( self._app_id, self._agent_id, - properties=properties, + properties=legacy_update_body(properties), request_options=self._request_options(), ) def get_history(self) -> typing.Any: """Get the conversation history.""" + if self._session_backend is not None: + raise NotImplementedError("get_history is not documented for stored or ephemeral sessions") if not self._agent_id: raise RuntimeError("No agent ID available") @@ -835,6 +901,17 @@ def get_history(self) -> typing.Any: def get_info(self) -> typing.Any: """Get the current session info.""" + if self._session_backend is not None: + if not self._agent_id: + raise RuntimeError("No session ID available") + self._v3_state_revision += 1 + revision = self._v3_state_revision + session_id = self._agent_id + transition_pending = self._v3_transition_pending + response = self._client.v3.sessions.get(agent_session_id=session_id) + if revision == self._v3_state_revision and session_id == self._agent_id and not transition_pending and not self._v3_transition_pending: + self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() + return response if not self._agent_id: raise RuntimeError("No agent ID available") @@ -850,6 +927,8 @@ def get_turns( options: typing.Optional["GetTurnsOptions"] = None, ) -> GetTurnsAgentsResponse: """Get turn-by-turn analytics and timing details for this session.""" + if self._session_backend is not None: + raise NotImplementedError("get_turns is not documented for stored or ephemeral sessions") if not self._agent_id: raise RuntimeError("No agent ID available") @@ -877,6 +956,8 @@ def get_all_turns( the fields required to advance, or if requesting the next page returns a page index that did not advance. """ + if self._session_backend is not None: + raise NotImplementedError("get_all_turns is not documented for stored or ephemeral sessions") response = self.get_turns(page_index=1, page_size=page_size) all_turns = self._response_turns(response) pagination = self._response_pagination(response) @@ -966,6 +1047,36 @@ async def start(self) -> str: ValueError If avatar/TTS configuration is invalid. """ + if self._session_backend is not None: + if self._agent_id is not None and self._session_backend["body"].get("idempotency_key") is not None: + raise RuntimeError("Create a new session handle and key after a successful idempotent start") + if self._status not in ("idle", "stopped", "error"): + raise RuntimeError(f"Cannot start session in {self._status} state") + self._v3_state_revision += 1 + self._v3_transition_pending = True + self._agent_id = None + self._status = "starting" + try: + resource = self._client.v3.sessions + method = resource.start if self._session_backend["stored"] else resource.start_ephemeral + from .session_backend import prepare_start_body + response = await method(**prepare_start_body(self)) + identifier = response.get("agent_session_id") + if not isinstance(identifier, str) or not identifier: + raise RuntimeError("Session response is missing agent_session_id") + self._agent_id = identifier + self._session_backend["successfully_started"] = True + self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("started", {"agent_id": identifier, "agent_session_id": identifier}) + return identifier + except BaseException as error: + self._status = "error" + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("error", error) + raise if self._status not in ("idle", "stopped", "error"): raise RuntimeError(f"Cannot start session in {self._status} state") @@ -1000,6 +1111,7 @@ async def start(self) -> str: properties, ) + apply_preview_shape(resolved_properties) self._bind_session_clients(required_preview_features(resolved_properties)) if self._debug: @@ -1052,6 +1164,35 @@ async def stop(self) -> None: server returns 404, which this method treats as a successful stop rather than raising an error. """ + if self._session_backend is not None: + if self._status not in ("starting", "running") or not self._agent_id: + raise RuntimeError(f"Cannot stop session in {self._status} state") + self._v3_state_revision += 1 + self._v3_transition_pending = True + self._status = "stopping" + try: + await self._client.v3.sessions.stop(agent_session_id=self._agent_id) + except BaseException as error: + if getattr(error, "status_code", None) == 409: + error_body = getattr(error, "body", None) + reason = error_body.get("reason") if isinstance(error_body, dict) else None + if reason == "TaskNotRunning": + self._status = "stopped" + elif reason == "TaskConflict": + self._status = "stopping" + else: + self._status = "error" + else: + self._status = "error" + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("error", error) + raise + self._status = "stopped" + self._v3_state_revision += 1 + self._v3_transition_pending = False + self._emit("stopped", {"agent_id": self._agent_id, "agent_session_id": self._agent_id}) + return if self._status != "running": raise RuntimeError(f"Cannot stop session in {self._status} state") if not self._agent_id: @@ -1097,6 +1238,8 @@ async def say( interruptable : bool, optional Whether the message can be interrupted by the user. """ + if self._session_backend is not None: + raise NotImplementedError("say is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot say in {self._status} state") if not self._agent_id: @@ -1119,6 +1262,8 @@ async def say( async def interrupt(self) -> None: """Interrupt the agent while it is speaking or thinking.""" + if self._session_backend is not None: + raise NotImplementedError("interrupt is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot interrupt in {self._status} state") if not self._agent_id: @@ -1145,6 +1290,8 @@ async def think( ``"interrupt"``. Pass ``on_listening_action="inject"`` explicitly to preserve the pre-v2.7 behavior. """ + if self._session_backend is not None: + raise NotImplementedError("think is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot think in {self._status} state") if not self._agent_id: @@ -1179,20 +1326,29 @@ async def update(self, properties: typing.Any) -> None: properties : UpdateAgentsRequestProperties Partial configuration to update. """ + if self._session_backend is not None: + if self._status != "running" or not self._agent_id: + raise RuntimeError(f"Cannot update in {self._status} state") + from .session_backend import update_body + await self._client.v3.sessions.update(agent_session_id=self._agent_id, **update_body(properties)) + return if self._status != "running": raise RuntimeError(f"Cannot update in {self._status} state") if not self._agent_id: raise RuntimeError("No agent ID available") + from .session_backend import legacy_update_body await self._agents.update( self._app_id, self._agent_id, - properties=properties, + properties=legacy_update_body(properties), request_options=self._request_options(), ) async def get_history(self) -> typing.Any: """Get the conversation history.""" + if self._session_backend is not None: + raise NotImplementedError("get_history is not documented for stored or ephemeral sessions") if not self._agent_id: raise RuntimeError("No agent ID available") @@ -1202,6 +1358,17 @@ async def get_history(self) -> typing.Any: async def get_info(self) -> typing.Any: """Get the current session info.""" + if self._session_backend is not None: + if not self._agent_id: + raise RuntimeError("No session ID available") + self._v3_state_revision += 1 + revision = self._v3_state_revision + session_id = self._agent_id + transition_pending = self._v3_transition_pending + response = await self._client.v3.sessions.get(agent_session_id=session_id) + if revision == self._v3_state_revision and session_id == self._agent_id and not transition_pending and not self._v3_transition_pending: + self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() + return response if not self._agent_id: raise RuntimeError("No agent ID available") @@ -1217,6 +1384,8 @@ async def get_turns( options: typing.Optional["GetTurnsOptions"] = None, ) -> GetTurnsAgentsResponse: """Get turn-by-turn analytics and timing details for this session.""" + if self._session_backend is not None: + raise NotImplementedError("get_turns is not documented for stored or ephemeral sessions") if not self._agent_id: raise RuntimeError("No agent ID available") @@ -1244,6 +1413,8 @@ async def get_all_turns( the fields required to advance, or if requesting the next page returns a page index that did not advance. """ + if self._session_backend is not None: + raise NotImplementedError("get_all_turns is not documented for stored or ephemeral sessions") response = await self.get_turns(page_index=1, page_size=page_size) all_turns = self._response_turns(response) pagination = self._response_pagination(response) diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py index 1cd30d6..a458fc7 100644 --- a/src/agora_agent/agentkit/preview/client.py +++ b/src/agora_agent/agentkit/preview/client.py @@ -117,6 +117,7 @@ def create_preview_session_clients( if isinstance(source, AsyncClientWrapper): kwargs["httpx_client"] = typing.cast(httpx.AsyncClient, _GatedAsyncHttpxClient(source.httpx_client.httpx_client, features)) async_wrapper = AsyncClientWrapper(**kwargs) + async_wrapper._authorization_supplier = getattr(source, "_authorization_supplier", None) return ( AsyncAgentsClient(client_wrapper=async_wrapper), AsyncAgentManagementClient(client_wrapper=async_wrapper), @@ -124,6 +125,7 @@ def create_preview_session_clients( if isinstance(source, SyncClientWrapper): kwargs["httpx_client"] = typing.cast(httpx.Client, _GatedSyncHttpxClient(source.httpx_client.httpx_client, features)) sync_wrapper = SyncClientWrapper(**kwargs) + sync_wrapper._authorization_supplier = getattr(source, "_authorization_supplier", None) return ( AgentsClient(client_wrapper=sync_wrapper), AgentManagementClient(client_wrapper=sync_wrapper), diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py new file mode 100644 index 0000000..2a2a23c --- /dev/null +++ b/src/agora_agent/agentkit/session_backend.py @@ -0,0 +1,187 @@ +"""Private adapter for the Bruno-documented session lifecycle.""" +import copy +import math + + +def needs_session_backend(agent): + def contains(value): + if isinstance(value, str): + return value.startswith("$secrets.") + if isinstance(value, dict): + return any(contains(v) for v in value.values()) + if isinstance(value, (list, tuple)): + return any(contains(v) for v in value) + return False + params = agent._parameters or {} + if hasattr(params, "model_dump"): + params = params.model_dump(exclude_none=True) + return bool(getattr(agent, "_v3_variables", {})) or contains(agent.config) or any(key in params for key in ("opt_out", "farewell_config")) + + +def create_session(owner, *, asynchronous=False, channel=None, agent_uid=None, remote_uids=None, + name=None, client_reference=None, idle_timeout=None, max_duration=None, + labels=None, values=None, idempotency_key=None, token=None, encryption=None, + audio_scenario=None, graceful_stop=None, data_policy=None, geofence=None, debug=None, warn=None, _telephony=None, **unsupported): + from ..pool_client import Agora, AsyncAgora + from .agent_session import AgentSession, AsyncAgentSession + from .v3 import build_body, dump + client = owner._client + if not isinstance(client, AsyncAgora if asynchronous else Agora): + raise TypeError("Session requires AsyncAgora" if asynchronous else "Session requires Agora") + if idempotency_key is not None: + from ..core.public_auth import validate_public_headers + validate_public_headers({"Idempotency-Key": idempotency_key}) + if client_reference is not None: + raise ValueError("client_reference has been removed; use labels for application metadata") + invalid = [key for key, value in unsupported.items() if value is not None] + if invalid: + raise ValueError("Unsupported session options: " + ", ".join(sorted(invalid))) + if _telephony is not None: + if any(value is not None for value in (channel, agent_uid, remote_uids, token, encryption, audio_scenario, geofence)): + raise ValueError("RTC options cannot be combined with a phone call") + body = {"transport": copy.deepcopy(_telephony)} + else: + if not isinstance(channel, str) or not channel.strip(): + raise ValueError("channel must be nonempty") + if agent_uid is not None and (not isinstance(agent_uid, str) or not agent_uid.strip()): + raise ValueError("agent_uid must be nonempty") + if not isinstance(remote_uids, list) or any(not isinstance(v, str) or not v.strip() for v in remote_uids): + raise ValueError("remote_uids must be a list of nonempty strings") + body = {"transport": {"type": "rtc", "channel": channel, "subscribe_uids": list(remote_uids)}} + if agent_uid is not None: + body["transport"]["uid"] = agent_uid + fence = geofence if geofence is not None else getattr(owner, "_geofence", None) + if fence is not None: + fence = dump(fence) + body["transport"]["geofence"] = {key: value.lower() if isinstance(value, str) else value for key, value in fence.items()} + for key, value in (("token", token), ("encryption", encryption), ("audio_scenario", audio_scenario)): + if value is not None: + body["transport"][key] = copy.deepcopy(value) + if labels is None: + labels = getattr(owner, "_labels", None) + reference = name + for key, value in (("values", values), ("labels", labels), ("data_policy", data_policy)): + if value is not None: + body[key] = copy.deepcopy(value) + lifecycle = {} + for key, value in (("idle_timeout_ms", idle_timeout), ("max_duration_ms", max_duration)): + if value is not None: + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0 or value * 1000 != int(value * 1000): + raise ValueError(key + " must be a nonnegative duration in seconds with millisecond precision") + lifecycle[key] = int(value * 1000) + if graceful_stop is not None: + lifecycle["graceful_stop"] = copy.deepcopy(graceful_stop) + if lifecycle: + body["lifecycle"] = lifecycle + stored = hasattr(owner, "id") + if stored: + body["agent_id"] = owner.id + else: + params = dump(owner._parameters or {}) + inline = owner._clone() + inline._geofence = None + inline._labels = None + inline._parameters = {k: v for k, v in params.items() if k not in {"opt_out", "farewell_config", "audio_scenario"}} + if "opt_out" in params and not isinstance(params["opt_out"], bool): + raise ValueError("parameters.opt_out must be bool") + if data_policy is None and "opt_out" in params: + body["data_policy"] = {"retention": "none" if params["opt_out"] else "30_days"} + if _telephony is not None and ("audio_scenario" in params or owner._geofence is not None): + raise ValueError("RTC settings cannot be combined with a phone call") + if audio_scenario is None and "audio_scenario" in params: + body["transport"]["audio_scenario"] = params["audio_scenario"] + if graceful_stop is None and "farewell_config" in params: + farewell = params["farewell_config"] + unknown = set(farewell) - {"graceful_enabled", "graceful_timeout_seconds"} + if unknown: + raise ValueError("Unsupported farewell settings: " + ", ".join(sorted(unknown))) + graceful = {} + if "graceful_enabled" in farewell: + graceful["enabled"] = farewell["graceful_enabled"] + if "graceful_timeout_seconds" in farewell: + seconds = farewell["graceful_timeout_seconds"] + if isinstance(seconds, bool) or not isinstance(seconds, (int, float)) or not math.isfinite(seconds) or seconds * 1000 != int(seconds * 1000): + raise ValueError("Invalid farewell timeout in seconds") + graceful["timeout_ms"] = int(seconds * 1000) + body.setdefault("lifecycle", {})["graceful_stop"] = graceful + agent = build_body(inline) + agent.pop("agent_name", None) + body["agent"] = agent + from ..v3.session_contract import validate_session + validate_session(body) + if idempotency_key is not None: + body["idempotency_key"] = idempotency_key + certificate = getattr(client, "app_certificate", None) + signing = None + if _telephony is None and token is None and certificate and isinstance(agent_uid, str) and agent_uid.isascii() and agent_uid.isdigit() and 0 < int(agent_uid) <= 4294967295: + signing = {"app_id": client.app_id, "app_certificate": certificate, "channel_name": channel, "uid": int(agent_uid)} + session_type = AsyncAgentSession if asynchronous else AgentSession + return session_type(client=client, agent=owner, app_id=client.app_id, + name=reference or "", channel=channel or "", agent_uid=agent_uid, + remote_uids=list(remote_uids or []), debug=debug, warn=warn, + _session_backend={"stored": stored, "body": copy.deepcopy(body), "signing": signing}) + + +def update_body(properties): + """Map the existing runtime-update convenience shape to the public PATCH.""" + from ..v3.contract import validate_write + value = properties.model_dump(exclude_unset=True) if hasattr(properties, "model_dump") else copy.deepcopy(properties) + if not isinstance(value, dict) or set(value) - {"token", "messages", "llm"}: + raise ValueError("Preview session update supports token, messages and llm.params only; MLLM is not supported") + result = {} + if "token" in value: + result["transport"] = {"token": value["token"]} + overrides = {} + if "messages" in value: + overrides["messages"] = value["messages"] + if "llm" in value: + module = value["llm"] + if not isinstance(module, dict) or set(module) - {"params", "system_messages"}: + raise ValueError("Session update supports only provider params and llm.system_messages") + if "system_messages" in module: + if "messages" in overrides: + raise ValueError("Supply messages or llm.system_messages, not both") + overrides["messages"] = module["system_messages"] + if "params" in module: + overrides["pipeline"] = {"llm": {"params": module["params"]}} + if overrides: + result["agent"] = {"overrides": overrides} + validate_write("PATCH", "/conversational-ai/v1/projects/project/sessions/session", result) + return result + + +def legacy_update_body(properties): + """Keep the messages shortcut independent of the selected session transport.""" + value = properties.model_dump(exclude_unset=True) if hasattr(properties, "model_dump") else copy.deepcopy(properties) + if not isinstance(value, dict) or "messages" not in value: + return properties + from ..v3.contract import validate + messages = value.pop("messages") + validate(messages, {"type": "array", "items": {"$ref": "#/components/schemas/Message"}}, "messages") + llm = value.get("llm", {}) + if not isinstance(llm, dict) or "system_messages" in llm: + raise ValueError("Supply messages or llm.system_messages, not both") + value["llm"] = {**llm, "system_messages": messages} + return value + + +def create_call(owner, *, from_number: str, to: str, call_policy=None, asynchronous=False, **options): + """Configure an outbound call using the same session lifecycle as RTC.""" + transport = {"type": "telephony", "from": from_number, "to": to} + if call_policy is not None: + transport["call_policy"] = copy.deepcopy(call_policy) + return create_session(owner, asynchronous=asynchronous, _telephony=transport, **options) + + +def prepare_start_body(session): + """Generate a fixed-UID RTC token once, preserving idempotent retry bodies.""" + body = session._session_backend["body"] + transport = body["transport"] + signing = session._session_backend.get("signing") + generate = "token" not in transport or (session._session_backend.get("generated_rtc_token") and session._session_backend.get("successfully_started") and body.get("idempotency_key") is None) + if signing and generate: + from .token import generate_convo_ai_token + transport["token"] = generate_convo_ai_token(**signing) + session._session_backend["generated_rtc_token"] = True + session._session_backend["successfully_started"] = False + return body diff --git a/src/agora_agent/agentkit/token.py b/src/agora_agent/agentkit/token.py index 9647137..45c07e0 100644 --- a/src/agora_agent/agentkit/token.py +++ b/src/agora_agent/agentkit/token.py @@ -1,6 +1,7 @@ import base64 import hashlib import hmac +import re import secrets import struct import time @@ -209,12 +210,10 @@ def _require_valid_credentials(app_id: str, app_certificate: str) -> None: if len(value) != AGORA_CREDENTIAL_LENGTH: wrong.append((name, value)) continue - try: - bytes.fromhex(value) - except (TypeError, ValueError): + if re.fullmatch(r"[0-9a-fA-F]{32}", value) is None: raise ValueError( f"Failed to build an Agora token: {name} must be a 32-character hexadecimal value." - ) from None + ) if not wrong: return detail = " and ".join(f"{name} is {len(value)} characters" for name, value in wrong) diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py new file mode 100644 index 0000000..0810a7f --- /dev/null +++ b/src/agora_agent/agentkit/v3.py @@ -0,0 +1,647 @@ +"""Stored-agent preview and strict mapping from existing immutable builders.""" +from __future__ import annotations + +import copy +import re +import time +from typing import TYPE_CHECKING, List, Mapping, Optional +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit + +from ..v3 import PreconditionFailedError +from ..v3.types import AgentResponse, NumberResponse + +_ASR_PROVIDER_LANGUAGE_VENDORS = {"assemblyai", "google", "microsoft", "sarvam", "speechmatics"} + +if TYPE_CHECKING: + from .agent import Agent + + +class UnsupportedFieldError(ValueError): + def __init__(self, fields): + self.fields = sorted(set(fields)) + super().__init__("Unsupported v3 fields: " + ", ".join(self.fields)) + + +class NumberNotFoundError(LookupError): + pass + + +class SecretRefNotSupportedError(ValueError): + pass + + +class SecretRef(str): + """A validated reference to a write-only project secret.""" + + +def secret_ref(secret_set: str, key: str) -> SecretRef: + pattern = r"[A-Za-z0-9_]{1,64}" + if not isinstance(secret_set, str) or not re.fullmatch(pattern, secret_set): + raise ValueError("secret_set must contain 1-64 letters, digits, underscores") + if not isinstance(key, str) or not re.fullmatch(pattern, key): + raise ValueError("key must contain 1-64 letters, digits, underscores") + return SecretRef(f"$secrets.{secret_set}.{key}") + + +def guard_v2(value): + if isinstance(value, str) and value.startswith("$secrets."): + raise SecretRefNotSupportedError("secret_ref requires save() or deploy()") + if isinstance(value, Mapping): + for item in value.values(): + guard_v2(item) + elif isinstance(value, (list, tuple)): + for item in value: + guard_v2(item) + elif hasattr(value, "model_dump"): + guard_v2(value.model_dump(exclude_none=True)) + + +def variable_definition(name, definition=None, **fields): + if not isinstance(name, str) or not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", name): + raise ValueError("Variable name must match ^[a-z][a-z0-9_]{0,63}$") + result = {**(definition or {}), **fields} + unknown = set(result) - {"type", "required", "options", "extract", "description"} + if unknown: + raise ValueError("Unknown variable fields: " + ", ".join(sorted(unknown))) + if result.get("type") not in ("str", "int", "float", "bool", "date", "datetime"): + raise ValueError("Variable type must be str, int, float, bool, date or datetime") + for key in ("extract", "required"): + if key in result and not isinstance(result[key], bool): + raise ValueError(key + " must be a bool") + description = result.get("description") + if description is not None and (not isinstance(description, str) or len(description) > 2000): + raise ValueError("description must be a string of at most 2000 characters") + if result.get("extract") and (not isinstance(description, str) or not description.strip()): + raise ValueError("Extracted variables require a nonempty description") + if "options" in result: + options = result["options"] + if not isinstance(options, list) or not 1 <= len(options) <= 100: + raise ValueError("options must contain 1-100 unique primitive values") + if any(not isinstance(v, (str, int, float, bool)) for v in options) or len({(bool if isinstance(v, bool) else str if isinstance(v, str) else float, v) for v in options}) != len(options): + raise ValueError("options must contain unique primitive values") + return copy.deepcopy(result) + + +def dump(value): + if hasattr(value, "model_dump"): + return value.model_dump(exclude_none=True) + if isinstance(value, dict): + return {k: dump(v) for k, v in value.items() if v is not None} + if isinstance(value, (tuple, list)): + return [dump(v) for v in value] + return copy.deepcopy(value) + + +def module(config, category, errors): + config = dump(config) + params = dict(config.pop("params", {}) or {}) + if category == "tts" and config.get("vendor") == "minimax": + preset_model = config.pop("_minimax_preset_model", None) + if preset_model is not None: + params.setdefault("model", preset_model) + if category == "llm" and config.get("style") == "bedrock": + config.setdefault("vendor", "amazon") + if "region" in config: + params.setdefault("region", config.pop("region")) + if category == "avatar" and config.get("vendor") == "heygen": + config["vendor"] = "liveavatar" + vendor = config.get("vendor") + if category == "asr": + if "keywords" in config: + params["keywords"] = config.pop("keywords") + language = config.get("language", params.get("language")) + # These recognizers require their provider language even when the + # conversation uses a separate block-level interaction language. + if vendor in _ASR_PROVIDER_LANGUAGE_VENDORS: + if language is not None: + params.setdefault("language", language) + else: + params.pop("language", None) + if language is not None: + config["language"] = language + elif vendor in {"ares", "fengming"}: + config["language"] = "en-US" + endpoint = {"assemblyai": "ws_url", "speechmatics": "uri"}.get(vendor) + if endpoint and endpoint in params: + config.setdefault("url", params.pop(endpoint)) + if category == "llm" and (vendor == "gemini" or config.get("style") == "gemini") and config.get("url"): + parts = urlsplit(config["url"]) + entries = parse_qsl(parts.query, keep_blank_values=True) + for key, value in entries: + if key == "key": + config.setdefault("api_key", value) + break + query = [(key, value) for key, value in entries if key != "key"] + config["url"] = urlunsplit(parts._replace(query=urlencode(query))) + provider_fields = { + ("asr", "amazon"): ("access_key_id", "secret_access_key"), + ("asr", "google"): ("adc_credentials_string",), + ("asr", "microsoft"): ("key",), + ("asr", "speechmatics"): ("key",), + ("llm", "amazon"): ("api_key", "access_key", "secret_key"), + ("tts", "amazon"): ("aws_access_key_id", "aws_secret_access_key"), + ("tts", "google"): ("credentials",), + ("tts", "sarvam"): ("api_subscription_key",), + **{("tts", name): ("key",) for name in ("elevenlabs", "humeai", "microsoft", "minimax")}, + ("avatar", "sensetime"): ("app_key",), + ("avatar", "spatius"): ("spatius_api_key",), + }.get((category, vendor), ("api_key",)) + fields = {} + all_credentials = {"api_key", "key", "api_subscription_key", "app_key", "spatius_api_key", "access_key_id", "secret_access_key", "aws_access_key_id", "aws_secret_access_key", "access_key", "secret_key", "adc_credentials_string", "service_account", "credentials"} + for source in (config, params): + for field in tuple(source): + if field not in all_credentials: + continue + value = source.pop(field) + target = field if field in provider_fields else provider_fields[0] if field in {"api_key", "key", "api_subscription_key"} and len(provider_fields) == 1 else None + if target is None: + errors.append(category + ".credential." + field) + elif target in fields and fields[target] != value: + errors.append(category + ".credential (multiple different " + target + " values)") + else: + fields[target] = value + result = {"credential": {"mode": "byok", "fields": fields} if fields else {"mode": "managed"}} + allowed = { + "asr": {"vendor", "url", "model", "language"}, + "llm": {"vendor", "url", "style", "model", "input_modalities", "output_modalities", "max_history", "failure_message", "tools", "mcp_servers", "headers"}, + "tts": {"vendor", "url", "skip_patterns", "headers"}, + "avatar": {"vendor", "url", "enabled"}, + }[category] + # v2 provider serializers keep endpoints and ASR/LLM model choices in params. + for field in ("url", "model"): + if field in params and field in allowed: + result[field] = params.pop(field) + if "base_url" in params and "url" not in result: + result["url"] = params.pop("base_url") + if category == "avatar" and "enable" in config: + config["enabled"] = config.pop("enable") + for field, value in config.items(): + if field not in allowed: + errors.append(category + "." + field) + else: + result[field] = value + if category == "tts" and any(not isinstance(v, str) for v in result.get("skip_patterns", [])): + errors.append("tts.skip_patterns (numeric v2 patterns have no confirmed v3 mapping)") + if not fields: + result.pop("url", None) + for key in ("url", "base_url"): + params.pop(key, None) + if params: + result["params"] = params + return result + + +def _turn(config, interruption, errors): + config = dump(config or {}) + result = {} + nested = config.pop("config", {}) or {} + if config.get("mode") == "default": + config.pop("mode") + if "speech_threshold" in nested: + result["speech_threshold"] = nested.pop("speech_threshold") + if config.get("type") == "agora_vad": + config.pop("type") + start = {k: config.pop(k) for k in ("interrupt_duration_ms", "prefix_padding_ms") if k in config} + end = {k: config.pop(k) for k in ("silence_duration_ms",) if k in config} + if start: + nested.setdefault("start_of_speech", {"mode": "vad", "vad_config": start}) + if end: + nested.setdefault("end_of_speech", {"mode": "vad", "vad_config": end}) + if "threshold" in config: + result["speech_threshold"] = config.pop("threshold") + interrupt_mode = config.pop("interrupt_mode", None) + if interrupt_mode in ("append", "ignore"): + result["interruption"] = {"mode": "off", "when_off": interrupt_mode} + elif interrupt_mode == "keyword": + result["interruption"] = {"mode": "keywords", "keywords": config.pop("interrupt_keywords", [])} + elif interrupt_mode == "interrupt": + result["interruption"] = {"mode": "start_of_speech"} + elif interrupt_mode is not None: + errors.append("turn_detection.interrupt_mode") + config.pop("language", None) # ASR language, not a v3 turn-detection field. + for key in ("start_of_speech", "end_of_speech"): + boundary = dump(nested.pop(key, config.pop(key, None))) + if boundary is None: + continue + mode = boundary.pop("mode", boundary.pop("type", None)) + if key == "start_of_speech" and mode in ("keywords", "disabled"): + if mode == "keywords": + keywords = boundary.pop("keywords_config", {}) + result["interruption"] = {"mode": "keywords", "keywords": keywords.get("trigger_keywords", keywords.get("triggered_keywords", keywords.get("keywords", [])))} + else: + disabled = boundary.pop("disabled_config", {}) + result["interruption"] = {"mode": "off", "when_off": disabled.get("strategy", "append")} + if boundary: + errors.extend("turn_detection." + key + "." + k for k in boundary) + continue + flat = {"type": mode} + for field, value in boundary.items(): + if field in ("vad_config", "semantic_config"): + flat.update(value) + else: + flat[field] = value + if flat.get("max_wait_ms", 500) < 500: + errors.append("turn_detection.end_of_speech.max_wait_ms") + result[key] = flat + if nested: + errors.extend("turn_detection.config." + k for k in nested) + if "speech_threshold" in config: + result["speech_threshold"] = config.pop("speech_threshold") + if config: + errors.extend("turn_detection." + k for k in config) + if interruption is not None: + incoming = dump(interruption) + enabled = incoming.pop("enable", True) + mode = incoming.pop("mode", "start_of_speech") + disabled = incoming.pop("disabled_config", {}) + keywords = incoming.pop("keywords_config", {}) + if enabled is False or mode in ("disabled", "off"): + converted = {"mode": "off", "when_off": disabled.get("strategy", incoming.pop("strategy", incoming.pop("when_off", "append")))} + elif mode == "keywords": + converted = {"mode": "keywords", "keywords": keywords.get("trigger_keywords", keywords.get("triggered_keywords", incoming.pop("keywords", [])))} + else: + converted = {"mode": mode} + errors.extend("interruption." + k for k in incoming) + result["interruption"] = converted + return result + + +def build_body(agent, name=None): + if agent._mllm is not None: + raise UnsupportedFieldError(["mllm (realtime pipelines are not supported by the preview API)"]) + errors = [] + for field in ("sal", "advanced_features", "geofence", "labels", "rtc", "pipeline_id"): + if getattr(agent, "_" + field, None) is not None: + errors.append(field) + params = dump(agent._parameters or {}) + errors.extend("parameters." + k for k in params if k != "silence_config") + result = {"agent_name": name if name is not None else f"agent-{time.time_ns()}"} + if not isinstance(result["agent_name"], str) or not result["agent_name"].strip(): + raise ValueError("name must be nonempty") + pipeline = {"mode": "cascaded"} + conversational = dump(agent._resolve_llm_config(platform=True)) or {} + messages = conversational.pop("system_messages", None) + if messages is not None: + source = "llm.system_messages" + if not isinstance(messages, list): + errors.append(source) + else: + valid = [] + for index, message in enumerate(messages): + if not isinstance(message, dict) or set(message) != {"role", "content"} or message.get("role") not in {"system", "user", "assistant"} or not isinstance(message.get("content"), str): + errors.append(f"{source}[{index}]") + else: + valid.append(message) + system = [m["content"] for m in valid if m["role"] == "system"] + if system: + result["instructions"] = "\n".join(system) + non_system = [m for m in valid if m["role"] != "system"] + if non_system: + result["messages"] = non_system + if "instructions" not in result and agent._instructions is not None: + result["instructions"] = agent._instructions + greeting_text = conversational.pop("greeting_message", agent._greeting) + greeting_url = conversational.pop("greeting_audio_url", None) + greeting = dump(conversational.pop("greeting_configs", agent._greeting_configs) or {}) + if "audio_download_timeout_ms" in greeting: + greeting["download_timeout_ms"] = greeting.pop("audio_download_timeout_ms") + if "audio_pcm_sample_rate" in greeting: + greeting["pcm_sample_rate"] = greeting.pop("audio_pcm_sample_rate") + errors.extend("greeting_configs." + k for k in greeting if k not in {"mode", "on", "delay_ms", "download_timeout_ms", "pcm_sample_rate"}) + if "mode" in greeting: + mode = greeting.pop("mode") + mapped = {"single_every": "each_join", "single_first": "first_join", "each_join": "each_join", "first_join": "first_join"}.get(mode) if isinstance(mode, str) else None + if mapped is None or ("on" in greeting and greeting["on"] != mapped): + errors.append("greeting_configs.mode") + else: + greeting["on"] = mapped + if "on" in greeting and greeting["on"] not in ("each_join", "first_join"): + errors.append("greeting_configs.on") + if not greeting_url: + errors.extend("greeting_configs." + key for key in ("download_timeout_ms", "pcm_sample_rate") if key in greeting) + if greeting_url and not greeting_text: + errors.append("greeting_audio_url (v3 requires companion greeting text)") + if greeting_url or greeting_text is not None: + result["greeting"] = {"mode": "audio" if greeting_url else "text", "text": greeting_text or "", **greeting} + if greeting_url: + result["greeting"]["url"] = greeting_url + elif greeting: + errors.append("greeting_configs (requires greeting)") + if conversational.pop("template_variables", None): + errors.append("llm.template_variables (use with_variable)") + td = _turn(agent._turn_detection, agent._interruption, errors) + if td: + pipeline["turn_detection"] = td + if conversational: + pipeline["llm"] = module(conversational, "llm", errors) + for source, target in (("stt", "asr"), ("tts", "tts"), ("avatar", "avatar")): + value = getattr(agent, "_" + source, None) + if target == "asr": + language = agent._field_value(agent._turn_detection, "language") + if value is not None or language is not None: + value = dump(value or {"vendor": "fengming" if getattr(agent._client, "area_scope", None) == "cn" else "ares"}) + if language is not None: + if value.get("vendor") in _ASR_PROVIDER_LANGUAGE_VENDORS and value.get("language") is not None: + value.setdefault("params", {}).setdefault("language", value["language"]) + value["language"] = language + if value is not None: + pipeline[target] = module(value, target, errors) + if "silence_config" in params: + pipeline["silence_config"] = {"enabled": True, **params["silence_config"]} + if agent._filler_words is not None: + filler = dump(agent._filler_words) + for key, nested in (("trigger", "fixed_time_config"), ("content", "static_config")): + if key in filler: + part = filler[key] + sub = part.pop(nested, {}) + generated = part.pop("generated_config", {}) + if "prompt" in generated: + generated["instructions"] = generated.pop("prompt") + generated.pop("llm_provider", None) + part.update(sub) + part.update(generated) + result["filler_words"] = filler + variables = getattr(agent, "_v3_variables", {}) + if variables: + result["variables"] = copy.deepcopy(variables) + result["pipeline"] = pipeline + if errors: + raise UnsupportedFieldError(errors) + return result + + +_MUTABLE = {"agent_name", "instructions", "messages", "greeting", "variables", "pipeline", "filler_words"} + + +def replacement_patch(previous, desired, category=None): + """Clear readable stale keys recursively. Never echo IDs or credential metadata.""" + result = copy.deepcopy(desired) + # Responses hide credential fields. Clear the known old vendor's field names + # before merging a new vendor, so hidden write-only keys cannot survive. + if isinstance(previous.get("credential"), dict) and isinstance(desired.get("credential"), dict) and desired["credential"].get("mode") == "byok": + old_fields = { + ("asr", "amazon"): {"access_key_id", "secret_access_key"}, + ("asr", "google"): {"adc_credentials_string"}, + ("asr", "microsoft"): {"key"}, ("asr", "speechmatics"): {"key"}, + ("llm", "amazon"): {"api_key", "access_key", "secret_key"}, + ("tts", "amazon"): {"aws_access_key_id", "aws_secret_access_key"}, + ("tts", "google"): {"credentials"}, ("tts", "sarvam"): {"api_subscription_key"}, + **{("tts", vendor): {"key"} for vendor in ("elevenlabs", "humeai", "microsoft", "minimax")}, + ("avatar", "sensetime"): {"app_key"}, ("avatar", "spatius"): {"spatius_api_key"}, + }.get((category, previous.get("vendor")), {"api_key"}) + result["credential"]["fields"] = {**{field: None for field in old_fields}, **result["credential"].get("fields", {})} + for key, old in previous.items(): + if key not in result: + result[key] = None + elif isinstance(old, dict) and isinstance(result[key], dict): + if key == "credential": + if result[key].get("mode") == "managed": + result[key]["fields"] = None + continue # Resolved credentials have read-only metadata and hide API keys. + result[key] = replacement_patch(old, result[key], key if key in {"asr", "llm", "tts", "avatar"} else category) + return result + + +def _require_etag(response): + if not response.etag: + raise ValueError("v3 response omitted ETag; refusing an unguarded update") + return response.etag + + +def _require_client(agent, async_): + from ..pool_client import Agora, AsyncAgora + expected = AsyncAgora if async_ else Agora + if not isinstance(agent._client, expected): + raise TypeError("save_async requires AsyncAgora" if async_ else "save requires Agora") + + +def save(agent, *, name=None, agent_id=None) -> SavedAgent: + _require_client(agent, False) + body = build_body(agent, name) + from ..v3.contract import _extras + _extras(body, saved=True) + raw = agent._client.v3.agents.with_raw_response + if agent_id is None: + response = raw.create(**body) + else: + previous = raw.get(agent_id=agent_id) + old = {k: v for k, v in previous.data.items() if k in _MUTABLE} + body = replacement_patch(old, body) + response = raw.update(agent_id=agent_id, if_match=_require_etag(previous), **body) + return SavedAgent(agent._client, response) + + +async def save_async(agent, *, name=None, agent_id=None) -> AsyncSavedAgent: + _require_client(agent, True) + body = build_body(agent, name) + from ..v3.contract import _extras + _extras(body, saved=True) + raw = agent._client.v3.agents.with_raw_response + if agent_id is None: + response = await raw.create(**body) + else: + previous = await raw.get(agent_id=agent_id) + old = {k: v for k, v in previous.data.items() if k in _MUTABLE} + response = await raw.update(agent_id=agent_id, if_match=_require_etag(previous), **replacement_patch(old, body)) + return AsyncSavedAgent(agent._client, response) + + +def _normalize(number): + return re.sub(r"[\s().-]", "", number) + + +def _number_id(number): + if isinstance(number, Mapping): + return number["number_id"] + if not isinstance(number, str) or not number.strip(): + raise ValueError("number must be a number ID, E.164 string or v3 number object") + return number + + +def _policy(max_call_duration_seconds, max_silence_duration_ms, call_policy=None): + if call_policy is None and max_call_duration_seconds is None and max_silence_duration_ms is None: + return None + policy = copy.deepcopy(call_policy) if call_policy is not None else {} + if not isinstance(policy, dict): + raise ValueError("call_policy must be a mapping") + for key, value in (("max_call_duration_seconds", max_call_duration_seconds), ("max_silence_duration_ms", max_silence_duration_ms)): + if value is not None: + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise ValueError(key + " must be a positive integer") + policy[key] = value + from ..v3.contract import validate + validate(policy, {"$ref": "#/components/schemas/InboundCallPolicy"}, "call_policy") + return policy + + +class _SavedAgentBase: + def __init__(self, client, response): + self._client = client + self.id: str = response.data["agent_id"] + self.name: str = response.data["agent_name"] + self.etag: Optional[str] = response.etag + self.data: AgentResponse = response.data + self.raw = client.v3.agents + +class SavedAgent(_SavedAgentBase): + def create_session(self, *, channel: str, agent_uid: Optional[str] = None, remote_uids: Optional[List[str]] = None, + name=None, client_reference=None, idle_timeout=None, + max_duration=None, labels=None, values=None, idempotency_key=None, token=None, + encryption=None, audio_scenario=None, graceful_stop=None, data_policy=None, geofence=None, debug=None, warn=None): + from .session_backend import create_session + return create_session(self, channel=channel, agent_uid=agent_uid, + remote_uids=remote_uids, name=name, client_reference=client_reference, + idle_timeout=idle_timeout, max_duration=max_duration, labels=labels, + values=values, idempotency_key=idempotency_key, token=token, encryption=encryption, + audio_scenario=audio_scenario, graceful_stop=graceful_stop, data_policy=data_policy, geofence=geofence, + debug=debug, warn=warn) + + def call(self, *, from_number: str, to: str, call_policy=None, **options): + """Configure an outbound phone session; call start() to place the call.""" + from .session_backend import create_call + return create_call(self, asynchronous=False, from_number=from_number, to=to, call_policy=call_policy, **options) + + @classmethod + def get(cls, client, agent_id) -> SavedAgent: + return cls(client, client.v3.agents.with_raw_response.get(agent_id=agent_id)) + + def update(self, config: Agent, *, name: Optional[str] = None) -> SavedAgent: + """Save a replacement configuration to this ID, preserving its name.""" + if config._client is not self._client: + raise ValueError("SavedAgent.update requires a configuration bound to the same client") + return config.save(name=self.name if name is None else name, agent_id=self.id) + + def delete(self): + return self.raw.delete(agent_id=self.id) + + def list_numbers(self) -> List[NumberResponse]: + """Scan project number pages and return numbers assigned to this agent.""" + return [ + number for number in self._client.v3.numbers.iter_all(limit=100) + if (number.get("inbound") or {}).get("agent") == self.id + ] + + def _resolve_number(self, number): + identifier = _number_id(number) + if identifier.startswith("+"): + cursor = None + seen = set() + while True: + page = self._client.v3.numbers.list(limit=100, cursor=cursor) + for item in page["data"]["list"]: + if _normalize(item["number"]) == _normalize(identifier): + return item["number_id"] + if not page.get("has_more"): + break + cursor = page.get("next_cursor") + if not cursor or cursor in seen: + raise ValueError("Invalid or repeated number pagination cursor") + seen.add(cursor) + raise NumberNotFoundError(f"Number {identifier} not found in project {self._client.app_id}") + return identifier + + def _assign(self, number, agent_id, policy): + identifier = self._resolve_number(number) + raw = self._client.v3.numbers.with_raw_response + for attempt in range(2): + previous = raw.get(number_id=identifier) + inbound = {"agent": agent_id} + if policy is not None: + inbound["call_policy"] = policy + try: + return raw.update(number_id=identifier, if_match=_require_etag(previous), inbound=inbound).data + except PreconditionFailedError: + if attempt: + raise + + def assign_number(self, number, *, max_call_duration_seconds=None, max_silence_duration_ms=None, call_policy=None) -> NumberResponse: + return self._assign(number, self.id, _policy(max_call_duration_seconds, max_silence_duration_ms, call_policy)) + + def unassign_number(self, number) -> NumberResponse: + return self._assign(number, None, None) + + +class AsyncSavedAgent(_SavedAgentBase): + def create_session(self, *, channel: str, agent_uid: Optional[str] = None, remote_uids: Optional[List[str]] = None, + name=None, client_reference=None, idle_timeout=None, + max_duration=None, labels=None, values=None, idempotency_key=None, token=None, + encryption=None, audio_scenario=None, graceful_stop=None, data_policy=None, geofence=None, debug=None, warn=None): + from .session_backend import create_session + return create_session(self, asynchronous=True, channel=channel, + agent_uid=agent_uid, remote_uids=remote_uids, name=name, + client_reference=client_reference, idle_timeout=idle_timeout, + max_duration=max_duration, labels=labels, values=values, idempotency_key=idempotency_key, + token=token, encryption=encryption, audio_scenario=audio_scenario, graceful_stop=graceful_stop, + data_policy=data_policy, geofence=geofence, debug=debug, warn=warn) + + create_async_session = create_session + + def call(self, *, from_number: str, to: str, call_policy=None, **options): + """Configure an outbound phone session; call start() to place the call.""" + from .session_backend import create_call + return create_call(self, asynchronous=True, from_number=from_number, to=to, call_policy=call_policy, **options) + + @classmethod + async def get(cls, client, agent_id) -> AsyncSavedAgent: + return cls(client, await client.v3.agents.with_raw_response.get(agent_id=agent_id)) + + async def update(self, config: Agent, *, name: Optional[str] = None) -> AsyncSavedAgent: + """Save a replacement configuration to this ID, preserving its name.""" + if config._client is not self._client: + raise ValueError("AsyncSavedAgent.update requires a configuration bound to the same client") + return await config.save_async(name=self.name if name is None else name, agent_id=self.id) + + async def delete(self): + return await self.raw.delete(agent_id=self.id) + + async def list_numbers(self) -> List[NumberResponse]: + """Scan project number pages and return numbers assigned to this agent.""" + return [ + number async for number in self._client.v3.numbers.iter_all(limit=100) + if (number.get("inbound") or {}).get("agent") == self.id + ] + + async def _resolve_number(self, number): + identifier = _number_id(number) + if identifier.startswith("+"): + cursor = None + seen = set() + while True: + page = await self._client.v3.numbers.list(limit=100, cursor=cursor) + for item in page["data"]["list"]: + if _normalize(item["number"]) == _normalize(identifier): + return item["number_id"] + if not page.get("has_more"): + break + cursor = page.get("next_cursor") + if not cursor or cursor in seen: + raise ValueError("Invalid or repeated number pagination cursor") + seen.add(cursor) + raise NumberNotFoundError(f"Number {identifier} not found in project {self._client.app_id}") + return identifier + + async def _assign(self, number, agent_id, policy): + identifier = await self._resolve_number(number) + raw = self._client.v3.numbers.with_raw_response + for attempt in range(2): + previous = await raw.get(number_id=identifier) + inbound = {"agent": agent_id} + if policy is not None: + inbound["call_policy"] = policy + try: + return (await raw.update(number_id=identifier, if_match=_require_etag(previous), inbound=inbound)).data + except PreconditionFailedError: + if attempt: + raise + + async def assign_number(self, number, *, max_call_duration_seconds=None, max_silence_duration_ms=None, call_policy=None) -> NumberResponse: + return await self._assign(number, self.id, _policy(max_call_duration_seconds, max_silence_duration_ms, call_policy)) + + async def unassign_number(self, number) -> NumberResponse: + return await self._assign(number, None, None) + +# Compatibility names are exact aliases; all paths share the same implementation. +DeployedAgent = SavedAgent +AsyncDeployedAgent = AsyncSavedAgent +deploy = save +deploy_async = save_async diff --git a/src/agora_agent/agentkit/vendors/llm.py b/src/agora_agent/agentkit/vendors/llm.py index 9398931..ed9e390 100644 --- a/src/agora_agent/agentkit/vendors/llm.py +++ b/src/agora_agent/agentkit/vendors/llm.py @@ -4,9 +4,10 @@ from .base import BaseLLM, McpServerInput, dump_config_models, ensure_mcp_transport from pydantic import BaseModel, ConfigDict, Field, model_validator +_OPENAI_MANAGED_MODELS = {"gpt-4o-mini", "gpt-4.1-mini", "gpt-5-nano", "gpt-5-mini"} # Legacy CN compatibility only. + LlmGreetingConfigs = Dict[str, Any] LlmToolInput = Union[Dict[str, Any], LlmTool] -_OPENAI_MANAGED_MODELS = {"gpt-4o-mini", "gpt-4.1-mini", "gpt-5-nano", "gpt-5-mini"} _dump_optional_model = dump_config_models @@ -44,10 +45,6 @@ def _validate_byok_params(self) -> "OpenAIOptions": raise ValueError("OpenAI requires base_url when api_key is set") if self.api_key is None and self.base_url is not None: raise ValueError("OpenAI base_url is only valid when api_key is set") - if self.api_key is None and self.model.strip().lower() not in _OPENAI_MANAGED_MODELS: - raise ValueError("OpenAI requires api_key unless using a supported Agora-managed model") - if self.api_key is None and self.vendor is not None: - raise ValueError("OpenAI Agora-managed mode does not allow vendor") return self @@ -90,8 +87,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "openai" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: @@ -107,7 +103,8 @@ class AzureOpenAIOptions(BaseModel): api_key: str = Field(..., description="Azure OpenAI API key") model: str = Field(..., description="Azure deployment model name") - endpoint: str = Field(..., description="Azure endpoint URL") + endpoint: Optional[str] = Field(default=None, description="Azure endpoint URL") + resource_name: Optional[str] = Field(default=None, description="Azure resource name; alternative to endpoint") deployment_name: str = Field(..., description="Azure deployment name") api_version: str = Field(default="2024-08-01-preview", description="Azure API version") temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0) @@ -128,11 +125,18 @@ class AzureOpenAIOptions(BaseModel): tools: Optional[List[LlmToolInput]] = Field(default=None) max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache") + @model_validator(mode="after") + def _validate_endpoint(self) -> "AzureOpenAIOptions": + if not self.endpoint and not self.resource_name: + raise ValueError("AzureOpenAI requires endpoint or resource_name") + return self + class AzureOpenAI(AzureOpenAIOptions, BaseLLM): def to_config(self) -> Dict[str, Any]: + endpoint = self.endpoint or f"https://{self.resource_name}.openai.azure.com" url = ( - f"{self.endpoint}/openai/deployments/" + f"{endpoint.rstrip('/')}/openai/deployments/" f"{self.deployment_name}/chat/completions" f"?api-version={self.api_version}" ) @@ -240,8 +244,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "anthropic" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: @@ -296,6 +299,7 @@ def to_config(self) -> Dict[str, Any]: f"https://generativelanguage.googleapis.com/v1beta/models/" f"{self.model}:streamGenerateContent?alt=sse&key={self.api_key}" ), + "api_key": self.api_key, "params": params, "style": "gemini", "input_modalities": self.input_modalities or ["text"], @@ -317,8 +321,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "gemini" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: @@ -396,8 +399,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "groq" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: @@ -558,8 +560,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "vertexai" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: @@ -574,8 +575,9 @@ def to_config(self) -> Dict[str, Any]: class AmazonBedrockOptions(BaseModel): model_config = ConfigDict(extra="forbid") - access_key: str = Field(..., description="AWS access key ID") - secret_key: str = Field(..., description="AWS secret access key") + api_key: Optional[str] = Field(default=None, description="Bedrock API key") + access_key: Optional[str] = Field(default=None, description="AWS access key ID") + secret_key: Optional[str] = Field(default=None, description="AWS secret access key") region: str = Field(..., description="AWS region") model: str = Field(..., description="Amazon Bedrock model identifier") max_tokens: Optional[int] = Field(default=None, gt=0) @@ -597,6 +599,15 @@ class AmazonBedrockOptions(BaseModel): tools: Optional[List[LlmToolInput]] = Field(default=None) max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache") + @model_validator(mode="after") + def _validate_credentials(self) -> "AmazonBedrockOptions": + if self.api_key is not None: + if not self.api_key.strip() or self.access_key is not None or self.secret_key is not None: + raise ValueError("Provide a Bedrock api_key or access_key and secret_key, not both") + elif not self.access_key or not self.secret_key: + raise ValueError("Provide a Bedrock api_key or access_key and secret_key") + return self + class AmazonBedrock(AmazonBedrockOptions, BaseLLM): def to_config(self) -> Dict[str, Any]: @@ -610,14 +621,17 @@ def to_config(self) -> Dict[str, Any]: config: Dict[str, Any] = { "url": self.url or f"https://bedrock-runtime.{self.region}.amazonaws.com/model/{self.model}/converse-stream", - "access_key": self.access_key, - "secret_key": self.secret_key, "region": self.region, "model": self.model, "params": params, "style": "bedrock", "input_modalities": self.input_modalities or ["text"], } + if self.api_key is not None: + config["api_key"] = self.api_key + else: + config["access_key"] = self.access_key + config["secret_key"] = self.secret_key if self.system_messages is not None: config["system_messages"] = self.system_messages if self.headers is not None: @@ -634,8 +648,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "amazon" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: @@ -700,8 +713,7 @@ def to_config(self) -> Dict[str, Any]: config["greeting_configs"] = _dump_optional_model(self.greeting_configs) if self.template_variables is not None: config["template_variables"] = self.template_variables - if self.vendor is not None: - config["vendor"] = self.vendor + config["vendor"] = self.vendor or "dify" if self.mcp_servers is not None: config["mcp_servers"] = ensure_mcp_transport(self.mcp_servers) if self.tools is not None: diff --git a/src/agora_agent/agentkit/vendors/tts.py b/src/agora_agent/agentkit/vendors/tts.py index 6e5a60b..5915a9c 100644 --- a/src/agora_agent/agentkit/vendors/tts.py +++ b/src/agora_agent/agentkit/vendors/tts.py @@ -633,7 +633,7 @@ class SarvamTTSOptions(BaseModel): default=None, description="Normalize English words and numeric entities before synthesis", ) - model: Optional[str] = Field(default=None, description="Sarvam TTS model") + model: Optional[str] = Field(default=None, description="Sarvam TTS model; defaults to bulbul:v3 when omitted") sample_rate: Optional[int] = Field( default=None, gt=0, @@ -687,6 +687,7 @@ def to_config(self) -> Dict[str, Any]: params["enable_preprocessing"] = self.enable_preprocessing if self.model is not None: params["model"] = self.model + params.setdefault("model", "bulbul:v3") result: Dict[str, Any] = {"vendor": "sarvam", "params": params} if self.skip_patterns is not None: diff --git a/src/agora_agent/client.py b/src/agora_agent/client.py index 8a981af..1ec2270 100644 --- a/src/agora_agent/client.py +++ b/src/agora_agent/client.py @@ -34,8 +34,6 @@ class Agora: authorization : str - username : typing.Union[str, typing.Callable[[], str]] - password : typing.Union[str, typing.Callable[[], str]] headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -53,9 +51,7 @@ class Agora: from agora_agent import Agora client = Agora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + authorization="agora token=YOUR_TOKEN", ) """ @@ -65,21 +61,22 @@ def __init__( base_url: typing.Optional[str] = None, environment: AgoraEnvironment = AgoraEnvironment.DEFAULT, authorization: str, - username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, ): + from .core.public_auth import validate_public_headers + validate_public_headers(headers) + validate_public_headers({"Authorization": authorization}, allow_empty_authorization=True) _defaulted_timeout = ( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) self._client_wrapper = SyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), authorization=authorization, - username=username, - password=password, + username="", + password="", headers=headers, httpx_client=httpx_client if httpx_client is not None @@ -145,8 +142,6 @@ class AsyncAgora: authorization : str - username : typing.Union[str, typing.Callable[[], str]] - password : typing.Union[str, typing.Callable[[], str]] headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -164,9 +159,7 @@ class AsyncAgora: from agora_agent import AsyncAgora client = AsyncAgora( - authorization="YOUR_AUTHORIZATION", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", + authorization="agora token=YOUR_TOKEN", ) """ @@ -176,21 +169,22 @@ def __init__( base_url: typing.Optional[str] = None, environment: AgoraEnvironment = AgoraEnvironment.DEFAULT, authorization: str, - username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, ): + from .core.public_auth import validate_public_headers + validate_public_headers(headers) + validate_public_headers({"Authorization": authorization}, allow_empty_authorization=True) _defaulted_timeout = ( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) self._client_wrapper = AsyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), authorization=authorization, - username=username, - password=password, + username="", + password="", headers=headers, httpx_client=httpx_client if httpx_client is not None diff --git a/src/agora_agent/core/client_wrapper.py b/src/agora_agent/core/client_wrapper.py index 4f480c9..01a9171 100644 --- a/src/agora_agent/core/client_wrapper.py +++ b/src/agora_agent/core/client_wrapper.py @@ -3,6 +3,7 @@ import typing import httpx +from ..version import __version__ from .http_client import AsyncHttpClient, HttpClient @@ -18,6 +19,7 @@ def __init__( timeout: typing.Optional[float] = None, ): self._authorization = authorization + self._authorization_supplier: typing.Optional[typing.Callable[[], str]] = None self._username = username self._password = password self._headers = headers @@ -26,14 +28,14 @@ def __init__( def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { - "User-Agent": "agora-agents/v2.13.0", + "User-Agent": f"agora-agents/v{__version__}", "X-Fern-Language": "Python", "X-Fern-SDK-Name": "agora-agents", - "X-Fern-SDK-Version": "v2.13.0", + "X-Fern-SDK-Version": f"v{__version__}", **(self.get_custom_headers() or {}), } - headers["Authorization"] = httpx.BasicAuth(self._get_username(), self._get_password())._auth_header - headers["Authorization"] = self._authorization + supplier = getattr(self, "_authorization_supplier", None) + headers["Authorization"] = supplier() if supplier is not None else self._authorization return headers def _get_username(self) -> str: diff --git a/src/agora_agent/core/http_client.py b/src/agora_agent/core/http_client.py index e4173f9..e1fdbd5 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -13,6 +13,12 @@ from .file import File, convert_file_dict_to_httpx_tuples from .force_multipart import FORCE_MULTIPART from .jsonable_encoder import jsonable_encoder +from .public_auth import ( + merge_public_headers, + validate_effective_public_headers, + validate_public_headers, + validate_public_options, +) from .query_encoder import encode_query from .remove_none_from_dict import remove_none_from_dict from .request_options import RequestOptions @@ -192,6 +198,23 @@ def request( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: + validate_public_options(request_options) + # App-certificate clients can have an empty base Authorization + # placeholder because sessions supply a fresh ConvoAI token per call. + # Explicit client/per-request overrides are validated separately. + base_headers = self.base_headers() + validate_public_headers(base_headers, allow_empty_authorization=True) + validate_public_headers(headers) + transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) + validate_public_headers(getattr(transport_client, "headers", {})) + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") + request_headers = merge_public_headers( + base_headers, + headers or {}, + (request_options or {}).get("additional_headers", {}) or {}, + ) + validate_effective_public_headers(getattr(transport_client, "headers", {}), request_headers) base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -210,18 +233,10 @@ def request( if (request_files is None or len(request_files) == 0) and force_multipart: request_files = FORCE_MULTIPART - response = self.httpx_client.request( + response = _request_with_reload_headers(self.httpx_client, bool((request_options or {}).get("_strip_write_preconditions")), method=method, url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ), + headers=jsonable_encoder(remove_none_from_dict(request_headers)), params=encode_query( jsonable_encoder( remove_none_from_dict( @@ -289,6 +304,20 @@ def stream( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: + validate_public_options(request_options) + base_headers = self.base_headers() + validate_public_headers(base_headers, allow_empty_authorization=True) + validate_public_headers(headers) + transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) + validate_public_headers(getattr(transport_client, "headers", {})) + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") + request_headers = merge_public_headers( + base_headers, + headers or {}, + (request_options or {}).get("additional_headers", {}), + ) + validate_effective_public_headers(getattr(transport_client, "headers", {}), request_headers) base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -310,15 +339,7 @@ def stream( with self.httpx_client.stream( method=method, url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) if request_options is not None else {}), - } - ) - ), + headers=jsonable_encoder(remove_none_from_dict(request_headers)), params=encode_query( jsonable_encoder( remove_none_from_dict( @@ -390,6 +411,20 @@ async def request( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: + validate_public_options(request_options) + base_headers = self.base_headers() + validate_public_headers(base_headers, allow_empty_authorization=True) + validate_public_headers(headers) + transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) + validate_public_headers(getattr(transport_client, "headers", {})) + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") + request_headers = merge_public_headers( + base_headers, + headers or {}, + (request_options or {}).get("additional_headers", {}) or {}, + ) + validate_effective_public_headers(getattr(transport_client, "headers", {}), request_headers) base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -409,18 +444,10 @@ async def request( json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) # Add the input to each of these and do None-safety checks - response = await self.httpx_client.request( + response = await _async_request_with_reload_headers(self.httpx_client, bool((request_options or {}).get("_strip_write_preconditions")), method=method, url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ), + headers=jsonable_encoder(remove_none_from_dict(request_headers)), params=encode_query( jsonable_encoder( remove_none_from_dict( @@ -487,6 +514,20 @@ async def stream( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: + validate_public_options(request_options) + base_headers = self.base_headers() + validate_public_headers(base_headers, allow_empty_authorization=True) + validate_public_headers(headers) + transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) + validate_public_headers(getattr(transport_client, "headers", {})) + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") + request_headers = merge_public_headers( + base_headers, + headers or {}, + (request_options or {}).get("additional_headers", {}), + ) + validate_effective_public_headers(getattr(transport_client, "headers", {}), request_headers) base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -508,15 +549,7 @@ async def stream( async with self.httpx_client.stream( method=method, url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) if request_options is not None else {}), - } - ) - ), + headers=jsonable_encoder(remove_none_from_dict(request_headers)), params=encode_query( jsonable_encoder( remove_none_from_dict( @@ -541,3 +574,26 @@ async def stream( timeout=timeout, ) as stream: yield stream + + +def _without_write_conditions(request: httpx.Request) -> httpx.Request: + # Applied after httpx merges SDK and HTTP-client defaults, without mutating + # any shared client/header maps or losing auth, cookies and request context. + for name in ("If-Match", "If-None-Match", "Idempotency-Key"): + if name in request.headers: + del request.headers[name] + return request + + +def _request_with_reload_headers(client, strip_conditions, **kwargs): + if not strip_conditions: + return client.request(**kwargs) + request = _without_write_conditions(client.build_request(**kwargs)) + return client.send(request) + + +async def _async_request_with_reload_headers(client, strip_conditions, **kwargs): + if not strip_conditions: + return await client.request(**kwargs) + request = _without_write_conditions(client.build_request(**kwargs)) + return await client.send(request) diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py new file mode 100644 index 0000000..7c7d5e2 --- /dev/null +++ b/src/agora_agent/core/public_auth.py @@ -0,0 +1,78 @@ +"""Public SDK authentication policy (including transport overrides).""" +import base64 +import binascii +import re + + +def resolve_client_auth(app_certificate, auth_token, customer_id, customer_secret): + """Resolve REST authentication independently of optional RTC signing.""" + if customer_id is not None or customer_secret is not None: + if not all(isinstance(value, str) and value.strip() for value in (customer_id, customer_secret)): + raise ValueError("Basic authentication requires nonempty customer_id and customer_secret") + if ":" in customer_id: + raise ValueError("customer_id must not contain a colon") + if auth_token is not None: + raise ValueError("Supply customer_id/customer_secret or auth_token, not both") + encoded = base64.b64encode(f"{customer_id}:{customer_secret}".encode("utf-8")).decode("ascii") + return "basic", f"Basic {encoded}" + if auth_token is not None: + if not isinstance(auth_token, str) or not auth_token.strip(): + raise ValueError("auth_token must be a nonempty raw token") + return "token", f"agora token={auth_token}" + if not isinstance(app_certificate, str) or not app_certificate.strip(): + raise ValueError("app_certificate is required unless auth_token or customer_id/customer_secret is supplied") + return "app-credentials", "" + + +def _is_basic_authorization(value): + match = re.fullmatch(r"Basic ([A-Za-z0-9+/]+={0,2})", value, re.IGNORECASE) + if match is None: + return False + try: + decoded = base64.b64decode(match.group(1), validate=True).decode("utf-8") + except (ValueError, binascii.Error, UnicodeDecodeError): + return False + username, separator, password = decoded.partition(":") + return bool(separator and username.strip() and password.strip()) + + +def validate_public_headers(headers, *, allow_empty_authorization=False): + source = headers or {} + items = source.multi_items() if hasattr(source, "multi_items") else source.items() + for name, value in items: + lower = name.lower() + if lower.startswith("x-studio-") or lower == "x-consumer-username" or lower.startswith("x-agora-gateway") or lower in {"gateway", "x-gateway-token", "x-gateway-key"}: + raise ValueError("Internal gateway/region headers are not public: " + name) + if lower == "idempotency-key" and value is not None and (not isinstance(value, str) or re.fullmatch(r"[!-~]{1,256}", value) is None): + raise ValueError("Idempotency-Key must contain 1-256 visible ASCII characters") + if lower == "authorization" and value is not None and not (allow_empty_authorization and value == ""): + if not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE) and not _is_basic_authorization(str(value)): + raise ValueError("Authorization must use an Agora token or valid Basic credentials") + + +def validate_public_options(options): + forbidden = {"customer_id", "customer_secret", "client_id", "client_secret", "customerId", "customerSecret", "clientId", "clientSecret", "username", "password"} + if forbidden.intersection(options or {}): + raise ValueError("Configure Basic credentials with customer_id/customer_secret on the client, or an Authorization header") + validate_public_headers((options or {}).get("additional_headers", {})) + + +def validate_effective_public_headers(*sources): + """Validate the case-insensitive merged headers that will reach HTTPX.""" + merged = merge_public_headers(*sources) + validate_public_headers(merged) + if not any(name.lower() == "authorization" for name in merged): + raise ValueError("Agora token or Basic authorization is required") + + +def merge_public_headers(*sources): + """Merge header sources with HTTPX's case-insensitive precedence rules.""" + import httpx + + merged = httpx.Headers() + for source in sources: + if not source: + continue + items = source.multi_items() if hasattr(source, "multi_items") else source.items() + merged.update([(name, value) for name, value in items if value is not None]) + return dict(merged) diff --git a/src/agora_agent/core/pydantic_utilities.py b/src/agora_agent/core/pydantic_utilities.py index 1da6f10..d3fe7ad 100644 --- a/src/agora_agent/core/pydantic_utilities.py +++ b/src/agora_agent/core/pydantic_utilities.py @@ -37,7 +37,7 @@ def parse_obj_as(type_: Type[T], object_: Any) -> T: if IS_PYDANTIC_V2: - adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + adapter: "pydantic.TypeAdapter[T]" = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] return adapter.validate_python(object_) return pydantic.parse_obj_as(type_, object_) @@ -51,13 +51,13 @@ def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any class UniversalBaseModel(pydantic.BaseModel): - class Config: - populate_by_name = True - smart_union = True - allow_population_by_field_name = True - json_encoders = {dt.datetime: serialize_datetime} - # Allow fields beginning with `model_` to be used in the model - protected_namespaces = () + if IS_PYDANTIC_V2: + model_config = pydantic.ConfigDict(populate_by_name=True, protected_namespaces=(), json_encoders={dt.datetime: serialize_datetime}) + else: + class Config: + smart_union = True + allow_population_by_field_name = True + json_encoders = {dt.datetime: serialize_datetime} def json(self, **kwargs: Any) -> str: kwargs_with_defaults = { diff --git a/src/agora_agent/pool_client.py b/src/agora_agent/pool_client.py index 34a1514..61735ef 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -2,24 +2,25 @@ from __future__ import annotations -import base64 import json import logging import typing import httpx import typing_extensions - +from .agentkit.debug import redact_headers, redact_secrets, redact_url from .agentkit.regional_agent import RegionalAgent +from .agentkit.token import generate_convo_ai_token from .agentkit.vendors.region import AreaScope, area_to_scope from .client import Agora as BaseAgora from .client import AsyncAgora as BaseAsyncAgora from .core.api_error import ApiError from .core.domain import Area, Pool -from .agentkit.debug import redact_headers, redact_secrets, redact_url -from .agentkit.token import generate_convo_ai_token +from .v3 import AsyncV3Client, V3Client, V3Options +from .v3.campaigns import AsyncCampaignsClient, CampaignsClient +from .v3.client import AsyncNumbersClient, AsyncSecretsClient, NumbersClient, SecretsClient -_AUTH_MODE = typing.Literal["app-credentials", "basic", "token"] +_AUTH_MODE = typing.Literal["app-credentials", "token", "basic"] _DEBUG_LOGGER = logging.getLogger("agora_agent") _AreaT = typing.TypeVar("_AreaT", bound=Area) _GlobalArea = typing_extensions.Literal[Area.US, Area.EU, Area.AP] @@ -107,10 +108,6 @@ def _create_debug_async_client( ) -def _basic_auth_header(customer_id: str, customer_secret: str) -> str: - encoded = base64.b64encode(f"{customer_id}:{customer_secret}".encode()).decode() - return f"Basic {encoded}" - class Agora(BaseAgora, typing.Generic[_AreaT]): """ @@ -133,21 +130,17 @@ class Agora(BaseAgora, typing.Generic[_AreaT]): app_certificate : str Your Agora App Certificate. - customer_id : typing.Optional[str] - Customer ID for Basic auth. When provided together with - ``customer_secret``, the client uses - ``Authorization: Basic base64(customer_id:customer_secret)``. - When omitted the client operates in *app-credentials* mode and - auto-generates a ConvoAI token per request. - - customer_secret : typing.Optional[str] - Customer secret for Basic auth (required when ``customer_id`` is set). - auth_token : typing.Optional[str] Pre-built raw token for REST API authentication. Pass the raw token string — the SDK automatically sets the header as ``Authorization: agora token=``. - Mutually exclusive with ``customer_id``/``customer_secret``. + + customer_id : typing.Optional[str] + Customer ID for Basic REST authentication; requires customer_secret. + + customer_secret : typing.Optional[str] + Customer Secret for Basic REST authentication. Cannot be combined with auth_token. + An optional app_certificate remains available for RTC token signing. headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -184,15 +177,14 @@ class Agora(BaseAgora, typing.Generic[_AreaT]): app_certificate="YOUR_APP_CERTIFICATE", ) - # Basic auth mode + # Prebuilt token mode from agora_agent import Agora, Area client = Agora( area=Area.US, app_id="YOUR_APP_ID", app_certificate="YOUR_APP_CERTIFICATE", - customer_id="YOUR_CUSTOMER_ID", - customer_secret="YOUR_CUSTOMER_SECRET", + auth_token="YOUR_TOKEN", ) # Pre-built token mode (for debugging or custom token lifecycles) @@ -214,15 +206,16 @@ def __new__( *, area: typing_extensions.Literal[Area.CN], app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "CNAgora": ... @typing.overload @@ -231,15 +224,16 @@ def __new__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "GlobalAgoraClient": ... @typing.overload @@ -248,15 +242,16 @@ def __new__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "Agora[_AreaT]": ... def __new__(cls, **kwargs: typing.Any) -> typing.Any: @@ -274,15 +269,16 @@ def __init__( *, area: typing_extensions.Literal[Area.CN], app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -291,15 +287,16 @@ def __init__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -308,15 +305,16 @@ def __init__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... def __init__( @@ -324,35 +322,26 @@ def __init__( *, area: typing.Any, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ): + from .core.public_auth import resolve_client_auth, validate_public_headers + validate_public_headers(headers) + self.auth_mode, authorization = resolve_client_auth(app_certificate, auth_token, customer_id, customer_secret) + self._v3_options = v3 self._pool = Pool(area) self._area = area self.app_id = app_id self.app_certificate = app_certificate - if customer_id is not None: - if customer_secret is None: - raise ValueError("customer_secret is required when customer_id is provided") - if auth_token is not None: - raise ValueError("auth_token and customer_id/customer_secret are mutually exclusive") - self.auth_mode: _AUTH_MODE = "basic" - authorization = _basic_auth_header(customer_id, customer_secret) - elif auth_token is not None: - self.auth_mode = "token" - authorization = f"agora token={auth_token}" - else: - self.auth_mode = "app-credentials" - authorization = "" - if debug and httpx_client is None: _DEBUG_LOGGER.setLevel(logging.DEBUG) if not _DEBUG_LOGGER.handlers: @@ -367,13 +356,36 @@ def __init__( super().__init__( base_url=self._pool.get_current_url(), authorization=authorization, - username="", - password="", headers=headers, timeout=timeout, follow_redirects=follow_redirects, httpx_client=httpx_client, ) + if self.auth_mode == "app-credentials": + self._client_wrapper._authorization_supplier = lambda: "agora token=" + generate_convo_ai_token( + app_id=self.app_id, app_certificate=typing.cast(str, self.app_certificate), channel_name="", uid=0, + ) + + @property + def v3(self) -> V3Client: + if not hasattr(self, "_v3_client"): + self._v3_client = V3Client(self, self._v3_options) + return self._v3_client + + @property + def campaigns(self) -> CampaignsClient: + """Project campaign operations using the shared token transport.""" + return self.v3.campaigns + + @property + def secrets(self) -> SecretsClient: + """Project secret provisioning with the shared client transport.""" + return self.v3.secrets + + @property + def numbers(self) -> NumbersClient: + """Project number provisioning with the shared client transport.""" + return self.v3.numbers @property def pool(self) -> Pool: @@ -450,7 +462,7 @@ def stop_agent(self, agent_id: str) -> None: if self.auth_mode == "app-credentials": token = generate_convo_ai_token( app_id=self.app_id, - app_certificate=self.app_certificate, + app_certificate=typing.cast(str, self.app_certificate), channel_name="stop", uid=0, ) @@ -485,21 +497,17 @@ class AsyncAgora(BaseAsyncAgora, typing.Generic[_AreaT]): app_certificate : str Your Agora App Certificate. - customer_id : typing.Optional[str] - Customer ID for Basic auth. When provided together with - ``customer_secret``, the client uses - ``Authorization: Basic base64(customer_id:customer_secret)``. - When omitted the client operates in *app-credentials* mode and - auto-generates a ConvoAI token per request. - - customer_secret : typing.Optional[str] - Customer secret for Basic auth (required when ``customer_id`` is set). - auth_token : typing.Optional[str] Pre-built raw token for REST API authentication. Pass the raw token string — the SDK automatically sets the header as ``Authorization: agora token=``. - Mutually exclusive with ``customer_id``/``customer_secret``. + + customer_id : typing.Optional[str] + Customer ID for Basic REST authentication; requires customer_secret. + + customer_secret : typing.Optional[str] + Customer Secret for Basic REST authentication. Cannot be combined with auth_token. + An optional app_certificate remains available for RTC token signing. headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -532,15 +540,14 @@ class AsyncAgora(BaseAsyncAgora, typing.Generic[_AreaT]): app_certificate="YOUR_APP_CERTIFICATE", ) - # Basic auth mode + # Prebuilt token mode from agora_agent import AsyncAgora, Area client = AsyncAgora( area=Area.US, app_id="YOUR_APP_ID", app_certificate="YOUR_APP_CERTIFICATE", - customer_id="YOUR_CUSTOMER_ID", - customer_secret="YOUR_CUSTOMER_SECRET", + auth_token="YOUR_TOKEN", ) """ @@ -550,15 +557,16 @@ def __new__( *, area: typing_extensions.Literal[Area.CN], app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "CNAsyncAgora": ... @typing.overload @@ -567,15 +575,16 @@ def __new__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "GlobalAsyncAgoraClient": ... @typing.overload @@ -584,15 +593,16 @@ def __new__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "AsyncAgora[_AreaT]": ... def __new__(cls, **kwargs: typing.Any) -> typing.Any: @@ -610,15 +620,16 @@ def __init__( *, area: typing_extensions.Literal[Area.CN], app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -627,15 +638,16 @@ def __init__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -644,15 +656,16 @@ def __init__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... def __init__( @@ -660,35 +673,26 @@ def __init__( *, area: typing.Any, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, + auth_token: typing.Optional[str] = None, customer_id: typing.Optional[str] = None, customer_secret: typing.Optional[str] = None, - auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ): + from .core.public_auth import resolve_client_auth, validate_public_headers + validate_public_headers(headers) + self.auth_mode, authorization = resolve_client_auth(app_certificate, auth_token, customer_id, customer_secret) + self._v3_options = v3 self._pool = Pool(area) self._area = area self.app_id = app_id self.app_certificate = app_certificate - if customer_id is not None: - if customer_secret is None: - raise ValueError("customer_secret is required when customer_id is provided") - if auth_token is not None: - raise ValueError("auth_token and customer_id/customer_secret are mutually exclusive") - self.auth_mode: _AUTH_MODE = "basic" - authorization = _basic_auth_header(customer_id, customer_secret) - elif auth_token is not None: - self.auth_mode = "token" - authorization = f"agora token={auth_token}" - else: - self.auth_mode = "app-credentials" - authorization = "" - if debug and httpx_client is None: _DEBUG_LOGGER.setLevel(logging.DEBUG) if not _DEBUG_LOGGER.handlers: @@ -703,13 +707,36 @@ def __init__( super().__init__( base_url=self._pool.get_current_url(), authorization=authorization, - username="", - password="", headers=headers, timeout=timeout, follow_redirects=follow_redirects, httpx_client=httpx_client, ) + if self.auth_mode == "app-credentials": + self._client_wrapper._authorization_supplier = lambda: "agora token=" + generate_convo_ai_token( + app_id=self.app_id, app_certificate=typing.cast(str, self.app_certificate), channel_name="", uid=0, + ) + + @property + def v3(self) -> AsyncV3Client: + if not hasattr(self, "_v3_client"): + self._v3_client = AsyncV3Client(self, self._v3_options) + return self._v3_client + + @property + def campaigns(self) -> AsyncCampaignsClient: + """Project campaign operations using the shared token transport.""" + return self.v3.campaigns + + @property + def secrets(self) -> AsyncSecretsClient: + """Async project secret provisioning with the shared transport.""" + return self.v3.secrets + + @property + def numbers(self) -> AsyncNumbersClient: + """Async project number provisioning with the shared transport.""" + return self.v3.numbers @property def pool(self) -> Pool: @@ -786,7 +813,7 @@ async def stop_agent(self, agent_id: str) -> None: if self.auth_mode == "app-credentials": token = generate_convo_ai_token( app_id=self.app_id, - app_certificate=self.app_certificate, + app_certificate=typing.cast(str, self.app_certificate), channel_name="stop", uid=0, ) diff --git a/src/agora_agent/v3/__init__.py b/src/agora_agent/v3/__init__.py new file mode 100644 index 0000000..5868df4 --- /dev/null +++ b/src/agora_agent/v3/__init__.py @@ -0,0 +1,6 @@ +"""Stored-agent, session lifecycle and campaign preview resources.""" +from . import types +from ._transport import PreconditionFailedError, RawResponse, V3Options +from .client import AsyncV3Client, V3Client + +__all__ = ["V3Options", "V3Client", "AsyncV3Client", "RawResponse", "PreconditionFailedError", "types"] diff --git a/src/agora_agent/v3/_transport.py b/src/agora_agent/v3/_transport.py new file mode 100644 index 0000000..84ce72f --- /dev/null +++ b/src/agora_agent/v3/_transport.py @@ -0,0 +1,201 @@ +"""v3 transport adapter: share the owning client's auth, HTTP client and pool.""" +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Any, Generic, Mapping, Optional, TypeVar +from urllib.parse import quote, urlsplit + +import httpx +from ..agentkit.token import generate_convo_ai_token +from ..core.api_error import ApiError +from ..core.public_auth import validate_public_headers as _validate_public_headers + +_UNSET: Any = object() +T = TypeVar("T") + + +@dataclass(frozen=True) +class V3Options: + """Optional origin/header overrides; default public preview uses the v1 project path.""" + base_url: Optional[str] = None + headers: Mapping[str, str] = field(default_factory=dict) + + +@dataclass(frozen=True) +class RawResponse(Generic[T]): + data: T + headers: httpx.Headers + status_code: int + response: httpx.Response + + @property + def etag(self) -> Optional[str]: + return self.headers.get("ETag") + + +class PreconditionFailedError(ApiError): + """The server rejected an outdated If-Match. latest() reloads the resource. + + With AsyncAgora, await error.latest(). No write is automatically retried. + """ + def latest(self): + return self._latest() + + + +def segment(value: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError("v3 path identifiers must be nonempty strings") + # RFC 3986 permits unescaped dots in a path segment. HTTP clients and + # proxies normalize the two dot-segments before sending the request. + if value in (".", ".."): + raise ValueError("v3 path identifiers cannot be dot segments") + return quote(value, safe="") + + +def _body(value: Any) -> Any: + if hasattr(value, "model_dump"): + value = value.model_dump(exclude_unset=True) + if isinstance(value, dict): + return {k: _body(v) for k, v in value.items() if v is not _UNSET} + if isinstance(value, (list, tuple)): + return [_body(v) for v in value] + return value + + +class Resource: + def __init__(self, owner: Any, options: Optional[V3Options]): + self.owner = owner + self.options = options or V3Options() + + def _arguments(self, method, path, body, headers, params, request_options): + wrapper = self.owner._client_wrapper + body = _body(body) + from .contract import validate_write + additional = (request_options or {}).get("additional_body_parameters", {}) or {} + if body is None and additional and method not in {"GET", "DELETE"}: + raise ValueError("This operation does not accept a request body") + validate_write(method, path, {**body, **additional} if isinstance(body, dict) else body) + if method == "POST" and path.endswith(("/sessions", "/sessions/ephemeral")): + from .session_contract import validate_session + validate_session({**body, **((request_options or {}).get("additional_body_parameters", {}) or {})}) + base = self.options.base_url or os.environ.get("AGORA_AGENTS_V3_BASE_URL") + if not base: + base = "https://preview.ai.agora.io" + parsed = urlsplit(base) + if parsed.scheme not in ("https", "http") or not parsed.netloc or parsed.path not in ("", "/") or parsed.query or parsed.fragment or parsed.username is not None or parsed.password is not None or "?" in base or "#" in base: + raise ValueError("v3 base_url must be an HTTP(S) origin without query or fragment") + base = base.rstrip("/") + _validate_public_headers(self.options.headers) + merged = dict(self.options.headers) + merged.update({k: v for k, v in headers.items() if v is not None}) + if getattr(self.owner, "auth_mode", None) == "app-credentials" and not any(name.lower() == "authorization" for name in merged): + token = generate_convo_ai_token(app_id=self.owner.app_id, + app_certificate=self.owner.app_certificate, channel_name="v3", uid=0) + merged["Authorization"] = f"agora token={token}" + options = dict(request_options or {}) + forbidden = {"customer_id", "customer_secret", "client_id", "client_secret", "customerId", "customerSecret", "clientId", "clientSecret", "username", "password"} + if forbidden.intersection(options): + raise ValueError("Configure Basic credentials with customer_id/customer_secret on the client, or an Authorization header") + _validate_public_headers(wrapper.get_headers()) + _validate_public_headers(wrapper.get_custom_headers() or {}) + _validate_public_headers(merged) + _validate_public_headers(options.get("additional_headers", {})) + if method == "POST" and path.endswith(("/secrets", "/numbers", "/agents")): + if any(k.lower() == "idempotency-key" for source in (wrapper.get_headers(), merged, options.get("additional_headers", {})) for k in source): + raise ValueError("Idempotency-Key is not supported for agent, secret or number creation") + # Retrying these POSTs can create duplicate resources; staging ignores number idempotency. + if method != "GET": + options["max_retries"] = 0 + return dict(path=path, method=method, base_url=base, json=_body(body), + headers=merged, params=params, request_options=options) + + @staticmethod + def _parse(response): + try: + data = response.json() if response.content else None + except ValueError: + data = response.text + if not 200 <= response.status_code < 300: + error = PreconditionFailedError if response.status_code == 412 else ApiError + raise error(status_code=response.status_code, headers=dict(response.headers), body=data) + if isinstance(data, dict) and "agent_session_id" in data: + data = dict(data) + if "created_at" in data and "create_ts" not in data: + data["create_ts"] = data.pop("created_at") + if isinstance(data.get("status"), str): + data["status"] = data["status"].upper() + return RawResponse(data=data, headers=response.headers, status_code=response.status_code, response=response) + + def _request(self, method, path, *, body, headers, params, request_options): + try: + return self._parse(self.owner._client_wrapper.httpx_client.request( + **self._arguments(method, path, body, headers, params, request_options))) + except PreconditionFailedError as error: + latest_path = path.split("/keys/")[0] + error._latest = lambda: self._request("GET", latest_path, body=None, headers={}, params={}, request_options=_reload_options(request_options)).data + raise + + def iter_pages(self, **kwargs): + """Yield cursor pages for this resource; list() itself returns one typed wire page.""" + seen = set() + while True: + page = self.list(**kwargs) + yield page + if not page.get("has_more"): + return + cursor = page.get("next_cursor") + if not cursor or cursor in seen: + raise ValueError("Invalid or repeated v3 pagination cursor") + seen.add(cursor) + kwargs["cursor"] = cursor + + def iter_all(self, **kwargs): + for page in self.iter_pages(**kwargs): + yield from page["data"]["list"] + + +def _reload_options(request_options): + """Keep request context without replaying write preconditions on the reload.""" + options = dict(request_options or {}) + options["_strip_write_preconditions"] = True + # Additional body parameters belong to the failed write. The reload is a + # GET and must not carry that body into the fresh read. + options.pop("additional_body_parameters", None) + options["additional_headers"] = { + key: value for key, value in options.get("additional_headers", {}).items() + if key.lower() not in {"if-match", "if-none-match", "idempotency-key"} + } + return options + + +class AsyncResource(Resource): + async def _request(self, method, path, *, body, headers, params, request_options): + try: + return self._parse(await self.owner._client_wrapper.httpx_client.request( + **self._arguments(method, path, body, headers, params, request_options))) + except PreconditionFailedError as error: + async def latest(): + response = await self._request("GET", path.split("/keys/")[0], body=None, headers={}, params={}, request_options=_reload_options(request_options)) + return response.data + error._latest = latest + raise + + async def iter_pages(self, **kwargs): + seen = set() + while True: + page = await self.list(**kwargs) + yield page + if not page.get("has_more"): + return + cursor = page.get("next_cursor") + if not cursor or cursor in seen: + raise ValueError("Invalid or repeated v3 pagination cursor") + seen.add(cursor) + kwargs["cursor"] = cursor + + async def iter_all(self, **kwargs): + async for page in self.iter_pages(**kwargs): + for item in page["data"]["list"]: + yield item diff --git a/src/agora_agent/v3/campaigns.py b/src/agora_agent/v3/campaigns.py new file mode 100644 index 0000000..3b8d7f2 --- /dev/null +++ b/src/agora_agent/v3/campaigns.py @@ -0,0 +1,4 @@ +"""Compatibility re-exports of generated campaign resources.""" +from .client import AsyncCampaignsClient, CampaignsClient + +__all__ = ["CampaignsClient", "AsyncCampaignsClient"] diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py new file mode 100644 index 0000000..67ab947 --- /dev/null +++ b/src/agora_agent/v3/client.py @@ -0,0 +1,459 @@ +"""Generated resources from root openapi.json; shared token transport is handwritten.""" +from __future__ import annotations + +from typing import Dict, List, Optional, Union + +from . import types as T +from ._transport import _UNSET, AsyncResource, RawResponse, Resource, segment +from typing_extensions import Literal + + +class AgentsClient(Resource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AgentsRawClient(owner, options) + def create(self, *, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[Union[T.AgentRequest_TextGreeting, T.AgentRequest_AudioGreeting], None] = _UNSET, variables: Union[Dict[str, T.AgentRequest_VariableDefinition], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + return self.with_raw_response.create(agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, appid=appid, request_options=request_options).data + + def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentListResponse: + return self.with_raw_response.list(limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options).data + + def get(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + return self.with_raw_response.get(agent_id=agent_id, appid=appid, request_options=request_options).data + + def update(self, *, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[T.AgentRequest_GreetingPatch, None] = _UNSET, variables: Union[Dict[str, Union[T.AgentRequest_VariableDefinitionPatch, None]], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + return self.with_raw_response.update(agent_id=agent_id, if_match=if_match, agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, appid=appid, request_options=request_options).data + + def delete(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentDeleteResponse: + return self.with_raw_response.delete(agent_id=agent_id, appid=appid, request_options=request_options).data + +class AgentsRawClient(Resource): + def create(self, *, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[Union[T.AgentRequest_TextGreeting, T.AgentRequest_AudioGreeting], None] = _UNSET, variables: Union[Dict[str, T.AgentRequest_VariableDefinition], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents', body={'agent_name': agent_name, 'instructions': instructions, 'messages': messages, 'greeting': greeting, 'variables': variables, 'pipeline': pipeline, 'filler_words': filler_words}, headers={}, params={}, request_options=request_options) + + def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentListResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents', body=None, headers={}, params={'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + def get(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + + def update(self, *, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[T.AgentRequest_GreetingPatch, None] = _UNSET, variables: Union[Dict[str, Union[T.AgentRequest_VariableDefinitionPatch, None]], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + return self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body={'agent_name': agent_name, 'instructions': instructions, 'messages': messages, 'greeting': greeting, 'variables': variables, 'pipeline': pipeline, 'filler_words': filler_words}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + def delete(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentDeleteResponse]: + return self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + +class SecretsClient(Resource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = SecretsRawClient(owner, options) + def create(self, *, name: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return self.with_raw_response.create(name=name, secrets=secrets, appid=appid, request_options=request_options).data + + def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretList: + return self.with_raw_response.list(limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options).data + + def get(self, *, name: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return self.with_raw_response.get(name=name, appid=appid, request_options=request_options).data + + def replace(self, *, name: str, if_match: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return self.with_raw_response.replace(name=name, if_match=if_match, secrets=secrets, appid=appid, request_options=request_options).data + + def update(self, *, name: str, if_match: Optional[Union[str, None]] = None, secrets: Dict[str, Union[str, None]], appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return self.with_raw_response.update(name=name, if_match=if_match, secrets=secrets, appid=appid, request_options=request_options).data + + def delete(self, *, name: str, if_match: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretDeleted: + return self.with_raw_response.delete(name=name, if_match=if_match, appid=appid, request_options=request_options).data + + def set_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, if_none_match: Optional[Union[Literal['*'], None]] = None, value: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretKey: + return self.with_raw_response.set_key(name=name, key=key, if_match=if_match, if_none_match=if_none_match, value=value, appid=appid, request_options=request_options).data + + def delete_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretDeleted: + return self.with_raw_response.delete_key(name=name, key=key, if_match=if_match, appid=appid, request_options=request_options).data + +class SecretsRawClient(Resource): + def create(self, *, name: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body={'name': name, 'secrets': secrets}, headers={}, params={}, request_options=request_options) + + def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretList]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body=None, headers={}, params={'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + def get(self, *, name: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body=None, headers={}, params={}, request_options=request_options) + + def replace(self, *, name: str, if_match: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return self._request('PUT', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body={'secrets': secrets}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + def update(self, *, name: str, if_match: Optional[Union[str, None]] = None, secrets: Dict[str, Union[str, None]], appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body={'secrets': secrets}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + def delete(self, *, name: str, if_match: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretDeleted]: + return self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body=None, headers={'If-Match': if_match}, params={}, request_options=request_options) + + def set_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, if_none_match: Optional[Union[Literal['*'], None]] = None, value: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretKey]: + return self._request('PUT', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}/keys/{segment(key)}', body={'value': value}, headers={'If-Match': if_match, 'If-None-Match': if_none_match}, params={}, request_options=request_options) + + def delete_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretDeleted]: + return self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}/keys/{segment(key)}', body=None, headers={'If-Match': if_match}, params={}, request_options=request_options) + +class NumbersClient(Resource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = NumbersRawClient(owner, options) + def create(self, *, number: str, description: Union[str, None] = _UNSET, sip_trunk: T.SipTrunkInput, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberResponse: + return self.with_raw_response.create(number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, appid=appid, request_options=request_options).data + + def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberListResponse: + return self.with_raw_response.list(limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options).data + + def get(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberResponse: + return self.with_raw_response.get(number_id=number_id, appid=appid, request_options=request_options).data + + def update(self, *, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union[T.SipTrunkInput, None] = _UNSET, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberResponse: + return self.with_raw_response.update(number_id=number_id, if_match=if_match, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, appid=appid, request_options=request_options).data + + def delete(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberDeleteResponse: + return self.with_raw_response.delete(number_id=number_id, appid=appid, request_options=request_options).data + +class NumbersRawClient(Resource): + def create(self, *, number: str, description: Union[str, None] = _UNSET, sip_trunk: T.SipTrunkInput, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers', body={'number': number, 'description': description, 'sip_trunk': sip_trunk, 'inbound': inbound}, headers={}, params={}, request_options=request_options) + + def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberListResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers', body=None, headers={}, params={'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + def get(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + + def update(self, *, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union[T.SipTrunkInput, None] = _UNSET, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberResponse]: + return self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body={'number': number, 'description': description, 'sip_trunk': sip_trunk, 'inbound': inbound}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + def delete(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberDeleteResponse]: + return self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + +class SessionsClient(Resource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = SessionsRawClient(owner, options) + def start(self, *, idempotency_key: Optional[str] = None, agent_id: str, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionStartResponse: + return self.with_raw_response.start(idempotency_key=idempotency_key, agent_id=agent_id, values=values, labels=labels, transport=transport, lifecycle=lifecycle, data_policy=data_policy, appid=appid, request_options=request_options).data + + def list(self, *, channel: Optional[Union[str, None]] = None, status: Optional[str] = None, started_after: Optional[Union[int, None]] = None, started_before: Optional[Union[int, None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionList: + return self.with_raw_response.list(channel=channel, status=status, started_after=started_after, started_before=started_before, limit=limit, cursor=cursor, appid=appid, request_options=request_options).data + + def start_ephemeral(self, *, idempotency_key: Optional[str] = None, agent: T.PublicAgentInput, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionStartResponse: + return self.with_raw_response.start_ephemeral(idempotency_key=idempotency_key, agent=agent, values=values, labels=labels, transport=transport, lifecycle=lifecycle, data_policy=data_policy, appid=appid, request_options=request_options).data + + def get(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionResponse: + return self.with_raw_response.get(agent_session_id=agent_session_id, appid=appid, request_options=request_options).data + + def update(self, *, agent_session_id: str, transport: T.SessionTransportPatch = _UNSET, agent: T.SessionAgentPatch = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionUpdateResponse: + return self.with_raw_response.update(agent_session_id=agent_session_id, transport=transport, agent=agent, appid=appid, request_options=request_options).data + + def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionStopResponse: + return self.with_raw_response.stop(agent_session_id=agent_session_id, appid=appid, request_options=request_options).data + +class SessionsRawClient(Resource): + def start(self, *, idempotency_key: Optional[str] = None, agent_id: str, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionStartResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body={'agent_id': agent_id, 'values': values, 'labels': labels, 'transport': transport, 'lifecycle': lifecycle, 'data_policy': data_policy}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def list(self, *, channel: Optional[Union[str, None]] = None, status: Optional[str] = None, started_after: Optional[Union[int, None]] = None, started_before: Optional[Union[int, None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionList]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body=None, headers={}, params={'channel': channel, 'status': status, 'started_after': started_after, 'started_before': started_before, 'limit': limit, 'cursor': cursor}, request_options=request_options) + + def start_ephemeral(self, *, idempotency_key: Optional[str] = None, agent: T.PublicAgentInput, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionStartResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/ephemeral', body={'agent': agent, 'values': values, 'labels': labels, 'transport': transport, 'lifecycle': lifecycle, 'data_policy': data_policy}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def get(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/{segment(agent_session_id)}', body=None, headers={}, params={}, request_options=request_options) + + def update(self, *, agent_session_id: str, transport: T.SessionTransportPatch = _UNSET, agent: T.SessionAgentPatch = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionUpdateResponse]: + return self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/{segment(agent_session_id)}', body={'transport': transport, 'agent': agent}, headers={}, params={}, request_options=request_options) + + def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionStopResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/{segment(agent_session_id)}/stop', body=None, headers={}, params={}, request_options=request_options) + +class CampaignsClient(Resource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = CampaignsRawClient(owner, options) + def create(self, *, idempotency_key: Optional[str] = None, name: str, agent_id: str, transport: T.CampaignTransport, contacts: List[T.ContactInput], lifecycle: T.CampaignLifecycle = _UNSET, pacing: T.Pacing = _UNSET, schedule: Union[T.ImmediateSchedule, T.ScheduledSchedule], data_policy: T.DataPolicy = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return self.with_raw_response.create(idempotency_key=idempotency_key, name=name, agent_id=agent_id, transport=transport, contacts=contacts, lifecycle=lifecycle, pacing=pacing, schedule=schedule, data_policy=data_policy, appid=appid, request_options=request_options).data + + def list(self, *, status: Optional[Union[Literal['scheduled', 'running', 'paused', 'completed', 'canceled', 'failed'], None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignListResponse: + return self.with_raw_response.list(status=status, limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options).data + + def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return self.with_raw_response.pause(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data + + def resume(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return self.with_raw_response.resume(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data + + def cancel(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return self.with_raw_response.cancel(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data + + def get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return self.with_raw_response.get(campaign_id=campaign_id, appid=appid, request_options=request_options).data + + def update(self, *, campaign_id: str, if_match: str, lifecycle: Union[T.CampaignLifecycle, None] = _UNSET, pacing: Union[T.PacingPatch, None] = _UNSET, schedule: Union[Union[T.ImmediateSchedule, T.ScheduledSchedule], None] = _UNSET, transport: Union[T.CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return self.with_raw_response.update(campaign_id=campaign_id, if_match=if_match, lifecycle=lifecycle, pacing=pacing, schedule=schedule, transport=transport, appid=appid, request_options=request_options).data + + def delete(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignDeleteResponse: + return self.with_raw_response.delete(campaign_id=campaign_id, appid=appid, request_options=request_options).data + +class CampaignsRawClient(Resource): + def create(self, *, idempotency_key: Optional[str] = None, name: str, agent_id: str, transport: T.CampaignTransport, contacts: List[T.ContactInput], lifecycle: T.CampaignLifecycle = _UNSET, pacing: T.Pacing = _UNSET, schedule: Union[T.ImmediateSchedule, T.ScheduledSchedule], data_policy: T.DataPolicy = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body={'name': name, 'agent_id': agent_id, 'transport': transport, 'contacts': contacts, 'lifecycle': lifecycle, 'pacing': pacing, 'schedule': schedule, 'data_policy': data_policy}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def list(self, *, status: Optional[Union[Literal['scheduled', 'running', 'paused', 'completed', 'canceled', 'failed'], None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignListResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body=None, headers={}, params={'status': status, 'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}:pause', body=None, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def resume(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}:resume', body=None, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def cancel(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}:cancel', body=None, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}', body=None, headers={}, params={}, request_options=request_options) + + def update(self, *, campaign_id: str, if_match: str, lifecycle: Union[T.CampaignLifecycle, None] = _UNSET, pacing: Union[T.PacingPatch, None] = _UNSET, schedule: Union[Union[T.ImmediateSchedule, T.ScheduledSchedule], None] = _UNSET, transport: Union[T.CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}', body={'lifecycle': lifecycle, 'pacing': pacing, 'schedule': schedule, 'transport': transport}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + def delete(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignDeleteResponse]: + return self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}', body=None, headers={}, params={}, request_options=request_options) + +class V3Client: + def __init__(self, owner, options): + self.agents = AgentsClient(owner, options) + self.secrets = SecretsClient(owner, options) + self.numbers = NumbersClient(owner, options) + self.sessions = SessionsClient(owner, options) + self.campaigns = CampaignsClient(owner, options) + +class AsyncAgentsClient(AsyncResource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AsyncAgentsRawClient(owner, options) + async def create(self, *, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[Union[T.AgentRequest_TextGreeting, T.AgentRequest_AudioGreeting], None] = _UNSET, variables: Union[Dict[str, T.AgentRequest_VariableDefinition], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + return (await self.with_raw_response.create(agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, appid=appid, request_options=request_options)).data + + async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentListResponse: + return (await self.with_raw_response.list(limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options)).data + + async def get(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + return (await self.with_raw_response.get(agent_id=agent_id, appid=appid, request_options=request_options)).data + + async def update(self, *, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[T.AgentRequest_GreetingPatch, None] = _UNSET, variables: Union[Dict[str, Union[T.AgentRequest_VariableDefinitionPatch, None]], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + return (await self.with_raw_response.update(agent_id=agent_id, if_match=if_match, agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, appid=appid, request_options=request_options)).data + + async def delete(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentDeleteResponse: + return (await self.with_raw_response.delete(agent_id=agent_id, appid=appid, request_options=request_options)).data + +class AsyncAgentsRawClient(AsyncResource): + async def create(self, *, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[Union[T.AgentRequest_TextGreeting, T.AgentRequest_AudioGreeting], None] = _UNSET, variables: Union[Dict[str, T.AgentRequest_VariableDefinition], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents', body={'agent_name': agent_name, 'instructions': instructions, 'messages': messages, 'greeting': greeting, 'variables': variables, 'pipeline': pipeline, 'filler_words': filler_words}, headers={}, params={}, request_options=request_options) + + async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentListResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents', body=None, headers={}, params={'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + async def get(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + + async def update(self, *, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List[T.AgentRequest_Message], None] = _UNSET, greeting: Union[T.AgentRequest_GreetingPatch, None] = _UNSET, variables: Union[Dict[str, Union[T.AgentRequest_VariableDefinitionPatch, None]], None] = _UNSET, pipeline: Union[T.AgentRequest_CascadedPipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + return await self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body={'agent_name': agent_name, 'instructions': instructions, 'messages': messages, 'greeting': greeting, 'variables': variables, 'pipeline': pipeline, 'filler_words': filler_words}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + async def delete(self, *, agent_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentDeleteResponse]: + return await self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + +class AsyncSecretsClient(AsyncResource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AsyncSecretsRawClient(owner, options) + async def create(self, *, name: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return (await self.with_raw_response.create(name=name, secrets=secrets, appid=appid, request_options=request_options)).data + + async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretList: + return (await self.with_raw_response.list(limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options)).data + + async def get(self, *, name: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return (await self.with_raw_response.get(name=name, appid=appid, request_options=request_options)).data + + async def replace(self, *, name: str, if_match: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return (await self.with_raw_response.replace(name=name, if_match=if_match, secrets=secrets, appid=appid, request_options=request_options)).data + + async def update(self, *, name: str, if_match: Optional[Union[str, None]] = None, secrets: Dict[str, Union[str, None]], appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretSet: + return (await self.with_raw_response.update(name=name, if_match=if_match, secrets=secrets, appid=appid, request_options=request_options)).data + + async def delete(self, *, name: str, if_match: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretDeleted: + return (await self.with_raw_response.delete(name=name, if_match=if_match, appid=appid, request_options=request_options)).data + + async def set_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, if_none_match: Optional[Union[Literal['*'], None]] = None, value: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretKey: + return (await self.with_raw_response.set_key(name=name, key=key, if_match=if_match, if_none_match=if_none_match, value=value, appid=appid, request_options=request_options)).data + + async def delete_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SecretDeleted: + return (await self.with_raw_response.delete_key(name=name, key=key, if_match=if_match, appid=appid, request_options=request_options)).data + +class AsyncSecretsRawClient(AsyncResource): + async def create(self, *, name: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body={'name': name, 'secrets': secrets}, headers={}, params={}, request_options=request_options) + + async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretList]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body=None, headers={}, params={'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + async def get(self, *, name: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body=None, headers={}, params={}, request_options=request_options) + + async def replace(self, *, name: str, if_match: str, secrets: Dict[str, str], appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return await self._request('PUT', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body={'secrets': secrets}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + async def update(self, *, name: str, if_match: Optional[Union[str, None]] = None, secrets: Dict[str, Union[str, None]], appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretSet]: + return await self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body={'secrets': secrets}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + async def delete(self, *, name: str, if_match: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretDeleted]: + return await self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body=None, headers={'If-Match': if_match}, params={}, request_options=request_options) + + async def set_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, if_none_match: Optional[Union[Literal['*'], None]] = None, value: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretKey]: + return await self._request('PUT', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}/keys/{segment(key)}', body={'value': value}, headers={'If-Match': if_match, 'If-None-Match': if_none_match}, params={}, request_options=request_options) + + async def delete_key(self, *, name: str, key: str, if_match: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SecretDeleted]: + return await self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}/keys/{segment(key)}', body=None, headers={'If-Match': if_match}, params={}, request_options=request_options) + +class AsyncNumbersClient(AsyncResource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AsyncNumbersRawClient(owner, options) + async def create(self, *, number: str, description: Union[str, None] = _UNSET, sip_trunk: T.SipTrunkInput, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberResponse: + return (await self.with_raw_response.create(number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, appid=appid, request_options=request_options)).data + + async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberListResponse: + return (await self.with_raw_response.list(limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options)).data + + async def get(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberResponse: + return (await self.with_raw_response.get(number_id=number_id, appid=appid, request_options=request_options)).data + + async def update(self, *, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union[T.SipTrunkInput, None] = _UNSET, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberResponse: + return (await self.with_raw_response.update(number_id=number_id, if_match=if_match, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, appid=appid, request_options=request_options)).data + + async def delete(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.NumberDeleteResponse: + return (await self.with_raw_response.delete(number_id=number_id, appid=appid, request_options=request_options)).data + +class AsyncNumbersRawClient(AsyncResource): + async def create(self, *, number: str, description: Union[str, None] = _UNSET, sip_trunk: T.SipTrunkInput, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers', body={'number': number, 'description': description, 'sip_trunk': sip_trunk, 'inbound': inbound}, headers={}, params={}, request_options=request_options) + + async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberListResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers', body=None, headers={}, params={'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + async def get(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + + async def update(self, *, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union[T.SipTrunkInput, None] = _UNSET, inbound: Union[T.InboundInput, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberResponse]: + return await self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body={'number': number, 'description': description, 'sip_trunk': sip_trunk, 'inbound': inbound}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + async def delete(self, *, number_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.NumberDeleteResponse]: + return await self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + +class AsyncSessionsClient(AsyncResource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AsyncSessionsRawClient(owner, options) + async def start(self, *, idempotency_key: Optional[str] = None, agent_id: str, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionStartResponse: + return (await self.with_raw_response.start(idempotency_key=idempotency_key, agent_id=agent_id, values=values, labels=labels, transport=transport, lifecycle=lifecycle, data_policy=data_policy, appid=appid, request_options=request_options)).data + + async def list(self, *, channel: Optional[Union[str, None]] = None, status: Optional[str] = None, started_after: Optional[Union[int, None]] = None, started_before: Optional[Union[int, None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionList: + return (await self.with_raw_response.list(channel=channel, status=status, started_after=started_after, started_before=started_before, limit=limit, cursor=cursor, appid=appid, request_options=request_options)).data + + async def start_ephemeral(self, *, idempotency_key: Optional[str] = None, agent: T.PublicAgentInput, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionStartResponse: + return (await self.with_raw_response.start_ephemeral(idempotency_key=idempotency_key, agent=agent, values=values, labels=labels, transport=transport, lifecycle=lifecycle, data_policy=data_policy, appid=appid, request_options=request_options)).data + + async def get(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionResponse: + return (await self.with_raw_response.get(agent_session_id=agent_session_id, appid=appid, request_options=request_options)).data + + async def update(self, *, agent_session_id: str, transport: T.SessionTransportPatch = _UNSET, agent: T.SessionAgentPatch = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionUpdateResponse: + return (await self.with_raw_response.update(agent_session_id=agent_session_id, transport=transport, agent=agent, appid=appid, request_options=request_options)).data + + async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.SessionStopResponse: + return (await self.with_raw_response.stop(agent_session_id=agent_session_id, appid=appid, request_options=request_options)).data + +class AsyncSessionsRawClient(AsyncResource): + async def start(self, *, idempotency_key: Optional[str] = None, agent_id: str, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionStartResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body={'agent_id': agent_id, 'values': values, 'labels': labels, 'transport': transport, 'lifecycle': lifecycle, 'data_policy': data_policy}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def list(self, *, channel: Optional[Union[str, None]] = None, status: Optional[str] = None, started_after: Optional[Union[int, None]] = None, started_before: Optional[Union[int, None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionList]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body=None, headers={}, params={'channel': channel, 'status': status, 'started_after': started_after, 'started_before': started_before, 'limit': limit, 'cursor': cursor}, request_options=request_options) + + async def start_ephemeral(self, *, idempotency_key: Optional[str] = None, agent: T.PublicAgentInput, values: Union[Dict[str, Union[str, int, float, bool]], None] = _UNSET, labels: Union[Dict[str, str], None] = _UNSET, transport: T.SessionTransport, lifecycle: Union[T.Lifecycle, None] = _UNSET, data_policy: Union[T.DataPolicy, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionStartResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/ephemeral', body={'agent': agent, 'values': values, 'labels': labels, 'transport': transport, 'lifecycle': lifecycle, 'data_policy': data_policy}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def get(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/{segment(agent_session_id)}', body=None, headers={}, params={}, request_options=request_options) + + async def update(self, *, agent_session_id: str, transport: T.SessionTransportPatch = _UNSET, agent: T.SessionAgentPatch = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionUpdateResponse]: + return await self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/{segment(agent_session_id)}', body={'transport': transport, 'agent': agent}, headers={}, params={}, request_options=request_options) + + async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.SessionStopResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/{segment(agent_session_id)}/stop', body=None, headers={}, params={}, request_options=request_options) + +class AsyncCampaignsClient(AsyncResource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AsyncCampaignsRawClient(owner, options) + async def create(self, *, idempotency_key: Optional[str] = None, name: str, agent_id: str, transport: T.CampaignTransport, contacts: List[T.ContactInput], lifecycle: T.CampaignLifecycle = _UNSET, pacing: T.Pacing = _UNSET, schedule: Union[T.ImmediateSchedule, T.ScheduledSchedule], data_policy: T.DataPolicy = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return (await self.with_raw_response.create(idempotency_key=idempotency_key, name=name, agent_id=agent_id, transport=transport, contacts=contacts, lifecycle=lifecycle, pacing=pacing, schedule=schedule, data_policy=data_policy, appid=appid, request_options=request_options)).data + + async def list(self, *, status: Optional[Union[Literal['scheduled', 'running', 'paused', 'completed', 'canceled', 'failed'], None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignListResponse: + return (await self.with_raw_response.list(status=status, limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options)).data + + async def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return (await self.with_raw_response.pause(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data + + async def resume(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return (await self.with_raw_response.resume(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data + + async def cancel(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return (await self.with_raw_response.cancel(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data + + async def get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return (await self.with_raw_response.get(campaign_id=campaign_id, appid=appid, request_options=request_options)).data + + async def update(self, *, campaign_id: str, if_match: str, lifecycle: Union[T.CampaignLifecycle, None] = _UNSET, pacing: Union[T.PacingPatch, None] = _UNSET, schedule: Union[Union[T.ImmediateSchedule, T.ScheduledSchedule], None] = _UNSET, transport: Union[T.CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignResponse: + return (await self.with_raw_response.update(campaign_id=campaign_id, if_match=if_match, lifecycle=lifecycle, pacing=pacing, schedule=schedule, transport=transport, appid=appid, request_options=request_options)).data + + async def delete(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.CampaignDeleteResponse: + return (await self.with_raw_response.delete(campaign_id=campaign_id, appid=appid, request_options=request_options)).data + +class AsyncCampaignsRawClient(AsyncResource): + async def create(self, *, idempotency_key: Optional[str] = None, name: str, agent_id: str, transport: T.CampaignTransport, contacts: List[T.ContactInput], lifecycle: T.CampaignLifecycle = _UNSET, pacing: T.Pacing = _UNSET, schedule: Union[T.ImmediateSchedule, T.ScheduledSchedule], data_policy: T.DataPolicy = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body={'name': name, 'agent_id': agent_id, 'transport': transport, 'contacts': contacts, 'lifecycle': lifecycle, 'pacing': pacing, 'schedule': schedule, 'data_policy': data_policy}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def list(self, *, status: Optional[Union[Literal['scheduled', 'running', 'paused', 'completed', 'canceled', 'failed'], None]] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignListResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body=None, headers={}, params={'status': status, 'limit': limit, 'cursor': cursor, 'order': order}, request_options=request_options) + + async def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}:pause', body=None, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def resume(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}:resume', body=None, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def cancel(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}:cancel', body=None, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return await self._request('GET', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}', body=None, headers={}, params={}, request_options=request_options) + + async def update(self, *, campaign_id: str, if_match: str, lifecycle: Union[T.CampaignLifecycle, None] = _UNSET, pacing: Union[T.PacingPatch, None] = _UNSET, schedule: Union[Union[T.ImmediateSchedule, T.ScheduledSchedule], None] = _UNSET, transport: Union[T.CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignResponse]: + return await self._request('PATCH', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}', body={'lifecycle': lifecycle, 'pacing': pacing, 'schedule': schedule, 'transport': transport}, headers={'If-Match': if_match}, params={}, request_options=request_options) + + async def delete(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.CampaignDeleteResponse]: + return await self._request('DELETE', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns/{segment(campaign_id)}', body=None, headers={}, params={}, request_options=request_options) + +class AsyncV3Client: + def __init__(self, owner, options): + self.agents = AsyncAgentsClient(owner, options) + self.secrets = AsyncSecretsClient(owner, options) + self.numbers = AsyncNumbersClient(owner, options) + self.sessions = AsyncSessionsClient(owner, options) + self.campaigns = AsyncCampaignsClient(owner, options) diff --git a/src/agora_agent/v3/contract.py b/src/agora_agent/v3/contract.py new file mode 100644 index 0000000..b4fe29a --- /dev/null +++ b/src/agora_agent/v3/contract.py @@ -0,0 +1,162 @@ +"""Validate public write shapes against the packaged, shared OpenAPI contract. + +Formats (UUID/date-time) remain server validated. No defaults are injected and no +response fields are discarded; this keeps rollout responses forward compatible. +""" +import copy +import ipaddress +import json +import math +import re +from functools import lru_cache +from pathlib import Path + + +@lru_cache(maxsize=1) +def specification(): + return json.loads(Path(__file__).with_name("openapi.json").read_text()) + + +def _equal(left, right): + if isinstance(left, bool) != isinstance(right, bool): + return False + return left == right + + +def validate(value, schema, path="body"): + if schema is True: + return + if schema is False: + raise ValueError(path + " is not allowed") + if "$ref" in schema: + target = specification() + for key in schema["$ref"][2:].split("/"): + target = target[key.replace("~1", "/").replace("~0", "~")] + validate(value, target, path) + for keyword in ("anyOf", "oneOf"): + if keyword in schema: + count = 0 + for candidate in schema[keyword]: + try: + validate(value, candidate, path) + count += 1 + except ValueError: + pass + if count == 0 or keyword == "oneOf" and count != 1: + raise ValueError(path + " does not match " + keyword + " in the public contract") + for candidate in schema.get("allOf", []): + validate(value, candidate, path) + if "if" in schema: + try: + validate(value, schema["if"], path) + except ValueError: + branch = schema.get("else") + else: + branch = schema.get("then") + if branch is not None: + validate(value, branch, path) + if "not" in schema: + try: + validate(value, schema["not"], path) + except ValueError: + pass + else: + raise ValueError(path + " contains an unsupported field combination") + if "const" in schema and not _equal(value, schema["const"]): + raise ValueError(path + " has an invalid constant") + if "enum" in schema and not any(_equal(value, item) for item in schema["enum"]): + raise ValueError(path + " has an invalid enum value") + kind = schema.get("type") + matches = {"object": isinstance(value, dict), "array": isinstance(value, list), + "string": isinstance(value, str), "boolean": isinstance(value, bool), + "null": value is None, + "integer": isinstance(value, int) and not isinstance(value, bool), + "number": isinstance(value, (int, float)) and not isinstance(value, bool)} + if kind and not matches.get(kind, True): + raise ValueError(path + " must be " + kind) + if isinstance(value, dict): + for key in schema.get("required", []): + if key not in value: + raise ValueError(path + "." + key + " is required") + for limit, compare in (("minProperties", lambda a, b: a < b), ("maxProperties", lambda a, b: a > b)): + if limit in schema and compare(len(value), schema[limit]): + raise ValueError(path + " has an invalid number of properties") + props = schema.get("properties", {}) + patterns = schema.get("patternProperties", {}) + for key, item in value.items(): + if not isinstance(key, str): + raise ValueError(path + " keys must be strings") + candidates = ([props[key]] if key in props else []) + [node for pattern, node in patterns.items() if re.search(pattern, key)] + if not candidates: + candidates = [schema.get("additionalProperties", True)] + for candidate in candidates: + validate(item, candidate, path + "." + key) + if isinstance(value, list): + if len(value) < schema.get("minItems", 0) or len(value) > schema.get("maxItems", math.inf): + raise ValueError(path + " has an invalid number of items") + if schema.get("uniqueItems") and any(_equal(item, previous) for index, item in enumerate(value) for previous in value[:index]): + raise ValueError(path + " must contain unique items") + for index, item in enumerate(value): + validate(item, schema.get("items", {}), f"{path}[{index}]") + if isinstance(value, str): + if len(value) < schema.get("minLength", 0) or len(value) > schema.get("maxLength", math.inf) or "pattern" in schema and re.search(schema["pattern"], value) is None: + raise ValueError(path + " has an invalid length or pattern") + if isinstance(value, (int, float)) and not isinstance(value, bool): + if not math.isfinite(value) or value < schema.get("minimum", -math.inf) or value > schema.get("maximum", math.inf): + raise ValueError(path + " is outside its supported range") + + +def _extras(value, *, saved=False): + if not isinstance(value, dict): + return + credential = value.get("credential") + if isinstance(credential, dict): + fields = credential.get("fields") + if saved and isinstance(fields, dict): + for item in fields.values(): + if item is not None and (not isinstance(item, str) or re.fullmatch(r"\$secrets\.[A-Za-z0-9_]{1,64}\.[A-Za-z0-9_]{1,64}", item) is None): + raise ValueError("Saved BYOK credentials require secret_ref values; inline credentials are allowed only for ephemeral sessions") + if "allowed_ips" in value and value["allowed_ips"] is not None: + for address in value["allowed_ips"]: + try: + if address == "*": + continue + if "/" in address: + host, prefix = address.split("/") + if not re.fullmatch(r"0|[1-9][0-9]?", prefix) or int(prefix) > 32: + raise ValueError() + ipaddress.IPv4Address(host) + else: + ipaddress.IPv4Address(address) + except (ValueError, TypeError): + raise ValueError("allowed_ips requires IPv4 addresses, IPv4 CIDRs, or '*'") from None + for item in value.values(): + if isinstance(item, dict): + _extras(item, saved=saved) + + +def validate_write(method, path, body): + if method in {"GET", "DELETE"} or body is None: + return + tail = path.split("/projects/", 1)[-1].split("/", 1)[-1] + for template, operations in specification()["paths"].items(): + candidate = template.split("{appid}/", 1)[-1] + pattern = re.sub(r"\\\{[^}]+\\\}", "[^/]+", re.escape(candidate)) + if re.fullmatch(pattern, tail) and method.lower() in operations: + op = operations[method.lower()] + schema = op.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema") + if schema: + checking = copy.deepcopy(body) + # Existing optional geofence supplement awaits backend rollout. + if tail in {"sessions", "sessions/ephemeral"} and isinstance(checking.get("transport"), dict): + checking["transport"].pop("geofence", None) + validate(checking, schema) + _extras(checking, saved=tail == "agents" or tail.startswith("agents/")) + if tail == "campaigns" and method == "POST" and len(json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()) > 10 * 1024 * 1024: + raise ValueError("Campaign create body exceeds 10 MiB") + if tail.startswith("sessions/") and method == "PATCH": + pipeline = body.get("agent", {}).get("overrides", {}).get("pipeline", {}) + for module in pipeline.values(): + if {"url", "base_url", "api_base_url", "endpoint"}.intersection(module.get("params", {})): + raise ValueError("Session update cannot change endpoint aliases") + return diff --git a/src/agora_agent/v3/openapi.json b/src/agora_agent/v3/openapi.json new file mode 100644 index 0000000..188b0e5 --- /dev/null +++ b/src/agora_agent/v3/openapi.json @@ -0,0 +1,14634 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Conversational AI V3 API", + "version": "3.0.0", + "description": "Public Conversational AI preview contract used by the Agent SDKs. Authenticate with Agora token007 (Authorization: agora token=) or HTTP Basic authentication using Customer ID and Customer Secret." + }, + "paths": { + "/conversational-ai/v1/projects/{appid}/agents": { + "post": { + "tags": [ + "agents" + ], + "summary": "Create Agent", + "operationId": "create_agent_conversational_ai_v3_projects__appid__agents_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentCreateInput" + } + } + } + } + }, + "get": { + "tags": [ + "agents" + ], + "summary": "List Agents", + "operationId": "list_agents_conversational_ai_v3_projects__appid__agents_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.desc", + "create_ts.asc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentListResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/agents/{id}": { + "get": { + "tags": [ + "agents" + ], + "summary": "Retrieve Agent", + "operationId": "retrieve_agent_conversational_ai_v3_projects__appid__agents__id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "patch": { + "tags": [ + "agents" + ], + "summary": "Update Agent", + "operationId": "update_agent_conversational_ai_v3_projects__appid__agents__id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentPatch" + } + } + } + } + }, + "delete": { + "tags": [ + "agents" + ], + "summary": "Delete Agent", + "operationId": "delete_agent_conversational_ai_v3_projects__appid__agents__id__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentDeleteResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Create Campaign", + "description": "Create a campaign with up to 50,000 contacts. The complete JSON request body is limited to 10 MiB (10,485,760 bytes).", + "operationId": "create_campaign_conversational_ai_v3_projects__appid__campaigns_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "get": { + "tags": [ + "campaigns" + ], + "summary": "List Campaigns", + "operationId": "list_campaigns_conversational_ai_v3_projects__appid__campaigns_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "scheduled", + "running", + "paused", + "completed", + "canceled", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.desc", + "create_ts.asc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignListResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:pause": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Pause Campaign", + "operationId": "pause_campaign_conversational_ai_v3_projects__appid__campaigns__id__pause_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:resume": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Resume Campaign", + "operationId": "resume_campaign_conversational_ai_v3_projects__appid__campaigns__id__resume_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}:cancel": { + "post": { + "tags": [ + "campaigns" + ], + "summary": "Cancel Campaign", + "operationId": "cancel_campaign_conversational_ai_v3_projects__appid__campaigns__id__cancel_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + }, + "description": "Conflict IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours." + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/campaigns/{id}": { + "get": { + "tags": [ + "campaigns" + ], + "summary": "Get Campaign", + "operationId": "get_campaign_conversational_ai_v3_projects__appid__campaigns__id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "patch": { + "tags": [ + "campaigns" + ], + "summary": "Update Campaign", + "operationId": "update_campaign_conversational_ai_v3_projects__appid__campaigns__id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "delete": { + "tags": [ + "campaigns" + ], + "summary": "Delete Campaign", + "operationId": "delete_campaign_conversational_ai_v3_projects__appid__campaigns__id__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CampaignDeleteResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/secrets": { + "post": { + "tags": [ + "secrets" + ], + "summary": "Create", + "operationId": "create_conversational_ai_v3_projects__appid__secrets_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "get": { + "tags": [ + "secrets" + ], + "summary": "List Sets", + "operationId": "list_sets_conversational_ai_v3_projects__appid__secrets_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.asc", + "create_ts.desc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretList" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/secrets/{name}": { + "get": { + "tags": [ + "secrets" + ], + "summary": "Retrieve", + "operationId": "retrieve_conversational_ai_v3_projects__appid__secrets__name__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "put": { + "tags": [ + "secrets" + ], + "summary": "Replace", + "operationId": "replace_conversational_ai_v3_projects__appid__secrets__name__put", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretReplace" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "patch": { + "tags": [ + "secrets" + ], + "summary": "Update", + "operationId": "update_conversational_ai_v3_projects__appid__secrets__name__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "If-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ], + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretSet" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "delete": { + "tags": [ + "secrets" + ], + "summary": "Delete", + "operationId": "delete_conversational_ai_v3_projects__appid__secrets__name__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretDeleted" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/secrets/{name}/keys/{key}": { + "put": { + "tags": [ + "secrets" + ], + "summary": "Set Key", + "operationId": "set_key_conversational_ai_v3_projects__appid__secrets__name__keys__key__put", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Key" + } + }, + { + "name": "If-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ], + "title": "If-Match" + } + }, + { + "name": "If-None-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "const": "*", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "If-None-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretKeyWrite" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretKey" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretKey" + } + } + }, + "description": "Created" + } + } + }, + "delete": { + "tags": [ + "secrets" + ], + "summary": "Delete Key", + "operationId": "delete_key_conversational_ai_v3_projects__appid__secrets__name__keys__key__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + } + }, + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Key" + } + }, + { + "name": "If-Match", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + { + "type": "null" + } + ], + "title": "If-Match" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretDeleted" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/numbers": { + "post": { + "tags": [ + "numbers" + ], + "summary": "Create Number", + "operationId": "create_number_conversational_ai_v3_projects__appid__numbers_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "get": { + "tags": [ + "numbers" + ], + "summary": "List Numbers", + "operationId": "list_numbers_conversational_ai_v3_projects__appid__numbers_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": [ + "create_ts.desc", + "create_ts.asc" + ], + "type": "string", + "default": "create_ts.desc", + "title": "Order" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberListResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/numbers/{id}": { + "get": { + "tags": [ + "numbers" + ], + "summary": "Get Number", + "operationId": "get_number_conversational_ai_v3_projects__appid__numbers__id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "patch": { + "tags": [ + "numbers" + ], + "summary": "Update Number", + "operationId": "update_number_conversational_ai_v3_projects__appid__numbers__id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + }, + { + "name": "If-Match", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "title": "If-Match" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + }, + "delete": { + "tags": [ + "numbers" + ], + "summary": "Delete Number", + "operationId": "delete_number_conversational_ai_v3_projects__appid__numbers__id__delete", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NumberDeleteResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Method Not Allowed" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Precondition Failed" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + }, + "504": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Gateway Timeout" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions": { + "post": { + "tags": [ + "Sessions" + ], + "summary": "Create Session", + "operationId": "create_session_conversational_ai_v3_projects__appid__sessions_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStartResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "409": { + "description": "TaskConflict: an agent already runs with the same RTC channel and UID. IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "TaskConflict": { + "value": { + "detail": "An agent is already running with the same channel and UID.", + "reason": "TaskConflict" + } + }, + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "get": { + "tags": [ + "Sessions" + ], + "summary": "List Sessions", + "operationId": "list_sessions_conversational_ai_v3_projects__appid__sessions_get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "channel", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Channel" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "RUNNING", + "title": "Status" + } + }, + { + "name": "started_after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Started After" + } + }, + { + "name": "started_before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Started Before" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 20, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionList" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions/ephemeral": { + "post": { + "tags": [ + "Sessions" + ], + "summary": "Create Ephemeral Session", + "operationId": "create_ephemeral_session_conversational_ai_v3_projects__appid__sessions_ephemeral_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicEphemeralSessionCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStartResponse" + } + } + }, + "headers": { + "Idempotent-Replayed": { + "$ref": "#/components/headers/IdempotentReplayed" + } + } + }, + "409": { + "description": "TaskConflict: an agent already runs with the same RTC channel and UID. IdempotencyKeyConflict: the key was used with a different body or its original request is still in progress. A completed request repeated with the same key and body replays its original response for 24 hours.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "TaskConflict": { + "value": { + "detail": "An agent is already running with the same channel and UID.", + "reason": "TaskConflict" + } + }, + "IdempotencyKeyConflict": { + "summary": "Key reused with a different body", + "value": { + "detail": "The Idempotency-Key is bound to a request with a different body.", + "reason": "IdempotencyKeyConflict" + } + }, + "IdempotencyRequestInProgress": { + "summary": "Original request is still in progress", + "value": { + "detail": "A request with this Idempotency-Key is still in progress.", + "reason": "IdempotencyKeyConflict" + } + } + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions/{agent_session_id}": { + "get": { + "tags": [ + "Sessions" + ], + "summary": "Retrieve Session", + "operationId": "retrieve_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__get", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "agent_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + }, + "patch": { + "tags": [ + "Sessions" + ], + "summary": "Update Session", + "operationId": "update_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__patch", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "agent_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionPatch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionUpdateResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + }, + "/conversational-ai/v1/projects/{appid}/sessions/{agent_session_id}/stop": { + "post": { + "tags": [ + "Sessions" + ], + "summary": "Stop Session", + "operationId": "stop_session_conversational_ai_v3_projects__appid__sessions__agent_session_id__stop_post", + "parameters": [ + { + "name": "appid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "title": "Appid" + } + }, + { + "name": "agent_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStopResponse" + } + } + } + }, + "4XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Client Error" + }, + "5XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server Error" + } + } + } + } + }, + "components": { + "schemas": { + "AgentDeleteResponse": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "deleted" + ], + "title": "AgentDeleteResponse" + }, + "AgentListData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/AgentListItem" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "AgentListData" + }, + "AgentListItem": { + "properties": { + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "update_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Update Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_id", + "agent_name", + "create_ts", + "update_ts" + ], + "title": "AgentListItem" + }, + "AgentListResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/AgentListData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "AgentListResponse" + }, + "AgentResponse": { + "properties": { + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "update_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Update Ts" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextGreeting" + }, + { + "$ref": "#/components/schemas/AudioGreeting" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "audio": "#/components/schemas/AudioGreeting", + "text": "#/components/schemas/TextGreeting" + } + } + }, + { + "type": "null" + } + ], + "title": "Greeting" + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "$ref": "#/components/schemas/VariableDefinition" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCascadedPipeline" + }, + { + "type": "null" + } + ] + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/FillerWords" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_id", + "create_ts", + "update_ts", + "agent_name" + ], + "title": "AgentResponse" + }, + "AsrInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "access_key_id": { + "type": "string", + "minLength": 1 + }, + "secret_access_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "access_key_id", + "secret_access_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "assemblyai", + "deepgram", + "gemini", + "openai", + "sarvam", + "smallestai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "ares", + "assemblyai", + "deepgram", + "gemini", + "google", + "microsoft", + "openai", + "sarvam", + "smallestai", + "speechmatics", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "adc_credentials_string": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "adc_credentials_string" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "microsoft", + "speechmatics" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + } + }, + "additionalProperties": false, + "type": "object", + "title": "AsrInput" + }, + "AudioGreeting": { + "properties": { + "mode": { + "type": "string", + "const": "audio", + "title": "Mode" + }, + "on": { + "type": "string", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "default": "each_join" + }, + "delay_ms": { + "type": "integer", + "title": "Delay Ms", + "default": 0, + "minimum": 0, + "maximum": 5000 + }, + "url": { + "type": "string", + "title": "Url", + "maxLength": 2048 + }, + "text": { + "type": "string", + "title": "Text", + "minLength": 1 + }, + "download_timeout_ms": { + "type": "integer", + "title": "Download Timeout Ms", + "default": 1000, + "minimum": 200, + "maximum": 10000 + }, + "pcm_sample_rate": { + "type": "integer", + "title": "Pcm Sample Rate", + "default": 16000 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "url", + "text" + ], + "title": "AudioGreeting" + }, + "AvatarInput": { + "properties": { + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarVendor" + }, + { + "type": "null" + } + ] + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + } + }, + "additionalProperties": false, + "type": "object", + "title": "AvatarInput" + }, + "AvatarVendor": { + "type": "string", + "enum": [ + "akool", + "liveavatar", + "anam", + "sensetime", + "spatius", + "generic" + ] + }, + "CallPolicy": { + "properties": { + "max_ring_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Ring Duration Ms" + }, + "max_call_duration_seconds": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Call Duration Seconds" + }, + "voicemail": { + "anyOf": [ + { + "$ref": "#/components/schemas/VoicemailPolicy" + }, + { + "type": "null" + } + ] + }, + "max_silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Silence Duration Ms" + }, + "end_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionEndCallPolicy" + }, + { + "type": "null" + } + ] + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransferPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CallPolicy" + }, + "CallingDay": { + "properties": { + "weekday": { + "type": "string", + "enum": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ], + "title": "Weekday" + }, + "ranges": { + "items": { + "$ref": "#/components/schemas/TimeRange" + }, + "type": "array", + "maxItems": 10, + "minItems": 1, + "title": "Ranges" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "weekday", + "ranges" + ], + "title": "CallingDay" + }, + "CampaignCallPolicy": { + "properties": { + "max_ring_duration_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 259200000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Ring Duration Ms" + }, + "max_call_duration_seconds": { + "anyOf": [ + { + "type": "integer", + "maximum": 259200.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Call Duration Seconds" + }, + "voicemail": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignVoicemailPolicy" + }, + { + "type": "null" + } + ] + }, + "max_silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 259200000.0, + "minimum": 30000.0 + }, + { + "type": "null" + } + ], + "title": "Max Silence Duration Ms" + }, + "end_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignEndCallPolicy" + }, + { + "type": "null" + } + ] + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignTransferPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignCallPolicy" + }, + "CampaignCounts": { + "properties": { + "contacts": { + "type": "integer", + "minimum": 0.0, + "title": "Contacts" + }, + "pending": { + "type": "integer", + "minimum": 0.0, + "title": "Pending" + }, + "calling": { + "type": "integer", + "minimum": 0.0, + "title": "Calling" + }, + "completed": { + "type": "integer", + "minimum": 0.0, + "title": "Completed" + }, + "failed": { + "type": "integer", + "minimum": 0.0, + "title": "Failed" + }, + "canceled": { + "type": "integer", + "minimum": 0.0, + "title": "Canceled" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "contacts", + "pending", + "calling", + "completed", + "failed", + "canceled" + ], + "title": "CampaignCounts" + }, + "CampaignCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Name" + }, + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "transport": { + "$ref": "#/components/schemas/CampaignTransport" + }, + "contacts": { + "items": { + "$ref": "#/components/schemas/ContactInput" + }, + "type": "array", + "maxItems": 50000, + "minItems": 1, + "title": "Contacts" + }, + "lifecycle": { + "$ref": "#/components/schemas/CampaignLifecycle" + }, + "pacing": { + "$ref": "#/components/schemas/Pacing" + }, + "schedule": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImmediateSchedule" + }, + { + "$ref": "#/components/schemas/ScheduledSchedule" + } + ], + "title": "Schedule", + "discriminator": { + "propertyName": "mode", + "mapping": { + "immediate": "#/components/schemas/ImmediateSchedule", + "scheduled": "#/components/schemas/ScheduledSchedule" + } + } + }, + "data_policy": { + "$ref": "#/components/schemas/DataPolicy", + "description": "Campaign data-retention policy. Defaults to 30-day retention.", + "default": { + "retention": "30_days" + } + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "agent_id", + "transport", + "contacts", + "schedule" + ], + "title": "CampaignCreate" + }, + "CampaignDeleteResponse": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignDeleteResponse" + }, + "CampaignEndCallPolicy": { + "properties": { + "on_conversation_complete": { + "type": "boolean", + "title": "On Conversation Complete", + "default": false + }, + "on_user_request": { + "type": "boolean", + "title": "On User Request", + "default": false + }, + "on_fax": { + "type": "boolean", + "title": "On Fax", + "default": false + }, + "on_ai_assistant": { + "type": "boolean", + "title": "On Ai Assistant", + "default": false + }, + "on_voicemail": { + "type": "boolean", + "title": "On Voicemail", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignEndCallPolicy" + }, + "CampaignFailure": { + "properties": { + "code": { + "type": "string", + "title": "Code" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + }, + "message": { + "type": "string", + "title": "Message" + }, + "ts": { + "type": "integer", + "minimum": 0.0, + "title": "Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "code", + "message", + "ts" + ], + "title": "CampaignFailure" + }, + "CampaignLifecycle": { + "properties": { + "idle_timeout_ms": { + "type": "integer", + "maximum": 259200000.0, + "minimum": 0.0, + "title": "Idle Timeout Ms", + "default": 30000 + }, + "max_duration_ms": { + "type": "integer", + "maximum": 259200000.0, + "minimum": 1000.0, + "title": "Max Duration Ms", + "default": 259200000 + }, + "graceful_stop": { + "$ref": "#/components/schemas/GracefulStop" + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignLifecycle" + }, + "CampaignListData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/CampaignResponse" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "CampaignListData" + }, + "CampaignListResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/CampaignListData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "CampaignListResponse" + }, + "CampaignPatch": { + "properties": { + "lifecycle": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignLifecycle" + }, + { + "type": "null" + } + ] + }, + "pacing": { + "anyOf": [ + { + "$ref": "#/components/schemas/PacingPatch" + }, + { + "type": "null" + } + ] + }, + "schedule": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ImmediateSchedule" + }, + { + "$ref": "#/components/schemas/ScheduledSchedule" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "immediate": "#/components/schemas/ImmediateSchedule", + "scheduled": "#/components/schemas/ScheduledSchedule" + } + } + }, + { + "type": "null" + } + ], + "title": "Schedule" + }, + "transport": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignTransportPatch" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignPatch" + }, + "CampaignResponse": { + "properties": { + "campaign_id": { + "type": "string", + "format": "uuid", + "title": "Campaign Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "status": { + "type": "string", + "enum": [ + "scheduled", + "running", + "paused", + "completed", + "canceled", + "failed" + ], + "title": "Status" + }, + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "transport": { + "$ref": "#/components/schemas/CampaignTransportResponse" + }, + "lifecycle": { + "$ref": "#/components/schemas/CampaignLifecycle" + }, + "schedule": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImmediateSchedule" + }, + { + "$ref": "#/components/schemas/ScheduledSchedule" + } + ], + "title": "Schedule", + "discriminator": { + "propertyName": "mode", + "mapping": { + "immediate": "#/components/schemas/ImmediateSchedule", + "scheduled": "#/components/schemas/ScheduledSchedule" + } + } + }, + "counts": { + "$ref": "#/components/schemas/CampaignCounts" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "start_ts": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Start Ts" + }, + "complete_ts": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Complete Ts" + }, + "failure": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignFailure" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "campaign_id", + "name", + "status", + "agent_id", + "transport", + "lifecycle", + "schedule", + "counts", + "create_ts", + "start_ts", + "complete_ts", + "failure" + ], + "title": "CampaignResponse" + }, + "CampaignTransferPolicy": { + "properties": { + "phone_number": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "Phone Number" + }, + "description": { + "type": "string", + "maxLength": 1000, + "minLength": 1, + "title": "Description" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "phone_number", + "description" + ], + "title": "CampaignTransferPolicy" + }, + "CampaignTransport": { + "properties": { + "type": { + "type": "string", + "const": "telephony", + "title": "Type" + }, + "from": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array" + } + ], + "title": "From" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "from" + ], + "title": "CampaignTransport" + }, + "CampaignTransportPatch": { + "properties": { + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "CampaignTransportPatch" + }, + "CampaignTransportResponse": { + "properties": { + "type": { + "type": "string", + "const": "telephony", + "title": "Type" + }, + "from": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "From" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CampaignCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "from" + ], + "title": "CampaignTransportResponse" + }, + "CampaignVoicemailPolicy": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "continue", + "hangup" + ], + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "CampaignVoicemailPolicy" + }, + "CascadedPipelineInput": { + "properties": { + "mode": { + "type": "string", + "const": "cascaded", + "title": "Mode" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarInput" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/TurnDetection" + }, + { + "type": "null" + } + ] + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/AsrInput" + }, + { + "type": "null" + } + ] + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/TtsInput" + }, + { + "type": "null" + } + ] + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/LlmInput" + }, + { + "type": "null" + } + ] + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SilenceConfig" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "CascadedPipelineInput" + }, + "ContactInput": { + "properties": { + "phone": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "Phone" + }, + "values": { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object", + "maxProperties": 100, + "title": "Values" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "phone" + ], + "title": "ContactInput" + }, + "DataPolicy": { + "properties": { + "retention": { + "type": "string", + "enum": [ + "30_days", + "none" + ], + "title": "Retention", + "default": "30_days" + } + }, + "additionalProperties": false, + "type": "object", + "title": "DataPolicy" + }, + "DisabledInterruption": { + "properties": { + "mode": { + "type": "string", + "const": "off", + "title": "Mode" + }, + "when_off": { + "type": "string", + "enum": [ + "append", + "ignore" + ], + "title": "When Off", + "default": "append" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "DisabledInterruption" + }, + "ErrorReason": { + "type": "string", + "enum": [ + "InvalidRequestBody", + "MissingRequiredField", + "InvalidFieldValue", + "InvalidModuleParameter", + "UnknownSecretReference", + "NotImplemented", + "InvalidPermission", + "ServiceNotEnabled", + "AccountSuspended", + "TaskNotFound", + "ResourceNotFound", + "AgentNotFound", + "NumberNotFound", + "NumberAlreadyExists", + "ResourceConflict", + "MethodNotAllowed", + "PayloadTooLarge", + "UnsupportedMediaType", + "TaskConflict", + "CampaignActive", + "CampaignInvalidState", + "CampaignNotEditable", + "IdempotencyKeyConflict", + "SecretSetAlreadyExists", + "SecretSetInUse", + "SecretSetEmpty", + "TaskNotRunning", + "RateLimitExceeded", + "PreconditionFailed", + "PreconditionRequired", + "ResourceQuotaLimitExceeded", + "ConcurrencyLimitExceeded", + "InternalError", + "ServiceUnavailable", + "ResourceAllocationFailed", + "TaskOperationTimeout" + ], + "title": "ErrorReason" + }, + "ErrorResponse": { + "properties": { + "detail": { + "type": "string", + "minLength": 1, + "title": "Detail" + }, + "reason": { + "$ref": "#/components/schemas/ErrorReason" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "detail", + "reason" + ], + "title": "ErrorResponse" + }, + "FillerTrigger": { + "properties": { + "mode": { + "type": "string", + "const": "fixed_time", + "title": "Mode" + }, + "response_wait_ms": { + "type": "integer", + "title": "Response Wait Ms" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "response_wait_ms" + ], + "title": "FillerTrigger" + }, + "FillerWords": { + "properties": { + "enable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable" + }, + "trigger": { + "anyOf": [ + { + "$ref": "#/components/schemas/FillerTrigger" + }, + { + "type": "null" + } + ] + }, + "content": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/StaticFillerContent" + }, + { + "$ref": "#/components/schemas/GeneratedFillerContent" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "generated": "#/components/schemas/GeneratedFillerContent", + "static": "#/components/schemas/StaticFillerContent" + } + } + }, + { + "type": "null" + } + ], + "title": "Content" + } + }, + "additionalProperties": false, + "type": "object", + "title": "FillerWords" + }, + "FunctionTool": { + "properties": { + "type": { + "type": "string", + "const": "function", + "title": "Type" + }, + "name": { + "type": "string", + "pattern": "^(?![mM][cC][pP])[A-Za-z][A-Za-z0-9]{0,63}$", + "title": "Name" + }, + "description": { + "type": "string", + "maxLength": 1024, + "minLength": 1, + "title": "Description" + }, + "parameters": { + "$ref": "#/components/schemas/JSONObject", + "description": "JSON Schema (draft 2020-12) describing the tool arguments." + }, + "execution": { + "$ref": "#/components/schemas/ToolExecution" + }, + "server": { + "$ref": "#/components/schemas/ToolServer" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "name", + "description", + "parameters", + "execution", + "server" + ], + "title": "FunctionTool" + }, + "GeneratedFillerContent": { + "properties": { + "mode": { + "type": "string", + "const": "generated", + "title": "Mode" + }, + "instructions": { + "type": "string", + "title": "Instructions" + }, + "fallback_strategy": { + "anyOf": [ + { + "type": "string", + "const": "static" + }, + { + "type": "null" + } + ], + "title": "Fallback Strategy" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "instructions" + ], + "title": "GeneratedFillerContent" + }, + "GracefulStop": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": false + }, + "timeout_ms": { + "type": "integer", + "maximum": 120000.0, + "minimum": 0.0, + "title": "Timeout Ms", + "default": 30000 + } + }, + "additionalProperties": false, + "type": "object", + "title": "GracefulStop" + }, + "ImmediateSchedule": { + "properties": { + "mode": { + "type": "string", + "const": "immediate", + "title": "Mode" + }, + "timezone": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Timezone" + }, + "days": { + "items": { + "$ref": "#/components/schemas/CallingDay" + }, + "type": "array", + "maxItems": 7, + "minItems": 1, + "title": "Days" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ImmediateSchedule" + }, + "InboundCallPolicy": { + "properties": { + "max_call_duration_seconds": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Call Duration Seconds" + }, + "max_silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Silence Duration Ms" + }, + "end_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundEndCallPolicy" + }, + { + "type": "null" + } + ], + "description": "Inbound agent end-call controls. Requires an agent; omitted flags default to false. An empty/all-false object disables the owned policy." + }, + "transfer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransferPolicy" + }, + { + "type": "null" + } + ], + "description": "Reserved for runtime support; requests containing this field are rejected." + } + }, + "additionalProperties": false, + "type": "object", + "title": "InboundCallPolicy" + }, + "InboundInput": { + "properties": { + "agent": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Agent" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent" + ], + "title": "InboundInput" + }, + "InboundOutput": { + "properties": { + "agent": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Agent" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundCallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent" + ], + "title": "InboundOutput" + }, + "JSONObject": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "JsonValue": {}, + "KeywordInterruption": { + "properties": { + "mode": { + "type": "string", + "const": "keywords", + "title": "Mode" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Keywords", + "maxItems": 128 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "keywords" + ], + "title": "KeywordInterruption" + }, + "Lifecycle": { + "properties": { + "idle_timeout_ms": { + "type": "integer", + "maximum": 3600000.0, + "minimum": 0.0, + "title": "Idle Timeout Ms", + "default": 30000 + }, + "max_duration_ms": { + "type": "integer", + "maximum": 3600000.0, + "minimum": 1000.0, + "title": "Max Duration Ms", + "default": 3600000 + }, + "graceful_stop": { + "anyOf": [ + { + "$ref": "#/components/schemas/GracefulStop" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "Lifecycle" + }, + "LlmInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + }, + { + "properties": { + "fields": { + "properties": { + "access_key": { + "type": "string", + "minLength": 1 + }, + "secret_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "access_key", + "secret_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/FunctionTool" + }, + "type": "array", + "maxItems": 32 + }, + { + "type": "null" + } + ], + "title": "Tools" + }, + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "type": "integer", + "maximum": 1024.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Failure Message" + } + }, + "additionalProperties": false, + "type": "object", + "title": "LlmInput" + }, + "ManagedCredential": { + "properties": { + "mode": { + "type": "string", + "const": "managed", + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ManagedCredential" + }, + "ManualTurnBoundary": { + "properties": { + "type": { + "type": "string", + "const": "manual", + "title": "Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "ManualTurnBoundary" + }, + "McpServer": { + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Za-z0-9]{1,48}$", + "title": "Name" + }, + "endpoint": { + "type": "string", + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Endpoint" + }, + "transport": { + "type": "string", + "const": "streamable_http", + "title": "Transport" + }, + "headers": { + "anyOf": [ + { + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object", + "maxProperties": 32 + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "allowed_tools": { + "anyOf": [ + { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Tools" + }, + "timeout_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 100000.0, + "minimum": 1000.0 + }, + { + "type": "null" + } + ], + "title": "Timeout Ms" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "endpoint", + "transport" + ], + "title": "McpServer" + }, + "Message": { + "properties": { + "role": { + "type": "string", + "enum": [ + "system", + "user", + "assistant" + ], + "title": "Role" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "role", + "content" + ], + "title": "Message" + }, + "NumberCreate": { + "properties": { + "number": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "title": "Number", + "description": "International inputs normalize to E.164; national inputs normalize to national significant digits." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "sip_trunk": { + "$ref": "#/components/schemas/SipTrunkInput" + }, + "inbound": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundInput" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "number", + "sip_trunk" + ], + "title": "NumberCreate" + }, + "NumberDeleteResponse": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "title": "NumberDeleteResponse" + }, + "NumberListData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/NumberResponse" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "NumberListData" + }, + "NumberListResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/NumberListData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "NumberListResponse" + }, + "NumberPatch": { + "properties": { + "number": { + "anyOf": [ + { + "type": "string", + "maxLength": 128, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Number", + "description": "May reformat the existing number but must not change its normalized identity." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "sip_trunk": { + "anyOf": [ + { + "$ref": "#/components/schemas/SipTrunkInput" + }, + { + "type": "null" + } + ] + }, + "inbound": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundInput" + }, + { + "type": "null" + } + ], + "description": "Explicit null unbinds inbound; omission leaves inbound unchanged." + } + }, + "additionalProperties": false, + "type": "object", + "title": "NumberPatch" + }, + "NumberResponse": { + "properties": { + "number_id": { + "type": "string", + "format": "uuid", + "title": "Number Id" + }, + "number": { + "type": "string", + "title": "Number" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "sip_trunk": { + "anyOf": [ + { + "$ref": "#/components/schemas/SipTrunkOutput" + }, + { + "type": "null" + } + ] + }, + "inbound": { + "anyOf": [ + { + "$ref": "#/components/schemas/InboundOutput" + }, + { + "type": "null" + } + ] + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "number_id", + "number", + "create_ts" + ], + "title": "NumberResponse" + }, + "Pacing": { + "properties": { + "max_concurrent": { + "type": "integer", + "maximum": 10000.0, + "minimum": 1.0, + "title": "Max Concurrent", + "default": 10 + }, + "max_calls_per_second": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Calls Per Second" + } + }, + "additionalProperties": false, + "type": "object", + "title": "Pacing" + }, + "PacingPatch": { + "properties": { + "max_concurrent": { + "anyOf": [ + { + "type": "integer", + "maximum": 10000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Concurrent" + }, + "max_calls_per_second": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Max Calls Per Second" + } + }, + "additionalProperties": false, + "type": "object", + "title": "PacingPatch" + }, + "PublicAgentInput": { + "properties": { + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextGreeting" + }, + { + "$ref": "#/components/schemas/AudioGreeting" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "audio": "#/components/schemas/AudioGreeting", + "text": "#/components/schemas/TextGreeting" + } + } + }, + { + "type": "null" + } + ], + "title": "Greeting" + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "$ref": "#/components/schemas/VariableDefinition" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/CascadedPipelineInput" + }, + { + "type": "null" + } + ] + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/FillerWords" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "PublicAgentInput" + }, + "PublicByokCredential": { + "properties": { + "mode": { + "type": "string", + "const": "byok", + "title": "Mode" + }, + "fields": { + "additionalProperties": { + "type": "string", + "minLength": 1 + }, + "type": "object", + "maxProperties": 8, + "minProperties": 1, + "description": "Provider credential fields for the selected vendor, for example {\"api_key\": \"$secrets..\"}. Agent writes require $secrets references; ephemeral sessions also accept direct values." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "fields", + "mode" + ], + "title": "PublicByokCredential" + }, + "PublicEphemeralSessionCreate": { + "properties": { + "agent": { + "$ref": "#/components/schemas/PublicAgentInput" + }, + "values": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Values" + }, + "labels": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Labels" + }, + "transport": { + "oneOf": [ + { + "$ref": "#/components/schemas/RtcTransport" + }, + { + "$ref": "#/components/schemas/TelephonyTransport" + } + ], + "title": "Transport", + "discriminator": { + "propertyName": "type", + "mapping": { + "rtc": "#/components/schemas/RtcTransport", + "telephony": "#/components/schemas/TelephonyTransport" + } + } + }, + "lifecycle": { + "anyOf": [ + { + "$ref": "#/components/schemas/Lifecycle" + }, + { + "type": "null" + } + ] + }, + "data_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/DataPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent", + "transport" + ], + "title": "PublicEphemeralSessionCreate" + }, + "ResolvedAsr": { + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Language" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedAsr" + }, + "ResolvedAvatar": { + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedAvatar" + }, + "ResolvedCascadedPipeline": { + "properties": { + "mode": { + "type": "string", + "const": "cascaded", + "title": "Mode" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedAvatar" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/TurnDetection" + }, + { + "type": "null" + } + ] + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedAsr" + }, + { + "type": "null" + } + ] + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedTts" + }, + { + "type": "null" + } + ] + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedLlm" + }, + { + "type": "null" + } + ] + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SilenceConfig" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ResolvedCascadedPipeline" + }, + "ResolvedCredential": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "managed", + "byok" + ], + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ResolvedCredential" + }, + "ResolvedLlm": { + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Failure Message" + }, + "mcp_servers": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/McpServer" + } + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/FunctionTool" + } + }, + { + "type": "null" + } + ], + "title": "Tools" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedLlm" + }, + "ResolvedTts": { + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedCredential" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "params": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Params" + }, + "skip_patterns": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Skip Patterns" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedTts" + }, + "RtcEncryption": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "aes_128_xts", + "aes_128_ecb", + "aes_256_xts", + "sm4_128_ecb", + "aes_128_gcm", + "aes_256_gcm", + "aes_128_gcm2", + "aes_256_gcm2" + ], + "title": "Mode" + }, + "key": { + "type": "string", + "minLength": 1, + "format": "password", + "title": "Key", + "writeOnly": true + }, + "salt": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "pattern": "^[A-Za-z0-9+/]{43}=$", + "title": "Salt" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "key" + ], + "title": "RtcEncryption" + }, + "RtcTransport": { + "properties": { + "type": { + "type": "string", + "const": "rtc", + "title": "Type" + }, + "channel": { + "type": "string", + "maxLength": 256, + "minLength": 1, + "title": "Channel" + }, + "token": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "Token" + }, + "uid": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "title": "Uid", + "description": "UID the agent uses to join the channel. Omit it to let the server assign one." + }, + "subscribe_uids": { + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "type": "array", + "maxItems": 1, + "minItems": 1, + "title": "Subscribe Uids", + "uniqueItems": true + }, + "encryption": { + "anyOf": [ + { + "$ref": "#/components/schemas/RtcEncryption" + }, + { + "type": "null" + } + ] + }, + "audio_scenario": { + "type": "string", + "enum": [ + "aiserver", + "chorus" + ], + "title": "Audio Scenario", + "default": "aiserver" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "channel", + "subscribe_uids" + ], + "title": "RtcTransport" + }, + "ScheduledSchedule": { + "properties": { + "mode": { + "type": "string", + "const": "scheduled", + "title": "Mode" + }, + "timezone": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Timezone" + }, + "start": { + "type": "string", + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]$", + "title": "Start" + }, + "end": { + "type": "string", + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]$", + "title": "End" + }, + "days": { + "items": { + "$ref": "#/components/schemas/CallingDay" + }, + "type": "array", + "maxItems": 7, + "minItems": 1, + "title": "Days" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "timezone", + "start", + "end" + ], + "title": "ScheduledSchedule" + }, + "SecretCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + }, + "secrets": { + "patternProperties": { + "^[A-Za-z0-9_]+$": { + "type": "string", + "format": "password", + "writeOnly": true + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1 + }, + "type": "object", + "minProperties": 1, + "title": "Secrets", + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "secrets" + ], + "title": "SecretCreate" + }, + "SecretDeleted": { + "properties": { + "deleted": { + "type": "boolean", + "const": true, + "title": "Deleted" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "deleted" + ], + "title": "SecretDeleted" + }, + "SecretKey": { + "properties": { + "key": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Key" + }, + "create_ts": { + "type": "integer", + "minimum": 0, + "description": "Key timestamp when returned by the service." + }, + "update_ts": { + "type": "integer", + "minimum": 0, + "description": "Key timestamp when returned by the service." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ], + "title": "SecretKey" + }, + "SecretKeyWrite": { + "properties": { + "value": { + "type": "string", + "minLength": 1, + "format": "password", + "title": "Value", + "writeOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "value" + ], + "title": "SecretKeyWrite" + }, + "SecretList": { + "properties": { + "data": { + "$ref": "#/components/schemas/SecretPage" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "SecretList" + }, + "SecretPage": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/SecretSet" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "SecretPage" + }, + "SecretReplace": { + "properties": { + "secrets": { + "patternProperties": { + "^[A-Za-z0-9_]+$": { + "type": "string", + "format": "password", + "writeOnly": true + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1 + }, + "type": "object", + "minProperties": 1, + "title": "Secrets", + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "secrets" + ], + "title": "SecretReplace" + }, + "SecretSet": { + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$", + "title": "Name" + }, + "keys": { + "items": { + "anyOf": [ + { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$" + }, + { + "$ref": "#/components/schemas/SecretKey" + } + ] + }, + "type": "array", + "title": "Keys", + "description": "Accepts legacy key names and launch key metadata objects during rollout." + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "update_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Update Ts" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "keys", + "create_ts", + "update_ts" + ], + "title": "SecretSet" + }, + "SecretUpdate": { + "properties": { + "secrets": { + "patternProperties": { + "^[A-Za-z0-9_]+$": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ] + }, + "^[A-Za-z0-9_]*-[A-Za-z0-9_-]*$": { + "type": "null", + "description": "An existing legacy hyphenated key may only be deleted, not written." + } + }, + "propertyNames": { + "maxLength": 64, + "minLength": 1 + }, + "type": "object", + "minProperties": 1, + "title": "Secrets", + "description": "Non-null writes require ASCII letters, digits or underscores; existing hyphenated keys may only be removed with null.", + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "secrets" + ], + "title": "SecretUpdate" + }, + "SemanticTurnEnd": { + "properties": { + "type": { + "type": "string", + "const": "semantic", + "title": "Type", + "default": "semantic" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "title": "Silence Duration Ms", + "default": 320 + }, + "max_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 500, + "maximum": 10000 + }, + { + "type": "integer", + "const": -1 + }, + { + "type": "null" + } + ], + "title": "Max Wait Ms", + "default": 3000, + "description": "500-10000 ms, or -1 to wait without a timeout." + }, + "pause_state_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Pause State Enabled", + "default": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "SemanticTurnEnd" + }, + "SemanticTurnStart": { + "properties": { + "type": { + "type": "string", + "const": "semantic", + "title": "Type", + "default": "semantic" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Speaking Interrupt Duration Ms", + "default": 160 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "SemanticTurnStart" + }, + "SessionAgentOverridesPatch": { + "properties": { + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "title": "Messages" + }, + "pipeline": { + "$ref": "#/components/schemas/SessionPipelinePatch", + "title": "Pipeline" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionAgentOverridesPatch" + }, + "SessionAgentPatch": { + "properties": { + "overrides": { + "$ref": "#/components/schemas/SessionAgentOverridesPatch", + "title": "Overrides" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionAgentPatch" + }, + "SessionCreate": { + "properties": { + "agent_id": { + "type": "string", + "format": "uuid", + "title": "Agent Id" + }, + "values": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Values" + }, + "labels": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Labels" + }, + "transport": { + "oneOf": [ + { + "$ref": "#/components/schemas/RtcTransport" + }, + { + "$ref": "#/components/schemas/TelephonyTransport" + } + ], + "title": "Transport", + "discriminator": { + "propertyName": "type", + "mapping": { + "rtc": "#/components/schemas/RtcTransport", + "telephony": "#/components/schemas/TelephonyTransport" + } + } + }, + "lifecycle": { + "anyOf": [ + { + "$ref": "#/components/schemas/Lifecycle" + }, + { + "type": "null" + } + ] + }, + "data_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/DataPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_id", + "transport" + ], + "title": "SessionCreate" + }, + "SessionData": { + "properties": { + "count": { + "type": "integer", + "minimum": 0.0, + "title": "Count" + }, + "list": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "List" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "count", + "list" + ], + "title": "SessionData" + }, + "SessionList": { + "properties": { + "data": { + "$ref": "#/components/schemas/SessionData" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data", + "has_more", + "next_cursor" + ], + "title": "SessionList" + }, + "SessionModulePatch": { + "properties": { + "params": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object", + "title": "Params", + "description": "Complete provider params replacement. Runtime endpoint aliases are not mutable." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionModulePatch" + }, + "SessionPatch": { + "properties": { + "transport": { + "$ref": "#/components/schemas/SessionTransportPatch", + "title": "Transport" + }, + "agent": { + "$ref": "#/components/schemas/SessionAgentPatch", + "title": "Agent" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionPatch" + }, + "SessionPipelinePatch": { + "properties": { + "llm": { + "$ref": "#/components/schemas/SessionModulePatch", + "title": "Llm" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionPipelinePatch" + }, + "SessionResponse": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name", + "description": "Name of the agent the session runs." + }, + "start_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Start Ts" + }, + "status": { + "type": "string", + "enum": [ + "IDLE", + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "FAILED" + ], + "title": "Status" + }, + "stop_ts": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Stop Ts" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message" + }, + "analysis": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionAnalysis" + }, + { + "type": "null" + } + ], + "description": "Optional extraction analysis. May be absent while analysis support is rolling out." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "start_ts", + "status" + ], + "title": "SessionResponse" + }, + "SessionStartResponse": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "create_ts": { + "type": "integer", + "minimum": 0, + "title": "Create Ts", + "description": "Unix timestamp in seconds when the session was created." + }, + "status": { + "type": "string", + "enum": [ + "STARTING", + "RUNNING" + ], + "title": "Status" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "create_ts", + "status" + ], + "title": "SessionStartResponse" + }, + "SessionStopResponse": { + "properties": {}, + "additionalProperties": false, + "type": "object", + "title": "SessionStopResponse" + }, + "SessionSummary": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "start_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Start Ts" + }, + "status": { + "type": "string", + "enum": [ + "IDLE", + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "FAILED" + ], + "title": "Status" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "start_ts", + "status" + ], + "title": "SessionSummary" + }, + "SessionTransportPatch": { + "properties": { + "token": { + "type": "string", + "minLength": 1, + "format": "password", + "title": "Token", + "writeOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "token" + ], + "title": "SessionTransportPatch" + }, + "SessionUpdateResponse": { + "properties": { + "agent_session_id": { + "type": "string", + "pattern": "^[a-z0-9]{32}$", + "title": "Agent Session Id" + }, + "create_ts": { + "type": "integer", + "minimum": 0.0, + "title": "Create Ts" + }, + "status": { + "type": "string", + "enum": [ + "IDLE", + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "FAILED" + ], + "title": "Status" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "agent_session_id", + "create_ts", + "status" + ], + "title": "SessionUpdateResponse" + }, + "SilenceConfig": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled" + }, + "timeout_ms": { + "type": "integer", + "title": "Timeout Ms" + }, + "action": { + "type": "string", + "enum": [ + "speak", + "think" + ], + "title": "Action" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "enabled", + "timeout_ms", + "action", + "content" + ], + "title": "SilenceConfig" + }, + "SipAuth": { + "properties": { + "username": { + "type": "string", + "title": "Username", + "writeOnly": true + }, + "password": { + "type": "string", + "format": "password", + "title": "Password", + "description": "Literal SIP password; secret references are not supported.", + "writeOnly": true + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "username", + "password" + ], + "title": "SipAuth" + }, + "SipTrunkInput": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "hostname": { + "type": "string", + "pattern": "^[^:/\\s]+$", + "title": "Hostname" + }, + "port": { + "anyOf": [ + { + "type": "integer", + "maximum": 65535.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Port" + }, + "transport": { + "type": "string", + "enum": [ + "udp", + "tcp", + "tls" + ], + "title": "Transport", + "default": "tls" + }, + "auth": { + "anyOf": [ + { + "$ref": "#/components/schemas/SipAuth" + }, + { + "type": "null" + } + ] + }, + "allowed_ips": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Ips", + "description": "Inbound source IPv4 addresses, IPv4 CIDRs (decimal prefix length 0-32 without leading zeros), or '*'. Host bits are accepted; entries are preserved unchanged. IPv6, dotted masks and hostnames unsupported. Omission defaults runtime inbound allowed_addresses to ['*']; [] explicitly clears it." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "hostname" + ], + "title": "SipTrunkInput" + }, + "SipTrunkOutput": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "hostname": { + "type": "string", + "title": "Hostname" + }, + "port": { + "type": "integer", + "title": "Port" + }, + "transport": { + "type": "string", + "enum": [ + "udp", + "tcp", + "tls" + ], + "title": "Transport" + }, + "allowed_ips": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Ips", + "description": "Inbound source IPv4 addresses, IPv4 CIDRs (decimal prefix length 0-32 without leading zeros), or '*'. Host bits are accepted; entries are preserved unchanged. IPv6, dotted masks and hostnames unsupported. Omission defaults runtime inbound allowed_addresses to ['*']; [] explicitly clears it." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "hostname", + "port", + "transport" + ], + "title": "SipTrunkOutput" + }, + "SkipPattern": { + "type": "string", + "enum": [ + "fullwidth_parentheses", + "lenticular_brackets", + "parentheses", + "square_brackets", + "curly_braces" + ] + }, + "SpeechInterruption": { + "properties": { + "mode": { + "type": "string", + "const": "start_of_speech", + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "SpeechInterruption" + }, + "StaticFillerContent": { + "properties": { + "mode": { + "type": "string", + "const": "static", + "title": "Mode" + }, + "phrases": { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array", + "title": "Phrases", + "minItems": 1, + "maxItems": 100 + }, + "selection_rule": { + "type": "string", + "enum": [ + "shuffle", + "round_robin" + ], + "title": "Selection Rule", + "default": "shuffle" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "phrases" + ], + "title": "StaticFillerContent" + }, + "TelephonyTransport": { + "properties": { + "type": { + "type": "string", + "const": "telephony", + "title": "Type" + }, + "from": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "title": "From", + "description": "ID of a Number in the project. Phone numbers are not accepted." + }, + "to": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "To" + }, + "call_policy": { + "anyOf": [ + { + "$ref": "#/components/schemas/CallPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "from", + "to" + ], + "title": "TelephonyTransport" + }, + "TextGreeting": { + "properties": { + "mode": { + "type": "string", + "const": "text", + "title": "Mode" + }, + "on": { + "type": "string", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "default": "each_join" + }, + "delay_ms": { + "type": "integer", + "title": "Delay Ms", + "default": 0, + "minimum": 0, + "maximum": 5000 + }, + "text": { + "type": "string", + "title": "Text" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode", + "text" + ], + "title": "TextGreeting" + }, + "TimeRange": { + "properties": { + "start": { + "type": "string", + "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", + "title": "Start" + }, + "end": { + "type": "string", + "pattern": "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$", + "title": "End" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "start", + "end" + ], + "title": "TimeRange" + }, + "ToolExecution": { + "properties": { + "mode": { + "type": "string", + "const": "sync", + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ToolExecution" + }, + "ToolServer": { + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST" + ], + "title": "Method" + }, + "url": { + "type": "string", + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object", + "maxProperties": 32 + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "body": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "timeout_ms": { + "anyOf": [ + { + "type": "integer", + "maximum": 100000.0, + "minimum": 1000.0 + }, + { + "type": "null" + } + ], + "title": "Timeout Ms", + "default": 10000 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "method", + "url" + ], + "title": "ToolServer" + }, + "TransferPolicy": { + "properties": { + "phone_number": { + "type": "string", + "pattern": "^\\+[1-9][0-9]{1,14}$", + "title": "Phone Number" + }, + "description": { + "type": "string", + "maxLength": 1000, + "minLength": 1, + "title": "Description" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "phone_number", + "description" + ], + "title": "TransferPolicy" + }, + "TtsInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "aws_access_key_id": { + "type": "string", + "minLength": 1 + }, + "aws_secret_access_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "aws_access_key_id", + "aws_secret_access_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "cartesia", + "deepgram", + "fishaudio", + "gemini", + "generic_http", + "gradium", + "mistral", + "murf", + "openai", + "rime", + "smallestai", + "typecast", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "cartesia", + "deepgram", + "elevenlabs", + "fishaudio", + "gemini", + "generic_http", + "google", + "gradium", + "humeai", + "microsoft", + "minimax", + "mistral", + "murf", + "openai", + "rime", + "sarvam", + "smallestai", + "typecast", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "elevenlabs", + "humeai", + "microsoft", + "minimax" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "credentials": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "credentials" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "sarvam" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "properties": { + "api_subscription_key": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "api_subscription_key" + ] + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "skip_patterns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/SkipPattern" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "uniqueItems": true, + "title": "Skip Patterns" + }, + "credential": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ManagedCredential" + }, + { + "$ref": "#/components/schemas/PublicByokCredential" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "byok": "#/components/schemas/PublicByokCredential", + "managed": "#/components/schemas/ManagedCredential" + } + } + }, + { + "type": "null" + } + ], + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/JSONObject" + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers" + } + }, + "additionalProperties": false, + "type": "object", + "title": "TtsInput" + }, + "TurnDetection": { + "properties": { + "speech_threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "title": "Speech Threshold", + "default": 0.5 + }, + "start_of_speech": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/SemanticTurnStart" + }, + { + "$ref": "#/components/schemas/VadTurnStart" + }, + { + "$ref": "#/components/schemas/ManualTurnBoundary" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "manual": "#/components/schemas/ManualTurnBoundary", + "semantic": "#/components/schemas/SemanticTurnStart", + "vad": "#/components/schemas/VadTurnStart" + } + } + }, + { + "type": "null" + } + ], + "title": "Start Of Speech" + }, + "end_of_speech": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/SemanticTurnEnd" + }, + { + "$ref": "#/components/schemas/VadTurnEnd" + }, + { + "$ref": "#/components/schemas/ManualTurnBoundary" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "manual": "#/components/schemas/ManualTurnBoundary", + "semantic": "#/components/schemas/SemanticTurnEnd", + "vad": "#/components/schemas/VadTurnEnd" + } + } + }, + { + "type": "null" + } + ], + "title": "End Of Speech" + }, + "interruption": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/SpeechInterruption" + }, + { + "$ref": "#/components/schemas/KeywordInterruption" + }, + { + "$ref": "#/components/schemas/DisabledInterruption" + } + ], + "discriminator": { + "propertyName": "mode", + "mapping": { + "keywords": "#/components/schemas/KeywordInterruption", + "off": "#/components/schemas/DisabledInterruption", + "start_of_speech": "#/components/schemas/SpeechInterruption" + } + } + }, + { + "type": "null" + } + ], + "title": "Interruption", + "default": { + "mode": "start_of_speech" + } + } + }, + "additionalProperties": false, + "type": "object", + "title": "TurnDetection" + }, + "VadTurnEnd": { + "properties": { + "type": { + "type": "string", + "const": "vad", + "title": "Type" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "title": "Silence Duration Ms", + "default": 640 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "VadTurnEnd" + }, + "VadTurnStart": { + "properties": { + "type": { + "type": "string", + "const": "vad", + "title": "Type" + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "title": "Interrupt Duration Ms", + "default": 160 + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "title": "Speaking Interrupt Duration Ms", + "default": 160 + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "title": "Prefix Padding Ms", + "default": 800 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "VadTurnStart" + }, + "VariableDefinition": { + "properties": { + "type": { + "type": "string", + "enum": [ + "str", + "int", + "float", + "bool", + "date", + "datetime" + ], + "title": "Type" + }, + "required": { + "type": "boolean", + "title": "Required", + "default": false + }, + "options": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "type": "array", + "maxItems": 100, + "minItems": 1 + }, + { + "type": "null" + } + ], + "title": "Options" + }, + "extract": { + "type": "boolean", + "title": "Extract", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "VariableDefinition" + }, + "VoicemailPolicy": { + "properties": { + "mode": { + "type": "string", + "enum": [ + "continue", + "hangup" + ], + "title": "Mode" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "VoicemailPolicy" + }, + "AgentRequest_AgentCreateInput": { + "additionalProperties": false, + "properties": { + "agent_name": { + "maxLength": 255, + "minLength": 1, + "title": "Agent Name", + "type": "string" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "audio": "#/components/schemas/AgentRequest_AudioGreeting", + "text": "#/components/schemas/AgentRequest_TextGreeting" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TextGreeting" + }, + { + "$ref": "#/components/schemas/AgentRequest_AudioGreeting" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Greeting" + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "$ref": "#/components/schemas/AgentRequest_VariableDefinition" + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedPipelineInput" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Omission, null, or an empty object selects the managed ARES, OpenAI gpt-4.1-mini, and MiniMax speech-2.8-turbo default pipeline." + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerWords" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "agent_name" + ], + "title": "AgentCreateInput", + "type": "object" + }, + "AgentRequest_AgentPatch": { + "additionalProperties": false, + "properties": { + "agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Name" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Instructions" + }, + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_Message" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Messages" + }, + "greeting": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_GreetingPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "variables": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_VariableDefinitionPatch" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Variables" + }, + "pipeline": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedPipelinePatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "filler_words": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerWordsPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "AgentPatch", + "type": "object" + }, + "AgentRequest_AsrInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "access_key_id": { + "minLength": 1, + "type": "string" + }, + "secret_access_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "access_key_id", + "secret_access_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "assemblyai", + "deepgram", + "gemini", + "openai", + "sarvam", + "smallestai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "ares", + "assemblyai", + "deepgram", + "gemini", + "google", + "microsoft", + "openai", + "sarvam", + "smallestai", + "speechmatics", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "adc_credentials_string": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "adc_credentials_string" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "microsoft", + "speechmatics" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + } + }, + "title": "AsrInput", + "type": "object" + }, + "AgentRequest_AsrPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + }, + "then": { + "properties": { + "url": { + "type": "null" + } + } + } + } + ], + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Non-null URLs require effective BYOK mode; null clears a saved override.", + "title": "Url" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Language" + } + }, + "title": "AsrPatch", + "type": "object" + }, + "AgentRequest_AudioGreeting": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "audio", + "title": "Mode", + "type": "string" + }, + "on": { + "default": "each_join", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "type": "string" + }, + "delay_ms": { + "default": 0, + "title": "Delay Ms", + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + "url": { + "title": "Url", + "type": "string", + "maxLength": 2048 + }, + "text": { + "title": "Text", + "type": "string", + "minLength": 1 + }, + "download_timeout_ms": { + "default": 1000, + "title": "Download Timeout Ms", + "type": "integer", + "minimum": 200, + "maximum": 10000 + }, + "pcm_sample_rate": { + "default": 16000, + "title": "Pcm Sample Rate", + "type": "integer" + } + }, + "required": [ + "mode", + "url", + "text" + ], + "title": "AudioGreeting", + "type": "object" + }, + "AgentRequest_AvatarInput": { + "additionalProperties": false, + "properties": { + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarVendor" + }, + { + "type": "null" + } + ], + "default": null + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + } + }, + "title": "AvatarInput", + "type": "object" + }, + "AgentRequest_AvatarPatch": { + "additionalProperties": false, + "properties": { + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarVendor" + }, + { + "type": "null" + } + ], + "default": null + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + } + }, + "title": "AvatarPatch", + "type": "object" + }, + "AgentRequest_AvatarVendor": { + "enum": [ + "akool", + "liveavatar", + "anam", + "sensetime", + "spatius", + "generic" + ], + "type": "string" + }, + "AgentRequest_CascadedPipelineInput": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "cascaded", + "title": "Mode", + "type": "string" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TurnDetection" + }, + { + "type": "null" + } + ], + "default": null + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AsrInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TtsInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_LlmInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SilenceConfig" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "mode" + ], + "title": "CascadedPipelineInput", + "type": "object" + }, + "AgentRequest_CascadedPipelinePatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "const": "cascaded", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "asr": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AsrPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "tts": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TtsPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "llm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_LlmPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "silence_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SilenceConfigPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "CascadedPipelinePatch", + "type": "object" + }, + "AgentRequest_CascadedTurnDetectionPatch": { + "additionalProperties": false, + "properties": { + "speech_threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Speech Threshold" + }, + "start_of_speech": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TurnBoundaryPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "end_of_speech": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_TurnBoundaryPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "interruption": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_InterruptionPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "CascadedTurnDetectionPatch", + "type": "object" + }, + "AgentRequest_DisabledInterruption": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "off", + "title": "Mode", + "type": "string" + }, + "when_off": { + "default": "append", + "enum": [ + "append", + "ignore" + ], + "title": "When Off", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "DisabledInterruption", + "type": "object" + }, + "AgentRequest_FillerContentPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "static", + "generated" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "phrases": { + "anyOf": [ + { + "items": { + "type": "string", + "minLength": 1 + }, + "type": "array", + "minItems": 1, + "maxItems": 100 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Phrases" + }, + "selection_rule": { + "anyOf": [ + { + "enum": [ + "shuffle", + "round_robin" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Selection Rule" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Instructions" + }, + "fallback_strategy": { + "anyOf": [ + { + "const": "static", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fallback Strategy" + } + }, + "title": "FillerContentPatch", + "type": "object" + }, + "AgentRequest_FillerTrigger": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "fixed_time", + "title": "Mode", + "type": "string" + }, + "response_wait_ms": { + "title": "Response Wait Ms", + "type": "integer" + } + }, + "required": [ + "mode", + "response_wait_ms" + ], + "title": "FillerTrigger", + "type": "object" + }, + "AgentRequest_FillerTriggerPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "const": "fixed_time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "response_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 100, + "maximum": 10000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Response Wait Ms" + } + }, + "title": "FillerTriggerPatch", + "type": "object" + }, + "AgentRequest_FillerWords": { + "additionalProperties": false, + "properties": { + "enable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enable" + }, + "trigger": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerTrigger" + }, + { + "type": "null" + } + ], + "default": null + }, + "content": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "generated": "#/components/schemas/AgentRequest_GeneratedFillerContent", + "static": "#/components/schemas/AgentRequest_StaticFillerContent" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_StaticFillerContent" + }, + { + "$ref": "#/components/schemas/AgentRequest_GeneratedFillerContent" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Content" + } + }, + "title": "FillerWords", + "type": "object" + }, + "AgentRequest_FillerWordsPatch": { + "additionalProperties": false, + "properties": { + "enable": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enable" + }, + "trigger": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerTriggerPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "content": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_FillerContentPatch" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "FillerWordsPatch", + "type": "object" + }, + "AgentRequest_FunctionTool": { + "additionalProperties": false, + "properties": { + "type": { + "const": "function", + "title": "Type", + "type": "string" + }, + "name": { + "pattern": "^(?![mM][cC][pP])[A-Za-z][A-Za-z0-9]{0,63}$", + "title": "Name", + "type": "string" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "title": "Description", + "type": "string" + }, + "parameters": { + "$ref": "#/components/schemas/AgentRequest_JSONObject", + "description": "JSON Schema (draft 2020-12) describing the tool arguments." + }, + "execution": { + "$ref": "#/components/schemas/AgentRequest_ToolExecution" + }, + "server": { + "$ref": "#/components/schemas/AgentRequest_ToolServer" + } + }, + "required": [ + "type", + "name", + "description", + "parameters", + "execution", + "server" + ], + "title": "FunctionTool", + "type": "object" + }, + "AgentRequest_GeneratedFillerContent": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "generated", + "title": "Mode", + "type": "string" + }, + "instructions": { + "title": "Instructions", + "type": "string" + }, + "fallback_strategy": { + "anyOf": [ + { + "const": "static", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fallback Strategy" + } + }, + "required": [ + "mode", + "instructions" + ], + "title": "GeneratedFillerContent", + "type": "object" + }, + "AgentRequest_GreetingPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "text", + "audio" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "on": { + "anyOf": [ + { + "enum": [ + "each_join", + "first_join" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "On" + }, + "delay_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Delay Ms" + }, + "url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2048 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Text" + }, + "download_timeout_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 200, + "maximum": 10000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Download Timeout Ms" + }, + "pcm_sample_rate": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pcm Sample Rate" + } + }, + "title": "GreetingPatch", + "type": "object" + }, + "AgentRequest_InterruptionPatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "start_of_speech", + "keywords", + "off" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "keywords": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "maxItems": 128 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Keywords" + }, + "when_off": { + "anyOf": [ + { + "enum": [ + "append", + "ignore" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "When Off" + } + }, + "title": "InterruptionPatch", + "type": "object" + }, + "AgentRequest_JSONObject": { + "additionalProperties": { + "$ref": "#/components/schemas/AgentRequest_JsonValue" + }, + "type": "object" + }, + "AgentRequest_JsonValue": {}, + "AgentRequest_KeywordInterruption": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "keywords", + "title": "Mode", + "type": "string" + }, + "keywords": { + "items": { + "type": "string" + }, + "title": "Keywords", + "type": "array", + "maxItems": 128 + } + }, + "required": [ + "mode", + "keywords" + ], + "title": "KeywordInterruption", + "type": "object" + }, + "AgentRequest_LlmInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + }, + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "access_key": { + "minLength": 1, + "type": "string" + }, + "secret_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "access_key", + "secret_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "azure", + "anthropic", + "dify", + "gemini", + "vertexai", + "groq", + "openai", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_FunctionTool" + }, + "maxItems": 32, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tools" + }, + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "maximum": 1024, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Failure Message" + } + }, + "title": "LlmInput", + "type": "object" + }, + "AgentRequest_LlmPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + }, + "then": { + "properties": { + "url": { + "type": "null" + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mcp Servers" + }, + "tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_FunctionTool" + }, + "maxItems": 32, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tools" + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Non-null URLs require effective BYOK mode; null clears a saved override.", + "title": "Url" + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Style" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "input_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Modalities" + }, + "output_modalities": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Modalities" + }, + "max_history": { + "anyOf": [ + { + "maximum": 1024, + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max History" + }, + "failure_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Failure Message" + } + }, + "title": "LlmPatch", + "type": "object" + }, + "AgentRequest_ManagedCredential": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "managed", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "ManagedCredential", + "type": "object" + }, + "AgentRequest_ManualTurnBoundary": { + "additionalProperties": false, + "properties": { + "type": { + "const": "manual", + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ManualTurnBoundary", + "type": "object" + }, + "AgentRequest_McpServer": { + "additionalProperties": false, + "properties": { + "name": { + "pattern": "^[A-Za-z0-9]{1,48}$", + "title": "Name", + "type": "string" + }, + "endpoint": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Endpoint", + "type": "string" + }, + "transport": { + "const": "streamable_http", + "title": "Transport", + "type": "string" + }, + "headers": { + "anyOf": [ + { + "maxProperties": 32, + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "allowed_tools": { + "anyOf": [ + { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Allowed Tools" + }, + "timeout_ms": { + "anyOf": [ + { + "maximum": 100000, + "minimum": 1000, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Ms" + } + }, + "required": [ + "name", + "endpoint", + "transport" + ], + "title": "McpServer", + "type": "object" + }, + "AgentRequest_Message": { + "additionalProperties": false, + "properties": { + "role": { + "enum": [ + "system", + "user", + "assistant" + ], + "title": "Role", + "type": "string" + }, + "content": { + "title": "Content", + "type": "string" + } + }, + "required": [ + "role", + "content" + ], + "title": "Message", + "type": "object" + }, + "AgentRequest_PublicByokCredential": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "byok", + "title": "Mode", + "type": "string" + }, + "fields": { + "additionalProperties": { + "minLength": 1, + "type": "string", + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + }, + "maxProperties": 8, + "minProperties": 1, + "type": "object", + "description": "Provider credential fields for the selected vendor, for example {\"api_key\": \"$secrets..\"}. Agent writes require $secrets references; ephemeral sessions also accept direct values." + } + }, + "required": [ + "fields", + "mode" + ], + "title": "PublicByokCredential", + "type": "object" + }, + "AgentRequest_PublicCredentialPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "properties": { + "fields": { + "type": "null" + } + } + } + } + ], + "properties": { + "mode": { + "anyOf": [ + { + "enum": [ + "managed", + "byok" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "fields": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + }, + { + "type": "null" + } + ] + }, + "maxProperties": 8, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fields" + } + }, + "title": "PublicCredentialPatch", + "type": "object", + "description": "JSON Merge Patch of provider credentials. Non-null field values on saved-agent writes must be $secrets references. Set fields to null when switching to managed credentials; individual null map entries remove stored fields. The merged result must satisfy the credential schema for the selected provider." + }, + "AgentRequest_SemanticTurnEnd": { + "additionalProperties": false, + "properties": { + "type": { + "const": "semantic", + "title": "Type", + "type": "string", + "default": "semantic" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "default": 320, + "title": "Silence Duration Ms" + }, + "max_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 500, + "maximum": 10000 + }, + { + "type": "integer", + "const": -1 + }, + { + "type": "null" + } + ], + "default": 3000, + "title": "Max Wait Ms", + "description": "500-10000 ms, or -1 to wait without a timeout." + }, + "pause_state_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "title": "Pause State Enabled" + } + }, + "required": [ + "type" + ], + "title": "SemanticTurnEnd", + "type": "object" + }, + "AgentRequest_SemanticTurnStart": { + "additionalProperties": false, + "properties": { + "type": { + "const": "semantic", + "title": "Type", + "type": "string", + "default": "semantic" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 160, + "title": "Speaking Interrupt Duration Ms" + } + }, + "required": [ + "type" + ], + "title": "SemanticTurnStart", + "type": "object" + }, + "AgentRequest_SilenceConfig": { + "additionalProperties": false, + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "timeout_ms": { + "title": "Timeout Ms", + "type": "integer" + }, + "action": { + "enum": [ + "speak", + "think" + ], + "title": "Action", + "type": "string" + }, + "content": { + "title": "Content", + "type": "string" + } + }, + "required": [ + "enabled", + "timeout_ms", + "action", + "content" + ], + "title": "SilenceConfig", + "type": "object" + }, + "AgentRequest_SilenceConfigPatch": { + "additionalProperties": false, + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Enabled" + }, + "timeout_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Ms" + }, + "action": { + "anyOf": [ + { + "enum": [ + "speak", + "think" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Content" + } + }, + "title": "SilenceConfigPatch", + "type": "object" + }, + "AgentRequest_SkipPattern": { + "enum": [ + "fullwidth_parentheses", + "lenticular_brackets", + "parentheses", + "square_brackets", + "curly_braces" + ], + "type": "string" + }, + "AgentRequest_SpeechInterruption": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "start_of_speech", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "SpeechInterruption", + "type": "object" + }, + "AgentRequest_StaticFillerContent": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "static", + "title": "Mode", + "type": "string" + }, + "phrases": { + "items": { + "type": "string", + "minLength": 1 + }, + "title": "Phrases", + "type": "array", + "minItems": 1, + "maxItems": 100 + }, + "selection_rule": { + "default": "shuffle", + "enum": [ + "shuffle", + "round_robin" + ], + "title": "Selection Rule", + "type": "string" + } + }, + "required": [ + "mode", + "phrases" + ], + "title": "StaticFillerContent", + "type": "object" + }, + "AgentRequest_TextGreeting": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "text", + "title": "Mode", + "type": "string" + }, + "on": { + "default": "each_join", + "enum": [ + "each_join", + "first_join" + ], + "title": "On", + "type": "string" + }, + "delay_ms": { + "default": 0, + "title": "Delay Ms", + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + "text": { + "title": "Text", + "type": "string" + } + }, + "required": [ + "mode", + "text" + ], + "title": "TextGreeting", + "type": "object" + }, + "AgentRequest_ToolExecution": { + "additionalProperties": false, + "properties": { + "mode": { + "const": "sync", + "title": "Mode", + "type": "string" + } + }, + "required": [ + "mode" + ], + "title": "ToolExecution", + "type": "object" + }, + "AgentRequest_ToolServer": { + "additionalProperties": false, + "properties": { + "method": { + "enum": [ + "GET", + "POST" + ], + "title": "Method", + "type": "string" + }, + "url": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^https://", + "title": "Url", + "type": "string" + }, + "headers": { + "anyOf": [ + { + "maxProperties": 32, + "patternProperties": { + "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$": { + "type": "string" + } + }, + "propertyNames": { + "maxLength": 128, + "minLength": 1 + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "body": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "timeout_ms": { + "anyOf": [ + { + "maximum": 100000, + "minimum": 1000, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 10000, + "title": "Timeout Ms" + } + }, + "required": [ + "method", + "url" + ], + "title": "ToolServer", + "type": "object" + }, + "AgentRequest_TtsInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "amazon" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "aws_access_key_id": { + "minLength": 1, + "type": "string" + }, + "aws_secret_access_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "aws_access_key_id", + "aws_secret_access_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "properties": { + "vendor": { + "enum": [ + "cartesia", + "deepgram", + "fishaudio", + "gemini", + "generic_http", + "gradium", + "mistral", + "murf", + "openai", + "rime", + "smallestai", + "typecast", + "xai" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "not": { + "required": [ + "vendor" + ] + } + }, + { + "properties": { + "vendor": { + "not": { + "enum": [ + "amazon", + "cartesia", + "deepgram", + "elevenlabs", + "fishaudio", + "gemini", + "generic_http", + "google", + "gradium", + "humeai", + "microsoft", + "minimax", + "mistral", + "murf", + "openai", + "rime", + "sarvam", + "smallestai", + "typecast", + "xai" + ] + } + } + } + } + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "elevenlabs", + "humeai", + "microsoft", + "minimax" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "google" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "credentials": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "credentials" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + }, + { + "if": { + "allOf": [ + { + "properties": { + "vendor": { + "enum": [ + "sarvam" + ] + } + }, + "required": [ + "vendor" + ] + }, + { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + ] + }, + "then": { + "properties": { + "credential": { + "anyOf": [ + { + "properties": { + "fields": { + "additionalProperties": false, + "properties": { + "api_subscription_key": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "api_subscription_key" + ], + "type": "object" + } + }, + "required": [ + "fields" + ] + } + ] + } + } + } + } + ], + "properties": { + "skip_patterns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_SkipPattern" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Skip Patterns", + "uniqueItems": true + }, + "credential": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "byok": "#/components/schemas/AgentRequest_PublicByokCredential", + "managed": "#/components/schemas/AgentRequest_ManagedCredential" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_ManagedCredential" + }, + { + "$ref": "#/components/schemas/AgentRequest_PublicByokCredential" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential" + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + } + }, + "title": "TtsInput", + "type": "object" + }, + "AgentRequest_TtsPatch": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "managed" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + }, + "then": { + "properties": { + "url": { + "type": "null" + } + } + } + } + ], + "properties": { + "skip_patterns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_SkipPattern" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Skip Patterns", + "uniqueItems": true + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_PublicCredentialPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "vendor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Vendor" + }, + "params": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_JSONObject" + }, + { + "type": "null" + } + ], + "default": null + }, + "url": { + "anyOf": [ + { + "format": "uri", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Non-null URLs require effective BYOK mode; null clears a saved override.", + "title": "Url" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + } + }, + "title": "TtsPatch", + "type": "object" + }, + "AgentRequest_TurnBoundaryPatch": { + "additionalProperties": false, + "properties": { + "type": { + "anyOf": [ + { + "enum": [ + "semantic", + "vad", + "manual" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Type" + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Interrupt Duration Ms" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Speaking Interrupt Duration Ms" + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Prefix Padding Ms" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Silence Duration Ms" + }, + "max_wait_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 500, + "maximum": 10000 + }, + { + "type": "integer", + "const": -1 + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Wait Ms", + "description": "500-10000 ms, or -1 to wait without a timeout." + }, + "pause_state_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pause State Enabled" + } + }, + "title": "TurnBoundaryPatch", + "type": "object" + }, + "AgentRequest_TurnDetection": { + "additionalProperties": false, + "properties": { + "speech_threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ], + "default": 0.5, + "title": "Speech Threshold" + }, + "start_of_speech": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "manual": "#/components/schemas/AgentRequest_ManualTurnBoundary", + "semantic": "#/components/schemas/AgentRequest_SemanticTurnStart", + "vad": "#/components/schemas/AgentRequest_VadTurnStart" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SemanticTurnStart" + }, + { + "$ref": "#/components/schemas/AgentRequest_VadTurnStart" + }, + { + "$ref": "#/components/schemas/AgentRequest_ManualTurnBoundary" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Of Speech" + }, + "end_of_speech": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "manual": "#/components/schemas/AgentRequest_ManualTurnBoundary", + "semantic": "#/components/schemas/AgentRequest_SemanticTurnEnd", + "vad": "#/components/schemas/AgentRequest_VadTurnEnd" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SemanticTurnEnd" + }, + { + "$ref": "#/components/schemas/AgentRequest_VadTurnEnd" + }, + { + "$ref": "#/components/schemas/AgentRequest_ManualTurnBoundary" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "End Of Speech" + }, + "interruption": { + "anyOf": [ + { + "discriminator": { + "mapping": { + "keywords": "#/components/schemas/AgentRequest_KeywordInterruption", + "off": "#/components/schemas/AgentRequest_DisabledInterruption", + "start_of_speech": "#/components/schemas/AgentRequest_SpeechInterruption" + }, + "propertyName": "mode" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/AgentRequest_SpeechInterruption" + }, + { + "$ref": "#/components/schemas/AgentRequest_KeywordInterruption" + }, + { + "$ref": "#/components/schemas/AgentRequest_DisabledInterruption" + } + ] + }, + { + "type": "null" + } + ], + "default": { + "mode": "start_of_speech" + }, + "title": "Interruption" + } + }, + "title": "TurnDetection", + "type": "object" + }, + "AgentRequest_VadTurnEnd": { + "additionalProperties": false, + "properties": { + "type": { + "const": "vad", + "title": "Type", + "type": "string" + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 2000 + }, + { + "type": "null" + } + ], + "default": 640, + "title": "Silence Duration Ms" + } + }, + "required": [ + "type" + ], + "title": "VadTurnEnd", + "type": "object" + }, + "AgentRequest_VadTurnStart": { + "additionalProperties": false, + "properties": { + "type": { + "const": "vad", + "title": "Type", + "type": "string" + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": 160, + "title": "Interrupt Duration Ms" + }, + "speaking_interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 120, + "maximum": 1200 + }, + { + "type": "null" + } + ], + "default": 160, + "title": "Speaking Interrupt Duration Ms" + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + { + "type": "null" + } + ], + "default": 800, + "title": "Prefix Padding Ms" + } + }, + "required": [ + "type" + ], + "title": "VadTurnStart", + "type": "object" + }, + "AgentRequest_VariableDefinition": { + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "str", + "int", + "float", + "bool", + "date", + "datetime" + ], + "title": "Type", + "type": "string" + }, + "required": { + "default": false, + "title": "Required", + "type": "boolean" + }, + "options": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "maxItems": 100, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Options" + }, + "extract": { + "default": false, + "title": "Extract", + "type": "boolean" + }, + "description": { + "anyOf": [ + { + "maxLength": 2000, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + } + }, + "required": [ + "type" + ], + "title": "VariableDefinition", + "type": "object" + }, + "AgentRequest_VariableDefinitionPatch": { + "additionalProperties": false, + "properties": { + "type": { + "anyOf": [ + { + "enum": [ + "str", + "int", + "float", + "bool", + "date", + "datetime" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Type" + }, + "required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Required" + }, + "options": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "maxItems": 100, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Options" + }, + "extract": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Extract" + }, + "description": { + "anyOf": [ + { + "maxLength": 2000, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + } + }, + "title": "VariableDefinitionPatch", + "type": "object" + }, + "AgentCreateInput": { + "$ref": "#/components/schemas/AgentRequest_AgentCreateInput" + }, + "AgentPatch": { + "$ref": "#/components/schemas/AgentRequest_AgentPatch" + }, + "SessionEndCallPolicy": { + "properties": { + "on_conversation_complete": { + "type": "boolean", + "title": "On Conversation Complete", + "default": false + }, + "on_user_request": { + "type": "boolean", + "title": "On User Request", + "default": false + }, + "on_fax": { + "type": "boolean", + "title": "On Fax", + "default": false + }, + "on_ai_assistant": { + "type": "boolean", + "title": "On Ai Assistant", + "default": false + }, + "on_voicemail": { + "type": "boolean", + "title": "On Voicemail", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "EndCallPolicy" + }, + "InboundEndCallPolicy": { + "properties": { + "on_conversation_complete": { + "type": "boolean", + "title": "On Conversation Complete", + "default": false + }, + "on_user_request": { + "type": "boolean", + "title": "On User Request", + "default": false + }, + "on_voicemail": { + "type": "boolean", + "title": "On Voicemail", + "default": false + }, + "on_ai_assistant": { + "type": "boolean", + "title": "On Ai Assistant", + "default": false + }, + "on_fax": { + "type": "boolean", + "title": "On Fax", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "EndCallPolicy" + }, + "SessionAnalysis": { + "properties": { + "status": { + "type": "string", + "enum": [ + "pending", + "completed", + "failed", + "skipped" + ], + "title": "Status" + }, + "skip_reason": { + "anyOf": [ + { + "type": "string", + "enum": [ + "no_user_turns", + "retention_none" + ] + }, + { + "type": "null" + } + ], + "title": "Skip Reason" + }, + "complete_ts": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Complete Ts" + }, + "result": { + "anyOf": [ + { + "patternProperties": { + "^[a-z][a-z0-9_]{0,63}$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Result" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "status", + "skip_reason", + "complete_ts", + "result", + "error" + ], + "title": "SessionAnalysis" + } + }, + "securitySchemes": { + "AgoraTokenAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "An Agora AccessToken2 token. Platform session routes require the ConvoAI service (service type 9); an RTC + RTM token alone is insufficient. Use a dedicated ConvoAI token builder. Send the complete header value: agora token=. SDKs accept a supplied token or generate one from App ID and App Certificate. HTTP Basic authentication is also supported as an alternative." + }, + "BasicAuth": { + "type": "http", + "scheme": "basic", + "description": "HTTP Basic authentication using an Agora Customer ID and Customer Secret. Send Authorization: Basic . This is an alternative to AgoraTokenAuth; supply one authentication method." + } + }, + "parameters": { + "IdempotencyKey": { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[\\x21-\\x7E]+$" + }, + "description": "Optional idempotency key. The server retains each key for 24 hours. Repeating the same request with the same key and body returns the first response without performing the operation again, with Idempotent-Replayed: true. Reusing a key with a different body, or while the original request is still in progress, returns 409 IdempotencyKeyConflict. If validation rejects a request before the operation starts, correct the request and reuse the same key. If the operation was accepted or its outcome is unknown, keep the same key; using a new key can start a second operation. This header is supported only for session starts and campaign creation/pause/resume/cancel, not agent, secret, or number creation.", + "example": "6f1c2b0e-support-4821" + } + }, + "headers": { + "IdempotentReplayed": { + "description": "Present with value true when this response replays the original response for the same idempotency key and body within the 24-hour retention period.", + "schema": { + "type": "boolean", + "const": true + }, + "example": true + } + } + }, + "servers": [ + { + "url": "https://preview.ai.agora.io", + "description": "Agora Conversational AI API" + } + ], + "security": [ + { + "AgoraTokenAuth": [] + }, + { + "BasicAuth": [] + } + ] +} diff --git a/src/agora_agent/v3/session_contract.py b/src/agora_agent/v3/session_contract.py new file mode 100644 index 0000000..97d641f --- /dev/null +++ b/src/agora_agent/v3/session_contract.py @@ -0,0 +1,120 @@ +"""Validation shared by public session requests and AgentKit's internal adapter.""" +import math +import re +import uuid + + +def _object(value, allowed, field): + if not isinstance(value, dict) or set(value) - allowed: + raise ValueError("Invalid " + field + " fields") + + +def _integer(value, minimum, maximum, field): + if isinstance(value, bool) or not isinstance(value, int) or not minimum <= value <= maximum: + raise ValueError(field + " is outside its supported integer range") + + +def validate_session(body): + _object(body, {"agent_id", "agent", "transport", "values", "labels", "lifecycle", "data_policy"}, "session") + transport = body["transport"] + if not isinstance(transport, dict): + raise ValueError("transport must be an object") + if transport.get("type") == "rtc": + _validate_rtc_transport(transport) + elif transport.get("type") == "telephony": + _validate_telephony_transport(transport) + else: + raise ValueError("Unsupported session transport type") + if body.get("labels") is not None: + if not isinstance(body["labels"], dict) or any(not isinstance(k, str) or not isinstance(v, str) for k, v in body["labels"].items()): + raise ValueError("labels must map strings to strings") + if body.get("values") is not None: + if not isinstance(body["values"], dict) or any(not isinstance(k, str) or not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", k) or not isinstance(v, (str, int, float, bool)) or isinstance(v, float) and not math.isfinite(v) for k, v in body["values"].items()): + raise ValueError("values must map variable names to finite JSON primitives") + if body.get("data_policy") is not None: + policy = body["data_policy"] + _object(policy, {"retention"}, "data_policy") + if policy.get("retention", "30_days") not in {"30_days", "none"}: + raise ValueError("Unsupported retention") + if body.get("lifecycle") is not None: + life = body["lifecycle"] + _object(life, {"idle_timeout_ms", "max_duration_ms", "graceful_stop"}, "lifecycle") + for field, minimum in (("idle_timeout_ms", 0), ("max_duration_ms", 1000)): + if field in life: + _integer(life[field], minimum, 3600000, field) + if life.get("graceful_stop") is not None: + graceful = life["graceful_stop"] + _object(graceful, {"enabled", "timeout_ms"}, "graceful_stop") + if "enabled" in graceful and not isinstance(graceful["enabled"], bool): + raise ValueError("graceful_stop.enabled must be bool") + if "timeout_ms" in graceful: + _integer(graceful["timeout_ms"], 0, 120000, "graceful_stop.timeout_ms") + + +def _validate_rtc_transport(transport): + _object(transport, {"type", "channel", "uid", "subscribe_uids", "token", "encryption", "audio_scenario", "geofence"}, "transport") + if not isinstance(transport.get("channel"), str) or not 1 <= len(transport["channel"]) <= 256 or not transport["channel"].strip(): + raise ValueError("channel must contain 1-256 characters") + uid = transport.get("uid") + subscribers = transport.get("subscribe_uids") + if uid is not None and (not isinstance(uid, str) or not uid.strip() or len(uid) > 255): + raise ValueError("uid must be a nonempty string") + if not isinstance(subscribers, list) or len(subscribers) != 1 or not isinstance(subscribers[0], str) or not subscribers[0].strip() or len(subscribers[0]) > 255 or (subscribers[0] == "*" or re.fullmatch(r"0+", subscribers[0])): + raise ValueError("subscribe_uids must contain exactly one non-wildcard UID") + numeric = lambda value: re.fullmatch(r"[0-9]+", value) is not None + if uid is not None and uid != "0" and numeric(uid) != numeric(subscribers[0]): + raise ValueError("agent and subscriber must use the same UID type") + if transport.get("geofence") is not None: + fence = transport["geofence"] + _object(fence, {"area", "exclude_area"}, "geofence") + areas = {"global", "north_america", "europe", "asia", "india", "japan"} + if fence.get("area") not in areas: + raise ValueError("Invalid geofence.area") + if fence.get("exclude_area") is not None and (fence["area"] != "global" or fence["exclude_area"] not in areas - {"global"}): + raise ValueError("geofence.exclude_area is only supported with global area") + if "token" in transport and not isinstance(transport["token"], str): + raise ValueError("RTC token must be a string") + if transport.get("audio_scenario", "aiserver") not in {"aiserver", "chorus"}: + raise ValueError("Unsupported audio_scenario") + if "encryption" in transport: + enc = transport["encryption"] + _object(enc, {"mode", "key", "salt"}, "encryption") + if enc.get("mode") not in {"aes_128_xts", "aes_128_ecb", "aes_256_xts", "sm4_128_ecb", "aes_128_gcm", "aes_256_gcm", "aes_128_gcm2", "aes_256_gcm2"} or not isinstance(enc.get("key"), str) or not enc["key"]: + raise ValueError("Invalid RTC encryption mode or key") + if enc.get("salt") is not None and (not isinstance(enc["salt"], str) or not re.fullmatch(r"[A-Za-z0-9+/]{43}=", enc["salt"])): + raise ValueError("Invalid RTC encryption salt") + + +def _validate_telephony_transport(transport): + _object(transport, {"type", "from", "to", "call_policy"}, "telephony transport") + source = transport.get("from") + try: + if not isinstance(source, str) or str(uuid.UUID(source)) != source.lower(): + raise ValueError("invalid UUID") + except (ValueError, AttributeError): + raise ValueError("telephony from must be a registered project number UUID") from None + if not isinstance(transport.get("to"), str) or not re.fullmatch(r"\+[1-9][0-9]{1,14}", transport["to"]): + raise ValueError("telephony to must be an E.164 phone number") + policy = transport.get("call_policy") + if policy is None: + return + _object(policy, {"max_ring_duration_ms", "max_call_duration_seconds", "max_silence_duration_ms", "voicemail", "end_call", "transfer"}, "call_policy") + for field in ("max_ring_duration_ms", "max_call_duration_seconds", "max_silence_duration_ms"): + if policy.get(field) is not None: + _integer(policy[field], 1, math.inf, field) + if policy.get("voicemail") is not None: + _object(policy["voicemail"], {"mode"}, "voicemail") + if policy["voicemail"].get("mode") not in {"continue", "hangup"}: + raise ValueError("Unsupported voicemail mode") + if policy.get("end_call") is not None: + end = policy["end_call"] + _object(end, {"on_conversation_complete", "on_user_request", "on_fax", "on_ai_assistant", "on_voicemail"}, "end_call") + if any(not isinstance(v, bool) for v in end.values()): + raise ValueError("end_call settings must be bool") + if policy.get("transfer") is not None: + transfer = policy["transfer"] + _object(transfer, {"phone_number", "description"}, "transfer") + if not isinstance(transfer.get("phone_number"), str) or not re.fullmatch(r"\+[1-9][0-9]{1,14}", transfer["phone_number"]): + raise ValueError("transfer phone_number must be E.164") + if not isinstance(transfer.get("description"), str) or not 1 <= len(transfer["description"]) <= 1000: + raise ValueError("transfer requires a description of 1-1000 characters") diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py new file mode 100644 index 0000000..866ad1e --- /dev/null +++ b/src/agora_agent/v3/types.py @@ -0,0 +1,949 @@ +"""Generated wire types from root openapi.json; constraints live in the shared contract.""" +from __future__ import annotations + +from typing import Any, Dict, List, Union + +from typing_extensions import Literal, Required, TypedDict + + +class AgentDeleteResponse(TypedDict, total=False): + deleted: Required[Literal[True]] + +class AgentListData(TypedDict, total=False): + count: Required[int] + list: Required[List[AgentListItem]] + +class AgentListItem(TypedDict, total=False): + agent_id: Required[str] + agent_name: Required[str] + create_ts: Required[int] + update_ts: Required[int] + +class AgentListResponse(TypedDict, total=False): + data: Required[AgentListData] + has_more: Required[bool] + next_cursor: Required[Union[str, None]] + +class AgentResponse(TypedDict, total=False): + agent_id: Required[str] + create_ts: Required[int] + update_ts: Required[int] + agent_name: Required[str] + instructions: Union[str, None] + messages: Union[List[Message], None] + greeting: Union[Union[TextGreeting, AudioGreeting], None] + variables: Union[Dict[str, VariableDefinition], None] + pipeline: Union[ResolvedCascadedPipeline, None] + filler_words: Union[FillerWords, None] + +class AsrInput(TypedDict, total=False): + credential: Union[Union[ManagedCredential, PublicByokCredential], None] + vendor: Union[str, None] + params: Union[JSONObject, None] + url: Union[str, None] + model: Union[str, None] + language: Union[str, None] + +class AudioGreeting(TypedDict, total=False): + mode: Required[Literal['audio']] + on: Literal['each_join', 'first_join'] + delay_ms: int + url: Required[str] + text: Required[str] + download_timeout_ms: int + pcm_sample_rate: int + +class AvatarInput(TypedDict, total=False): + credential: Union[Union[ManagedCredential, PublicByokCredential], None] + vendor: Union[AvatarVendor, None] + params: Union[JSONObject, None] + enabled: Union[bool, None] + +class CallPolicy(TypedDict, total=False): + max_ring_duration_ms: Union[int, None] + max_call_duration_seconds: Union[int, None] + voicemail: Union[VoicemailPolicy, None] + max_silence_duration_ms: Union[int, None] + end_call: Union[SessionEndCallPolicy, None] + transfer: Union[TransferPolicy, None] + +class CallingDay(TypedDict, total=False): + weekday: Required[Literal['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']] + ranges: Required[List[TimeRange]] + +class CampaignCallPolicy(TypedDict, total=False): + max_ring_duration_ms: Union[int, None] + max_call_duration_seconds: Union[int, None] + voicemail: Union[CampaignVoicemailPolicy, None] + max_silence_duration_ms: Union[int, None] + end_call: Union[CampaignEndCallPolicy, None] + transfer: Union[CampaignTransferPolicy, None] + +class CampaignCounts(TypedDict, total=False): + contacts: Required[int] + pending: Required[int] + calling: Required[int] + completed: Required[int] + failed: Required[int] + canceled: Required[int] + +class CampaignCreate(TypedDict, total=False): + name: Required[str] + agent_id: Required[str] + transport: Required[CampaignTransport] + contacts: Required[List[ContactInput]] + lifecycle: CampaignLifecycle + pacing: Pacing + schedule: Required[Union[ImmediateSchedule, ScheduledSchedule]] + data_policy: DataPolicy + +class CampaignDeleteResponse(TypedDict, total=False): + deleted: Literal[True] + +class CampaignEndCallPolicy(TypedDict, total=False): + on_conversation_complete: bool + on_user_request: bool + on_fax: bool + on_ai_assistant: bool + on_voicemail: bool + +class CampaignFailure(TypedDict, total=False): + code: Required[str] + provider: Union[str, None] + message: Required[str] + ts: Required[int] + +class CampaignLifecycle(TypedDict, total=False): + idle_timeout_ms: int + max_duration_ms: int + graceful_stop: GracefulStop + +class CampaignListData(TypedDict, total=False): + count: Required[int] + list: Required[List[CampaignResponse]] + +class CampaignListResponse(TypedDict, total=False): + data: Required[CampaignListData] + has_more: Required[bool] + next_cursor: Required[Union[str, None]] + +class CampaignPatch(TypedDict, total=False): + lifecycle: Union[CampaignLifecycle, None] + pacing: Union[PacingPatch, None] + schedule: Union[Union[ImmediateSchedule, ScheduledSchedule], None] + transport: Union[CampaignTransportPatch, None] + +class CampaignResponse(TypedDict, total=False): + campaign_id: Required[str] + name: Required[str] + status: Required[Literal['scheduled', 'running', 'paused', 'completed', 'canceled', 'failed']] + agent_id: Required[str] + transport: Required[CampaignTransportResponse] + lifecycle: Required[CampaignLifecycle] + schedule: Required[Union[ImmediateSchedule, ScheduledSchedule]] + counts: Required[CampaignCounts] + create_ts: Required[int] + start_ts: Required[Union[int, None]] + complete_ts: Required[Union[int, None]] + failure: Required[Union[CampaignFailure, None]] + +class CampaignTransferPolicy(TypedDict, total=False): + phone_number: Required[str] + description: Required[str] + +CampaignTransport = TypedDict('CampaignTransport', {'type': Required["Literal['telephony']"], 'from': Required['Union[str, List[str]]'], 'call_policy': 'Union[CampaignCallPolicy, None]'}, total=False) + +class CampaignTransportPatch(TypedDict, total=False): + call_policy: Union[CampaignCallPolicy, None] + +CampaignTransportResponse = TypedDict('CampaignTransportResponse', {'type': Required["Literal['telephony']"], 'from': Required['List[str]'], 'call_policy': 'Union[CampaignCallPolicy, None]'}, total=False) + +class CampaignVoicemailPolicy(TypedDict, total=False): + mode: Required[Literal['continue', 'hangup']] + +class CascadedPipelineInput(TypedDict, total=False): + mode: Required[Literal['cascaded']] + avatar: Union[AvatarInput, None] + turn_detection: Union[TurnDetection, None] + asr: Union[AsrInput, None] + tts: Union[TtsInput, None] + llm: Union[LlmInput, None] + silence_config: Union[SilenceConfig, None] + +class ContactInput(TypedDict, total=False): + phone: Required[str] + values: Dict[str, Union[str, int, float, bool]] + +class DataPolicy(TypedDict, total=False): + retention: Literal['30_days', 'none'] + +class DisabledInterruption(TypedDict, total=False): + mode: Required[Literal['off']] + when_off: Literal['append', 'ignore'] + +class ErrorResponse(TypedDict, total=False): + detail: Required[str] + reason: Required[ErrorReason] + +class FillerTrigger(TypedDict, total=False): + mode: Required[Literal['fixed_time']] + response_wait_ms: Required[int] + +class FillerWords(TypedDict, total=False): + enable: Union[bool, None] + trigger: Union[FillerTrigger, None] + content: Union[Union[StaticFillerContent, GeneratedFillerContent], None] + +class FunctionTool(TypedDict, total=False): + type: Required[Literal['function']] + name: Required[str] + description: Required[str] + parameters: Required[JSONObject] + execution: Required[ToolExecution] + server: Required[ToolServer] + +class GeneratedFillerContent(TypedDict, total=False): + mode: Required[Literal['generated']] + instructions: Required[str] + fallback_strategy: Union[Literal['static'], None] + +class GracefulStop(TypedDict, total=False): + enabled: bool + timeout_ms: int + +class ImmediateSchedule(TypedDict, total=False): + mode: Required[Literal['immediate']] + timezone: str + days: List[CallingDay] + +class InboundCallPolicy(TypedDict, total=False): + max_call_duration_seconds: Union[int, None] + max_silence_duration_ms: Union[int, None] + end_call: Union[InboundEndCallPolicy, None] + transfer: Union[TransferPolicy, None] + +class InboundInput(TypedDict, total=False): + agent: Required[Union[str, None]] + call_policy: Union[InboundCallPolicy, None] + +class InboundOutput(TypedDict, total=False): + agent: Required[Union[str, None]] + call_policy: Union[InboundCallPolicy, None] + +class KeywordInterruption(TypedDict, total=False): + mode: Required[Literal['keywords']] + keywords: Required[List[str]] + +class Lifecycle(TypedDict, total=False): + idle_timeout_ms: int + max_duration_ms: int + graceful_stop: Union[GracefulStop, None] + +class LlmInput(TypedDict, total=False): + mcp_servers: Union[List[McpServer], None] + tools: Union[List[FunctionTool], None] + credential: Union[Union[ManagedCredential, PublicByokCredential], None] + vendor: Union[str, None] + params: Union[JSONObject, None] + url: Union[str, None] + style: Union[str, None] + model: Union[str, None] + headers: Union[Dict[str, str], None] + input_modalities: Union[List[str], None] + output_modalities: Union[List[str], None] + max_history: Union[int, None] + failure_message: Union[str, None] + +class ManagedCredential(TypedDict, total=False): + mode: Required[Literal['managed']] + +class ManualTurnBoundary(TypedDict, total=False): + type: Required[Literal['manual']] + +class McpServer(TypedDict, total=False): + name: Required[str] + endpoint: Required[str] + transport: Required[Literal['streamable_http']] + headers: Union[Dict[str, str], None] + allowed_tools: Union[List[str], None] + timeout_ms: Union[int, None] + +class Message(TypedDict, total=False): + role: Required[Literal['system', 'user', 'assistant']] + content: Required[str] + +class NumberCreate(TypedDict, total=False): + number: Required[str] + description: Union[str, None] + sip_trunk: Required[SipTrunkInput] + inbound: Union[InboundInput, None] + +class NumberDeleteResponse(TypedDict, total=False): + deleted: Literal[True] + +class NumberListData(TypedDict, total=False): + count: Required[int] + list: Required[List[NumberResponse]] + +class NumberListResponse(TypedDict, total=False): + data: Required[NumberListData] + has_more: Required[bool] + next_cursor: Required[Union[str, None]] + +class NumberPatch(TypedDict, total=False): + number: Union[str, None] + description: Union[str, None] + sip_trunk: Union[SipTrunkInput, None] + inbound: Union[InboundInput, None] + +class NumberResponse(TypedDict, total=False): + number_id: Required[str] + number: Required[str] + description: Union[str, None] + sip_trunk: Union[SipTrunkOutput, None] + inbound: Union[InboundOutput, None] + create_ts: Required[int] + +class Pacing(TypedDict, total=False): + max_concurrent: int + max_calls_per_second: Union[int, None] + +class PacingPatch(TypedDict, total=False): + max_concurrent: Union[int, None] + max_calls_per_second: Union[int, None] + +class PublicAgentInput(TypedDict, total=False): + instructions: Union[str, None] + messages: Union[List[Message], None] + greeting: Union[Union[TextGreeting, AudioGreeting], None] + variables: Union[Dict[str, VariableDefinition], None] + pipeline: Union[CascadedPipelineInput, None] + filler_words: Union[FillerWords, None] + +class PublicByokCredential(TypedDict, total=False): + mode: Required[Literal['byok']] + fields: Required[Dict[str, str]] + +class PublicEphemeralSessionCreate(TypedDict, total=False): + agent: Required[PublicAgentInput] + values: Union[Dict[str, Union[str, int, float, bool]], None] + labels: Union[Dict[str, str], None] + transport: Required[Union[RtcTransport, TelephonyTransport]] + lifecycle: Union[Lifecycle, None] + data_policy: Union[DataPolicy, None] + +class ResolvedAsr(TypedDict, total=False): + credential: Union[ResolvedCredential, None] + url: Union[str, None] + vendor: Union[str, None] + model: Union[str, None] + language: Union[str, None] + params: Union[Dict[str, JsonValue], None] + +class ResolvedAvatar(TypedDict, total=False): + enabled: Union[bool, None] + credential: Union[ResolvedCredential, None] + vendor: Union[str, None] + params: Union[Dict[str, JsonValue], None] + +class ResolvedCascadedPipeline(TypedDict, total=False): + mode: Required[Literal['cascaded']] + avatar: Union[ResolvedAvatar, None] + turn_detection: Union[TurnDetection, None] + asr: Union[ResolvedAsr, None] + tts: Union[ResolvedTts, None] + llm: Union[ResolvedLlm, None] + silence_config: Union[SilenceConfig, None] + +class ResolvedCredential(TypedDict, total=False): + mode: Required[Literal['managed', 'byok']] + +class ResolvedLlm(TypedDict, total=False): + credential: Union[ResolvedCredential, None] + url: Union[str, None] + vendor: Union[str, None] + style: Union[str, None] + model: Union[str, None] + params: Union[Dict[str, JsonValue], None] + input_modalities: Union[List[str], None] + output_modalities: Union[List[str], None] + max_history: Union[int, None] + failure_message: Union[str, None] + mcp_servers: Union[List[McpServer], None] + tools: Union[List[FunctionTool], None] + +class ResolvedTts(TypedDict, total=False): + credential: Union[ResolvedCredential, None] + url: Union[str, None] + vendor: Union[str, None] + model: Union[str, None] + params: Union[Dict[str, JsonValue], None] + skip_patterns: Union[List[str], None] + +class RtcEncryption(TypedDict, total=False): + mode: Required[Literal['aes_128_xts', 'aes_128_ecb', 'aes_256_xts', 'sm4_128_ecb', 'aes_128_gcm', 'aes_256_gcm', 'aes_128_gcm2', 'aes_256_gcm2']] + key: Required[str] + salt: Union[str, None] + +class RtcTransport(TypedDict, total=False): + type: Required[Literal['rtc']] + channel: Required[str] + token: Union[str, None] + uid: str + subscribe_uids: Required[List[str]] + encryption: Union[RtcEncryption, None] + audio_scenario: Literal['aiserver', 'chorus'] + +class ScheduledSchedule(TypedDict, total=False): + mode: Required[Literal['scheduled']] + timezone: Required[str] + start: Required[str] + end: Required[str] + days: List[CallingDay] + +class SecretCreate(TypedDict, total=False): + name: Required[str] + secrets: Required[Dict[str, str]] + +class SecretDeleted(TypedDict, total=False): + deleted: Required[Literal[True]] + +class SecretKey(TypedDict, total=False): + key: Required[str] + create_ts: int + update_ts: int + +class SecretKeyWrite(TypedDict, total=False): + value: Required[str] + +class SecretList(TypedDict, total=False): + data: Required[SecretPage] + has_more: Required[bool] + next_cursor: Required[Union[str, None]] + +class SecretPage(TypedDict, total=False): + count: Required[int] + list: Required[List[SecretSet]] + +class SecretReplace(TypedDict, total=False): + secrets: Required[Dict[str, str]] + +class SecretSet(TypedDict, total=False): + name: Required[str] + keys: Required[List[Union[str, SecretKey]]] + create_ts: Required[int] + update_ts: Required[int] + +class SecretUpdate(TypedDict, total=False): + secrets: Required[Dict[str, Union[str, None]]] + +class SemanticTurnEnd(TypedDict, total=False): + type: Required[Literal['semantic']] + silence_duration_ms: Union[int, None] + max_wait_ms: Union[int, Literal[-1], None] + pause_state_enabled: Union[bool, None] + +class SemanticTurnStart(TypedDict, total=False): + type: Required[Literal['semantic']] + speaking_interrupt_duration_ms: Union[int, None] + +class SessionAgentOverridesPatch(TypedDict, total=False): + messages: List[Message] + pipeline: SessionPipelinePatch + +class SessionAgentPatch(TypedDict, total=False): + overrides: SessionAgentOverridesPatch + +class SessionCreate(TypedDict, total=False): + agent_id: Required[str] + values: Union[Dict[str, Union[str, int, float, bool]], None] + labels: Union[Dict[str, str], None] + transport: Required[Union[RtcTransport, TelephonyTransport]] + lifecycle: Union[Lifecycle, None] + data_policy: Union[DataPolicy, None] + +class SessionData(TypedDict, total=False): + count: Required[int] + list: Required[List[SessionSummary]] + +class SessionList(TypedDict, total=False): + data: Required[SessionData] + has_more: Required[bool] + next_cursor: Required[Union[str, None]] + +class SessionModulePatch(TypedDict, total=False): + params: Dict[str, JsonValue] + +class SessionPatch(TypedDict, total=False): + transport: SessionTransportPatch + agent: SessionAgentPatch + +class SessionPipelinePatch(TypedDict, total=False): + llm: SessionModulePatch + +class SessionResponse(TypedDict, total=False): + agent_session_id: Required[str] + agent_name: str + start_ts: Required[int] + status: Required[Literal['IDLE', 'STARTING', 'RUNNING', 'STOPPING', 'STOPPED', 'FAILED']] + stop_ts: Union[int, None] + message: Union[str, None] + analysis: Union[SessionAnalysis, None] + +class SessionStartResponse(TypedDict, total=False): + agent_session_id: Required[str] + create_ts: Required[int] + status: Required[Literal['STARTING', 'RUNNING']] + +class SessionStopResponse(TypedDict, total=False): + pass + +class SessionSummary(TypedDict, total=False): + agent_session_id: Required[str] + start_ts: Required[int] + status: Required[Literal['IDLE', 'STARTING', 'RUNNING', 'STOPPING', 'STOPPED', 'FAILED']] + +class SessionTransportPatch(TypedDict, total=False): + token: Required[str] + +class SessionUpdateResponse(TypedDict, total=False): + agent_session_id: Required[str] + create_ts: Required[int] + status: Required[Literal['IDLE', 'STARTING', 'RUNNING', 'STOPPING', 'STOPPED', 'FAILED']] + +class SilenceConfig(TypedDict, total=False): + enabled: Required[bool] + timeout_ms: Required[int] + action: Required[Literal['speak', 'think']] + content: Required[str] + +class SipAuth(TypedDict, total=False): + username: Required[str] + password: Required[str] + +class SipTrunkInput(TypedDict, total=False): + name: Union[str, None] + hostname: Required[str] + port: Union[int, None] + transport: Literal['udp', 'tcp', 'tls'] + auth: Union[SipAuth, None] + allowed_ips: Union[List[str], None] + +class SipTrunkOutput(TypedDict, total=False): + name: Union[str, None] + hostname: Required[str] + port: Required[int] + transport: Required[Literal['udp', 'tcp', 'tls']] + allowed_ips: Union[List[str], None] + +class SpeechInterruption(TypedDict, total=False): + mode: Required[Literal['start_of_speech']] + +class StaticFillerContent(TypedDict, total=False): + mode: Required[Literal['static']] + phrases: Required[List[str]] + selection_rule: Literal['shuffle', 'round_robin'] + +TelephonyTransport = TypedDict('TelephonyTransport', {'type': Required["Literal['telephony']"], 'from': Required['str'], 'to': Required['str'], 'call_policy': 'Union[CallPolicy, None]'}, total=False) + +class TextGreeting(TypedDict, total=False): + mode: Required[Literal['text']] + on: Literal['each_join', 'first_join'] + delay_ms: int + text: Required[str] + +class TimeRange(TypedDict, total=False): + start: Required[str] + end: Required[str] + +class ToolExecution(TypedDict, total=False): + mode: Required[Literal['sync']] + +class ToolServer(TypedDict, total=False): + method: Required[Literal['GET', 'POST']] + url: Required[str] + headers: Union[Dict[str, str], None] + body: Union[JSONObject, None] + timeout_ms: Union[int, None] + +class TransferPolicy(TypedDict, total=False): + phone_number: Required[str] + description: Required[str] + +class TtsInput(TypedDict, total=False): + skip_patterns: Union[List[SkipPattern], None] + credential: Union[Union[ManagedCredential, PublicByokCredential], None] + vendor: Union[str, None] + params: Union[JSONObject, None] + url: Union[str, None] + headers: Union[Dict[str, str], None] + +class TurnDetection(TypedDict, total=False): + speech_threshold: Union[float, int, None] + start_of_speech: Union[Union[SemanticTurnStart, VadTurnStart, ManualTurnBoundary], None] + end_of_speech: Union[Union[SemanticTurnEnd, VadTurnEnd, ManualTurnBoundary], None] + interruption: Union[Union[SpeechInterruption, KeywordInterruption, DisabledInterruption], None] + +class VadTurnEnd(TypedDict, total=False): + type: Required[Literal['vad']] + silence_duration_ms: Union[int, None] + +class VadTurnStart(TypedDict, total=False): + type: Required[Literal['vad']] + interrupt_duration_ms: Union[int, None] + speaking_interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + +class VariableDefinition(TypedDict, total=False): + type: Required[Literal['str', 'int', 'float', 'bool', 'date', 'datetime']] + required: bool + options: Union[List[Union[str, int, float, bool]], None] + extract: bool + description: Union[str, None] + +class VoicemailPolicy(TypedDict, total=False): + mode: Required[Literal['continue', 'hangup']] + +class AgentRequest_AgentCreateInput(TypedDict, total=False): + agent_name: Required[str] + instructions: Union[str, None] + messages: Union[List[AgentRequest_Message], None] + greeting: Union[Union[AgentRequest_TextGreeting, AgentRequest_AudioGreeting], None] + variables: Union[Dict[str, AgentRequest_VariableDefinition], None] + pipeline: Union[AgentRequest_CascadedPipelineInput, None] + filler_words: Union[AgentRequest_FillerWords, None] + +class AgentRequest_AgentPatch(TypedDict, total=False): + agent_name: Union[str, None] + instructions: Union[str, None] + messages: Union[List[AgentRequest_Message], None] + greeting: Union[AgentRequest_GreetingPatch, None] + variables: Union[Dict[str, Union[AgentRequest_VariableDefinitionPatch, None]], None] + pipeline: Union[AgentRequest_CascadedPipelinePatch, None] + filler_words: Union[AgentRequest_FillerWordsPatch, None] + +class AgentRequest_AsrInput(TypedDict, total=False): + credential: Union[Union[AgentRequest_ManagedCredential, AgentRequest_PublicByokCredential], None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + model: Union[str, None] + language: Union[str, None] + +class AgentRequest_AsrPatch(TypedDict, total=False): + credential: Union[AgentRequest_PublicCredentialPatch, None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + model: Union[str, None] + language: Union[str, None] + +class AgentRequest_AudioGreeting(TypedDict, total=False): + mode: Required[Literal['audio']] + on: Literal['each_join', 'first_join'] + delay_ms: int + url: Required[str] + text: Required[str] + download_timeout_ms: int + pcm_sample_rate: int + +class AgentRequest_AvatarInput(TypedDict, total=False): + credential: Union[Union[AgentRequest_ManagedCredential, AgentRequest_PublicByokCredential], None] + vendor: Union[AgentRequest_AvatarVendor, None] + params: Union[AgentRequest_JSONObject, None] + enabled: Union[bool, None] + +class AgentRequest_AvatarPatch(TypedDict, total=False): + credential: Union[AgentRequest_PublicCredentialPatch, None] + vendor: Union[AgentRequest_AvatarVendor, None] + params: Union[AgentRequest_JSONObject, None] + enabled: Union[bool, None] + +class AgentRequest_CascadedPipelineInput(TypedDict, total=False): + mode: Required[Literal['cascaded']] + avatar: Union[AgentRequest_AvatarInput, None] + turn_detection: Union[AgentRequest_TurnDetection, None] + asr: Union[AgentRequest_AsrInput, None] + tts: Union[AgentRequest_TtsInput, None] + llm: Union[AgentRequest_LlmInput, None] + silence_config: Union[AgentRequest_SilenceConfig, None] + +class AgentRequest_CascadedPipelinePatch(TypedDict, total=False): + mode: Union[Literal['cascaded'], None] + avatar: Union[AgentRequest_AvatarPatch, None] + turn_detection: Union[AgentRequest_CascadedTurnDetectionPatch, None] + asr: Union[AgentRequest_AsrPatch, None] + tts: Union[AgentRequest_TtsPatch, None] + llm: Union[AgentRequest_LlmPatch, None] + silence_config: Union[AgentRequest_SilenceConfigPatch, None] + +class AgentRequest_CascadedTurnDetectionPatch(TypedDict, total=False): + speech_threshold: Union[float, int, None] + start_of_speech: Union[AgentRequest_TurnBoundaryPatch, None] + end_of_speech: Union[AgentRequest_TurnBoundaryPatch, None] + interruption: Union[AgentRequest_InterruptionPatch, None] + +class AgentRequest_DisabledInterruption(TypedDict, total=False): + mode: Required[Literal['off']] + when_off: Literal['append', 'ignore'] + +class AgentRequest_FillerContentPatch(TypedDict, total=False): + mode: Union[Literal['static', 'generated'], None] + phrases: Union[List[str], None] + selection_rule: Union[Literal['shuffle', 'round_robin'], None] + instructions: Union[str, None] + fallback_strategy: Union[Literal['static'], None] + +class AgentRequest_FillerTrigger(TypedDict, total=False): + mode: Required[Literal['fixed_time']] + response_wait_ms: Required[int] + +class AgentRequest_FillerTriggerPatch(TypedDict, total=False): + mode: Union[Literal['fixed_time'], None] + response_wait_ms: Union[int, None] + +class AgentRequest_FillerWords(TypedDict, total=False): + enable: Union[bool, None] + trigger: Union[AgentRequest_FillerTrigger, None] + content: Union[Union[AgentRequest_StaticFillerContent, AgentRequest_GeneratedFillerContent], None] + +class AgentRequest_FillerWordsPatch(TypedDict, total=False): + enable: Union[bool, None] + trigger: Union[AgentRequest_FillerTriggerPatch, None] + content: Union[AgentRequest_FillerContentPatch, None] + +class AgentRequest_FunctionTool(TypedDict, total=False): + type: Required[Literal['function']] + name: Required[str] + description: Required[str] + parameters: Required[AgentRequest_JSONObject] + execution: Required[AgentRequest_ToolExecution] + server: Required[AgentRequest_ToolServer] + +class AgentRequest_GeneratedFillerContent(TypedDict, total=False): + mode: Required[Literal['generated']] + instructions: Required[str] + fallback_strategy: Union[Literal['static'], None] + +class AgentRequest_GreetingPatch(TypedDict, total=False): + mode: Union[Literal['text', 'audio'], None] + on: Union[Literal['each_join', 'first_join'], None] + delay_ms: Union[int, None] + url: Union[str, None] + text: Union[str, None] + download_timeout_ms: Union[int, None] + pcm_sample_rate: Union[int, None] + +class AgentRequest_InterruptionPatch(TypedDict, total=False): + mode: Union[Literal['start_of_speech', 'keywords', 'off'], None] + keywords: Union[List[str], None] + when_off: Union[Literal['append', 'ignore'], None] + +class AgentRequest_KeywordInterruption(TypedDict, total=False): + mode: Required[Literal['keywords']] + keywords: Required[List[str]] + +class AgentRequest_LlmInput(TypedDict, total=False): + mcp_servers: Union[List[AgentRequest_McpServer], None] + tools: Union[List[AgentRequest_FunctionTool], None] + credential: Union[Union[AgentRequest_ManagedCredential, AgentRequest_PublicByokCredential], None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + style: Union[str, None] + model: Union[str, None] + headers: Union[Dict[str, str], None] + input_modalities: Union[List[str], None] + output_modalities: Union[List[str], None] + max_history: Union[int, None] + failure_message: Union[str, None] + +class AgentRequest_LlmPatch(TypedDict, total=False): + mcp_servers: Union[List[AgentRequest_McpServer], None] + tools: Union[List[AgentRequest_FunctionTool], None] + credential: Union[AgentRequest_PublicCredentialPatch, None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + style: Union[str, None] + model: Union[str, None] + headers: Union[Dict[str, str], None] + input_modalities: Union[List[str], None] + output_modalities: Union[List[str], None] + max_history: Union[int, None] + failure_message: Union[str, None] + +class AgentRequest_ManagedCredential(TypedDict, total=False): + mode: Required[Literal['managed']] + +class AgentRequest_ManualTurnBoundary(TypedDict, total=False): + type: Required[Literal['manual']] + +class AgentRequest_McpServer(TypedDict, total=False): + name: Required[str] + endpoint: Required[str] + transport: Required[Literal['streamable_http']] + headers: Union[Dict[str, str], None] + allowed_tools: Union[List[str], None] + timeout_ms: Union[int, None] + +class AgentRequest_Message(TypedDict, total=False): + role: Required[Literal['system', 'user', 'assistant']] + content: Required[str] + +class AgentRequest_PublicByokCredential(TypedDict, total=False): + mode: Required[Literal['byok']] + fields: Required[Dict[str, str]] + +class AgentRequest_PublicCredentialPatch(TypedDict, total=False): + mode: Union[Literal['managed', 'byok'], None] + fields: Union[Dict[str, Union[str, None]], None] + +class AgentRequest_SemanticTurnEnd(TypedDict, total=False): + type: Required[Literal['semantic']] + silence_duration_ms: Union[int, None] + max_wait_ms: Union[int, Literal[-1], None] + pause_state_enabled: Union[bool, None] + +class AgentRequest_SemanticTurnStart(TypedDict, total=False): + type: Required[Literal['semantic']] + speaking_interrupt_duration_ms: Union[int, None] + +class AgentRequest_SilenceConfig(TypedDict, total=False): + enabled: Required[bool] + timeout_ms: Required[int] + action: Required[Literal['speak', 'think']] + content: Required[str] + +class AgentRequest_SilenceConfigPatch(TypedDict, total=False): + enabled: Union[bool, None] + timeout_ms: Union[int, None] + action: Union[Literal['speak', 'think'], None] + content: Union[str, None] + +class AgentRequest_SpeechInterruption(TypedDict, total=False): + mode: Required[Literal['start_of_speech']] + +class AgentRequest_StaticFillerContent(TypedDict, total=False): + mode: Required[Literal['static']] + phrases: Required[List[str]] + selection_rule: Literal['shuffle', 'round_robin'] + +class AgentRequest_TextGreeting(TypedDict, total=False): + mode: Required[Literal['text']] + on: Literal['each_join', 'first_join'] + delay_ms: int + text: Required[str] + +class AgentRequest_ToolExecution(TypedDict, total=False): + mode: Required[Literal['sync']] + +class AgentRequest_ToolServer(TypedDict, total=False): + method: Required[Literal['GET', 'POST']] + url: Required[str] + headers: Union[Dict[str, str], None] + body: Union[AgentRequest_JSONObject, None] + timeout_ms: Union[int, None] + +class AgentRequest_TtsInput(TypedDict, total=False): + skip_patterns: Union[List[AgentRequest_SkipPattern], None] + credential: Union[Union[AgentRequest_ManagedCredential, AgentRequest_PublicByokCredential], None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + headers: Union[Dict[str, str], None] + +class AgentRequest_TtsPatch(TypedDict, total=False): + skip_patterns: Union[List[AgentRequest_SkipPattern], None] + credential: Union[AgentRequest_PublicCredentialPatch, None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + headers: Union[Dict[str, str], None] + +class AgentRequest_TurnBoundaryPatch(TypedDict, total=False): + type: Union[Literal['semantic', 'vad', 'manual'], None] + interrupt_duration_ms: Union[int, None] + speaking_interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + max_wait_ms: Union[int, Literal[-1], None] + pause_state_enabled: Union[bool, None] + +class AgentRequest_TurnDetection(TypedDict, total=False): + speech_threshold: Union[float, int, None] + start_of_speech: Union[Union[AgentRequest_SemanticTurnStart, AgentRequest_VadTurnStart, AgentRequest_ManualTurnBoundary], None] + end_of_speech: Union[Union[AgentRequest_SemanticTurnEnd, AgentRequest_VadTurnEnd, AgentRequest_ManualTurnBoundary], None] + interruption: Union[Union[AgentRequest_SpeechInterruption, AgentRequest_KeywordInterruption, AgentRequest_DisabledInterruption], None] + +class AgentRequest_VadTurnEnd(TypedDict, total=False): + type: Required[Literal['vad']] + silence_duration_ms: Union[int, None] + +class AgentRequest_VadTurnStart(TypedDict, total=False): + type: Required[Literal['vad']] + interrupt_duration_ms: Union[int, None] + speaking_interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + +class AgentRequest_VariableDefinition(TypedDict, total=False): + type: Required[Literal['str', 'int', 'float', 'bool', 'date', 'datetime']] + required: bool + options: Union[List[Union[str, int, float, bool]], None] + extract: bool + description: Union[str, None] + +class AgentRequest_VariableDefinitionPatch(TypedDict, total=False): + type: Union[Literal['str', 'int', 'float', 'bool', 'date', 'datetime'], None] + required: Union[bool, None] + options: Union[List[Union[str, int, float, bool]], None] + extract: Union[bool, None] + description: Union[str, None] + +class SessionEndCallPolicy(TypedDict, total=False): + on_conversation_complete: bool + on_user_request: bool + on_fax: bool + on_ai_assistant: bool + on_voicemail: bool + +class InboundEndCallPolicy(TypedDict, total=False): + on_conversation_complete: bool + on_user_request: bool + on_voicemail: bool + on_ai_assistant: bool + on_fax: bool + +class SessionAnalysis(TypedDict, total=False): + status: Required[Literal['pending', 'completed', 'failed', 'skipped']] + skip_reason: Required[Union[Literal['no_user_turns', 'retention_none'], None]] + complete_ts: Required[Union[int, None]] + result: Required[Union[Dict[str, Union[str, int, float, bool, None]], None]] + error: Required[Union[str, None]] + +AvatarVendor = Literal['akool', 'liveavatar', 'anam', 'sensetime', 'spatius', 'generic'] +ErrorReason = Literal['InvalidRequestBody', 'MissingRequiredField', 'InvalidFieldValue', 'InvalidModuleParameter', 'UnknownSecretReference', 'NotImplemented', 'InvalidPermission', 'ServiceNotEnabled', 'AccountSuspended', 'TaskNotFound', 'ResourceNotFound', 'AgentNotFound', 'NumberNotFound', 'NumberAlreadyExists', 'ResourceConflict', 'MethodNotAllowed', 'PayloadTooLarge', 'UnsupportedMediaType', 'TaskConflict', 'CampaignActive', 'CampaignInvalidState', 'CampaignNotEditable', 'IdempotencyKeyConflict', 'SecretSetAlreadyExists', 'SecretSetInUse', 'SecretSetEmpty', 'TaskNotRunning', 'RateLimitExceeded', 'PreconditionFailed', 'PreconditionRequired', 'ResourceQuotaLimitExceeded', 'ConcurrencyLimitExceeded', 'InternalError', 'ServiceUnavailable', 'ResourceAllocationFailed', 'TaskOperationTimeout'] +JSONObject = Dict[str, 'JsonValue'] +JsonValue = Any +SkipPattern = Literal['fullwidth_parentheses', 'lenticular_brackets', 'parentheses', 'square_brackets', 'curly_braces'] +AgentRequest_AvatarVendor = Literal['akool', 'liveavatar', 'anam', 'sensetime', 'spatius', 'generic'] +AgentRequest_JSONObject = Dict[str, 'AgentRequest_JsonValue'] +AgentRequest_JsonValue = Any +AgentRequest_SkipPattern = Literal['fullwidth_parentheses', 'lenticular_brackets', 'parentheses', 'square_brackets', 'curly_braces'] +AgentCreateInput = AgentRequest_AgentCreateInput +AgentPatch = AgentRequest_AgentPatch + +# Compatibility names retained for the additive preview namespace. +AgentCreateModel = AgentCreateInput +AgentPatchRequest = AgentPatch +SessionLifecycle = Lifecycle +EphemeralSessionAgent = PublicAgentInput +StartSessionResponse = SessionStartResponse +class RtcGeofence(TypedDict, total=False): + area: Required[Literal["global", "north_america", "europe", "asia", "india", "japan"]] + exclude_area: Literal["north_america", "europe", "asia", "india", "japan"] +class RtcSessionTransport(RtcTransport, total=False): + geofence: RtcGeofence +SessionTransport = Union[RtcSessionTransport, TelephonyTransport] + +__all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AsrInput', 'AudioGreeting', 'AvatarInput', 'AvatarVendor', 'CallPolicy', 'CallingDay', 'CampaignCallPolicy', 'CampaignCounts', 'CampaignCreate', 'CampaignDeleteResponse', 'CampaignEndCallPolicy', 'CampaignFailure', 'CampaignLifecycle', 'CampaignListData', 'CampaignListResponse', 'CampaignPatch', 'CampaignResponse', 'CampaignTransferPolicy', 'CampaignTransport', 'CampaignTransportPatch', 'CampaignTransportResponse', 'CampaignVoicemailPolicy', 'CascadedPipelineInput', 'ContactInput', 'DataPolicy', 'DisabledInterruption', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'FunctionTool', 'GeneratedFillerContent', 'GracefulStop', 'ImmediateSchedule', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JSONObject', 'JsonValue', 'KeywordInterruption', 'Lifecycle', 'LlmInput', 'ManagedCredential', 'ManualTurnBoundary', 'McpServer', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'Pacing', 'PacingPatch', 'PublicAgentInput', 'PublicByokCredential', 'PublicEphemeralSessionCreate', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedTts', 'RtcEncryption', 'RtcTransport', 'ScheduledSchedule', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SessionAgentOverridesPatch', 'SessionAgentPatch', 'SessionCreate', 'SessionData', 'SessionList', 'SessionModulePatch', 'SessionPatch', 'SessionPipelinePatch', 'SessionResponse', 'SessionStartResponse', 'SessionStopResponse', 'SessionSummary', 'SessionTransportPatch', 'SessionUpdateResponse', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SkipPattern', 'SpeechInterruption', 'StaticFillerContent', 'TelephonyTransport', 'TextGreeting', 'TimeRange', 'ToolExecution', 'ToolServer', 'TransferPolicy', 'TtsInput', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'VoicemailPolicy', 'AgentRequest_AgentCreateInput', 'AgentRequest_AgentPatch', 'AgentRequest_AsrInput', 'AgentRequest_AsrPatch', 'AgentRequest_AudioGreeting', 'AgentRequest_AvatarInput', 'AgentRequest_AvatarPatch', 'AgentRequest_AvatarVendor', 'AgentRequest_CascadedPipelineInput', 'AgentRequest_CascadedPipelinePatch', 'AgentRequest_CascadedTurnDetectionPatch', 'AgentRequest_DisabledInterruption', 'AgentRequest_FillerContentPatch', 'AgentRequest_FillerTrigger', 'AgentRequest_FillerTriggerPatch', 'AgentRequest_FillerWords', 'AgentRequest_FillerWordsPatch', 'AgentRequest_FunctionTool', 'AgentRequest_GeneratedFillerContent', 'AgentRequest_GreetingPatch', 'AgentRequest_InterruptionPatch', 'AgentRequest_JSONObject', 'AgentRequest_JsonValue', 'AgentRequest_KeywordInterruption', 'AgentRequest_LlmInput', 'AgentRequest_LlmPatch', 'AgentRequest_ManagedCredential', 'AgentRequest_ManualTurnBoundary', 'AgentRequest_McpServer', 'AgentRequest_Message', 'AgentRequest_PublicByokCredential', 'AgentRequest_PublicCredentialPatch', 'AgentRequest_SemanticTurnEnd', 'AgentRequest_SemanticTurnStart', 'AgentRequest_SilenceConfig', 'AgentRequest_SilenceConfigPatch', 'AgentRequest_SkipPattern', 'AgentRequest_SpeechInterruption', 'AgentRequest_StaticFillerContent', 'AgentRequest_TextGreeting', 'AgentRequest_ToolExecution', 'AgentRequest_ToolServer', 'AgentRequest_TtsInput', 'AgentRequest_TtsPatch', 'AgentRequest_TurnBoundaryPatch', 'AgentRequest_TurnDetection', 'AgentRequest_VadTurnEnd', 'AgentRequest_VadTurnStart', 'AgentRequest_VariableDefinition', 'AgentRequest_VariableDefinitionPatch', 'AgentCreateInput', 'AgentPatch', 'SessionEndCallPolicy', 'InboundEndCallPolicy', 'SessionAnalysis', 'AgentCreateModel', 'AgentPatchRequest', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse', 'RtcGeofence', 'RtcSessionTransport', 'SessionTransport'] diff --git a/tests/custom/test_basic_auth.py b/tests/custom/test_basic_auth.py new file mode 100644 index 0000000..465c824 --- /dev/null +++ b/tests/custom/test_basic_auth.py @@ -0,0 +1,128 @@ +"""Basic REST auth stays separate from RTC transport token provisioning.""" + +import base64 +import json + +import httpx +import pytest +from test_convoai_token_services import decode_token + +from agora_agent import Agent, Agora, Area, AsyncAgora, AsyncSavedAgent, OpenAI, OpenAITTS, SavedAgent +from agora_agent.core.public_auth import validate_public_headers +from agora_agent.v3 import V3Options + +BASIC = "Basic " + base64.b64encode(b"customer:secret:with-colon").decode() + + +@pytest.mark.parametrize("cls", [Agora, AsyncAgora]) +@pytest.mark.parametrize("credentials", [ + {"customer_id": "id"}, {"customer_secret": "secret"}, + {"customer_id": "", "customer_secret": "secret"}, + {"customer_id": "id", "customer_secret": " "}, + {"customer_id": "bad:id", "customer_secret": "secret"}, + {"customer_id": "id", "customer_secret": "secret", "auth_token": "token"}, +]) +def test_basic_credentials_are_complete_and_unambiguous(cls, credentials): + with pytest.raises(ValueError) as error: + cls(area=Area.US, app_id="a" * 32, **credentials) + assert "secret" not in str(error.value).replace("customer_secret", "") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("with_certificate", [False, True]) +async def test_basic_auth_on_low_level_and_agentkit_sessions(async_, with_certificate): + requests = [] + + def handler(request): + requests.append(request) + assert request.headers["Authorization"] == BASIC + if request.url.path.endswith("/agents/saved"): + return httpx.Response(200, json={"agent_id": "saved", "agent_name": "Support"}, headers={"ETag": '"1"'}) + return httpx.Response(200, json={"agent_id": "legacy", "agent_session_id": "session", "status": "RUNNING", "create_ts": 1}) + + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(handler)) + client = (AsyncAgora if async_ else Agora)( + area=Area.US, app_id="a" * 32, app_certificate="b" * 32 if with_certificate else None, + customer_id="customer", customer_secret="secret:with-colon", httpx_client=http, + ) + assert client.auth_mode == "basic" + for result in (client.agents.get("a" * 32, "legacy"), client.v3.sessions.list()): + if async_: + await result + saved = await AsyncSavedAgent.get(client, "saved") if async_ else SavedAgent.get(client, "saved") + agent = Agent(client).with_llm(OpenAI(model="gpt-5-mini")).with_tts(OpenAITTS(model="tts-1", voice="alloy")) + ephemeral = agent.create_async_ephemeral_session if async_ else agent.create_ephemeral_session + legacy = agent.create_async_session if async_ else agent.create_session + for create in (saved.create_session, ephemeral, legacy): + # Legacy RTC needs a supplied token without a certificate; Platform RTC + # can leave provisioning to the server. REST auth remains Basic either way. + options = {"token": "provided-rtc"} if create == legacy and not with_certificate else {} + session = create(channel="room", agent_uid="123", remote_uids=["2"], **options) + result = session.start() + if async_: + await result + body = json.loads(requests[-1].content) + rtc = body.get("transport", body.get("properties", {})).get("token") + if with_certificate: + assert decode_token(rtc)[2][9] == ({}, []) + elif create != legacy: + assert rtc is None + else: + assert rtc == "provided-rtc" + for action in (session.get_info, session.stop): + result = action() + if async_: + await result + if async_: + await http.aclose() + else: + http.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_", [False, True]) +async def test_explicit_basic_header_override_reaches_both_resource_families(async_): + requests = [] + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport( + lambda request: requests.append(request) or httpx.Response(200, json={"agent_id": "legacy", "status": "RUNNING"}) + )) + client = (AsyncAgora if async_ else Agora)(area=Area.US, app_id="project", auth_token="provided-token", httpx_client=http) + options = {"additional_headers": {"authorization": BASIC}} + for result in (client.agents.get("project", "legacy", request_options=options), client.v3.sessions.list(request_options=options)): + if async_: + await result + assert len(requests) == 2 and all(request.headers["Authorization"] == BASIC for request in requests) + if async_: + await http.aclose() + else: + http.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("name", ["Authorization", "authorization", "AUTHORIZATION"]) +async def test_v3_basic_override_is_case_insensitive_with_app_credentials(async_, name): + requests = [] + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport( + lambda request: requests.append(request) or httpx.Response(200, json={"items": []}) + )) + client = (AsyncAgora if async_ else Agora)( + area=Area.US, app_id="a" * 32, app_certificate="b" * 32, + v3=V3Options(headers={name: BASIC}), httpx_client=http, + ) + result = client.v3.sessions.list() + if async_: + await result + assert len(requests) == 1 + assert requests[0].headers.get_list("Authorization") == [BASIC] + if async_: + await http.aclose() + else: + http.close() + + +@pytest.mark.parametrize("value", ["Basic stale", "Basic Og==", "Basic aWQ6", "Basic OnNlY3JldA==", "Basic bm9jb2xvbg=="]) +def test_invalid_basic_headers_are_rejected(value): + with pytest.raises(ValueError): + validate_public_headers({"Authorization": value}) diff --git a/tests/custom/test_convoai_token_services.py b/tests/custom/test_convoai_token_services.py new file mode 100644 index 0000000..671682f --- /dev/null +++ b/tests/custom/test_convoai_token_services.py @@ -0,0 +1,116 @@ +"""Verify signed service grants, not just the Token007 prefix.""" + +import base64 +import hashlib +import hmac +import io +import json +import struct +import sys +import types +import zlib + +import httpx +import pytest + +from agora_agent import Agent, Agora, Area, AsyncAgora, AsyncSavedAgent, OpenAI, SavedAgent +from agora_agent.agentkit.token import generate_convo_ai_token + + +def decode_token(token): + assert token.startswith("007") + raw = zlib.decompress(base64.b64decode(token[3:])) + stream = io.BytesIO(raw) + + def integer(fmt): + return struct.unpack(fmt, stream.read(struct.calcsize(fmt)))[0] + + def string(): + return stream.read(integer(" None: ).to_config() assert config["url"] == "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=google-key" - assert "api_key" not in config + assert config["api_key"] == "google-key" assert config["style"] == "gemini" assert config["params"]["model"] == "gemini-2.0-flash" @@ -154,11 +154,7 @@ def test_llm_vendors_reject_missing_required_models() -> None: AmazonBedrock(access_key="aws-access", secret_key="aws-secret", region="us-east-1") # type: ignore[call-arg] -def test_openai_managed_mode_is_restricted_to_supported_models() -> None: - assert OpenAI(model="gpt-5-mini").to_config()["params"]["model"] == "gpt-5-mini" - - with pytest.raises(Exception, match="api_key"): - OpenAI(model="gpt-4o") - - with pytest.raises(Exception, match="does not allow vendor"): - OpenAI(model="gpt-5-mini", vendor="custom") +def test_openai_managed_models_are_validated_by_the_service() -> None: + for model in ("gpt-5-mini", "gpt-5.6-luna", "future-model"): + assert OpenAI(model=model).to_config()["params"]["model"] == model + assert OpenAI(model="gpt-5.6-luna", vendor="openai").to_config()["vendor"] == "openai" diff --git a/tests/custom/test_platform_docs_regressions.py b/tests/custom/test_platform_docs_regressions.py new file mode 100644 index 0000000..f3aaea3 --- /dev/null +++ b/tests/custom/test_platform_docs_regressions.py @@ -0,0 +1,269 @@ +"""Regression cases reported by the Platform docs team.""" +import copy +import json +import subprocess +import sys +from types import SimpleNamespace + +import httpx +import pytest +from test_v3_preview import make_client + +from agora_agent import ( + Agent, + AmazonBedrock, + AresSTT, + AsyncSavedAgent, + AzureOpenAI, + DeepgramSTT, + Gemini, + OpenAI, + SavedAgent, + secret_ref, +) +from agora_agent.agentkit.session_backend import prepare_start_body +from agora_agent.agentkit.v3 import build_body, module +from agora_agent.agentkit.vendors.stt import AssemblyAISTT, GoogleSTT, MicrosoftSTT, SarvamSTT, SpeechmaticsSTT +from agora_agent.agentkit.vendors.tts import SarvamTTS + + +@pytest.mark.parametrize("vendor", [ + AssemblyAISTT(api_key=secret_ref("prod", "key"), language="fr", ws_url="wss://assembly.example"), + GoogleSTT(project_id="project", location="global", adc_credentials_string=secret_ref("prod", "key"), language="fr"), + MicrosoftSTT(key=secret_ref("prod", "key"), region="eastus", language="fr"), + SarvamSTT(api_key=secret_ref("prod", "key"), language="hi-IN"), + SpeechmaticsSTT(key=secret_ref("prod", "key"), language="fr", uri="wss://speech.example"), +]) +@pytest.mark.parametrize("interaction_language", [None, "de-DE"]) +def test_required_provider_language_survives_platform_mapping(vendor, interaction_language): + requests = [] + + def handler(request): + requests.append(request) + return httpx.Response(201, json={"agent_id": "saved", "agent_name": "languages"}, headers={"ETag": '"1"'}) + + agent = Agent(make_client(handler)).with_stt(vendor) + before = copy.deepcopy(agent.config) + if interaction_language: + agent = agent.with_turn_detection({"language": interaction_language}) + agent.save(name="languages") + asr = json.loads(requests[0].content)["pipeline"]["asr"] + assert asr["params"]["language"] == vendor.language + assert asr["language"] == (interaction_language or vendor.language) + assert before["stt"] == agent.config["stt"] + assert "ws_url" not in asr["params"] and "uri" not in asr["params"] + + +@pytest.mark.parametrize("vendor", ["assemblyai", "google", "microsoft", "sarvam", "speechmatics"]) +def test_raw_provider_top_level_language_preserved_before_interaction_override(vendor): + agent = Agent(make_client(lambda _: pytest.fail("no HTTP"))).with_stt(SimpleNamespace(to_config=lambda: {"vendor": vendor, "language": "fr", "params": {}})) + body = build_body(agent.with_turn_detection({"language": "de-DE"})) + assert body["pipeline"]["asr"]["language"] == "de-DE" + assert body["pipeline"]["asr"]["params"]["language"] == "fr" + + +@pytest.mark.parametrize("options,expected", [ + ({}, "bulbul:v3"), + ({"model": "explicit-model"}, "explicit-model"), + ({"additional_params": {"model": "additional-model"}}, "additional-model"), + ({"model": "explicit-model", "additional_params": {"model": "additional-model"}}, "explicit-model"), +]) +def test_sarvam_model_default_and_override(options, expected): + tts = SarvamTTS(key=secret_ref("prod", "key"), speaker="anushka", target_language_code="hi-IN", **options) + assert tts.to_config()["params"]["model"] == expected + body = build_body(Agent(make_client(lambda _: pytest.fail("no HTTP"))).with_tts(tts)) + assert body["pipeline"]["tts"]["params"]["model"] == expected + + +def test_managed_openai_and_provider_languages_and_keywords(): + client = make_client(lambda _: pytest.fail("no HTTP")) + agent = Agent(client).with_llm(OpenAI(model="gpt-5.6-luna")).with_stt(AresSTT(keywords=["Agora"])) + body = build_body(agent) + assert body["pipeline"]["llm"]["vendor"] == "openai" + assert body["pipeline"]["llm"]["credential"] == {"mode": "managed"} + assert body["pipeline"]["asr"]["params"]["keywords"] == ["Agora"] + assert body["pipeline"]["asr"]["language"] == "en-US" + stt = DeepgramSTT(api_key=secret_ref("prod", "key"), model="nova-3", language="fr") + asr = build_body(agent.with_stt(stt))["pipeline"]["asr"] + assert asr["language"] == "fr" and "language" not in asr.get("params", {}) + + +@pytest.mark.parametrize("vendor,expected", [ + (AssemblyAISTT(api_key="key", language="en", ws_url="wss://assembly.example"), "wss://assembly.example"), + (SpeechmaticsSTT(key="key", language="en", uri="wss://speech.example"), "wss://speech.example"), +]) +def test_asr_custom_endpoints(vendor, expected): + errors = [] + result = module(vendor.to_config(), "asr", errors) + assert not errors + assert result["url"] == expected + assert "ws_url" not in result.get("params", {}) and "uri" not in result.get("params", {}) + + +@pytest.mark.parametrize("vendor", [None, "custom"]) +@pytest.mark.parametrize("url", [None, "https://gemini.example/custom?alt=sse"]) +def test_gemini_credentials_are_not_embedded_in_platform_url(url, vendor): + key = secret_ref("models", "gemini") + agent = Agent(make_client(lambda _: pytest.fail("no HTTP"))).with_llm(Gemini(model="gemini-flash", api_key=key, url=url, vendor=vendor)) + before = copy.deepcopy(agent.config) + llm = build_body(agent)["pipeline"]["llm"] + assert llm["vendor"] == (vendor or "gemini") + assert llm["credential"] == {"mode": "byok", "fields": {"api_key": key}} + assert "key=" not in llm["url"] and "$secrets" not in llm["url"] + assert before == agent.config + + +def test_bedrock_api_key_and_azure_resource_name(): + config = AmazonBedrock(api_key=secret_ref("models", "bedrock"), model="model", region="us-east-1") + llm = module(config.to_config(), "llm", []) + assert llm["vendor"] == "amazon" and llm["credential"]["fields"] == {"api_key": "$secrets.models.bedrock"} + with pytest.raises(ValueError): + AmazonBedrock(api_key="key", access_key="a", secret_key="b", model="model", region="us-east-1") + azure = AzureOpenAI(api_key="key", resource_name="resource", model="gpt", deployment_name="deploy") + assert azure.to_config()["url"].startswith("https://resource.openai.azure.com/openai/deployments/deploy/") + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_explicit_ephemeral_and_calls(async_): + requests = [] + def handler(request): + requests.append(request) + if request.method == "GET": + return httpx.Response(200, json={"agent_id": "saved", "agent_name": "support"}, headers={"ETag": '"1"'}) + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING", "create_ts": 1}) + client = make_client(handler, async_) + agent = Agent(client).with_llm(OpenAI(model="gpt-5.6-luna")) + create = agent.create_async_ephemeral_session if async_ else agent.create_ephemeral_session + session = create(channel="room", agent_uid="1", remote_uids=["2"], idle_timeout=30) + result = session.start() + if async_: + await result + assert requests[-1].url.path.endswith("/sessions/ephemeral") + assert json.loads(requests[-1].content)["lifecycle"] == {"idle_timeout_ms": 30000} + for saved in (False, True): + owner = agent + if saved: + owner = await AsyncSavedAgent.get(client, "saved") if async_ else SavedAgent.get(client, "saved") + call = owner.call if saved or not async_ else owner.call_async + session = call(from_number="12345678-1234-1234-1234-123456789abc", to="+12025550123", call_policy={"max_ring_duration_ms": 30000}) + result = session.start() + if async_: + await result + body = json.loads(requests[-1].content) + assert body["transport"] == {"type": "telephony", "from": "12345678-1234-1234-1234-123456789abc", "to": "+12025550123", "call_policy": {"max_ring_duration_ms": 30000}} + assert ("agent_id" in body) == saved + with pytest.raises(ValueError, match="RTC"): + (agent.call_async if async_ else agent.call)(from_number="n", to="+12025550123", channel="room") + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_assign_full_policy_and_empty_clear(async_): + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(200, json={"agent_id": "saved", "agent_name": "support", "number_id": "n", "number": "+12025550123"}, headers={"ETag": '"1"'}) + client = make_client(handler, async_) + saved = await AsyncSavedAgent.get(client, "saved") if async_ else SavedAgent.get(client, "saved") + for policy in ({"end_call": {"on_user_request": True}}, {}): + result = saved.assign_number("n", call_policy=policy) + if async_: + await result + assert json.loads(requests[-1].content)["inbound"]["call_policy"] == policy + + +def test_rtc_token_generated_once_and_explicit_tokens_preserved(monkeypatch): + signed = [] + monkeypatch.setattr("agora_agent.agentkit.token.generate_convo_ai_token", lambda **kw: signed.append(kw) or "rtc-generated") + client = make_client(lambda _: pytest.fail("no HTTP")) + client.app_certificate = "certificate" + agent = Agent(client).with_llm(OpenAI(model="gpt-5.6-luna")) + session = agent.create_ephemeral_session(channel="room", agent_uid="1", remote_uids=["2"], idempotency_key="retry") + client.app_certificate = "changed" + assert prepare_start_body(session)["transport"]["token"] == "rtc-generated" + assert signed[0]["app_certificate"] == "certificate" + assert prepare_start_body(session)["transport"]["token"] == "rtc-generated" and len(signed) == 1 + for uid in (None, "0", "bot"): + other = agent.create_ephemeral_session(channel="room", agent_uid=uid, remote_uids=["customer"] if uid == "bot" else ["2"]) + assert "token" not in prepare_start_body(other)["transport"] + explicit = agent.create_ephemeral_session(channel="room", agent_uid="1", remote_uids=["2"], token="provided") + assert prepare_start_body(explicit)["transport"]["token"] == "provided" and len(signed) == 1 + + +def test_import_has_no_invalid_pydantic_config_warning(): + result = subprocess.run([sys.executable, "-W", "error::UserWarning", "-c", "import agora_agent"], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + +def test_response_metadata_fields_are_preserved(): + from agora_agent.v3.types import SecretKey, SecretSet, SessionResponse + assert "analysis" in SessionResponse.__annotations__ + assert {"key", "create_ts", "update_ts"} <= SecretKey.__annotations__.keys() + assert "keys" in SecretSet.__annotations__ + responses = [ + {"agent_session_id": "s", "status": "STOPPED", "analysis": {"status": "completed", "complete_ts": 12, "result": {"resolved": True}}}, + {"name": "prod", "keys": [{"key": "llm", "create_ts": 1, "update_ts": 2}]}, + {"name": "prod", "keys": ["llm"]}, + {"key": "llm", "create_ts": 1, "update_ts": 2}, + ] + client = make_client(lambda _: httpx.Response(200, json=responses.pop(0))) + assert client.v3.sessions.get(agent_session_id="s")["analysis"]["result"] == {"resolved": True} + assert client.v3.secrets.get(name="prod")["keys"][0]["update_ts"] == 2 + assert client.v3.secrets.get(name="prod")["keys"] == ["llm"] + assert client.v3.secrets.set_key(name="prod", key="llm", value="new", if_match='"1"')["update_ts"] == 2 + + +def test_rtc_token_identity_range_and_non_idempotent_refresh(monkeypatch): + signed = [] + monkeypatch.setattr("agora_agent.agentkit.token.generate_convo_ai_token", lambda **kw: signed.append(kw) or str(len(signed))) + client = make_client(lambda _: pytest.fail("no HTTP")) + client.app_certificate = "cert" + agent = Agent(client).with_llm(OpenAI(model="gpt-5-mini")) + session = agent.create_ephemeral_session(channel="r", agent_uid="1", remote_uids=["2"]) + assert prepare_start_body(session)["transport"]["token"] == "1" + session._session_backend["successfully_started"] = True + assert prepare_start_body(session)["transport"]["token"] == "2" + large = agent.create_ephemeral_session(channel="r", agent_uid="4294967296", remote_uids=["2"]) + assert "token" not in prepare_start_body(large)["transport"] + + +def test_raw_gemini_query_key_migrates_to_credentials(): + result = module({"vendor": "gemini", "url": "https://gemini.example/stream?alt=sse&key=inline", "params": {"model": "gemini"}}, "llm", []) + assert result["credential"] == {"mode": "byok", "fields": {"api_key": "inline"}} + assert result["url"] == "https://gemini.example/stream?alt=sse" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_rtc_token_refreshes_on_restart_not_failed_retry(async_, monkeypatch): + tokens = [] + bodies = [] + monkeypatch.setattr("agora_agent.agentkit.token.generate_convo_ai_token", lambda **kw: tokens.append(kw) or f"token-{len(tokens)}") + def handler(request): + if request.url.path.endswith("/stop"): + return httpx.Response(200, json={}) + bodies.append(json.loads(request.content)) + if len(bodies) == 1: + return httpx.Response(500, json={"reason": "Unknown", "detail": "retry"}) + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING", "create_ts": 1}) + client = make_client(handler, async_) + client.app_certificate = "certificate" + agent = Agent(client).with_llm(OpenAI(model="gpt-5-mini")) + create = agent.create_async_ephemeral_session if async_ else agent.create_ephemeral_session + session = create(channel="room", agent_uid="1", remote_uids=["2"]) + with pytest.raises(Exception): + result = session.start() + if async_: + await result + result = session.start() + if async_: + await result + assert bodies[0] == bodies[1] and len(tokens) == 1 + result = session.stop() + if async_: + await result + result = session.start() + if async_: + await result + assert bodies[-1]["transport"]["token"] == "token-2" and len(tokens) == 2 diff --git a/tests/custom/test_token_credentials.py b/tests/custom/test_token_credentials.py index b58ffda..cdb66f6 100644 --- a/tests/custom/test_token_credentials.py +++ b/tests/custom/test_token_credentials.py @@ -62,3 +62,30 @@ def test_guards_the_rtc_builder_too(): generate_rtc_token( app_id=VALID_APP_ID, app_certificate="too-short", channel="demo", uid=333 ) + + +@pytest.mark.parametrize("field", ["app_id", "app_certificate"]) +@pytest.mark.parametrize("invalid", [ + "a" * 30 + " ", + " " + "a" * 30, + "a" * 16 + " " + "a" * 14, + "a" * 30 + "\n\n", + "\n" + "a" * 30 + "\n", + "a" * 16 + "\n\t" + "a" * 14, + "g" * 32, +]) +@pytest.mark.parametrize("rtc_only", [False, True]) +def test_rejects_non_hex_characters_without_exposing_credentials(field, invalid, rtc_only): + credentials = {"app_id": VALID_APP_ID, "app_certificate": VALID_CERTIFICATE, field: invalid} + with pytest.raises(ValueError, match=f"{field} must be a 32-character hexadecimal value") as excinfo: + if rtc_only: + generate_rtc_token(**credentials, channel="demo", uid=333) + else: + generate_convo_ai_token(**credentials, channel_name="demo", uid=333) + assert invalid not in str(excinfo.value) + + +def test_accepts_uppercase_hex_credentials(): + assert generate_convo_ai_token( + app_id=VALID_APP_ID.upper(), app_certificate=VALID_CERTIFICATE.upper(), channel_name="demo", uid=333 + ).startswith("007") diff --git a/tests/custom/test_tts_vendors.py b/tests/custom/test_tts_vendors.py index 917a210..cc52cc6 100644 --- a/tests/custom/test_tts_vendors.py +++ b/tests/custom/test_tts_vendors.py @@ -462,8 +462,7 @@ def handle_request(request: httpx.Request) -> httpx.Response: area=Area.US, app_id="0" * 32, app_certificate="1" * 32, - customer_id="customer-id", - customer_secret="customer-secret", + auth_token="test-token", httpx_client=httpx_client, ) agent = ( diff --git a/tests/custom/test_v3_latest_contract.py b/tests/custom/test_v3_latest_contract.py new file mode 100644 index 0000000..a6cc86b --- /dev/null +++ b/tests/custom/test_v3_latest_contract.py @@ -0,0 +1,253 @@ +"""Corrected release contract regressions, entirely offline.""" +import copy +import json +from pathlib import Path + +import httpx +import pytest +from test_v3_preview import make_client + +from agora_agent import Agent, OpenAI, secret_ref +from agora_agent.agentkit.v3 import build_body, replacement_patch +from agora_agent.agentkit.vendors.llm import AmazonBedrock +from agora_agent.agentkit.vendors.stt import AmazonSTT, GoogleSTT +from agora_agent.agentkit.vendors.tts import AmazonTTS, GoogleTTS, SarvamTTS +from agora_agent.v3.contract import validate_write + + +@pytest.mark.parametrize("category,vendor,expected", [ + ("asr", AmazonSTT(access_key="access", secret_key="secret", region="us-east-1", language="en-US"), {"access_key_id": "access", "secret_access_key": "secret"}), + ("asr", GoogleSTT(project_id="p", location="global", adc_credentials_string="json", language="en"), {"adc_credentials_string": "json"}), + ("llm", AmazonBedrock(access_key="access", secret_key="secret", region="us-east-1", model="model"), {"access_key": "access", "secret_key": "secret"}), + ("tts", AmazonTTS(access_key="access", secret_key="secret", region="us-east-1", voice_id="Joanna", engine="neural"), {"aws_access_key_id": "access", "aws_secret_access_key": "secret"}), + ("tts", GoogleTTS(key="json", voice_name="voice"), {"credentials": "json"}), + ("tts", SarvamTTS(key="sarvam", target_language_code="en-IN", speaker="anushka"), {"api_subscription_key": "sarvam"}), +]) +def test_public_vendor_credentials_are_lossless_and_ephemeral_only(category, vendor, expected): + client = make_client(lambda _: pytest.fail("must not send")) + agent = getattr(Agent(client=client), "with_" + ("stt" if category == "asr" else category))(vendor) + before = copy.deepcopy(agent.config) + body = build_body(agent, "test") + assert body["pipeline"][category]["credential"] == {"mode": "byok", "fields": expected} + ephemeral = {"agent": {k: v for k, v in body.items() if k != "agent_name"}, "transport": {"type": "rtc", "channel": "r", "subscribe_uids": ["2"]}} + validate_write("POST", "/conversational-ai/v1/projects/p/sessions/ephemeral", ephemeral) + with pytest.raises(ValueError, match="secret_ref"): + agent.save(name="test") + assert agent.config == before + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_new_runtime_update_and_auto_uid(async_): + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(200, json={"agent_session_id": "session", "create_ts": 1, "status": "RUNNING"}) + client = make_client(handler, async_) + agent = Agent(client=client).with_llm(OpenAI(api_key="inline", model="gpt", base_url="https://api.openai.com/v1/chat/completions")) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", remote_uids=["customer"]) + result = session.start() + if async_: + await result + body = json.loads(requests[0].content) + assert "uid" not in body["transport"] + assert body["agent"]["pipeline"]["llm"]["credential"] == {"mode": "byok", "fields": {"api_key": "inline"}} + properties = {"token": "renewed", "messages": [{"role": "user", "content": "Hi"}], "llm": {"params": {"temperature": 0.4}}} + before = copy.deepcopy(properties) + result = session.update(properties) + if async_: + await result + assert requests[-1].method == "PATCH" + assert json.loads(requests[-1].content) == {"transport": {"token": "renewed"}, "agent": {"overrides": {"messages": properties["messages"], "pipeline": {"llm": properties["llm"]}}}} + assert properties == before + for invalid in ({"mllm": {"params": {"temperature": 0.2}}}, {"llm": {"params": {}}, "mllm": {"params": {}}}, {"llm": {"params": {"url": "https://other"}}}, {"mllm": {"params": {"endpoint": "https://other"}}}, {"token": None}, {"instructions": "no"}): + count = len(requests) + with pytest.raises(ValueError): + result = session.update(invalid) + if async_: + await result + assert len(requests) == count + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_latest_campaign_number_policy_and_retention(async_): + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(200, json={}) + client = make_client(handler, async_) + result = client.campaigns.create(name="campaign", agent_id="a", transport={"type": "telephony", "from": "n", "call_policy": {"end_call": {"on_voicemail": True}, "max_silence_duration_ms": 30000}}, schedule={"mode": "immediate"}, contacts=[{"phone": "+12025550123"}] * 2001, data_policy={"retention": "none"}, idempotency_key="campaign-1") + if async_: + await result + assert len(json.loads(requests[-1].content)["contacts"]) == 2001 + assert json.loads(requests[-1].content)["data_policy"] == {"retention": "none"} + result = client.numbers.create(number="+12025550123", sip_trunk={"hostname": "sip.example", "allowed_ips": ["*", "192.0.2.1/24"]}, inbound={"agent": "a", "call_policy": {"end_call": {"on_voicemail": True}}}) + if async_: + await result + assert json.loads(requests[-1].content)["inbound"]["call_policy"]["end_call"]["on_voicemail"] + for invalid in ({"max_attempts": 2}, {"retry_backoff_ms": 1000}): + with pytest.raises(ValueError): + result = client.campaigns.update(campaign_id="c", if_match='"e"', pacing=invalid) + if async_: + await result + for invalid in ("2001:db8::1", "192.0.2.1/024", "192.0.2.1/255.255.255.0", "example.org"): + with pytest.raises(ValueError): + result = client.numbers.create(number="+12025550123", sip_trunk={"hostname": "sip.example", "allowed_ips": [invalid]}) + if async_: + await result + + +@pytest.mark.parametrize("key", ["x" * 257, "a b", "new\nline", "非ASCII"]) +def test_idempotency_key_exact_contract(key): + client = make_client(lambda _: pytest.fail("must not send")) + with pytest.raises(ValueError): + client.v3.sessions.start(agent_id="a", transport={"type": "rtc", "channel": "r", "subscribe_uids": ["2"]}, idempotency_key=key) + with pytest.raises(ValueError): + Agent(client=client).create_session(channel="r", remote_uids=["2"], idempotency_key=key) + + +def test_redeploy_clears_hidden_previous_vendor_fields_and_managed_fields(): + previous = {"pipeline": {"mode": "cascaded", "tts": {"vendor": "amazon", "credential": {"mode": "byok"}}}} + desired = {"pipeline": {"mode": "cascaded", "tts": {"vendor": "google", "credential": {"mode": "byok", "fields": {"credentials": "$secrets.prod.google"}}}}} + patch = replacement_patch(previous, desired) + assert patch["pipeline"]["tts"]["credential"]["fields"] == {"aws_access_key_id": None, "aws_secret_access_key": None, "credentials": "$secrets.prod.google"} + validate_write("PATCH", "/conversational-ai/v1/projects/p/agents/a", patch) + managed = replacement_patch(previous, {"pipeline": {"mode": "cascaded", "tts": {"vendor": "google", "credential": {"mode": "managed"}}}}) + assert managed["pipeline"]["tts"]["credential"] == {"mode": "managed", "fields": None} + validate_write("PATCH", "/conversational-ai/v1/projects/p/agents/a", managed) + + +def test_shared_snapshot_and_public_saved_reference_only(): + root = Path(__file__).resolve().parents[2] + assert (root / "openapi.json").read_bytes() == (root / "src/agora_agent/v3/openapi.json").read_bytes() + client = make_client(lambda _: pytest.fail("must not send")) + with pytest.raises(ValueError): + client.v3.agents.create(agent_name="unsafe", pipeline={"mode": "cascaded", "llm": {"credential": {"mode": "byok", "fields": {"api_key": "inline"}}}}) + with pytest.raises(ValueError): + secret_ref("old-hyphen", "key") + body = build_body(Agent(client=client).with_llm(OpenAI(api_key=secret_ref("prod", "key"), model="gpt-4o-mini", base_url="https://api.openai.com/v1/chat/completions")), "safe") + validate_write("POST", "/conversational-ai/v1/projects/p/agents", body) + + +def test_campaign_contact_count_boundaries_and_body_size(): + body = {"name": "x", "agent_id": "a", "transport": {"type": "telephony", "from": "n"}, "schedule": {"mode": "immediate"}, "contacts": [{"phone": "+12025550123"}] * 50000} + path = "/conversational-ai/v1/projects/p/campaigns" + validate_write("POST", path, body) + body["contacts"].append({"phone": "+12025550123"}) + with pytest.raises(ValueError): + validate_write("POST", path, body) + body["contacts"] = [{"phone": "+12025550123", "values": {"text": "x" * (10 * 1024 * 1024)}}] + with pytest.raises(ValueError, match="10 MiB"): + validate_write("POST", path, body) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_no_body_operations_do_not_accept_hidden_write_body(async_): + client = make_client(lambda _: pytest.fail("must not send"), async_) + with pytest.raises(ValueError, match="request body"): + result = client.v3.sessions.stop(agent_session_id="s", request_options={"additional_body_parameters": {"unsupported": True}}) + if async_: + await result + + +def test_same_vendor_alternate_credential_form_clears_hidden_key(): + patch = replacement_patch({"vendor": "amazon", "credential": {"mode": "byok"}}, {"vendor": "amazon", "credential": {"mode": "byok", "fields": {"access_key": "$secrets.prod.access", "secret_key": "$secrets.prod.secret"}}}, "llm") + assert patch["credential"]["fields"] == {"api_key": None, "access_key": "$secrets.prod.access", "secret_key": "$secrets.prod.secret"} + + +@pytest.mark.parametrize("module", [ + {"vendor": "amazon", "credential": {"mode": "byok", "fields": {"api_key": "wrong-field"}}}, + {"vendor": "deepgram", "credential": {"mode": "managed"}, "url": "https://custom.example"}, +]) +def test_vendor_conditional_credentials_are_validated(module): + with pytest.raises(ValueError): + validate_write("POST", "/conversational-ai/v1/projects/p/sessions/ephemeral", {"agent": {"pipeline": {"mode": "cascaded", "asr": module}}, "transport": {"type": "rtc", "channel": "r", "subscribe_uids": ["2"]}}) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_messages_shortcut_is_transport_independent(async_): + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(200, json={"agent_id": "legacy-session"}) + client = make_client(handler, async_) + # Ordinary inline configuration keeps the established session transport. + agent = Agent(client=client).with_llm(OpenAI(api_key="inline", model="gpt", base_url="https://api.openai.com/v1/chat/completions")) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"], token="rtc") + session._status = "running" + session._agent_id = "legacy-session" + messages = [{"role": "system", "content": "Updated"}] + properties = {"messages": messages, "llm": {"params": {"temperature": 0.2}}} + before = copy.deepcopy(properties) + result = session.update(properties) + if async_: + await result + assert json.loads(requests[-1].content)["properties"] == {"llm": {"system_messages": messages, "params": {"temperature": 0.2}}} + assert properties == before + with pytest.raises(ValueError): + result = session.update({"messages": messages, "llm": {"system_messages": messages}}) + if async_: + await result + + +def test_llm_params_with_legacy_messages_spelling_is_supported(): + from agora_agent.agentkit.session_backend import update_body + messages = [{"role": "system", "content": "Updated"}] + assert update_body({"llm": {"system_messages": messages, "params": {"temperature": 0.2}}}) == {"agent": {"overrides": {"messages": messages, "pipeline": {"llm": {"params": {"temperature": 0.2}}}}}} + with pytest.raises(ValueError): + update_body({"messages": messages, "llm": {"system_messages": messages}}) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_preview_rejects_mllm_before_network(async_): + from agora_agent import OpenAIRealtime, UnsupportedFieldError + client = make_client(lambda _: pytest.fail("unsupported MLLM must not reach HTTP"), async_) + agent = Agent(client=client).with_mllm(OpenAIRealtime(api_key="inline")) + with pytest.raises(UnsupportedFieldError, match="realtime pipelines are not supported"): + result = agent.save_async(name="realtime") if async_ else agent.save(name="realtime") + if async_: + await result + create = agent.create_async_session if async_ else agent.create_session + for options in ({}, {"agent_uid": "1", "values": {}}, {"agent_uid": "1", "idempotency_key": "start"}): + with pytest.raises(UnsupportedFieldError, match="realtime pipelines are not supported"): + create(channel="r", remote_uids=["2"], **options) + for pipeline in ({"mode": "realtime"}, {"mode": "cascaded", "mllm": {}}, {"mllm": None}): + for method, kwargs in ((client.v3.agents.create, {"agent_name": "realtime", "pipeline": pipeline}), + (client.v3.agents.update, {"agent_id": "a", "if_match": '"e"', "pipeline": pipeline}), + (client.v3.sessions.start_ephemeral, {"agent": {"pipeline": pipeline}, "transport": {"type": "rtc", "channel": "r", "subscribe_uids": ["2"]}})): + with pytest.raises(ValueError): + result = method(**kwargs) + if async_: + await result + with pytest.raises(ValueError): + result = client.v3.sessions.update(agent_session_id="s", agent={"overrides": {"pipeline": {"mllm": {"params": {}}}}}) + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_legacy_mllm_start_and_update_remain_supported(async_): + from agora_agent import OpenAIRealtime + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(200, json={"agent_id": "legacy-session"}) + client = make_client(handler, async_) + agent = Agent(client=client).with_mllm(OpenAIRealtime(api_key="inline")) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"], token="rtc") + result = session.start() + if async_: + result = await result + assert result == "legacy-session" + assert requests[-1].url.path.endswith("/join") + assert json.loads(requests[-1].content)["properties"]["mllm"]["api_key"] == "inline" + result = session.update({"mllm": {"params": {"temperature": 0.2}}}) + if async_: + await result + assert requests[-1].url.path.endswith("/legacy-session/update") + assert json.loads(requests[-1].content)["properties"] == {"mllm": {"params": {"temperature": 0.2}}} diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py new file mode 100644 index 0000000..623dea9 --- /dev/null +++ b/tests/custom/test_v3_preview.py @@ -0,0 +1,1494 @@ +"""Offline contract tests for the additive v3 preview and v2 boundary.""" +import asyncio +import json +import threading + +import httpx +import pytest + +from agora_agent import ( + Agent, + Agora, + Area, + AsyncAgora, + AsyncDeployedAgent, + DeepgramSTT, + DeployedAgent, + NumberNotFoundError, + OpenAI, + OpenAITTS, + PreconditionFailedError, + SecretRefNotSupportedError, + UnsupportedFieldError, + V3Options, + secret_ref, +) +from agora_agent.agentkit.v3 import build_body, module + +OPERATIONS = [ + ("agents", "create", {"agent_name": "Support"}, "POST", "agents", {"agent_name": "Support"}), + ("agents", "list", {"limit": 5, "cursor": "next", "order": "create_ts.asc"}, "GET", "agents", None), + ("agents", "get", {"agent_id": "agent/a"}, "GET", "agents/agent%2Fa", None), + ("agents", "update", {"agent_id": "a", "if_match": '"1"', "instructions": None}, "PATCH", "agents/a", {"instructions": None}), + ("agents", "delete", {"agent_id": "a"}, "DELETE", "agents/a", None), + ("secrets", "create", {"name": "prod", "secrets": {"key": "private"}}, "POST", "secrets", {"name": "prod", "secrets": {"key": "private"}}), + ("secrets", "list", {}, "GET", "secrets", None), + ("secrets", "get", {"name": "prod"}, "GET", "secrets/prod", None), + ("secrets", "replace", {"name": "prod", "if_match": '"1"', "secrets": {"k": "v"}}, "PUT", "secrets/prod", {"secrets": {"k": "v"}}), + ("secrets", "update", {"name": "prod", "if_match": '"1"', "secrets": {"k": None}}, "PATCH", "secrets/prod", {"secrets": {"k": None}}), + ("secrets", "delete", {"name": "prod", "if_match": '"1"'}, "DELETE", "secrets/prod", None), + ("secrets", "set_key", {"name": "prod", "key": "k", "if_none_match": "*", "value": "v"}, "PUT", "secrets/prod/keys/k", {"value": "v"}), + ("secrets", "delete_key", {"name": "prod", "key": "k", "if_match": '"1"'}, "DELETE", "secrets/prod/keys/k", None), + ("numbers", "create", {"number": "+12025550123", "sip_trunk": {"hostname": "sip.example"}}, "POST", "numbers", {"number": "+12025550123", "sip_trunk": {"hostname": "sip.example"}}), + ("numbers", "list", {}, "GET", "numbers", None), + ("numbers", "get", {"number_id": "n"}, "GET", "numbers/n", None), + ("numbers", "update", {"number_id": "n", "if_match": '"1"', "inbound": {"agent": None}}, "PATCH", "numbers/n", {"inbound": {"agent": None}}), + ("numbers", "delete", {"number_id": "n"}, "DELETE", "numbers/n", None), + ("sessions", "list", {"channel": "room", "status": "ACTIVE", "started_after": 12, "started_before": 25}, "GET", "sessions", None), + ("sessions", "get", {"agent_session_id": "s"}, "GET", "sessions/s", None), + ("sessions", "start", {"agent_id": "saved", "transport": {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]}, "labels": {"owner": "test"}}, "POST", "sessions", {"agent_id": "saved", "transport": {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]}, "labels": {"owner": "test"}}), + ("sessions", "start_ephemeral", {"agent": {"instructions": "Help"}, "transport": {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]}, "lifecycle": {"max_duration_ms": 120000}}, "POST", "sessions/ephemeral", {"agent": {"instructions": "Help"}, "transport": {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]}, "lifecycle": {"max_duration_ms": 120000}}), + ("sessions", "update", {"agent_session_id": "s", "transport": {"token": "new-token"}, "agent": {"overrides": {"pipeline": {"llm": {"params": {"temperature": 0.2}}}}}}, "PATCH", "sessions/s", {"transport": {"token": "new-token"}, "agent": {"overrides": {"pipeline": {"llm": {"params": {"temperature": 0.2}}}}}}), + ("sessions", "stop", {"agent_session_id": "session/a"}, "POST", "sessions/session%2Fa/stop", None), + ('campaigns', 'create', {'name': 'Outreach', 'agent_id': 'a', 'contacts': [{'phone': '+12025550123'}], 'schedule': {'mode': 'immediate'}, 'transport': {'type': 'telephony', 'from': 'n'}, 'idempotency_key': 'key'}, 'POST', 'campaigns', {'name': 'Outreach', 'agent_id': 'a', 'contacts': [{'phone': '+12025550123'}], 'schedule': {'mode': 'immediate'}, 'transport': {'type': 'telephony', 'from': 'n'}}), + ('campaigns', 'list', {'limit': 5, 'status': 'running'}, 'GET', 'campaigns', None), + ('campaigns', 'get', {'campaign_id': 'c/a'}, 'GET', 'campaigns/c%2Fa', None), + ('campaigns', 'update', {'campaign_id': 'c', 'if_match': '"1"', 'pacing': {'max_concurrent': 5}}, 'PATCH', 'campaigns/c', {'pacing': {'max_concurrent': 5}}), + ('campaigns', 'delete', {'campaign_id': 'c'}, 'DELETE', 'campaigns/c', None), + ('campaigns', 'pause', {'campaign_id': 'c', 'idempotency_key': 'key'}, 'POST', 'campaigns/c:pause', None), + ('campaigns', 'resume', {'campaign_id': 'c', 'idempotency_key': 'key'}, 'POST', 'campaigns/c:resume', None), + ('campaigns', 'cancel', {'campaign_id': 'c', 'idempotency_key': 'key'}, 'POST', 'campaigns/c:cancel', None), +] + + +def make_client(handler, async_=False, **kwargs): + transport = httpx.MockTransport(handler) + cls = AsyncAgora if async_ else Agora + http = httpx.AsyncClient(transport=transport) if async_ else httpx.Client(transport=transport) + return cls(area=Area.US, app_id="project", auth_token="raw-token", + httpx_client=http, v3=V3Options(base_url="https://stage.example/", headers={"X-Test-Context": "test"}), **kwargs) + + +def check_request(request, operation): + _, _, kwargs, method, suffix, body = operation + assert request.method == method + assert request.url.raw_path.split(b"?")[0] == ("/conversational-ai/v1/projects/project/" + suffix).encode() + assert request.headers["Authorization"] == "agora token=raw-token" + assert request.headers["X-Test-Context"] == "test" + if body is not None: + assert json.loads(request.content) == body + elif method == "POST": + assert request.content == b"" + for key, header in (("if_match", "If-Match"), ("if_none_match", "If-None-Match"), ("idempotency_key", "Idempotency-Key")): + if key in kwargs: + assert request.headers[header] == kwargs[key] + for key in ("limit", "cursor", "order", "status", "channel", "started_after", "started_before"): + if key in kwargs: + assert request.url.params[key] == str(kwargs[key]) + + +@pytest.mark.parametrize("operation", OPERATIONS) +def test_all_documented_sync_wire_operations(operation): + def handler(request): + check_request(request, operation) + return httpx.Response(200, json={"ok": True}, headers={"ETag": '"etag"', "Agora-Request-Id": "trace"}) + client = make_client(handler) + resource, method, kwargs, *_ = operation + response = getattr(getattr(client.v3, resource).with_raw_response, method)(**kwargs) + assert response.data == {"ok": True} + assert response.etag == '"etag"' + assert response.headers["Agora-Request-Id"] == "trace" + assert getattr(getattr(client.v3, resource), method)(**kwargs) == {"ok": True} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("operation", OPERATIONS) +async def test_all_documented_async_wire_operations(operation): + def handler(request): + check_request(request, operation) + return httpx.Response(200, json={"ok": True}, headers={"ETag": '"etag"'}) + client = make_client(handler, True) + resource, method, kwargs, *_ = operation + response = await getattr(getattr(client.v3, resource).with_raw_response, method)(**kwargs) + assert response.etag == '"etag"' + assert await getattr(getattr(client.v3, resource), method)(**kwargs) == {"ok": True} + + +def test_namespace_is_additive_and_out_of_scope_features_absent(): + client = make_client(lambda _: httpx.Response(200, json={})) + assert client.agents is not client.v3.agents + assert not hasattr(client.v3.sessions, "create") + assert client.campaigns is client.v3.campaigns + assert not hasattr(DeployedAgent, "start_call") + + +def test_reject_empty_identifiers_and_required_etag(): + client = make_client(lambda _: pytest.fail("request must not be sent")) + for kwargs in ({"agent_id": ""}, {"agent_id": " "}, {"agent_id": "."}, {"agent_id": ".."}, {"agent_id": "a", "appid": ""}): + with pytest.raises(ValueError): + client.v3.agents.get(**kwargs) + with pytest.raises(TypeError): + client.v3.agents.update(agent_id="a", instructions="x") + + +def test_no_write_retries_and_latest_conflict(): + calls = [] + def handler(request): + calls.append(request) + return httpx.Response(412 if request.method == "PATCH" else 200, json={"agent_id": "a"}, headers={"Agora-Request-Id": "trace"}) + client = make_client(handler) + with pytest.raises(PreconditionFailedError) as exc: + client.v3.agents.update(agent_id="a", if_match='"old"', instructions="x", request_options={"max_retries": 3}) + assert len(calls) == 1 + assert exc.value.headers["agora-request-id"] == "trace" + assert exc.value.latest() == {"agent_id": "a"} + assert calls[-1].method == "GET" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_latest_reload_does_not_replay_additional_write_body(async_): + calls = [] + def handler(request): + calls.append(request) + return httpx.Response(412 if request.method == "PATCH" else 200, json={"agent_id": "a"}) + client = make_client(handler, async_) + with pytest.raises(PreconditionFailedError) as caught: + result = client.v3.agents.update( + agent_id="a", if_match='"old"', instructions="updated", + request_options={"additional_body_parameters": {"messages": [{"role": "user", "content": "write-only"}]}}, + ) + if async_: + await result + latest = caught.value.latest() + if async_: + latest = await latest + assert latest == {"agent_id": "a"} + assert calls[0].method == "PATCH" + assert json.loads(calls[0].content)["messages"] == [{"role": "user", "content": "write-only"}] + assert calls[1].method == "GET" + assert calls[1].content == b"" + + +@pytest.mark.parametrize("resource,kwargs", [("agents", {"agent_name": "x"}), ("numbers", {"number": "+12025550123", "sip_trunk": {"hostname": "sip.example"}})]) +def test_nonidempotent_create_is_never_retried(resource, kwargs): + calls = [] + def handler(request): + calls.append(request) + return httpx.Response(500, json={"detail": "unknown outcome"}) + client = make_client(handler) + with pytest.raises(Exception): + getattr(client.v3, resource).create(**kwargs, request_options={"max_retries": 4}) + assert len(calls) == 1 + + +def test_cursor_pagination_and_loop_guard(): + calls = [] + def handler(request): + calls.append(request) + cursor = request.url.params.get("cursor") + return httpx.Response(200, json={"data": {"count": 1, "list": [{"number_id": cursor or "first"}]}, "has_more": not cursor, "next_cursor": "second" if not cursor else None}) + client = make_client(handler) + assert [x["number_id"] for x in client.v3.numbers.iter_all(limit=1)] == ["first", "second"] + assert calls[-1].url.params["cursor"] == "second" + + +def configured(client): + return (Agent(client=client) + .with_stt(DeepgramSTT(api_key=secret_ref("prod", "dg"), model="nova-3")) + .with_llm(OpenAI(api_key=secret_ref("prod", "openai"), base_url="https://api.openai.com/v1/chat/completions", model="gpt-4o-mini", + system_messages=[{"role": "system", "content": "Vendor instructions"}, {"role": "user", "content": "Hello"}], greeting_message="Hi")) + .with_tts(OpenAITTS(api_key=secret_ref("prod", "tts"), base_url="https://api.openai.com/v1/audio/speech", voice="alloy", model="tts-1"))) + + +def test_deploy_mapping_and_immutable_variables(): + calls = [] + def handler(request): + calls.append(request) + return httpx.Response(201, json={"agent_id": "a", "agent_name": "Support"}, headers={"ETag": '"1"'}) + client = make_client(handler) + base = configured(client) + agent = base.with_variable("resolved", {"type": "bool", "extract": True, "description": "Was the issue resolved?"}).with_greeting("fallback") + deployed = agent.deploy(name="Support") + body = json.loads(calls[0].content) + assert body["instructions"] == "Vendor instructions" + assert body["messages"] == [{"role": "user", "content": "Hello"}] + assert body["greeting"] == {"mode": "text", "text": "Hi"} + assert body["variables"]["resolved"]["type"] == "bool" + assert body["pipeline"]["asr"] == {"vendor": "deepgram", "model": "nova-3", "credential": {"mode": "byok", "fields": {"api_key": "$secrets.prod.dg"}}} + assert body["pipeline"]["llm"]["credential"]["fields"]["api_key"] == "$secrets.prod.openai" + assert "variables" not in build_body(base) + assert deployed.raw is client.v3.agents + assert (deployed.id, deployed.name, deployed.etag) == ("a", "Support", '"1"') + + +def test_redeploy_recursively_clears_readable_fields_and_writeonly_key(): + calls = [] + old = {"agent_id": "a", "agent_name": "Old", "create_ts": 1, "update_ts": 2, + "instructions": "stale", "variables": {"stale": {"type": "str"}}, + "pipeline": {"mode": "cascaded", "llm": {"params": {"stale": True}, "credential": {"mode": "byok", "has_api_key": True, "api_key_source": "secret"}}, "asr": {"vendor": "old"}}} + def handler(request): + calls.append(request) + return httpx.Response(200, json=old if request.method == "GET" else {"agent_id": "a", "agent_name": "New"}, headers={"ETag": '"old"'}) + agent = Agent(client=make_client(handler)).with_llm(OpenAI(model="gpt-4o-mini", temperature=.3)) + agent.deploy(name="New", agent_id="a") + patch = json.loads(calls[-1].content) + assert calls[-1].headers["If-Match"] == '"old"' + assert patch["instructions"] is None and patch["variables"] is None + assert "agent_id" not in patch and "create_ts" not in patch and "update_ts" not in patch + assert patch["pipeline"]["asr"] is None + assert patch["pipeline"]["llm"]["params"]["stale"] is None + assert patch["pipeline"]["llm"]["credential"] == {"mode": "managed", "fields": None} + + +def test_real_turn_interruption_greeting_and_silence_mapping(): + client = make_client(lambda _: pytest.fail("pure mapping")) + agent = (configured(client) + .with_turn_detection({"mode": "default", "language": "en-US", "config": {"speech_threshold": .7, + "start_of_speech": {"mode": "vad", "vad_config": {"interrupt_duration_ms": 180, "prefix_padding_ms": 500}}, + "end_of_speech": {"mode": "semantic", "semantic_config": {"max_wait_ms": 2000, "silence_duration_ms": 320}}}}) + .with_interruption({"enable": True, "mode": "keywords", "keywords_config": {"trigger_keywords": ["stop"]}}) + .with_parameters({"silence_config": {"timeout_ms": 5000, "action": "speak", "content": "Hello?"}})) + body = build_body(agent) + turn = body["pipeline"]["turn_detection"] + assert turn["speech_threshold"] == .7 + assert turn["start_of_speech"] == {"type": "vad", "interrupt_duration_ms": 180, "prefix_padding_ms": 500} + assert turn["end_of_speech"]["max_wait_ms"] == 2000 + assert turn["interruption"] == {"mode": "keywords", "keywords": ["stop"]} + assert body["pipeline"]["asr"]["language"] == "en-US" + assert body["pipeline"]["silence_config"]["enabled"] is True + disabled = build_body(agent.with_interruption({"enable": False, "disabled_config": {"strategy": "ignore"}})) + assert disabled["pipeline"]["turn_detection"]["interruption"] == {"mode": "off", "when_off": "ignore"} + + +@pytest.mark.parametrize("name,definition", [("Bad", {"type": "str"}), ("x", {"type": "string"}), ("x", {"type": "bool", "extract": True}), ("x", {"type": "str", "extract": True, "description": " "})]) +def test_variable_validation(name, definition): + with pytest.raises(ValueError): + Agent(client=make_client(lambda _: None)).with_variable(name, definition) + + +def test_strict_unsupported_fields_are_aggregated_before_network(): + agent = (Agent(client=make_client(lambda _: pytest.fail("must not send")), labels={"tag": "v"}, rtc={}, sal={}, pipeline_id="p") + .with_parameters({"enable_metrics": True, "data_channel": "rtm"})) + with pytest.raises(UnsupportedFieldError) as exc: + agent.deploy() + assert {"labels", "rtc", "sal", "pipeline_id", "parameters.enable_metrics", "parameters.data_channel"} <= set(exc.value.fields) + + +def test_multipart_credentials_and_numeric_skip_patterns_rejected(): + errors = [] + module({"vendor": "amazon", "params": {"aws_access_key_id": "a", "aws_secret_access_key": "s"}, "skip_patterns": [1]}, "tts", errors) + assert not any("aws_secret_access_key" in x for x in errors) + assert any("skip_patterns" in x for x in errors) + errors = [] + result = module({"vendor": "microsoft", "params": {"api_subscription_key": "key", "region": "eastus"}}, "tts", errors) + assert not errors + assert result["credential"] == {"mode": "byok", "fields": {"key": "key"}} + assert result["params"] == {"region": "eastus"} + + +def test_secret_ref_v2_guard_at_all_boundaries(): + agent = configured(make_client(lambda _: pytest.fail("must not send"))) + with pytest.raises(SecretRefNotSupportedError, match="requires save"): + agent.to_properties(channel="c", agent_uid="1", remote_uids=["*"]) + assert agent.create_session(channel="c", agent_uid="1", remote_uids=["2"]).status == "idle" + assert configured(make_client(lambda _: pytest.fail("must not send"), True)).create_async_session(channel="c", agent_uid="1", remote_uids=["2"]).status == "idle" + + +def test_number_resolution_pagination_conflict_retry_and_unassign(): + calls = [] + patches = [] + def handler(request): + calls.append(request) + if request.url.path.endswith("/numbers"): + second = "cursor" in request.url.params + return httpx.Response(200, json={"data": {"list": [{"number_id": "n", "number": "+1 (202) 555-0123"}] if second else []}, "has_more": not second, "next_cursor": "second" if not second else None}) + if request.method == "GET": + return httpx.Response(200, json={"number_id": "n"}, headers={"ETag": '"fresh"'}) + patches.append(json.loads(request.content)) + return httpx.Response(412 if len(patches) == 1 else 200, json={"number_id": "n"}) + client = make_client(handler) + from agora_agent.v3 import RawResponse + deployed = DeployedAgent(client, RawResponse({"agent_id": "a", "agent_name": "Support"}, httpx.Headers(), 200, None)) + assert deployed.assign_number("+12025550123", max_call_duration_seconds=900)["number_id"] == "n" + assert len(calls) == 6 # Two list pages, two GETs and two PATCH attempts. + assert patches[-1] == {"inbound": {"agent": "a", "call_policy": {"max_call_duration_seconds": 900}}} + deployed.unassign_number({"number_id": "n"}) + assert patches[-1] == {"inbound": {"agent": None}} + + +def test_number_not_found_scoped_to_project(): + client = make_client(lambda _: httpx.Response(200, json={"data": {"list": []}, "has_more": False, "next_cursor": None})) + from agora_agent.v3 import RawResponse + deployed = DeployedAgent(client, RawResponse({"agent_id": "a", "agent_name": "Support"}, httpx.Headers(), 200, None)) + with pytest.raises(NumberNotFoundError, match="project"): + deployed.assign_number("+12025550123") + + +@pytest.mark.asyncio +async def test_async_deploy_assign_get_delete_and_conflict_latest(): + calls = [] + conflict = False + def handler(request): + nonlocal conflict + calls.append(request) + if "/numbers/n" in request.url.path: + if request.method == "PATCH" and not conflict: + conflict = True + return httpx.Response(412, json={"reason": "stale"}) + return httpx.Response(200, json={"number_id": "n"}, headers={"ETag": '"n"'}) + return httpx.Response(200, json={"agent_id": "a", "agent_name": "Support"}, headers={"ETag": '"a"'}) + client = make_client(handler, True) + deployed = await configured(client).deploy_async(name="Support") + assert isinstance(deployed, AsyncDeployedAgent) + assert (await deployed.assign_number("n"))["number_id"] == "n" + assert (await deployed.unassign_number("n"))["number_id"] == "n" + assert (await AsyncDeployedAgent.get(client, "a")).id == "a" + await deployed.delete() + assert calls[-1].method == "DELETE" + with pytest.raises(TypeError): + configured(client).deploy() + with pytest.raises(TypeError): + await configured(make_client(handler)).deploy_async() + + +@pytest.mark.asyncio +async def test_async_cursor_pages_conflict_latest_and_redeploy(): + calls = [] + def handler(request): + calls.append(request) + if request.url.path.endswith("/sessions"): + second = request.url.params.get("cursor") == "second" + return httpx.Response(200, json={"data": {"count": 1, "list": [{"agent_session_id": "s2" if second else "s1"}]}, "has_more": not second, "next_cursor": None if second else "second"}) + if "/secrets/" in request.url.path: + if request.method != "GET": + return httpx.Response(412, json={"detail": "stale"}) + return httpx.Response(200, json={"name": "prod"}, headers={"ETag": '"new"'}) + if request.method == "GET": + return httpx.Response(200, json={"agent_id": "a", "agent_name": "Old", "instructions": "remove"}, headers={"ETag": '"v1"'}) + return httpx.Response(200, json={"agent_id": "a", "agent_name": "New"}, headers={"ETag": '"v2"'}) + client = make_client(handler, True) + assert [x["agent_session_id"] async for x in client.v3.sessions.iter_all(limit=1)] == ["s1", "s2"] + with pytest.raises(PreconditionFailedError) as exc: + await client.v3.secrets.set_key(name="prod", key="k", value="hidden", if_match='"old"') + assert await exc.value.latest() == {"name": "prod"} + assert calls[-1].url.path.endswith("/secrets/prod") + deployed = await Agent(client=client).deploy_async(name="New", agent_id="a") + assert deployed.etag == '"v2"' + assert json.loads(calls[-1].content)["instructions"] is None + + +def test_environment_override_and_shared_token_auth(monkeypatch): + calls = [] + monkeypatch.setenv("AGORA_AGENTS_V3_BASE_URL", "https://env-stage.example") + client = Agora(area=Area.US, app_id="project", app_certificate="cert", auth_token="raw-token", + httpx_client=httpx.Client(transport=httpx.MockTransport(lambda r: calls.append(r) or httpx.Response(200, json={})))) + client.v3.agents.list() + assert calls[-1].url.host == "env-stage.example" + assert calls[-1].headers["Authorization"] == "agora token=raw-token" + # An explicit option wins, and does not change v2's configured base URL. + explicit = make_client(lambda r: calls.append(r) or httpx.Response(200, json={})) + before = explicit._client_wrapper.get_base_url() + explicit.v3.agents.list() + assert calls[-1].url.host == "stage.example" + assert explicit._client_wrapper.get_base_url() == before + + +def test_default_auth_token_is_refreshed_per_v3_call(monkeypatch): + calls = [] + generated = [] + def token(**kwargs): + generated.append(kwargs) + return "fresh-" + str(len(generated)) + monkeypatch.setattr("agora_agent.v3._transport.generate_convo_ai_token", token) + client = Agora(area=Area.US, app_id="0" * 32, app_certificate="1" * 32, + httpx_client=httpx.Client(transport=httpx.MockTransport(lambda r: calls.append(r) or httpx.Response(200, json={})))) + client.v3.agents.list() + client.v3.sessions.list() + assert [r.headers["Authorization"] for r in calls] == ["agora token=fresh-1", "agora token=fresh-2"] + assert generated[0]["app_id"] == "0" * 32 + + +def test_audio_greeting_mapping(): + base = configured(make_client(lambda _: None)) + base._llm = {**base._llm, "greeting_audio_url": "https://audio.example/greeting.wav", + "greeting_configs": {"audio_download_timeout_ms": 1000, "audio_pcm_sample_rate": 16000}} + greeting = build_body(base)["greeting"] + assert greeting == {"mode": "audio", "url": "https://audio.example/greeting.wav", "text": "Hi", "download_timeout_ms": 1000, "pcm_sample_rate": 16000} + + +def test_greeting_playback_mode_aliases_and_unsupported_mode(): + client = make_client(lambda _: pytest.fail("pure mapping")) + for old, new in (("single_first", "first_join"), ("single_every", "each_join")): + body = build_body(Agent(client=client, greeting="Hello", greeting_configs={"mode": old})) + assert body["greeting"] == {"mode": "text", "text": "Hello", "on": new} + with pytest.raises(UnsupportedFieldError, match="greeting_configs.mode"): + build_body(Agent(client=client, greeting="Hello", greeting_configs={"mode": "unknown"})) + with pytest.raises(UnsupportedFieldError, match="greeting_configs.pcm_sample_rate"): + build_body(Agent(client=client, greeting="Hello", greeting_configs={"audio_pcm_sample_rate": 16000})) + + +def test_managed_minimax_preset_hint_becomes_provider_param(): + from agora_agent import MiniMaxTTS + client = make_client(lambda _: pytest.fail("pure mapping")) + agent = Agent(client=client).with_tts(MiniMaxTTS(model="speech-2.6-turbo", voice_id="voice")) + tts = build_body(agent)["pipeline"]["tts"] + assert tts["credential"] == {"mode": "managed"} + assert tts["params"]["model"] == "speech-2.6-turbo" + assert "_minimax_preset_model" not in tts + explicit = Agent(client=client).with_tts(MiniMaxTTS( + model="speech-2.6-turbo", voice_id="voice", additional_params={"model": "speech-2.8-turbo"} + )) + assert build_body(explicit)["pipeline"]["tts"]["params"]["model"] == "speech-2.8-turbo" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_list_numbers_scans_pages_filters_agent_and_avoids_detail_gets(async_): + calls = [] + def handler(request): + calls.append(request) + assert request.method == "GET" and request.url.path.endswith("/numbers") + assert request.url.params["limit"] == "100" + second = request.url.params.get("cursor") == "second" + numbers = ([{"number_id": "match2", "inbound": {"agent": "a"}}, + {"number_id": "unassigned", "inbound": {"agent": None}}] if second else + [{"number_id": "match1", "inbound": {"agent": "a"}}, + {"number_id": "other", "inbound": {"agent": "b"}}, + {"number_id": "null", "inbound": None}, {"number_id": "missing"}]) + return httpx.Response(200, json={"data": {"count": len(numbers), "list": numbers}, + "has_more": not second, "next_cursor": None if second else "second"}) + from agora_agent.v3 import RawResponse + client = make_client(handler, async_) + cls = AsyncDeployedAgent if async_ else DeployedAgent + deployed = cls(client, RawResponse({"agent_id": "a", "agent_name": "Support"}, httpx.Headers(), 200, None)) + numbers = await deployed.list_numbers() if async_ else deployed.list_numbers() + assert [number["number_id"] for number in numbers] == ["match1", "match2"] + assert len(calls) == 2 + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_deploy_preserves_interaction_and_provider_languages(async_): + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(201, json={"agent_id": "a", "agent_name": "German"}, headers={"ETag": '"1"'}) + agent = (Agent(client=make_client(handler, async_)) + .with_stt(DeepgramSTT(api_key=secret_ref("prod", "dg"), model="nova-3", language="fr")) + .with_turn_detection({"mode": "default", "language": "de-DE"})) + if async_: + await agent.deploy_async(name="German") + else: + agent.deploy(name="German") + pipeline = json.loads(requests[0].content)["pipeline"] + assert pipeline["asr"]["language"] == "de-DE" + assert "language" not in pipeline["asr"].get("params", {}) + assert "language" not in pipeline.get("turn_detection", {}) + v2 = agent.with_stt(DeepgramSTT(api_key="inline", model="nova-3", language="fr")).to_properties(channel="room", agent_uid="1", remote_uids=["*"], token="rtc", + allow_missing_vendor_categories={"llm", "tts"}).model_dump(exclude_none=True) + assert v2["asr"]["language"] == pipeline["asr"]["language"] + assert v2["asr"]["params"]["language"] == "fr" + + +def test_explicit_interaction_language_uses_existing_managed_asr_fallback(): + requests = [] + def handler(request): + requests.append(request) + return httpx.Response(201, json={"agent_id": "a", "agent_name": "Japanese"}, headers={"ETag": '"1"'}) + client = make_client(handler) + Agent(client=client).with_turn_detection({"language": "ja-JP"}).deploy(name="Japanese") + pipeline = json.loads(requests[0].content)["pipeline"] + assert pipeline["asr"] == {"vendor": "ares", "language": "ja-JP", "credential": {"mode": "managed"}} + assert "language" not in pipeline.get("turn_detection", {}) + assert "asr" not in build_body(Agent(client=client))["pipeline"] + assert "asr" not in build_body(Agent(client=client).with_llm(OpenAI(model="gpt-4o-mini")))["pipeline"] + default = build_body(Agent(client=client).with_stt(DeepgramSTT(api_key=secret_ref("prod", "dg"), model="nova-3", language="fr"))) + assert default["pipeline"]["asr"]["language"] == "fr" + assert "language" not in default["pipeline"]["asr"].get("params", {}) + + +@pytest.mark.parametrize("options", [["same", "same"], [1, 1.0], [0, -0.0]]) +def test_variable_options_reject_json_numeric_duplicates(options): + with pytest.raises(ValueError, match="unique"): + Agent(client=make_client(lambda _: pytest.fail("no network expected"))).with_variable("choice", type="float", options=options) + + +def test_variable_options_distinguish_booleans_strings_and_numbers(): + Agent(client=make_client(lambda _: pytest.fail("no network expected"))).with_variable("choice", type="str", options=[True, 1, "1", False, 0]) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_client", [False, True]) +async def test_conflict_latest_preserves_context_without_write_conditions(async_client): + calls = [] + extra = {"iF-mAtCh": '"old"', "If-None-Match": "*", "Idempotency-Key": "once", + "Authorization": "agora token=override", "X-Custom": "kept"} + def handler(request): + calls.append(request) + if request.method == "PATCH": + return httpx.Response(412, json={"error": "stale"}) + assert not any(key in request.headers for key in ("If-Match", "If-None-Match", "Idempotency-Key")) + assert request.headers["Authorization"] == "agora token=override" + assert request.headers["X-Custom"] == "kept" + return httpx.Response(200, json={"agent_id": "a"}) + client = make_client(handler, async_client) + with pytest.raises(PreconditionFailedError) as exc: + result = client.v3.agents.update(agent_id="a", if_match='"old"', request_options={"additional_headers": extra}) + if async_client: + await result + result = exc.value.latest() + if async_client: + result = await result + assert result == {"agent_id": "a"} + assert len(calls) == 2 + assert extra["iF-mAtCh"] == '"old"' + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_one_builder_creates_session_or_saved_handle_with_unversioned_provisioning(async_): + import copy + + from agora_agent import AsyncSavedAgent, SavedAgent + from agora_agent.agentkit import AsyncSavedAgent as AgentKitAsyncSavedAgent + from agora_agent.agentkit import SavedAgent as AgentKitSavedAgent + assert SavedAgent is DeployedAgent is AgentKitSavedAgent + assert AsyncSavedAgent is AsyncDeployedAgent is AgentKitAsyncSavedAgent + calls = [] + stored = {} + def handler(request): + calls.append(request) + path = request.url.path + if path.endswith("/join"): + payload = json.loads(request.content) + assert payload["properties"]["llm"]["api_key"] == "inline-key" + return httpx.Response(200, json={"agent_id": "running-session"}) + if path.endswith("/secrets"): + return httpx.Response(201, json={"name": "support", "keys": ["openai"], "create_ts": 1, "update_ts": 1}) + if path.endswith("/numbers"): + return httpx.Response(201, json={"number_id": "n", "number": "+12025550123", "create_ts": 1}) + if "/numbers/n" in path: + return httpx.Response(200, json={"number_id": "n", "number": "+12025550123", "create_ts": 1}, headers={"ETag": '"n"'}) + if request.method in ("POST", "PATCH"): + payload = json.loads(request.content) + stored.update(payload) + stored.update(agent_id="a", create_ts=1, update_ts=2) + return httpx.Response(200, json=stored, headers={"ETag": '"saved"'}) + return httpx.Response(200, json=stored, headers={"ETag": '"current"'}) + client = make_client(handler, async_) + assert client.secrets is client.v3.secrets + assert client.numbers is client.v3.numbers + agent = (Agent(client=client) + .with_stt(DeepgramSTT(api_key="asr-key", model="nova-3")) + .with_llm(OpenAI(api_key="inline-key", base_url="https://api.openai.com/v1/chat/completions", model="gpt-4o-mini")) + .with_tts(OpenAITTS(api_key="tts-key", base_url="https://api.openai.com/v1/audio/speech", model="tts-1", voice="alloy"))) + snapshot = copy.deepcopy(agent.config) + saved_config = configured(client) + if async_: + session = agent.create_async_session(channel="room", agent_uid="1", remote_uids=["*"], token="rtc") + assert await session.start() == "running-session" + await client.secrets.create(name="support", secrets={"openai": "key"}) + number = await client.numbers.create(number="+12025550123", sip_trunk={"hostname": "sip.example"}) + saved = await saved_config.save_async(name="Support") + await saved.assign_number(number) + updated = await saved.update(saved_config.with_greeting("Updated")) + assert isinstance(saved, AsyncSavedAgent) + loaded = await AsyncSavedAgent.get(client, saved.id) + else: + session = agent.create_session(channel="room", agent_uid="1", remote_uids=["*"], token="rtc") + assert session.start() == "running-session" + client.secrets.create(name="support", secrets={"openai": "key"}) + number = client.numbers.create(number="+12025550123", sip_trunk={"hostname": "sip.example"}) + saved = saved_config.save(name="Support") + saved.assign_number(number) + updated = saved.update(saved_config.with_greeting("Updated")) + assert isinstance(saved, SavedAgent) + loaded = SavedAgent.get(client, saved.id) + assert updated.id == loaded.id == saved.id == "a" + assert updated.name == saved.name == "Support" + assert agent.config == snapshot + assert callable(saved.create_session) + assert hasattr(saved, "create_async_session") is async_ + assert sum(r.method == "POST" and r.url.path.endswith("/agents") for r in calls) == 1 + patch = next(r for r in calls if r.method == "PATCH" and r.url.path.endswith("/agents/a")) + assert patch.headers["If-Match"] == '"current"' + assert json.loads(patch.content)["agent_name"] == "Support" + if async_: + renamed = await saved.update(saved_config, name="Renamed") + else: + renamed = saved.update(saved_config, name="Renamed") + assert renamed.name == "Renamed" + assert saved.name == "Support" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_saved_update_rejects_another_client_before_network(async_): + from agora_agent import AsyncSavedAgent, SavedAgent + from agora_agent.v3 import RawResponse + client = make_client(lambda _: pytest.fail("must not send"), async_) + other = make_client(lambda _: pytest.fail("must not send"), async_) + cls = AsyncSavedAgent if async_ else SavedAgent + saved = cls(client, RawResponse({"agent_id": "a", "agent_name": "Support"}, httpx.Headers(), 200, None)) + with pytest.raises(ValueError, match="same client"): + if async_: + await saved.update(Agent(client=other)) + else: + saved.update(Agent(client=other)) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("stored", [False, True]) +@pytest.mark.asyncio +async def test_saved_and_secret_ephemeral_lifecycle(async_, stored): + from agora_agent import AsyncSavedAgent, SavedAgent + from agora_agent.v3 import RawResponse + calls = [] + def handler(request): + calls.append(request) + path = request.url.path + if request.method == "POST" and not path.endswith("/stop"): + body = json.loads(request.content) + assert body["transport"] == {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]} + assert body["lifecycle"] == {"idle_timeout_ms": 1500, "max_duration_ms": 120000} + assert "client_reference" not in body + if stored: + assert body["agent_id"] == "stored-id" + assert body["labels"] == {"owner": "test"} + assert path.endswith("/sessions") + else: + assert path.endswith("/sessions/ephemeral") + assert "agent_name" not in body["agent"] + assert body["agent"]["pipeline"]["llm"]["credential"]["fields"]["api_key"] == "$secrets.prod.openai" + assert "variables" in body["agent"] + return httpx.Response(201, json={"agent_session_id": "new-session-id", "status": "running", "created_at": "unspecified"}) + assert path.endswith("/sessions/new-session-id" + ("/stop" if request.method == "POST" else "")) + if request.method == "POST": + assert request.content == b"" + return httpx.Response(200, json={}) + return httpx.Response(200, json={"agent_session_id": "new-session-id", "status": "RUNNING", "start_ts": 1}) + client = make_client(handler, async_) + if stored: + cls = AsyncSavedAgent if async_ else SavedAgent + owner = cls(client, RawResponse(data={"agent_id": "stored-id", "agent_name": "Support"}, headers=httpx.Headers(), status_code=200, response=httpx.Response(200))) + else: + owner = configured(client).with_variable("customer", type="str") + remote = ["2"] + kwargs = dict(channel="room", agent_uid="1", remote_uids=remote, name="reference", idle_timeout=1.5, max_duration=120) + if stored: + kwargs["labels"] = {"owner": "test"} + session = (owner.create_async_session(**kwargs) if async_ and not stored else owner.create_session(**kwargs)) + remote.append("mutation") + assert session.raw is client.v3.sessions + assert session.raw_agent_management is None + if async_: + assert await session.start() == "new-session-id" + assert (await session.get_info())["status"] == "RUNNING" + with pytest.raises(NotImplementedError): + await session.say("No unsupported controls") + await session.stop() + else: + assert session.start() == "new-session-id" + assert session.get_info()["status"] == "RUNNING" + with pytest.raises(NotImplementedError): + session.say("No unsupported controls") + session.stop() + assert session.status == "stopped" + assert len(calls) == 3 + + +@pytest.mark.parametrize("option,value", [("preset", "test"), ("pipeline_id", "id"), ("expires_in", 60), ("enable_string_uid", False), ("client_reference", "removed")]) +def test_ephemeral_rejects_undocumented_options(option, value): + agent = configured(make_client(lambda _: pytest.fail("must not send"))) + with pytest.raises(ValueError): + agent.create_session(channel="room", agent_uid="1", remote_uids=["*"], **{option: value}) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_new_session_stop_404_propagates_and_controls_never_request(async_): + calls = [] + def handler(request): + calls.append(request) + if request.url.path.endswith("/stop"): + return httpx.Response(404, json={"reason": "TaskNotFound"}) + return httpx.Response(201, json={"agent_session_id": "new", "status": "running", "created_at": 1}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="room", agent_uid="1", remote_uids=["2"]) + if async_: + await session.start() + else: + session.start() + for method, args in (("interrupt", ()), ("think", ("test",)), ("get_history", ()), ("get_turns", ()), ("get_all_turns", ())): + with pytest.raises(NotImplementedError): + result = getattr(session, method)(*args) + if async_: + await result + from agora_agent.core.api_error import ApiError + with pytest.raises(ApiError, match="TaskNotFound"): + if async_: + await session.stop() + else: + session.stop() + assert session.status == "error" + assert len(calls) == 2 + + +@pytest.mark.parametrize("seconds", [-1, float("nan"), float("inf"), True, 0.00001]) +def test_new_session_duration_rejects_invalid_precision(seconds): + with pytest.raises(ValueError): + configured(make_client(lambda _: pytest.fail("must not send"))).create_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=seconds) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_new_session_stop_does_not_hide_unrelated_404(async_): + def handler(request): + if request.url.path.endswith("/stop"): + return httpx.Response(404, json={"reason": "ProjectNotFound"}) + return httpx.Response(201, json={"agent_session_id": "new", "status": "running", "created_at": 1}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"]) + if async_: + await session.start() + with pytest.raises(Exception) as caught: + await session.stop() + else: + session.start() + with pytest.raises(Exception) as caught: + session.stop() + assert caught.value.status_code == 404 + assert session.status == "error" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("stored", [False, True]) +def test_preview_does_not_hardcode_rollout_regions(async_, stored): + from agora_agent import AsyncSavedAgent, SavedAgent + from agora_agent.v3 import RawResponse + client = make_client(lambda _: pytest.fail("must not send"), async_) + client._area = Area.CN + owner = ((AsyncSavedAgent if async_ else SavedAgent)(client, RawResponse(data={"agent_id": "id", "agent_name": "name"}, headers=httpx.Headers(), status_code=200, response=httpx.Response(200))) if stored else configured(client)) + method = owner.create_async_session if async_ and not stored else owner.create_session + session = method(channel="r", agent_uid="1", remote_uids=["2"]) + assert session.status == "idle" + + +def test_new_sessions_reference_precedence_and_whitespace_uids(): + from agora_agent.agentkit.session_backend import create_session + agent = configured(make_client(lambda _: pytest.fail("must not send"))) + session = create_session(agent, channel="r", agent_uid="1", remote_uids=["2"], name="fallback") + assert "client_reference" not in session._session_backend["body"] + with pytest.raises(ValueError, match="remote_uids"): + create_session(agent, channel="r", agent_uid="1", remote_uids=[" "]) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("status,http_status,timestamp", [("STARTING", 200, "create_ts"), ("running", 201, "created_at")]) +@pytest.mark.asyncio +async def test_corrected_sessions_supported_fields_and_rollout(async_, status, http_status, timestamp): + calls = [] + def handler(request): + calls.append(request) + if request.url.path.endswith("/stop"): + assert request.content == b"" + return httpx.Response(200, json={}) + if request.method == "GET": + return httpx.Response(200, json={"agent_session_id": "s", "status": "FAILED", "start_ts": 1}) + body = json.loads(request.content) + assert "client_reference" not in body + assert body["values"] == {"customer": "Ada"} + assert body["labels"] == {"team": "support"} + assert body["data_policy"] == {"retention": "none"} + assert body["transport"]["token"] == "rtc-only" + assert body["transport"]["audio_scenario"] == "chorus" + assert body["transport"]["encryption"] == {"mode": "aes_128_gcm", "key": "secret"} + assert body["lifecycle"]["graceful_stop"] == {"enabled": True, "timeout_ms": 10000} + assert request.headers["Idempotency-Key"] == "session-key" + assert "rtc-only" not in request.headers["Authorization"] + return httpx.Response(http_status, json={"agent_session_id": "s", "status": status, timestamp: 1}, headers={"Idempotent-Replayed": "true"}) + client = make_client(handler, async_) + agent = configured(client) + method = agent.create_async_session if async_ else agent.create_session + session = method(channel="room", agent_uid="bot", remote_uids=["customer"], values={"customer": "Ada"}, labels={"team": "support"}, token="rtc-only", encryption={"mode": "aes_128_gcm", "key": "secret"}, audio_scenario="chorus", graceful_stop={"enabled": True, "timeout_ms": 10000}, data_policy={"retention": "none"}, idempotency_key="session-key") + if async_: + await session.start() + assert session.status == status.lower() + await session.stop() + assert (await session.get_info())["status"] == "FAILED" + else: + session.start() + assert session.status == status.lower() + session.stop() + assert session.get_info()["status"] == "FAILED" + assert session.status == "error" + + +def test_sync_session_info_cannot_overwrite_a_later_stop(): + entered = threading.Event() + release = threading.Event() + def handler(request): + if request.method == "GET": + entered.set() + assert release.wait(3) + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING"}) + if request.url.path.endswith("/stop"): + return httpx.Response(200, json={}) + return httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}) + session = configured(make_client(handler)).create_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=1) + session.start() + thread = threading.Thread(target=session.get_info) + thread.start() + assert entered.wait(3) + session.stop() + release.set() + thread.join(3) + assert not thread.is_alive() + assert session.status == "stopped" + + +@pytest.mark.asyncio +async def test_async_session_info_cannot_overwrite_a_later_stop(): + entered = asyncio.Event() + release = asyncio.Event() + async def handler(request): + if request.method == "GET": + entered.set() + await asyncio.wait_for(release.wait(), timeout=3) + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING"}) + if request.url.path.endswith("/stop"): + return httpx.Response(200, json={}) + return httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}) + session = configured(make_client(handler, True)).create_async_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=1) + await session.start() + pending = asyncio.create_task(session.get_info()) + await asyncio.wait_for(entered.wait(), timeout=3) + await session.stop() + release.set() + await pending + assert session.status == "stopped" + + +@pytest.mark.asyncio +async def test_async_session_restart_does_not_expose_previous_id_while_starting(): + entered = asyncio.Event() + release = asyncio.Event() + starts = 0 + stopped_ids = [] + async def handler(request): + nonlocal starts + if request.url.path.endswith("/stop"): + stopped_ids.append(request.url.path.split("/")[-2]) + return httpx.Response(200, json={}) + starts += 1 + if starts == 2: + entered.set() + await asyncio.wait_for(release.wait(), timeout=3) + return httpx.Response(200, json={"agent_session_id": f"s{starts}", "create_ts": starts, "status": "RUNNING"}) + session = configured(make_client(handler, True)).create_async_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=1) + assert await session.start() == "s1" + await session.stop() + restart = asyncio.create_task(session.start()) + await asyncio.wait_for(entered.wait(), timeout=3) + assert session.id is None + with pytest.raises(RuntimeError, match="starting"): + await session.stop() + release.set() + assert await restart == "s2" + await session.stop() + assert stopped_ids == ["s1", "s2"] + + +@pytest.mark.asyncio +async def test_async_session_info_uses_newest_issued_read(): + first_entered = asyncio.Event() + release_first = asyncio.Event() + gets = 0 + async def handler(request): + nonlocal gets + if request.method == "GET": + gets += 1 + if gets == 1: + first_entered.set() + await asyncio.wait_for(release_first.wait(), timeout=3) + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING"}) + return httpx.Response(200, json={"agent_session_id": "s", "status": "STOPPED"}) + return httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}) + session = configured(make_client(handler, True)).create_async_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=1) + await session.start() + older = asyncio.create_task(session.get_info()) + await asyncio.wait_for(first_entered.wait(), timeout=3) + await session.get_info() + assert session.status == "stopped" + release_first.set() + await older + assert session.status == "stopped" + + +@pytest.mark.asyncio +async def test_async_session_get_info_during_stop_cannot_restore_running(): + stop_entered = asyncio.Event() + release_stop = asyncio.Event() + async def handler(request): + if request.url.path.endswith("/stop"): + stop_entered.set() + await asyncio.wait_for(release_stop.wait(), timeout=3) + return httpx.Response(200, json={}) + if request.method == "GET": + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING"}) + return httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}) + session = configured(make_client(handler, True)).create_async_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=1) + await session.start() + stopping = asyncio.create_task(session.stop()) + await asyncio.wait_for(stop_entered.wait(), timeout=3) + await session.get_info() + assert session.status == "stopping" + release_stop.set() + await stopping + assert session.status == "stopped" + + +@pytest.mark.asyncio +async def test_async_stop_cancellation_clears_transition_and_allows_refresh(): + stop_entered = asyncio.Event() + async def handler(request): + if request.url.path.endswith("/stop"): + stop_entered.set() + await asyncio.Event().wait() + if request.method == "GET": + return httpx.Response(200, json={"agent_session_id": "s", "status": "STOPPED"}) + return httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}) + session = configured(make_client(handler, True)).create_async_session(channel="r", agent_uid="1", remote_uids=["2"], max_duration=1) + await session.start() + stopping = asyncio.create_task(session.stop()) + await asyncio.wait_for(stop_entered.wait(), timeout=3) + stopping.cancel() + with pytest.raises(asyncio.CancelledError): + await stopping + assert session.status == "error" + assert await session.get_info() == {"agent_session_id": "s", "status": "STOPPED"} + assert session.status == "stopped" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("reason,expected", [("TaskNotRunning", "stopped"), ("TaskConflict", "stopping"), ("Other", "error")]) +@pytest.mark.asyncio +async def test_new_stop_conflict_distinctions(async_, reason, expected): + from agora_agent.core.api_error import ApiError + def handler(request): + return httpx.Response(409, json={"reason": reason}) if request.url.path.endswith("/stop") else httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "STARTING"}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"]) + if async_: + await session.start() + with pytest.raises(ApiError): + await session.stop() + else: + session.start() + with pytest.raises(ApiError): + session.stop() + assert session.status == expected + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_session_can_refresh_after_stop_conflict_finishes(async_): + calls = [] + def handler(request): + calls.append(request) + if request.url.path.endswith("/stop"): + return httpx.Response(409, json={"reason": "TaskConflict"}) + if request.method == "POST": + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING", "create_ts": 1}) + return httpx.Response(200, json={"agent_session_id": "s", "status": "STOPPED", "create_ts": 1}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"]) + if async_: + await session.start() + with pytest.raises(Exception): + await session.stop() + assert session.status == "stopping" + await session.get_info() + else: + session.start() + with pytest.raises(Exception): + session.stop() + assert session.status == "stopping" + session.get_info() + assert session.status == "stopped" + assert len(calls) == 3 + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_default_public_origin_and_signed_token(async_): + calls = [] + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(lambda r: calls.append(r) or httpx.Response(200, json={}))) + client = (AsyncAgora if async_ else Agora)(area=Area.US, app_id="0" * 32, app_certificate="1" * 32, httpx_client=http) + result = client.v3.agents.list() + if async_: + await result + assert calls[0].url.host == "preview.ai.agora.io" + assert calls[0].url.path.startswith("/conversational-ai/v1/projects/") + assert calls[0].headers["Authorization"].startswith("agora token=") + + +@pytest.mark.parametrize("field", ["customer_id", "customer_secret", "client_id", "client_secret", "customerId", "customerSecret", "clientId", "clientSecret", "username", "password"]) +def test_partial_or_unknown_basic_credentials_rejected(field): + for cls in (Agora, AsyncAgora): + with pytest.raises(ValueError if field in {"customer_id", "customer_secret"} else TypeError): + cls(area=Area.US, app_id="p", app_certificate="c", **{field: "stale"}) + client = make_client(lambda _: pytest.fail("must not send")) + with pytest.raises(ValueError, match="Basic"): + client.v3.agents.list(request_options={field: "stale"}) + + +@pytest.mark.parametrize("header,value", [("Authorization", "Basic stale"), ("X-Studio-Region", "us"), ("X-Studio-V2-Gateway-Secret", "secret"), ("X-Consumer-Username", "user")]) +def test_forbidden_headers_rejected_in_all_new_transport_layers(header, value): + for location in ("base", "v3", "request"): + client = make_client(lambda _: pytest.fail("must not send")) + options = {} + if location == "base": + client._client_wrapper._headers = {header: value} + # Authorization normally overwrites custom headers; inspect custom values too. + elif location == "v3": + client._v3_options = V3Options(headers={header: value}) + else: + options = {"additional_headers": {header: value}} + with pytest.raises(ValueError): + client.v3.agents.list(request_options=options) + + +def test_blank_authorization_overrides_rejected_before_request(): + with pytest.raises(ValueError): + Agora(area=Area.US, app_id="project", app_certificate="cert", auth_token="token", headers={"Authorization": ""}) + for location in ("v3", "request"): + client = make_client(lambda _: pytest.fail("must not send")) + if location == "v3": + client._v3_options = V3Options(headers={"Authorization": ""}) + options = {} + else: + options = {"additional_headers": {"Authorization": ""}} + with pytest.raises(ValueError): + client.v3.agents.list(request_options=options) + from agora_agent.client import Agora as GeneratedAgora + raw = GeneratedAgora(authorization="", httpx_client=httpx.Client(transport=httpx.MockTransport(lambda _: pytest.fail("must not send")))) + with pytest.raises(ValueError): + raw.agents.get("project", "agent") + sent = [] + raw = GeneratedAgora(authorization="", httpx_client=httpx.Client(transport=httpx.MockTransport(lambda request: sent.append(request) or httpx.Response(200, json={})))) + raw.agents.get("project", "agent", request_options={"additional_headers": {"authorization": "agora token=override"}}) + assert sent[0].headers.get_list("authorization") == ["agora token=override"] + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_generated_client_requires_effective_token_before_http(async_): + from agora_agent.client import Agora as GeneratedAgora + from agora_agent.client import AsyncAgora as GeneratedAsyncAgora + + http = (httpx.AsyncClient if async_ else httpx.Client)( + transport=httpx.MockTransport(lambda _: pytest.fail("must not send")) + ) + raw = (GeneratedAsyncAgora if async_ else GeneratedAgora)(authorization=None, httpx_client=http) + with pytest.raises(ValueError, match="authorization is required"): + result = raw.agents.get("project", "agent") + if async_: + await result + if async_: + await http.aclose() + else: + http.close() + + +@pytest.mark.parametrize("resource,kwargs", [("secrets", {"name": "s", "secrets": {"k": "v"}}), ("numbers", {"number": "+12025550123", "sip_trunk": {"hostname": "sip.example"}})]) +def test_unsupported_provisioning_idempotency_rejected(resource, kwargs): + method = getattr(make_client(lambda _: pytest.fail("must not send")).v3, resource).create + with pytest.raises(TypeError): + method(**kwargs, idempotency_key="old") + with pytest.raises(ValueError): + method(**kwargs, request_options={"additional_headers": {"Idempotency-Key": "old"}}) + + +def test_managed_endpoints_removed_byok_preserved_and_supported_avatar(): + managed = module({"vendor": "openai", "url": "https://old", "params": {"url": "https://old", "base_url": "https://old"}}, "llm", []) + assert "url" not in managed and "params" not in managed + byok = module({"vendor": "openai", "api_key": "key", "url": "https://byok"}, "llm", []) + assert byok["url"] == "https://byok" + for vendor in ("sensetime", "spatius"): + errors = [] + assert module({"vendor": vendor, "enable": True, "params": {}}, "avatar", errors)["vendor"] == vendor + assert not errors + + +def test_builder_retention_farewell_mapping_uses_session_envelope(): + session = configured(make_client(lambda _: pytest.fail("must not send"))).with_parameters({"opt_out": True, "farewell_config": {"graceful_enabled": True, "graceful_timeout_seconds": 10}}).create_session(channel="r", agent_uid="1", remote_uids=["2"]) + body = session._session_backend["body"] + assert body["data_policy"] == {"retention": "none"} + assert body["lifecycle"]["graceful_stop"] == {"enabled": True, "timeout_ms": 10000} + assert "data_policy" not in body["agent"] + + +def test_builder_labels_map_to_new_session_labels(): + agent = configured(make_client(lambda _: pytest.fail("must not send"))).with_labels({"team": "support"}).with_variable("customer", type="str") + session = agent.create_session(channel="r", agent_uid="1", remote_uids=["2"]) + assert session._session_backend["body"]["labels"] == {"team": "support"} + assert "labels" not in session._session_backend["body"]["agent"] + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("override", ["header", "basic_client", "credentials"]) +@pytest.mark.asyncio +async def test_legacy_resource_token_policy_at_http_boundary(async_, override): + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(lambda _: pytest.fail("must not send")), **({"auth": httpx.BasicAuth("id", "secret")} if override == "basic_client" else {})) + client = (AsyncAgora if async_ else Agora)(area=Area.US, app_id="p", app_certificate="c", auth_token="token", httpx_client=http) + options = {"additional_headers": {"authorization": "Basic stale"}} if override == "header" else {"clientSecret": "old"} if override == "credentials" else {} + with pytest.raises(ValueError): + result = client.agents.get("p", "legacy-id", request_options=options) + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_campaign_pages_and_conditional_reload(async_): + calls = [] + def handler(request): + calls.append(request) + if request.method == "PATCH": + return httpx.Response(412, json={"reason": "PreconditionFailed"}, headers={"ETag": '"fresh"'}) + if request.url.path.endswith("/campaigns/c"): + assert "If-Match" not in request.headers + return httpx.Response(200, json={"campaign_id": "c", "name": "Fresh"}) + second = request.url.params.get("cursor") == "next" + return httpx.Response(200, json={"data": {"list": [{"campaign_id": "b" if second else "a"}]}, "has_more": not second, "next_cursor": None if second else "next"}) + client = make_client(handler, async_) + assert client.campaigns is client.v3.campaigns + if async_: + assert [item["campaign_id"] async for item in client.campaigns.iter_all(limit=1)] == ["a", "b"] + with pytest.raises(PreconditionFailedError) as caught: + await client.campaigns.update(campaign_id="c", if_match='"old"', pacing={"max_concurrent": 2}) + assert (await caught.value.latest())["name"] == "Fresh" + else: + assert [item["campaign_id"] for item in client.campaigns.iter_all(limit=1)] == ["a", "b"] + with pytest.raises(PreconditionFailedError) as caught: + client.campaigns.update(campaign_id="c", if_match='"old"', pacing={"max_concurrent": 2}) + assert caught.value.latest()["name"] == "Fresh" + assert len(calls) == 4 + + +@pytest.mark.parametrize("extra", [{"remote_uids": ["*"]}, {"remote_uids": ["2", "3"]}, {"remote_uids": []}, {"remote_uids": ["name"]}, {"max_duration": 0}, {"idle_timeout": 3601}, {"graceful_stop": {"timeout_ms": 120001}}, {"data_policy": {"retention": "forever"}}, {"encryption": {"mode": "bad", "key": "key"}}]) +def test_session_schema_limits_reject_before_network(extra): + options = {"channel": "r", "agent_uid": "1", "remote_uids": ["2"], **extra} + with pytest.raises(ValueError): + configured(make_client(lambda _: pytest.fail("must not send"))).create_session(**options) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_session_raw_rollout_normalizes_data_preserves_http_and_replay(async_): + client = make_client(lambda _: httpx.Response(201, json={"agent_session_id": "s", "created_at": 42, "status": "starting"}, headers={"Idempotent-Replayed": "true"}), async_) + result = client.v3.sessions.with_raw_response.start(agent_id="a", transport={"type": "rtc", "channel": "r", "uid": "1", "subscribe_uids": ["2"]}, idempotency_key="same") + raw = await result if async_ else result + assert raw.data == {"agent_session_id": "s", "create_ts": 42, "status": "STARTING"} + assert raw.status_code == 201 + assert raw.response.json()["created_at"] == 42 + assert raw.headers["Idempotent-Replayed"] == "true" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_successful_idempotent_handle_requires_new_handle_for_restart(async_): + calls = [] + def handler(request): + calls.append(request) + return httpx.Response(200, json={} if request.url.path.endswith("/stop") else {"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"], idempotency_key="one-session") + if async_: + await session.start() + await session.stop() + with pytest.raises(RuntimeError, match="new session handle"): + await session.start() + else: + session.start() + session.stop() + with pytest.raises(RuntimeError, match="new session handle"): + session.start() + assert len(calls) == 2 + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_unknown_start_can_retry_original_idempotency_key(async_): + calls = [] + def handler(request): + calls.append(request) + assert request.headers["Idempotency-Key"] == "original" + return httpx.Response(503, json={"reason": "Busy"}) if len(calls) == 1 else httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "STARTING"}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"], idempotency_key="original") + if async_: + with pytest.raises(Exception): + await session.start() + assert await session.start() == "s" + else: + with pytest.raises(Exception): + session.start() + assert session.start() == "s" + assert len(calls) == 2 + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("ephemeral", [False, True]) +@pytest.mark.asyncio +async def test_lowlevel_telephony_session_transport_union(async_, ephemeral): + calls = [] + source = "7b1e4c2d-5a6f-4e8b-9c0d-1f2a3b4c5d6e" + transport = {"type": "telephony", "from": source, "to": "+16505550188", "call_policy": {"max_ring_duration_ms": 30000, "max_call_duration_seconds": 900, "max_silence_duration_ms": 5000, "voicemail": {"mode": "hangup"}, "end_call": {"on_user_request": True}, "transfer": {"phone_number": "+12025550123", "description": "Talk to a person"}}} + def handler(request): + calls.append(request) + assert json.loads(request.content)["transport"] == transport + assert request.headers["Idempotency-Key"] == "call-1" + return httpx.Response(200, json={"agent_session_id": "phone-session", "create_ts": 1, "status": "STARTING"}) + client = make_client(handler, async_) + method = client.v3.sessions.start_ephemeral if ephemeral else client.v3.sessions.start + result = method(**({"agent": {"instructions": "Help"}} if ephemeral else {"agent_id": "saved"}), transport=transport, idempotency_key="call-1") + response = await result if async_ else result + assert response["agent_session_id"] == "phone-session" + assert len(calls) == 1 + + +@pytest.mark.parametrize("transport", [{"type": "telephony", "from": "+16505550188", "to": "+12025550123"}, {"type": "telephony", "from": "7b1e4c2d-5a6f-4e8b-9c0d-1f2a3b4c5d6e", "to": "national"}, {"type": "telephony", "from": "7b1e4c2d-5a6f-4e8b-9c0d-1f2a3b4c5d6e", "to": "+12025550123", "call_policy": {"max_ring_duration_ms": 0}}]) +def test_lowlevel_telephony_rejects_invalid_source_destination_and_policy(transport): + client = make_client(lambda _: pytest.fail("must not send")) + with pytest.raises(ValueError): + client.v3.sessions.start(agent_id="a", transport=transport) + + +@pytest.mark.parametrize("vendor", ["sensetime", "spatius"]) +@pytest.mark.parametrize("reference", [False, True]) +def test_public_avatar_vendor_keys_use_write_only_credential_envelope(vendor, reference): + import copy + + from agora_agent.agentkit.vendors.cn import SenseTimeAvatar, SpatiusAvatar + calls = [] + key = secret_ref("avatars", vendor) if reference else "inline-avatar-key" + avatar = (SenseTimeAvatar(agora_uid="3", app_id="app", app_key=key) if vendor == "sensetime" else SpatiusAvatar(agora_uid="3", spatius_app_id="app", spatius_avatar_id="avatar", spatius_api_key=key)) + def handler(request): + calls.append(request) + return httpx.Response(201, json={"agent_id": "a", "agent_name": "Avatar"}, headers={"ETag": '"1"'}) + agent = configured(make_client(handler)).with_avatar(avatar) + before = copy.deepcopy(agent.config) + if reference: + agent.save(name="Avatar") + body = json.loads(calls[0].content)["pipeline"]["avatar"] + else: + with pytest.raises(ValueError, match="secret_ref"): + agent.save(name="Avatar") + assert not calls + body = build_body(agent)["pipeline"]["avatar"] + assert body["vendor"] == vendor + assert body["credential"] == {"mode": "byok", "fields": {"app_key" if vendor == "sensetime" else "spatius_api_key": key}} + assert "app_key" not in body.get("params", {}) + assert "spatius_api_key" not in body.get("params", {}) + assert body["params"]["agora_uid"] == "3" + assert agent.config == before + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("origin", ["https://customer:secret@preview.ai.agora.io", "https://preview.ai.agora.io?", "https://preview.ai.agora.io#"]) +@pytest.mark.asyncio +async def test_public_origin_rejects_embedded_credentials(async_, origin): + client = make_client(lambda _: pytest.fail("must not send"), async_) + client._v3_options = V3Options(base_url=origin) + with pytest.raises(ValueError, match="origin"): + result = client.v3.agents.list() + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_advertised_app_credentials_sign_raw_legacy_reads_each_call(async_, monkeypatch): + calls = [] + tokens = [] + def sign(**kwargs): + assert kwargs["app_id"] == "app" and kwargs["app_certificate"] == "certificate" + token = "fresh-" + str(len(tokens)) + tokens.append(token) + return token + monkeypatch.setattr("agora_agent.pool_client.generate_convo_ai_token", sign) + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(lambda r: calls.append(r) or httpx.Response(200, json={"agent_id": "legacy", "status": "RUNNING"}))) + client = (AsyncAgora if async_ else Agora)(area=Area.US, app_id="app", app_certificate="certificate", httpx_client=http) + for _ in range(2): + result = client.agents.get("app", "legacy") + if async_: + await result + assert len(calls) == 2 + assert all(r.headers["Authorization"].startswith("agora token=fresh-") for r in calls) + assert calls[0].headers["Authorization"] != calls[1].headers["Authorization"] + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("extra", [False, True]) +@pytest.mark.asyncio +async def test_new_session_geofence_maps_builder_or_explicit_option(async_, extra): + calls = [] + client = make_client(lambda r: calls.append(r) or httpx.Response(200, json={"agent_session_id": "s", "create_ts": 1, "status": "RUNNING"}), async_) + agent = configured(client).with_geofence({"area": "GLOBAL", "exclude_area": "INDIA"}) + method = agent.create_async_session if async_ else agent.create_session + session = method(channel="r", agent_uid="1", remote_uids=["2"], **({"geofence": {"area": "europe"}} if extra else {})) + if async_: + await session.start() + else: + session.start() + body = json.loads(calls[0].content) + assert body["transport"]["geofence"] == ({"area": "europe"} if extra else {"area": "global", "exclude_area": "india"}) + assert "geofence" not in body["agent"] + assert agent.config["geofence"] == {"area": "GLOBAL", "exclude_area": "INDIA"} + + +@pytest.mark.parametrize("fence", [{"area": "invalid"}, {"area": "europe", "exclude_area": "india"}, {"area": "global", "exclude_area": "global"}]) +def test_new_session_geofence_invalid_combinations_reject_locally(fence): + with pytest.raises(ValueError, match="geofence"): + configured(make_client(lambda _: pytest.fail("must not send"))).create_session(channel="r", agent_uid="1", remote_uids=["2"], geofence=fence) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_duplicate_authorization_values_never_allow_hidden_basic(async_): + client = make_client(lambda _: pytest.fail("must not send"), async_) + headers = httpx.Headers([("Authorization", "agora token=valid"), ("Authorization", "Basic hidden")]) + for resource in (client.v3.agents, client.agents): + with pytest.raises(ValueError, match="token"): + result = (resource.get(agent_id="a", request_options={"additional_headers": headers}) if resource is client.v3.agents else resource.get("p", "legacy", request_options={"additional_headers": headers})) + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_prebuilt_rest_token_needs_no_app_certificate(async_): + calls = [] + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(lambda r: calls.append(r) or httpx.Response(200, json={"agent_id": "legacy", "status": "RUNNING"}))) + client = (AsyncAgora if async_ else Agora)(area=Area.US, app_id="project", auth_token="existing-token", httpx_client=http) + result = client.agents.get("project", "legacy") + if async_: + await result + result = client.v3.agents.list() + if async_: + await result + assert len(calls) == 2 + assert all(r.headers["Authorization"] == "agora token=existing-token" for r in calls) + assert client.app_certificate is None + + +@pytest.mark.parametrize("cls", [Agora, AsyncAgora]) +@pytest.mark.parametrize("options", [{}, {"auth_token": ""}, {"auth_token": " "}, {"app_certificate": ""}]) +def test_client_requires_nonempty_token_or_signing_credentials(cls, options): + with pytest.raises(ValueError): + cls(area=Area.US, app_id="project", **options) + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_duplicate_auth_in_resource_header_overrides_is_rejected(async_): + client = make_client(lambda _: pytest.fail("must not send"), async_) + client._v3_options = V3Options(headers=httpx.Headers([("Authorization", "agora token=good"), ("Authorization", "Basic hidden")])) + with pytest.raises(ValueError, match="token"): + result = client.v3.agents.list() + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.asyncio +async def test_latest_strips_global_and_http_client_write_conditions_without_mutation(async_): + calls = [] + defaults = httpx.Headers({"If-Match": "http-default", "If-None-Match": "*", "Idempotency-Key": "http-key", "X-HTTP-Context": "kept"}) + def handler(request): + calls.append(request) + if request.method == "PATCH": + return httpx.Response(412, json={"reason": "PreconditionFailed"}) + assert all(name not in request.headers for name in ("If-Match", "If-None-Match", "Idempotency-Key")) + assert request.headers["X-HTTP-Context"] == "kept" + assert request.headers["X-SDK-Context"] == "kept" + assert request.headers["X-V3-Context"] == "kept" + return httpx.Response(200, json={"agent_id": "a", "agent_name": "Latest"}) + http = (httpx.AsyncClient if async_ else httpx.Client)(headers=defaults, transport=httpx.MockTransport(handler)) + client = (AsyncAgora if async_ else Agora)(area=Area.US, app_id="p", auth_token="token", httpx_client=http, headers={"If-Match": "sdk-default", "X-SDK-Context": "kept"}, v3=V3Options(headers={"If-None-Match": "v3-default", "X-V3-Context": "kept"})) + with pytest.raises(PreconditionFailedError) as caught: + result = client.v3.agents.update(agent_id="a", if_match="explicit", instructions="next") + if async_: + await result + latest = await caught.value.latest() if async_ else caught.value.latest() + assert latest["agent_name"] == "Latest" + assert len(calls) == 2 + assert http.headers["If-Match"] == "http-default" + assert client._client_wrapper.get_custom_headers()["If-Match"] == "sdk-default" + assert client._v3_options.headers["If-None-Match"] == "v3-default" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("payload", ["plain conflict", ["unexpected", "array"]]) +@pytest.mark.asyncio +async def test_stop_preserves_nonobject_409_api_error(async_, payload): + from agora_agent.core.api_error import ApiError + def handler(request): + if request.url.path.endswith("/stop"): + return httpx.Response(409, text=payload) if isinstance(payload, str) else httpx.Response(409, json=payload) + return httpx.Response(200, json={"agent_session_id": "s", "status": "RUNNING", "create_ts": 1}) + agent = configured(make_client(handler, async_)) + session = (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"]) + if async_: + await session.start() + with pytest.raises(ApiError) as caught: + await session.stop() + else: + session.start() + with pytest.raises(ApiError) as caught: + session.stop() + assert caught.value.status_code == 409 + assert caught.value.body == payload + assert session.status == "error" + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("agent_uid", ["0", "1"]) +@pytest.mark.parametrize("subscriber_uid", ["0", "00", "000"]) +@pytest.mark.asyncio +async def test_subscriber_zero_rejected_even_when_agent_zero_is_allowed(async_, agent_uid, subscriber_uid): + client = make_client(lambda _: pytest.fail("must not send"), async_) + with pytest.raises(ValueError, match="subscribe_uids"): + result = client.v3.sessions.start(agent_id="a", transport={"type": "rtc", "channel": "r", "uid": agent_uid, "subscribe_uids": [subscriber_uid]}) + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("key", ["", " ", "\t"]) +@pytest.mark.parametrize("resource,method,kwargs", [ + ("sessions", "start", {"agent_id": "a", "transport": {"type": "rtc", "channel": "r", "uid": "1", "subscribe_uids": ["2"]}}), + ("sessions", "start_ephemeral", {"agent": {"instructions": "Help"}, "transport": {"type": "rtc", "channel": "r", "uid": "1", "subscribe_uids": ["2"]}}), + ("campaigns", "create", {"name": "Campaign", "agent_id": "a", "transport": {"type": "telephony", "from": ["n"]}, "contacts": [{"phone": "+12025550123"}], "schedule": {"mode": "immediate"}}), + ("campaigns", "pause", {"campaign_id": "c"}), + ("campaigns", "resume", {"campaign_id": "c"}), + ("campaigns", "cancel", {"campaign_id": "c"}), +]) +@pytest.mark.asyncio +async def test_explicit_blank_idempotency_key_rejected_before_http(async_, key, resource, method, kwargs): + client = make_client(lambda _: pytest.fail("must not send"), async_) + with pytest.raises(ValueError, match="Idempotency-Key"): + result = getattr(getattr(client.v3, resource), method)(**kwargs, idempotency_key=key) + if async_: + await result + + +@pytest.mark.parametrize("async_", [False, True]) +@pytest.mark.parametrize("key", ["", " ", "\t"]) +def test_agentkit_blank_idempotency_key_rejected_on_local_creation(async_, key): + agent = configured(make_client(lambda _: pytest.fail("must not send"), async_)) + with pytest.raises(ValueError, match="Idempotency-Key"): + (agent.create_async_session if async_ else agent.create_session)(channel="r", agent_uid="1", remote_uids=["2"], idempotency_key=key)