From eb6337f3981053d7c5c45ec46bc8ee06160ab243 Mon Sep 17 00:00:00 2001 From: digitallysavvy Date: Fri, 21 Aug 2026 13:33:54 -0400 Subject: [PATCH 01/57] feat(agentkit): add preview endpoint support and the Gemini preview MLLMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preview providers are not served by the production Conversational AI gateway. They live behind a single partner host and are gated by an `agora-feature` header, so reaching them needs a different base URL and a pinned request header — neither of which belongs in the generated core client while the providers are unannounced. AgoraPreview and AsyncAgoraPreview are drop-in replacements for Agora and AsyncAgora that supply both. Everything preview-only lives under `src/agora_agent/agentkit/preview/` so it can be deleted wholesale when these providers reach the production gateway, and preview symbols stay in that subpackage rather than being re-exported from `agentkit/__init__.py`. - The gate header is merged after caller-supplied headers, so a custom header map cannot drop or blank it. A request that loses the gate is not rejected; it routes to production, where these providers do not exist. - GeminiWalkieTalkie and GeminiCleverChatter, matching the preview gateway's wire contract: `params.api_key` repeated inside `params`, and `url` defaulting to the Gemini Developer API host rather than a WebSocket endpoint. `thinking_level` is exclusive to clever-chatter, so passing it to the other is a validation error rather than a silently ignored argument. - `language_codes` serializes to the plural `params.language_codes`; these models never send the singular `params.language`, which belongs to xAI Grok in the Agora schema. - apply_preview_shape retargets MLLM fields the shared Agent builder wrote with production spellings: `greeting_message` becomes `greeting`, the spelling these models actually read. Keeping the translation here rather than teaching the builder about preview spellings means it disappears with the preview package at GA. A config is recognised by model name or by its wire envelope, so an unlisted model name does not silently skip the rename. - The vendors reject `api_key=""`, which pydantic's required-field check alone would accept and send as a blank credential. - assert_preview_support raises before any request when preview providers are paired with a plain Agora client, reading the resolved request body so hand-written configs are covered too. - Preview configs bypass request validation: the generated request models mirror what production serves, so a preview config fails validation. Configs that required_preview_features() recognises are passed through unvalidated, with None entries stripped so the bypass puts the same bytes on the wire as the typed path. Production configs are unaffected. - redact_secrets keeps vendor API keys, RTC tokens and the App ID out of `debug=True` output and out of the httpx request log, which previously echoed raw request bodies. Empty strings stay visible so an unset environment variable remains diagnosable. - generate_rtc_token and generate_convo_ai_token raise ValueError on a malformed app_id or app_certificate. The signing path HMACs whatever it is given, so a truncated paste produced a well-formed token the gateway rejected with an error naming neither field; the message now names the field whose length is wrong without echoing either value. Documented in docs/guides/preview-endpoint.md, including the gateway's intake-node failure signatures, the validation bypass, and how to add a future preview family. Co-Authored-By: Claude Opus 5 --- changelog.md | 30 ++ docs/concepts/vendors.md | 2 + docs/guides/preview-endpoint.md | 262 ++++++++++ docs/index.md | 1 + src/agora_agent/agentkit/agent.py | 34 +- src/agora_agent/agentkit/agent_session.py | 49 +- src/agora_agent/agentkit/debug.py | 82 ++++ src/agora_agent/agentkit/preview/__init__.py | 45 ++ src/agora_agent/agentkit/preview/client.py | 312 ++++++++++++ src/agora_agent/agentkit/preview/vendors.py | 198 ++++++++ src/agora_agent/agentkit/token.py | 31 ++ src/agora_agent/pool_client.py | 19 +- tests/custom/test_preview.py | 475 +++++++++++++++++++ tests/custom/test_token_credentials.py | 64 +++ 14 files changed, 1584 insertions(+), 20 deletions(-) create mode 100644 docs/guides/preview-endpoint.md create mode 100644 src/agora_agent/agentkit/debug.py create mode 100644 src/agora_agent/agentkit/preview/__init__.py create mode 100644 src/agora_agent/agentkit/preview/client.py create mode 100644 src/agora_agent/agentkit/preview/vendors.py create mode 100644 tests/custom/test_preview.py create mode 100644 tests/custom/test_token_credentials.py diff --git a/changelog.md b/changelog.md index f5dda63..0c90305 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## [Unreleased] + +### Added + +- **Preview endpoint support** — `AgoraPreview` and `AsyncAgoraPreview` route every request to the preview gateway and pin the `agora-feature` gate header, which is merged after caller-supplied `headers` so it cannot be dropped or blanked. +- **Gemini preview MLLMs** — `GeminiWalkieTalkie` (`walkie-talkie`) and `GeminiCleverChatter` (`clever-chatter`), under `agora_agent.agentkit.preview`. `thinking_level` is exclusive to clever-chatter, so passing it to the walkie-talkie is a validation error rather than a silently ignored argument. +- **Preview MLLM rename constants** — `GeminiPreviewMllmModels.LIVE_35` (`gemini-3.5-live`) and `.LIVE_35_THINKING` (`gemini-3.5-live-thinking`) are the post-rename IDs for walkie-talkie and clever-chatter. Both spellings are accepted by `required_preview_features`, so the guard works throughout Google's rollout. The vendor classes still send the codenames — see `GEMINI_PREVIEW_MLLM_DEFAULTS`. +- **Preview MLLM language** — `language_codes` serializes to `params.language_codes`, omitted when unset. The preview MLLMs never send the singular `params.language`: that field belongs to xAI Grok in the Agora schema, and the production Gemini Live provider sends no language field at all. +- **Preview MLLM greeting** — the argument is `greeting` (not `greeting_message`) and serializes to `greeting`, the spelling the new Gemini preview models read. An agent-level `greeting` also works — the builder writes it as `mllm.greeting_message` and `apply_preview_shape` retargets it. +- **Preview wire-shape translation** — `apply_preview_shape` runs at session start and retargets MLLM fields the shared builder wrote with production spellings, so an agent-level `greeting` reaches the preview models as `greeting` rather than the `greeting_message` they ignore. A hand-written `greeting_message` migrates too, so an existing config keeps working after only swapping the model. `failure_message` is left alone as an Agora engine field, and production MLLMs are untouched. +- **Preview support guard** — `assert_preview_support` raises before any request when preview providers are paired with a plain `Agora` client. +- **Debug redaction** — `redact_secrets` replaces vendor API keys, RTC tokens, and the App ID with `[REDACTED]`. Empty strings stay visible so an unset environment variable remains diagnosable. + +### Fixed + +- **Debug logging leaked credentials** — the httpx debug hook logged raw request bodies, so vendor API keys and RTC tokens reached the log unredacted. Bodies are now JSON-decoded and redacted; a non-JSON body is described by size rather than echoed. +- **Malformed credentials are rejected before signing** — the Python signing path HMACs whatever it is given, so an `app_id` or `app_certificate` with a trailing newline or a truncated paste produced a well-formed token that the gateway rejected with an error naming neither field. `generate_rtc_token` and `generate_convo_ai_token` now raise `ValueError` and say which field is the wrong length. Credential values are never included in the message. +- **An unrecognised MLLM model no longer silently skips the wire-shape translation** — preview detection matched only the four known model names, so overriding `model` to a rename this SDK had not shipped yet turned off `apply_preview_shape` with no error: the greeting stayed in `greeting_message`, which these models ignore, and the agent never greeted. A config is now also recognised by its envelope (`params.api_key` set and `url` on the Gemini Developer API host), which the production `GeminiLive` vendor does not emit. +- **Preview vendors reject an empty API key** — the preview MLLM classes previously accepted `api_key=""` and sent it as a blank credential. They now fail at construction, matching the TypeScript and Go vendors. + +### Changed + +- **Preview configs bypass request validation** — the generated request models mirror what production serves, so a preview MLLM config fails validation. Configs that `required_preview_features()` recognises are now passed through unvalidated, with `None` entries stripped so the bypass puts the same bytes on the wire as the typed path. Production configs are unaffected. + +### Documentation + +- Added [Preview Endpoint](docs/guides/preview-endpoint.md), covering the gate header, intake-node failure signatures, the validation bypass, and how to add a future preview family. +- Documented every field `Agent.to_properties` injects into a vendor config, and why a preview provider must be verified against the resolved request body rather than the vendor class output — a value written under a spelling the route ignores fails silently. +- Documented the two signals that mark a config as preview, and why matching on the model name alone was a silent failure. + ## [v2.4.0] — 2026-06-30 ### Added diff --git a/docs/concepts/vendors.md b/docs/concepts/vendors.md index 34e2970..1da1a8e 100644 --- a/docs/concepts/vendors.md +++ b/docs/concepts/vendors.md @@ -145,6 +145,8 @@ from agora_agent import DeepgramSTT stt = DeepgramSTT(api_key='your-deepgram-key', language='en-US', model='nova-2') ``` +> **Preview providers** — `GeminiWalkieTalkie` and `GeminiCleverChatter` (MLLM) live in `agora_agent.agentkit.preview`, are served only by the preview gateway, and require `AgoraPreview`. See [Preview Endpoint](../guides/preview-endpoint.md). + ## MLLM Vendors Used with `agent.with_mllm()` for the [MLLM flow](../guides/mllm-flow.md). These handle audio input and output end-to-end. diff --git a/docs/guides/preview-endpoint.md b/docs/guides/preview-endpoint.md new file mode 100644 index 0000000..16203ef --- /dev/null +++ b/docs/guides/preview-endpoint.md @@ -0,0 +1,262 @@ +--- +sidebar_position: 10 +title: Preview Endpoint +description: How AgoraPreview routes to preview providers, and how the gateway's agora-feature gate header behaves. +--- + +# Preview Endpoint + +Some providers ship on a preview gateway before they reach the production Conversational AI environment. `AgoraPreview` (and `AsyncAgoraPreview`) are drop-in replacements for `Agora` / `AsyncAgora` that point every request at that gateway and attach the header it routes on. + +Everything in `agentkit/preview/` is temporary by design. When these providers go GA, the package is deleted and the vendor classes move into `vendors/mllm.py`. + +## Using the preview client + +```python +import os + +from agora_agent import Area +from agora_agent.agentkit import Agent +from agora_agent.agentkit.preview import AgoraPreview, GeminiCleverChatter + +client = AgoraPreview( + area=Area.US, + app_id=os.environ["AGORA_APP_ID"], + app_certificate=os.environ["AGORA_APP_CERTIFICATE"], +) + +session = ( + Agent(client=client) + .with_mllm(GeminiCleverChatter(api_key=os.environ["GOOGLE_API_KEY"], thinking_level="medium")) + .create_session(channel="demo", agent_uid="1", remote_uids=["100"]) +) +agent_id = session.start() +``` + +`AsyncAgoraPreview` is the same, with `await session.start()`. + +Auth, the `Agent` builder, `AgentSession`, and every vendor class work exactly as they do with `Agora`. Only two things differ: where requests go, and the gate header. + +## The gate header + +The gateway routes preview traffic on a single request header: + +``` +agora-feature: gemini-live +``` + +| Constant | Value | +|---|---| +| `PREVIEW_FEATURE_HEADER` | `agora-feature` | +| `PreviewFeatures.GEMINI_LIVE` | `gemini-live` | + +Select the families a client requests with `features`: + +```python +client = AgoraPreview(area=Area.US, app_id=..., app_certificate=..., features=["gemini-live"]) +``` + +### The header is not overridable + +The gate header is merged **after** caller-supplied `headers`, so custom headers cannot drop or blank it: + +```python +client = AgoraPreview( + area=Area.US, + app_id=..., + app_certificate=..., + headers={"agora-feature": "", "x-custom": "kept"}, +) +# Requests still send agora-feature: gemini-live, and x-custom: kept +``` + +This ordering is deliberate and load-bearing. A preview request that loses the header is not rejected — it routes to the production environment, where the preview providers do not exist. Use `features` to change the value; there is no supported way to remove it. + +The header rides **every** verb, not just `start()` — `say`, `interrupt`, `get_history`, `stop`, `client.agents.*`, and `client.stop_agent()`. `stop_agent()` matters in particular, because services typically build a fresh client in an end-call route rather than holding the session that started the agent. + +## Preview providers bypass request validation + +The generated request models mirror what production serves, so `asr.vendor = "gemini"` is not a member of the generated `Asr` union and pydantic rejects it. + +`_start_properties_from_mapping` catches that and, when `required_preview_features()` recognises the config, passes the mapping through unvalidated instead of raising. Production configs still get full validation — only configs the preview gateway understands take the bypass. + +This is worth knowing when adding a preview provider: if a new vendor is missing from the generated unions, register it in the detection sets in `preview/client.py` rather than editing generated code. + +## Intake node behavior + +The gateway decides where a request goes before it validates the body. That produces failure modes that look like outages but are routing problems. + +| Symptom | What it means | +|---|---| +| `503` `{"reason":"ServiceUnavailable"}` on `POST .../join` | The gate header was not recognised. The request died before validation, so the body is irrelevant. | +| `401` `Missing authorization header` | Routing worked; auth did not. | +| `404` `no Route matched with those values` | The base URL path is wrong. | +| `400` validation error | You are past the gate. The header is fine and the body is the problem. | + +The 503 is the one that misleads. It reads as a partner-side outage and invites waiting it out, when the fix is usually a one-line header change. + +Observed on the `gemini-live` rollout in August 2026, when the gateway had not yet been configured to route on `agora-feature` and every request fell through to a 503. That was fixed server-side on 2026-08-09, so the 503 is not currently reproducible — the mapping is recorded here because it is the failure signature a newly provisioned preview family is most likely to hit first. + +### Diagnosing without starting a billable agent + +Two probes, neither of which allocates an agent: + +1. **`GET .../v2/projects/{appId}/agents`** — a `200` proves host, auth, and routing are all healthy. If this succeeds while `join` fails, the problem is specific to the start path. +2. **A deliberately invalid start body** — send properties with no `llm` and no `mllm` at all. A `400` means you are past the gate; a `503` means you are not. This is what separates "my config is wrong" from "my header is wrong". + +### Known gap (as of 2026-08-09) + +A missing gate header is *intended* to route to the production environment, where a preview config would fail. In practice an ungated start currently **succeeds** against the preview host, so the fallback is not observable from the client side. + +The SDK cannot control the intake node, and this does not affect SDK users because the header is pinned. It matters only for callers hitting the REST API directly, who may get a request that appears to succeed while silently landing in the wrong environment. Flag it to the endpoint owner rather than working around it in SDK code. + +## Guard against the wrong client + +Preview providers used with a plain `Agora` client raise before any request is sent: + +``` +ValueError: This agent uses preview providers that require the preview endpoint +(gemini-live). Create the session with AgoraPreview instead of Agora, and make +sure its `features` include: gemini-live. +``` + +`assert_preview_support` reads the resolved request body rather than the vendor classes, so hand-written configs are covered too. `mllm.vendor` is `"gemini"` for both the production Gemini Live provider and the preview ones, so the vendor name alone settles nothing — see [How a config is recognised as preview](#how-a-config-is-recognised-as-preview) for the two signals it uses instead. + +## Preview vendors + +| Class | Wire vendor | Model | +|---|---|---| +| `GeminiWalkieTalkie` | `mllm.vendor = "gemini"` | `walkie-talkie` | +| `GeminiCleverChatter` | `mllm.vendor = "gemini"` | `clever-chatter` | + +MLLM sessions have no ASR, LLM, or TTS stage — the model handles the whole exchange end to end, so `transcribe_agent` and `transcribe_user` are the only source of transcripts. + +`thinking_level` is exclusive to clever-chatter, which is why it is a separate class rather than a field on a shared one — `GeminiWalkieTalkie(thinking_level=...)` is a validation error, not a silently ignored argument. + +The preview MLLMs differ from the production `GeminiLive` in two ways the gateway requires: `params.api_key` is repeated inside `params`, and `url` defaults to the Gemini Developer API host rather than a WebSocket endpoint. + +Language is set with `language_codes`, which serializes to `params.language_codes` and is omitted when unset. Note the plural: the singular `mllm.params.language` belongs to xAI Grok in the Agora schema, and the [production Gemini Live provider](https://docs.agora.io/en/ai/models/mllm/gemini) sends no language field at all — so the preview MLLMs never send it. + +In MLLM mode, agent-level `instructions` is not sent — pass `instructions` to the vendor instead, where it serializes to `mllm.params.instructions`. + +The greeting argument is `greeting`, not `greeting_message`, and it serializes to `greeting`. The new Gemini preview models read that spelling; `greeting_message` is what the production providers use. + +Both placements work — an agent-level `greeting` and a vendor-level one both reach the model: + +```python +Agent(client=client, greeting='Hello').with_mllm(GeminiWalkieTalkie(api_key=key)) +Agent(client=client).with_mllm(GeminiWalkieTalkie(api_key=key, greeting='Hello')) +``` + +That is not automatic: `Agent.to_properties` writes an agent-level greeting to `mllm.greeting_message`, the production spelling, which these models ignore. `apply_preview_shape` retargets it at session start — see [Preview wire-shape translation](#preview-wire-shape-translation). + +### The MLLM rename in flight + +Google is renaming the MLLM codenames to conventional model IDs: + +| Class | Model sent today | Post-rename model | +|---|---|---| +| `GeminiWalkieTalkie` | `walkie-talkie` | `gemini-3.5-live` | +| `GeminiCleverChatter` | `clever-chatter` | `gemini-3.5-live-thinking` | + +The classes still send the codenames. As of 2026-08-19 the new IDs are propagating through Google's fleet and are not live everywhere, while the codenames work — Google will confirm when the rename lands. Both spellings are in the known-model set, and a name that is in neither is still recognised by its envelope (see [How a config is recognised as preview](#how-a-config-is-recognised-as-preview)), so `model=` opts a single instance into a new name safely: + +```python +GeminiWalkieTalkie(api_key=..., model=GeminiPreviewMllmModels.LIVE_35) +``` + +When the rename lands, repoint `GEMINI_PREVIEW_MLLM_DEFAULTS` in `preview/vendors.py` at `LIVE_35` / `LIVE_35_THINKING`. That is the entire cutover. + +## Preview wire-shape translation + +The preview route spells two MLLM fields differently from the Agora schema. Where the vendor classes own the field they simply emit the preview spelling, but `greeting` is different: the shared `Agent` builder writes it, using the production spelling that is correct for every GA vendor. + +So `apply_preview_shape` runs at session start, immediately after `assert_preview_support`, and retargets those fields on a preview config: + +| Written by the builder | Sent to the preview route | +|---|---| +| `mllm.greeting_message` | `mllm.greeting` | + +The vendor's own value wins; the production-spelled one is the fallback. That also migrates a hand-written `greeting_message` onto the preview key, so an existing MLLM config keeps working after only swapping the model. + +`mllm.failure_message` is deliberately **not** translated — it is an Agora engine feature rather than a Gemini one, so it keeps its schema spelling. + +A production MLLM is left untouched, so the translation is safe to run unconditionally. Teaching the shared builder about preview spellings was the alternative; keeping it here means the whole thing disappears with `agentkit/preview/` at GA instead of leaving a vestigial hook behind. + +### How a config is recognised as preview + +Two independent signals, either one sufficient: + +1. **``mllm.params.model`` is in the known-model set** — the four names in ``preview/client.py`` (both codenames and both post-rename IDs). +2. **The config carries the preview envelope** — ``params.api_key`` set *and* ``url`` on the Gemini Developer API host. This is exactly what the preview vendor classes emit and what the production ``GeminiLive`` vendor does not: it leaves ``params.api_key`` unset and defaults ``url`` to ``""`` or a WebSocket endpoint. + +The second signal exists because the first one fails silently on its own. Overriding ``model`` is the documented escape hatch for a rename that lands ahead of an SDK release, so a name this SDK has never seen is reachable by following the advice above — and with model-name matching alone, that config would quietly stop getting ``apply_preview_shape``. No error, no warning: the greeting would land in ``greeting_message``, which these models ignore, and the agent would simply never greet. + +Adding a newly announced model name to the set is still worth doing — it keeps recognition working for hand-written configs, which carry no envelope. But forgetting to is no longer a silent failure. + +### The vendor class is not the whole wire shape + +This translation exists because of a bug worth not repeating. `GeminiWalkieTalkie` emitted `greeting` correctly and nothing in `preview/vendors.py` was wrong — but an agent-level greeting still never reached the model, because `Agent.to_properties` **also writes into the vendor config**, using production spellings, after the vendor is done with it. + +Every field the shared builder injects is listed below. Each one is a candidate for the same bug on a preview route: + +| Category | Field | Written from | When | +|---|---|---|---| +| `mllm` | `greeting_message` | agent-level `greeting` | only when the vendor left it unset (`setdefault`) | +| `mllm` | `failure_message` | agent-level `failure_message` | only when the vendor left it unset (`setdefault`) | +| `mllm` | `enable` | `with_mllm()` | always | +| `asr` | `language` | turn detection `language` | **always — overwrites any vendor value** | +| `llm` | `system_messages`, `greeting_message`, `greeting_configs`, `failure_message`, `max_history` | agent-level `instructions`, `greeting`, `greeting_configs`, `failure_message`, `max_history` | only when the vendor left them unset | + +So when a preview provider documents a field that appears in that table, putting it on the vendor class is not the whole fix. One of three things applies: + +- **The preview route spells it differently** — add an entry to `_PREVIEW_MLLM_FIELD_RENAMES` in `preview/client.py`. One entry, and both the vendor-level and agent-level placements start working. +- **The builder always overwrites it** (`asr.language`) — do not expose it on the vendor class at all; it would be an argument the builder silently discards, and `extra="forbid"` turns a stale keyword into an error rather than a no-op. +- **It is an Agora engine field rather than the provider's** (`failure_message`) — leave it in the schema spelling. + +### Verify against the request body, not the vendor output + +`to_config()` returning the right dict proves nothing about what ships, because the builder runs after it. Both checks are needed: + +1. A unit test on the vendor class, for the fields the vendor owns. +2. An **end-to-end test that starts a session against a mock transport and asserts on the captured request body** — the only check that sees the builder's injections and the preview translation. Every preview vendor has one in `tests/custom/test_preview.py`. + +The manual version is `debug=True`, which logs the fully resolved body. Diff it against the payload the provider documented, key by key. A value sitting under a name the route ignores fails **silently** — no error, no validation complaint, the agent simply never greets. That is the failure mode this whole section exists to catch, and it is invisible to type checking, to pydantic validation, and to any test that stops at the vendor class. + +Wire parity across the three SDKs is a hard requirement, so a change here lands in Python, TypeScript, and Go together, verified by diffing the serialized bodies. + +## Adding a future preview family + +Everything preview-only lives under `agentkit/preview/` so it can be deleted wholesale at GA. To add a family: + +1. Add an attribute to `PreviewFeatures` in `preview/client.py`. The value is what goes in the `agora-feature` header. +2. Add the vendor classes to `preview/vendors.py`, extending the same `BaseSTT` / `BaseMLLM` / `BaseLLM` bases as production vendors so the builder accepts them unchanged. +3. Register the detection keys — for an MLLM family, the model names in `_PREVIEW_MLLM_MODELS` plus an envelope check — so `required_preview_features()` recognises configs that need the new family. This is also what lets the config through the validation bypass described above. +4. Export from `preview/__init__.py`. Preview symbols stay in that subpackage rather than being re-exported from `agentkit/__init__.py`, which is what makes the GA deletion a single-directory change. +5. **Diff the resolved request body against the payload the provider documented**, not the vendor class output. Anything the shared builder writes under a different name belongs in `_PREVIEW_MLLM_FIELD_RENAMES` — see [The vendor class is not the whole wire shape](#the-vendor-class-is-not-the-whole-wire-shape). +6. Add an end-to-end test that starts a session and asserts on the captured body, alongside the vendor-class unit test. + +If the generated request models do not cover the new provider, rely on the bypass in `_start_properties_from_mapping` rather than editing generated code. Generated files are overwritten on the next Fern run; `.fernignore` protects `src/agora_agent/agentkit/`. + +At GA, delete `preview/` and move the vendor classes into `vendors/mllm.py`. + +## Base URL + +``` +https://partner.ai.agora.io/preview/api/conversational-ai-agent +``` + +Request paths append to it exactly as they do in production — `POST {base}/v2/projects/{appId}/join`. The service path segment is part of the base: `https://partner.ai.agora.io/preview/api/` alone returns `404 no Route matched with those values`. + +The preview host is a single partner endpoint with no regional replicas, so `next_region()` and `select_best_domain()` are no-ops and `area` affects only token generation. Override the host for a staging gateway with `base_url`. + +## Debug output + +`debug=True` prints the resolved start request. The body is passed through `redact_secrets` first, which replaces vendor API keys, the RTC token, and the App ID with `[REDACTED]` while leaving model names, voices, and instructions readable. The same redaction is applied to the httpx request log. + +Empty strings are left visible on purpose: `""` is the signature of an unset environment variable, and hiding it would disguise the exact misconfiguration the debug output exists to surface. + +## Related + +- [Regional Routing](./regional-routing.md) — the production domain pool the preview client bypasses +- [Error Handling](./error-handling.md) — `ApiError` and API error handling diff --git a/docs/index.md b/docs/index.md index 11f3196..f4a7da1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,6 +53,7 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on | [Pagination](./guides/pagination.md) | Iterate over paginated list endpoints | | [Advanced](./guides/advanced.md) | Raw response, retries, timeouts, custom httpx client | | [Low-Level API](./guides/low-level-api.md) | Generated REST APIs | +| [Preview Endpoint](./guides/preview-endpoint.md) | `AgoraPreview`, the `agora-feature` gate header, and preview routing | | [Client Reference](./reference/client.md) | Full `Agora` / `AsyncAgora` API | | [Agent Reference](./reference/agent.md) | Full `Agent` builder API | | [Session Reference](./reference/session.md) | Full `AgentSession` / `AsyncAgentSession` API | diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index e0080fd..6366613 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -195,10 +195,34 @@ class SessionOptions(typing_extensions.TypedDict, total=False): warn: typing.Callable[[str], None] + +def _drop_none(value: typing.Any) -> typing.Any: + """Recursively remove None-valued mapping entries.""" + if isinstance(value, dict): + return {k: _drop_none(v) for k, v in value.items() if v is not None} + if isinstance(value, list): + return [_drop_none(item) for item in value] + return value + + def _start_properties_from_mapping( properties: typing.Mapping[str, typing.Any], ) -> StartAgentsRequestProperties: - return parse_obj_as(StartAgentsRequestProperties, dict(properties)) + try: + return parse_obj_as(StartAgentsRequestProperties, dict(properties)) + except Exception: + # Preview providers are absent from the generated unions by design — the + # schema models what production serves. A config the preview gateway + # does understand is passed through unvalidated rather than rejected. + # Imported lazily: preview.client imports the pool client, which imports + # this module. + from .preview.client import required_preview_features + + if required_preview_features(properties): + # The typed path serializes with exclude_none; strip None here so the + # bypass puts the same bytes on the wire instead of explicit nulls. + return typing.cast(StartAgentsRequestProperties, _drop_none(dict(properties))) + raise # LLM sub-type aliases @@ -1012,6 +1036,10 @@ def to_properties( if is_mllm_mode: if self._mllm is not None: mllm_config = dict(self._mllm) + # These are production wire spellings. A route that spells one of them + # differently needs a rename entry in `preview/client.py`, or the value + # lands in a field the provider ignores and fails silently. See + # docs/guides/preview-endpoint.md#the-vendor-class-is-not-the-whole-wire-shape. if self._greeting is not None: mllm_config.setdefault("greeting_message", self._greeting) if self._failure_message is not None: @@ -1080,6 +1108,10 @@ def _resolve_asr_config(self, turn_detection_config: TurnDetectionInput) -> typi if not asr_config: area_scope = getattr(self._client, "area_scope", None) asr_config["vendor"] = "fengming" if area_scope == "cn" else "ares" + # Unconditional: turn detection is the single source of truth for the + # interaction language, so a vendor-level ``language`` would be silently + # discarded here. Do not add one to a vendor class — see + # docs/guides/preview-endpoint.md#the-vendor-class-is-not-the-whole-wire-shape. asr_config["language"] = self._field_value(turn_detection_config, "language") return asr_config diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index e2106a8..6e923ac 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -15,6 +15,8 @@ AgentThinkAgentManagementResponse as AgentThinkResponse, ) from ..agents.types.get_turns_agents_response import GetTurnsAgentsResponse +from .debug import redact_secrets +from .preview.client import apply_preview_shape, assert_preview_support from .agent import ( Agent, GetTurnsOptions, @@ -29,7 +31,6 @@ is_generic_avatar, is_heygen_avatar, is_live_avatar_avatar, - is_rtc_avatar, is_sensetime_avatar, is_spatius_avatar, validate_avatar_config, @@ -604,17 +605,26 @@ def start(self) -> str: properties, ) + assert_preview_support(self._client, resolved_properties) + # Retarget MLLM fields the shared builder wrote with production + # spellings, so a preview config follows the preview route's structure. + apply_preview_shape(resolved_properties) + if self._debug: print("[Agora Debug] Starting agent session...") + if hasattr(self._client, "get_current_url"): + print("[Agora Debug] API Endpoint:", self._client.get_current_url()) print( "[Agora Debug] Request:", - { - "appid": self._app_id, - "name": self._name, - "preset": resolved_preset, - "pipeline_id": pipeline_id, - "properties": resolved_properties, - }, + redact_secrets( + { + "appid": self._app_id, + "name": self._name, + "preset": resolved_preset, + "pipeline_id": pipeline_id, + "properties": resolved_properties, + } + ), ) request_properties = self._request_properties_for_start( @@ -965,17 +975,26 @@ async def start(self) -> str: properties, ) + assert_preview_support(self._client, resolved_properties) + # Retarget MLLM fields the shared builder wrote with production + # spellings, so a preview config follows the preview route's structure. + apply_preview_shape(resolved_properties) + if self._debug: print("[Agora Debug] Starting agent session...") + if hasattr(self._client, "get_current_url"): + print("[Agora Debug] API Endpoint:", self._client.get_current_url()) print( "[Agora Debug] Request:", - { - "appid": self._app_id, - "name": self._name, - "preset": resolved_preset, - "pipeline_id": pipeline_id, - "properties": resolved_properties, - }, + redact_secrets( + { + "appid": self._app_id, + "name": self._name, + "preset": resolved_preset, + "pipeline_id": pipeline_id, + "properties": resolved_properties, + } + ), ) request_properties = self._request_properties_for_start( diff --git a/src/agora_agent/agentkit/debug.py b/src/agora_agent/agentkit/debug.py new file mode 100644 index 0000000..b6cea9a --- /dev/null +++ b/src/agora_agent/agentkit/debug.py @@ -0,0 +1,82 @@ +"""Redaction for ``debug=True`` session logging. + +The debug dump is routinely pasted into issues and chat threads, so it must +never carry a usable credential. Header redaction already exists in +``pool_client``; nothing redacted the request *body*, which is where vendor API +keys and RTC tokens live. +""" + +from __future__ import annotations + +import typing + +#: Marker substituted for a redacted value. +REDACTED = "[REDACTED]" + +#: Body fields whose values are credentials or account identifiers. +#: +#: Compared case-insensitively, and matched on both snake_case and camelCase +#: spellings so this keeps working if a caller hand-builds a config. +_SENSITIVE_BODY_KEYS = frozenset( + { + # Vendor credentials + "api_key", + "apikey", + "key", + "secret", + "api_secret", + "apisecret", + "password", + "access_key_id", + "accesskeyid", + "secret_access_key", + "secretaccesskey", + "adc_credentials_string", + "adccredentialsstring", + "subscription_key", + "subscriptionkey", + # Agora credentials and account identifiers + "token", + "agora_token", + "agoratoken", + "authorization", + "appid", + "app_id", + "agora_appid", + "agoraappid", + "app_certificate", + "appcertificate", + "customer_secret", + "customersecret", + } +) + + +def _is_sensitive_key(key: str) -> bool: + return key.lower() in _SENSITIVE_BODY_KEYS + + +def redact_secrets(value: typing.Any) -> typing.Any: + """Deep-copy ``value``, replacing credential fields with :data:`REDACTED`. + + Empty strings are left visible on purpose: ``""`` is the signature of an + unset environment variable, and hiding it behind ``[REDACTED]`` would + disguise the exact misconfiguration the debug output exists to surface. + + Never mutates the input — the request that goes on the wire is untouched. + """ + if isinstance(value, dict): + result: typing.Dict[typing.Any, typing.Any] = {} + for key, item in value.items(): + if isinstance(key, str) and _is_sensitive_key(key) and isinstance(item, str) and item: + result[key] = REDACTED + else: + result[key] = redact_secrets(item) + return result + if isinstance(value, (list, tuple)): + redacted = [redact_secrets(item) for item in value] + return type(value)(redacted) if isinstance(value, tuple) else redacted + return value + + +__all__ = ["REDACTED", "redact_secrets"] diff --git a/src/agora_agent/agentkit/preview/__init__.py b/src/agora_agent/agentkit/preview/__init__.py new file mode 100644 index 0000000..16d5300 --- /dev/null +++ b/src/agora_agent/agentkit/preview/__init__.py @@ -0,0 +1,45 @@ +"""Preview endpoint support. + +Temporary package: delete it when these providers ship on the production +gateway. See ``client.py`` for the routing and gate header. +""" + +from .client import ( + PREVIEW_API_BASE_URL, + PREVIEW_FEATURE_HEADER, + AgoraPreview, + AsyncAgoraPreview, + PreviewFeature, + PreviewFeatures, + apply_preview_shape, + assert_preview_support, + required_preview_features, +) +from .vendors import ( + GEMINI_PREVIEW_MLLM_DEFAULTS, + GEMINI_PREVIEW_MLLM_URL, + GEMINI_THINKING_LEVELS, + GeminiCleverChatter, + GeminiPreviewMllmModels, + GeminiThinkingLevel, + GeminiWalkieTalkie, +) + +__all__ = [ + "GEMINI_PREVIEW_MLLM_DEFAULTS", + "GEMINI_PREVIEW_MLLM_URL", + "GEMINI_THINKING_LEVELS", + "PREVIEW_API_BASE_URL", + "PREVIEW_FEATURE_HEADER", + "AgoraPreview", + "AsyncAgoraPreview", + "GeminiCleverChatter", + "GeminiPreviewMllmModels", + "GeminiThinkingLevel", + "GeminiWalkieTalkie", + "PreviewFeature", + "PreviewFeatures", + "apply_preview_shape", + "assert_preview_support", + "required_preview_features", +] diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py new file mode 100644 index 0000000..b201008 --- /dev/null +++ b/src/agora_agent/agentkit/preview/client.py @@ -0,0 +1,312 @@ +"""Preview endpoint clients. + +Preview providers are not served by the production Conversational AI gateway. +They live behind a single partner host and are gated by an ``agora-feature`` +header. :class:`AgoraPreview` and :class:`AsyncAgoraPreview` are drop-in +replacements for ``Agora`` / ``AsyncAgora`` that point every request at that +host and send the header, so the rest of agentkit (``Agent``, ``AgentSession``, +vendor classes) works unchanged. + +Everything under ``agentkit/preview/`` is temporary. When these providers ship +on the production gateway, delete this package and move the vendor classes into +``vendors/mllm.py``. +""" + +from __future__ import annotations + +import typing + +from ...pool_client import Agora, AsyncAgora +from .vendors import GEMINI_PREVIEW_MLLM_URL + +#: Base URL that serves the preview providers. +PREVIEW_API_BASE_URL = "https://partner.ai.agora.io/preview/api/conversational-ai-agent" + +#: Request header that opts a request into a preview provider family. +#: +#: This is the header the preview gateway routes on. A request that reaches the +#: gateway without it is not rejected — it is routed to the production +#: environment, where the preview providers do not exist. +PREVIEW_FEATURE_HEADER = "agora-feature" + + +class PreviewFeatures: + """Preview provider families. + + Each value is one entry in the ``agora-feature`` header and gates a set of + vendors on the preview endpoint. + """ + + #: The Gemini walkie-talkie / clever-chatter MLLMs. + GEMINI_LIVE = "gemini-live" + + +PreviewFeature = str + +_DEFAULT_PREVIEW_FEATURES: typing.Tuple[str, ...] = (PreviewFeatures.GEMINI_LIVE,) + + +def _preview_headers( + features: typing.Sequence[str], + headers: typing.Optional[typing.Dict[str, str]], +) -> typing.Dict[str, str]: + """Merge the gate header over caller headers. + + The gate goes last on purpose: caller-supplied headers must not be able to + drop or blank it. A preview request that loses the header is not rejected — + it routes to the production environment, where the preview providers do not + exist. Use ``features`` to change the value. + """ + merged: typing.Dict[str, str] = dict(headers or {}) + merged[PREVIEW_FEATURE_HEADER] = ",".join(features) + return merged + + +class AgoraPreview(Agora): + """Client for the Agora Conversational AI preview endpoint. + + Use it exactly like ``Agora`` — same auth modes, same ``Agent`` and + ``AgentSession`` API. It differs only in where requests go and in the + ``agora-feature`` header it attaches. + + Parameters + ---------- + features : Sequence[str], optional + Preview families to request via the ``agora-feature`` header. + Defaults to ``("gemini-live",)``. + base_url : str, optional + Override the preview base URL. For internal testing against a staging host. + + Examples + -------- + >>> from agora_agent import Area + >>> from agora_agent.agentkit import Agent + >>> from agora_agent.agentkit.preview import AgoraPreview, GeminiCleverChatter + >>> client = AgoraPreview(area=Area.US, app_id="...", app_certificate="...") + >>> session = ( + ... Agent(client=client) + ... .with_mllm(GeminiCleverChatter(api_key="...", thinking_level="medium")) + ... .create_session(channel="demo", agent_uid="1", remote_uids=["100"]) + ... ) + """ + + def __init__( + self, + *, + area: typing.Any, + app_id: str, + app_certificate: str, + features: typing.Optional[typing.Sequence[str]] = None, + base_url: typing.Optional[str] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + **kwargs: typing.Any, + ): + self.preview_features: typing.Tuple[str, ...] = tuple( + features if features is not None else _DEFAULT_PREVIEW_FEATURES + ) + self._preview_base_url = base_url or PREVIEW_API_BASE_URL + + super().__init__( + area=area, + app_id=app_id, + app_certificate=app_certificate, + headers=_preview_headers(self.preview_features, headers), + **kwargs, + ) + # The base class resolves the URL from the regional pool; repoint it. + self._update_base_url() + + def _update_base_url(self) -> None: + self._client_wrapper._base_url = self._preview_base_url + + def get_current_url(self) -> str: + return self._preview_base_url + + def next_region(self) -> None: + """No-op: the preview endpoint has no regional replicas to fail over to.""" + + def select_best_domain(self) -> None: + """No-op: the preview endpoint has a single fixed host.""" + + +class AsyncAgoraPreview(AsyncAgora): + """Async client for the Agora Conversational AI preview endpoint. + + See :class:`AgoraPreview`. + """ + + def __init__( + self, + *, + area: typing.Any, + app_id: str, + app_certificate: str, + features: typing.Optional[typing.Sequence[str]] = None, + base_url: typing.Optional[str] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + **kwargs: typing.Any, + ): + self.preview_features: typing.Tuple[str, ...] = tuple( + features if features is not None else _DEFAULT_PREVIEW_FEATURES + ) + self._preview_base_url = base_url or PREVIEW_API_BASE_URL + + super().__init__( + area=area, + app_id=app_id, + app_certificate=app_certificate, + headers=_preview_headers(self.preview_features, headers), + **kwargs, + ) + self._update_base_url() + + def _update_base_url(self) -> None: + self._client_wrapper._base_url = self._preview_base_url + + def get_current_url(self) -> str: + return self._preview_base_url + + def next_region(self) -> None: + """No-op: the preview endpoint has no regional replicas to fail over to.""" + + async def select_best_domain(self) -> None: + """No-op: the preview endpoint has a single fixed host.""" + + +#: Preview MLLM models. ``mllm.vendor`` is ``"gemini"`` for both the production +#: Gemini Live provider and the preview ones, so the model name is one of two +#: things that mark a config as preview-only; see :func:`_has_preview_mllm_envelope` +#: for the other. +#: +#: Both the codenames and their post-rename IDs are listed, so recognition keeps +#: working through Google's rollout whichever name a caller sends. +_PREVIEW_MLLM_MODELS = frozenset( + { + "walkie-talkie", + "clever-chatter", + "gemini-3.5-live", + "gemini-3.5-live-thinking", + } +) + + +def _has_preview_mllm_envelope(mllm: typing.Mapping[str, typing.Any]) -> bool: + """Whether a config carries the envelope the preview MLLM classes emit. + + That envelope is the API key repeated inside ``params`` plus a ``url`` on the + Gemini Developer API host. The production ``GeminiLive`` vendor emits + neither — it leaves ``params.api_key`` unset and defaults ``url`` to ``""`` + or a WebSocket endpoint. + + This is the second recognition path, and it exists because keying only off + :data:`_PREVIEW_MLLM_MODELS` makes an unrecognised model name fail silently: + :func:`apply_preview_shape` would stop retargeting ``greeting_message``, the + greeting would land in a field these models ignore, and the agent would + simply never greet. A model name we have not listed yet is reachable by + following this SDK's own advice to override ``model`` when Google renames + one ahead of a release, so the failure has to not be silent. + """ + params = mllm.get("params") + api_key = params.get("api_key") if isinstance(params, dict) else None + url = mllm.get("url") + return isinstance(api_key, str) and isinstance(url, str) and url.startswith(GEMINI_PREVIEW_MLLM_URL) + + +def _is_preview_mllm(mllm: typing.Any) -> bool: + """Whether an MLLM config targets a preview model. + + Recognised by model name, or by the wire envelope only the preview vendor + classes produce. + """ + if not isinstance(mllm, dict) or mllm.get("vendor") != "gemini": + return False + params = mllm.get("params") + model = params.get("model") if isinstance(params, dict) else None + if isinstance(model, str) and model in _PREVIEW_MLLM_MODELS: + return True + return _has_preview_mllm_envelope(mllm) + + +#: MLLM wire keys the preview route spells differently from the Agora schema, +#: as production spelling -> preview spelling. +#: +#: ``failure_message`` is deliberately absent: it is an Agora engine feature +#: rather than a Gemini one, so it keeps its schema spelling. +_PREVIEW_MLLM_FIELD_RENAMES = {"greeting_message": "greeting"} + + +def apply_preview_shape(properties: typing.MutableMapping[str, typing.Any]) -> None: + """Retarget MLLM fields the shared builder wrote with production spellings. + + ``Agent`` fills ``mllm.greeting_message`` from an agent-level ``greeting`` + whenever the vendor has not set that key — correct for every GA vendor, but + the preview Gemini models read ``greeting``, so the value would land in a + field they ignore and the agent would silently never greet. + + Rather than teach the shared builder about preview providers, the + translation lives here and disappears with this package at GA. The vendor's + own value wins; the production-spelled one is the fallback, which also + migrates a hand-written ``greeting_message`` onto the preview key so an + existing config keeps working after only swapping the model. + + Mutates ``properties["mllm"]`` in place. Safe because the builder hands this + a fresh copy of the MLLM config rather than the Agent's stored one. + """ + mllm = properties.get("mllm") + if not isinstance(mllm, dict) or not _is_preview_mllm(mllm): + return + for production, preview in _PREVIEW_MLLM_FIELD_RENAMES.items(): + if production not in mllm: + continue + value = mllm.pop(production) + mllm.setdefault(preview, value) + + +def required_preview_features(properties: typing.Mapping[str, typing.Any]) -> typing.List[str]: + """Return the preview features a start request needs. + + Derived from the request body rather than from the vendor classes, so + hand-written configs are covered too. + """ + features: typing.List[str] = [] + + if _is_preview_mllm(properties.get("mllm")): + features.append(PreviewFeatures.GEMINI_LIVE) + + return features + + +def assert_preview_support(client: typing.Any, properties: typing.Mapping[str, typing.Any]) -> None: + """Fail fast when preview providers are paired with a non-preview client. + + Raises before the request goes out, instead of surfacing an opaque gateway + error — or worse, silently succeeding against the production environment. + """ + required = required_preview_features(properties) + if not required: + return + + enabled = getattr(client, "preview_features", ()) + missing = [feature for feature in required if feature not in enabled] + if not missing: + return + + joined = ", ".join(missing) + raise ValueError( + f"This agent uses preview providers that require the preview endpoint ({joined}). " + "Create the session with AgoraPreview instead of Agora, and make sure its " + f"`features` include: {joined}." + ) + + +__all__ = [ + "PREVIEW_API_BASE_URL", + "PREVIEW_FEATURE_HEADER", + "AgoraPreview", + "AsyncAgoraPreview", + "PreviewFeature", + "PreviewFeatures", + "apply_preview_shape", + "assert_preview_support", + "required_preview_features", +] diff --git a/src/agora_agent/agentkit/preview/vendors.py b/src/agora_agent/agentkit/preview/vendors.py new file mode 100644 index 0000000..4819958 --- /dev/null +++ b/src/agora_agent/agentkit/preview/vendors.py @@ -0,0 +1,198 @@ +"""Preview-only vendor classes. + +Wire shapes here match the preview gateway contract exactly and are not served +by the production environment. See ``preview/client.py`` for the routing and the +``agora-feature`` gate. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Literal, Optional + +from ..vendors.base import BaseMLLM +from ..vendors.mllm import MllmTurnDetectionConfig +from pydantic import ConfigDict, Field + +#: Rejects ``api_key=""``. ``Field(...)`` alone makes the key required but still +#: accepts the empty string, which would reach the provider as a blank +#: credential; the TypeScript and Go vendors both refuse it at construction. +_ApiKey = Field(..., min_length=1, description="Google API key") + + +class GeminiPreviewMllmModels: + """Preview MLLM model names. + + Google is renaming these from codenames to conventional model IDs + (``walkie-talkie`` -> ``gemini-3.5-live``, ``clever-chatter`` -> + ``gemini-3.5-live-thinking``). Both spellings are listed because both are + accepted during the rollout; see :data:`GEMINI_PREVIEW_MLLM_DEFAULTS` for + which one the vendor classes send. + """ + + #: Codename for the low-latency model. Currently the default. + WALKIE_TALKIE = "walkie-talkie" + #: Codename for the reasoning model. Currently the default. + CLEVER_CHATTER = "clever-chatter" + #: Post-rename name for :class:`GeminiWalkieTalkie`. + LIVE_35 = "gemini-3.5-live" + #: Post-rename name for :class:`GeminiCleverChatter`. + LIVE_35_THINKING = "gemini-3.5-live-thinking" + + +#: The model name each preview MLLM class sends by default. +#: +#: Still the codenames: as of 2026-08-19 the renamed IDs are propagating through +#: Google's fleet and are not live everywhere, while the codenames work. When +#: Google confirms the rename has landed, point these two entries at ``LIVE_35`` +#: / ``LIVE_35_THINKING`` — that is the whole cutover. Callers who need the new +#: names sooner can pass ``model=`` per instance. +GEMINI_PREVIEW_MLLM_DEFAULTS = { + "walkie_talkie": GeminiPreviewMllmModels.WALKIE_TALKIE, + "clever_chatter": GeminiPreviewMllmModels.CLEVER_CHATTER, +} + + +GEMINI_THINKING_LEVELS = ("low", "medium", "high") + +GeminiThinkingLevel = Literal["low", "medium", "high"] + +#: The preview MLLM talks to the Gemini Developer API rather than a WebSocket host. +GEMINI_PREVIEW_MLLM_URL = "https://generativelanguage.googleapis.com" + + +class _GeminiPreviewMLLM(BaseMLLM): + """Shared envelope for the preview Gemini MLLMs. + + Differs from the production ``GeminiLive`` in two ways the preview + gateway requires: ``params.api_key`` is repeated inside ``params``, and + ``url`` defaults to the Gemini Developer API host rather than a WebSocket + endpoint. + """ + + model_config = ConfigDict(extra="forbid") + + api_key: str = _ApiKey + model: Optional[str] = Field(default=None, description="Override the preview model name") + voice: Optional[str] = Field(default=None, description="Voice name (e.g., Puck)") + language_codes: Optional[List[str]] = Field( + default=None, + description=( + "Languages the model should work in, sent as params.language_codes. " + "The preview MLLMs take the plural array, not the singular `language` " + "(which belongs to xAI Grok). Omitted from the request when unset." + ), + ) + url: Optional[str] = Field(default=None, description="Gemini API host") + instructions: Optional[str] = Field(default=None, description="System instructions") + transcribe_agent: Optional[bool] = Field(default=None, description="Transcribe agent speech") + transcribe_user: Optional[bool] = Field(default=None, description="Transcribe user speech") + affective_dialog: Optional[bool] = Field(default=None, description="Enable affective dialog") + proactive_audio: Optional[bool] = Field(default=None, description="Enable proactive audio") + http_options: Optional[Dict[str, Any]] = Field(default=None, description="HTTP options") + greeting: Optional[str] = Field( + default=None, + description=( + "Agent greeting, serialized to `greeting`. The preview Gemini models " + "read this spelling, not the `greeting_message` the production " + "providers use." + ), + ) + failure_message: Optional[str] = Field(default=None, description="Message played on failure") + input_modalities: Optional[List[str]] = Field(default=None, description="Input modalities") + output_modalities: Optional[List[str]] = Field(default=None, description="Output modalities") + messages: Optional[List[Dict[str, Any]]] = Field(default=None, description="Conversation messages") + additional_params: Optional[Dict[str, Any]] = Field(default=None) + turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None) + + #: Set by each concrete subclass. + _default_model: str = "" + + def _model_params(self) -> Dict[str, Any]: + """Model-specific params merged after ``voice``.""" + return {} + + def to_config(self) -> Dict[str, Any]: + model = self.model if self.model is not None else self._default_model + voice = self.voice if self.voice is not None else "Puck" + url = self.url if self.url is not None else GEMINI_PREVIEW_MLLM_URL + + params: Dict[str, Any] = dict(self.additional_params or {}) + params["api_key"] = self.api_key + params["model"] = model + params["voice"] = voice + params.update(self._model_params()) + # Plural array, and omitted when unset. The singular ``params.language`` + # belongs to xAI Grok in the Agora schema, and the production Gemini Live + # provider sends no language field at all. + if self.language_codes is not None: + params["language_codes"] = list(self.language_codes) + + if self.instructions is not None: + params["instructions"] = self.instructions + if self.transcribe_agent is not None: + params["transcribe_agent"] = self.transcribe_agent + if self.transcribe_user is not None: + params["transcribe_user"] = self.transcribe_user + if self.affective_dialog is not None: + params["affective_dialog"] = self.affective_dialog + if self.proactive_audio is not None: + params["proactive_audio"] = self.proactive_audio + if self.http_options is not None: + params["http_options"] = self.http_options + + config: Dict[str, Any] = { + "vendor": "gemini", + "api_key": self.api_key, + "url": url, + "params": params, + } + if self.messages is not None: + config["messages"] = self.messages + # ``greeting``, not ``greeting_message``: the preview Gemini models read + # this spelling. See the preview-endpoint guide. + if self.greeting is not None: + config["greeting"] = self.greeting + if self.failure_message is not None: + config["failure_message"] = self.failure_message + if self.input_modalities is not None: + config["input_modalities"] = self.input_modalities + if self.output_modalities is not None: + config["output_modalities"] = self.output_modalities + if self.turn_detection is not None: + config["turn_detection"] = self.turn_detection + + return config + + +class GeminiWalkieTalkie(_GeminiPreviewMLLM): + """Gemini walkie-talkie MLLM (preview).""" + + _default_model: str = GEMINI_PREVIEW_MLLM_DEFAULTS["walkie_talkie"] + + +class GeminiCleverChatter(_GeminiPreviewMLLM): + """Gemini clever-chatter MLLM (preview). + + ``thinking_level`` is exclusive to this model — it is a separate class + rather than a flag on the walkie-talkie so the distinction is structural. + """ + + thinking_level: Optional[GeminiThinkingLevel] = Field( + default=None, description="Reasoning budget: low, medium, or high" + ) + + _default_model: str = GEMINI_PREVIEW_MLLM_DEFAULTS["clever_chatter"] + + def _model_params(self) -> Dict[str, Any]: + return {} if self.thinking_level is None else {"thinking_level": self.thinking_level} + + +__all__ = [ + "GEMINI_PREVIEW_MLLM_DEFAULTS", + "GEMINI_PREVIEW_MLLM_URL", + "GEMINI_THINKING_LEVELS", + "GeminiCleverChatter", + "GeminiPreviewMllmModels", + "GeminiThinkingLevel", + "GeminiWalkieTalkie", +] diff --git a/src/agora_agent/agentkit/token.py b/src/agora_agent/agentkit/token.py index 9dd1aaa..5351184 100644 --- a/src/agora_agent/agentkit/token.py +++ b/src/agora_agent/agentkit/token.py @@ -10,6 +10,9 @@ DEFAULT_EXPIRY_SECONDS = 86400 MAX_EXPIRY_SECONDS = 86400 +#: Length Agora requires of both the App ID and the App Certificate. +AGORA_CREDENTIAL_LENGTH = 32 + ROLE_PUBLISHER = 1 ROLE_SUBSCRIBER = 2 @@ -185,6 +188,32 @@ def _pack_service_rtm(user_id: str, privileges: typing.Dict[int, int]) -> bytes: ) +def _require_valid_credentials(app_id: str, app_certificate: str) -> None: + """Reject credentials Agora will not accept, before signing anything. + + Unlike the TypeScript and Go builders — which return an empty token and an + explicit error respectively — the Python signing path HMACs whatever it is + given, so a truncated or whitespace-padded value yields a well-formed token + that the gateway rejects with an auth error naming neither field. Checking + the lengths here turns that into a local failure that names the culprit. + + Credential values are never included in the message — only their lengths. + """ + wrong = [ + (name, value) + for name, value in (("app_id", app_id), ("app_certificate", app_certificate)) + if len(value) != AGORA_CREDENTIAL_LENGTH + ] + if not wrong: + return + detail = " and ".join(f"{name} is {len(value)} characters" for name, value in wrong) + raise ValueError( + f"Failed to build an Agora token: app_id and app_certificate must each be exactly " + f"{AGORA_CREDENTIAL_LENGTH} characters ({detail}). " + "Check the values for stray whitespace or a truncated paste." + ) + + def generate_rtc_token( app_id: str, app_certificate: str, @@ -215,6 +244,7 @@ def generate_rtc_token( str The generated RTC token. """ + _require_valid_credentials(app_id, app_certificate) try: from agora_token_builder import RtcTokenBuilder # type: ignore[import-not-found] @@ -274,6 +304,7 @@ def generate_convo_ai_token( str The AccessToken2 string for use in the Authorization header. """ + _require_valid_credentials(app_id, app_certificate) try: from agora_token_builder import RtcTokenBuilder # type: ignore[import-not-found] diff --git a/src/agora_agent/pool_client.py b/src/agora_agent/pool_client.py index 03b402a..6cc6161 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -3,6 +3,7 @@ from __future__ import annotations import base64 +import json import logging import typing @@ -15,6 +16,7 @@ from .client import AsyncAgora as BaseAsyncAgora from .core.api_error import ApiError from .core.domain import Area, Pool +from .agentkit.debug import redact_secrets from .agentkit.token import generate_convo_ai_token _AUTH_MODE = typing.Literal["app-credentials", "basic", "token"] @@ -41,15 +43,24 @@ def _redact_headers(headers: typing.Mapping[str, str]) -> typing.Dict[str, str]: def _debug_request(request: httpx.Request) -> None: - """Log HTTP request when debug is enabled.""" + """Log HTTP request when debug is enabled. + + The body is JSON-decoded and redacted before logging: it carries vendor API + keys, the RTC token, and the App ID, none of which may reach a log that gets + pasted into an issue. A body that is not JSON is described, never echoed. + """ headers = _redact_headers(request.headers) body_preview = "" if request.content: try: - preview = request.content[:500] if len(request.content) > 500 else request.content - body_preview = f" body={preview!r}..." + decoded = json.loads(request.content) except Exception: - body_preview = " body=" + body_preview = f" body=<{len(request.content)} bytes, not JSON>" + else: + redacted = json.dumps(redact_secrets(decoded)) + if len(redacted) > 2000: + redacted = redacted[:2000] + "..." + body_preview = f" body={redacted}" _DEBUG_LOGGER.debug( "HTTP request: %s %s headers=%s%s", request.method, diff --git a/tests/custom/test_preview.py b/tests/custom/test_preview.py new file mode 100644 index 0000000..e3f34af --- /dev/null +++ b/tests/custom/test_preview.py @@ -0,0 +1,475 @@ +"""Preview endpoint tests. + +Wire-shape expectations are copied from the TypeScript suite so the three SDKs +stay byte-identical on the wire. +""" + +import json + +import httpx +import pytest + +from agora_agent import Area +from agora_agent.agentkit import Agent +from agora_agent.agentkit.debug import REDACTED, redact_secrets +from agora_agent.agentkit.preview import ( + PREVIEW_API_BASE_URL, + AgoraPreview, + AsyncAgoraPreview, + GeminiCleverChatter, + GeminiWalkieTalkie, + apply_preview_shape, + required_preview_features, +) +from agora_agent.pool_client import Agora + +API_KEY = "test-google-api-key" +APP_ID = "test-app-id-0123456789abcdefghij" +APP_CERTIFICATE = "test-app-certificate-01234567890" + + +def _preview_client(transport=None, **kwargs): + httpx_client = httpx.Client(transport=transport) if transport is not None else None + return AgoraPreview( + area=Area.US, + app_id=APP_ID, + app_certificate=APP_CERTIFICATE, + httpx_client=httpx_client, + **kwargs, + ) + + +class _Recorder(httpx.MockTransport): + """Captures every outgoing request and answers with a generic success body.""" + + def __init__(self): + self.requests = [] + super().__init__(self._handle) + + def _handle(self, request: httpx.Request) -> httpx.Response: + self.requests.append(request) + return httpx.Response(200, json={"agent_id": "agent-1", "data": {"list": []}}) + + +# --- Vendor wire shapes ----------------------------------------------------- + + +def test_walkie_talkie_serialises_to_documented_mllm_shape(): + config = GeminiWalkieTalkie(api_key=API_KEY).to_config() + + assert config == { + "vendor": "gemini", + "api_key": API_KEY, + "url": "https://generativelanguage.googleapis.com", + "params": { + "api_key": API_KEY, + "model": "walkie-talkie", + "voice": "Puck", + }, + } + + +def test_clever_chatter_includes_thinking_level(): + config = GeminiCleverChatter(api_key=API_KEY, voice="Puck", thinking_level="medium").to_config() + + assert config == { + "vendor": "gemini", + "api_key": API_KEY, + "url": "https://generativelanguage.googleapis.com", + "params": { + "api_key": API_KEY, + "model": "clever-chatter", + "voice": "Puck", + "thinking_level": "medium", + }, + } + + +def test_walkie_talkie_rejects_thinking_level(): + # thinking_level is structurally exclusive to clever-chatter. + with pytest.raises(Exception): + GeminiWalkieTalkie(api_key=API_KEY, thinking_level="low") + + +def test_preview_mllm_carries_instructions_and_transcription_flags(): + config = GeminiWalkieTalkie( + api_key=API_KEY, + instructions="You are Ada.", + transcribe_agent=True, + transcribe_user=True, + ).to_config() + + assert config["params"]["instructions"] == "You are Ada." + assert config["params"]["transcribe_agent"] is True + assert config["params"]["transcribe_user"] is True + + +def test_sends_greeting_never_greeting_message(): + # The preview Gemini models read `greeting`. `greeting_message` is what the + # production providers use. + with_greeting = GeminiWalkieTalkie(api_key=API_KEY, greeting="Hi there.").to_config() + assert with_greeting["greeting"] == "Hi there." + assert "greeting_message" not in with_greeting + + without_greeting = GeminiWalkieTalkie(api_key=API_KEY).to_config() + assert "greeting" not in without_greeting + assert "greeting_message" not in without_greeting + + +# --- Routing ---------------------------------------------------------------- + + +def test_reports_the_preview_base_url(): + client = _preview_client() + + assert client.get_current_url() == PREVIEW_API_BASE_URL + assert client._client_wrapper.get_base_url() == PREVIEW_API_BASE_URL + + +def test_does_not_participate_in_regional_failover(): + client = _preview_client() + + client.next_region() + client.select_best_domain() + + assert client.get_current_url() == PREVIEW_API_BASE_URL + + +def test_accepts_a_base_url_override(): + client = _preview_client(base_url="https://staging.example.com/preview/api/conversational-ai-agent") + + assert client.get_current_url() == "https://staging.example.com/preview/api/conversational-ai-agent" + + +def test_honours_a_custom_feature_list(): + client = _preview_client(features=["gemini-live", "another-preview"]) + + assert client._client_wrapper.get_headers()["agora-feature"] == "gemini-live,another-preview" + + +def test_sends_requests_to_the_preview_endpoint_with_the_gate_header(): + recorder = _Recorder() + client = _preview_client(transport=recorder) + + ( + Agent(client=client) + .with_mllm(GeminiCleverChatter(api_key=API_KEY, thinking_level="high")) + .create_session(channel="preview-channel", agent_uid="1", remote_uids=["100"]) + .start() + ) + + request = recorder.requests[0] + assert str(request.url) == f"{PREVIEW_API_BASE_URL}/v2/projects/{APP_ID}/join" + assert request.headers["agora-feature"] == "gemini-live" + # Exactly one header may carry the preview value — the gateway accepts other + # spellings that are not part of the public contract, and none may ship here. + carriers = [name for name, value in request.headers.items() if "gemini-live" in value] + assert carriers == ["agora-feature"] + + +def test_keeps_the_gate_header_on_every_request(): + # A request that loses the header is routed to the production environment, + # where the preview providers do not exist — so each verb must carry it. + recorder = _Recorder() + client = _preview_client(transport=recorder) + + session = ( + Agent(client=client) + .with_mllm(GeminiWalkieTalkie(api_key=API_KEY)) + .create_session(channel="preview-channel", agent_uid="1", remote_uids=["100"]) + ) + session.start() + session.interrupt() + session.stop() + client.stop_agent("agent-2") + + assert len(recorder.requests) >= 4 + for request in recorder.requests: + assert request.headers.get("agora-feature") == "gemini-live", request.url + + +def test_caller_headers_cannot_drop_the_gate(): + recorder = _Recorder() + client = _preview_client(transport=recorder, headers={"agora-feature": "", "x-custom": "kept"}) + + ( + Agent(client=client) + .with_mllm(GeminiWalkieTalkie(api_key=API_KEY)) + .create_session(channel="preview-channel", agent_uid="1", remote_uids=["100"]) + .start() + ) + + request = recorder.requests[0] + assert request.headers["agora-feature"] == "gemini-live" + assert request.headers["x-custom"] == "kept" + + +def test_async_client_pins_the_preview_host_and_gate(): + client = AsyncAgoraPreview(area=Area.US, app_id=APP_ID, app_certificate=APP_CERTIFICATE) + + assert client.get_current_url() == PREVIEW_API_BASE_URL + assert client._client_wrapper.get_headers()["agora-feature"] == "gemini-live" + + +# --- Preview support guard -------------------------------------------------- + + +def test_required_preview_features_flags_preview_mllm_models(): + assert required_preview_features({"mllm": {"vendor": "gemini", "params": {"model": "walkie-talkie"}}}) == [ + "gemini-live" + ] + + +@pytest.mark.parametrize( + "model", + ["walkie-talkie", "clever-chatter", "gemini-3.5-live", "gemini-3.5-live-thinking"], +) +def test_required_preview_features_recognises_both_sides_of_the_rename(model): + # Google is renaming the codenames; the guard has to keep working through + # the rollout whichever spelling a caller sends. + assert required_preview_features({"mllm": {"vendor": "gemini", "params": {"model": model}}}) == ["gemini-live"] + + +def test_required_preview_features_leaves_ga_gemini_live_alone(): + ga = {"mllm": {"vendor": "gemini", "params": {"model": "gemini-live-2.5-flash-preview-native-audio-09-2025"}}} + + assert required_preview_features(ga) == [] + + +def test_start_rejects_preview_providers_on_the_production_client(): + client = Agora(area=Area.US, app_id=APP_ID, app_certificate=APP_CERTIFICATE) + session = ( + Agent(client=client) + .with_mllm(GeminiWalkieTalkie(api_key=API_KEY)) + .create_session(channel="preview-channel", agent_uid="1", remote_uids=["100"]) + ) + + with pytest.raises(ValueError, match="require the preview endpoint"): + session.start() + + +# --- Debug redaction -------------------------------------------------------- + + +def test_redact_secrets_walks_nested_structures(): + redacted = redact_secrets( + { + "appid": "81190c52971d4004b7244bdcd93e2f34", + "properties": { + "token": "007eJxTYKhsrH10", + "asr": {"vendor": "gemini", "params": {"api_key": API_KEY, "model": "m"}}, + "mcp_servers": [{"name": "a", "headers": {"authorization": "Bearer x"}}], + }, + } + ) + + assert redacted == { + "appid": REDACTED, + "properties": { + "token": REDACTED, + "asr": {"vendor": "gemini", "params": {"api_key": REDACTED, "model": "m"}}, + "mcp_servers": [{"name": "a", "headers": {"authorization": REDACTED}}], + }, + } + + +def test_redact_secrets_leaves_empty_values_visible(): + # "" is the signature of an unset env var and must stay diagnosable. + assert redact_secrets({"params": {"api_key": ""}}) == {"params": {"api_key": ""}} + + +def test_redact_secrets_does_not_mutate_input(): + original = {"properties": {"asr": {"params": {"api_key": API_KEY}}}} + redact_secrets(original) + + assert original["properties"]["asr"]["params"]["api_key"] == API_KEY + + +def test_debug_output_never_prints_a_live_credential(capsys): + recorder = _Recorder() + client = _preview_client(transport=recorder) + + ( + Agent(client=client) + .with_mllm(GeminiWalkieTalkie(api_key=API_KEY)) + .create_session(channel="c", agent_uid="1", remote_uids=["100"], debug=True) + .start() + ) + + output = capsys.readouterr().out + assert API_KEY not in output + assert "gemini" in output # non-secret config stays readable + + # The request itself still carries the real key. + sent = json.loads(recorder.requests[0].content) + assert sent["properties"]["mllm"]["params"]["api_key"] == API_KEY + + +def test_preview_mllm_sends_language_codes_never_singular_language(): + # `mllm.params.language` belongs to xAI Grok in the Agora schema, and the + # production Gemini Live provider sends no language field at all. The + # preview MLLMs take the plural array. + with_codes = GeminiWalkieTalkie(api_key=API_KEY, language_codes=["en-US"]).to_config() + assert with_codes["params"]["language_codes"] == ["en-US"] + assert "language" not in with_codes["params"] + + with_multiple = GeminiCleverChatter(api_key=API_KEY, language_codes=["en-US", "es-ES"]).to_config() + assert with_multiple["params"]["language_codes"] == ["en-US", "es-ES"] + + +def test_preview_mllm_omits_language_codes_when_unset(): + config = GeminiWalkieTalkie(api_key=API_KEY).to_config() + + assert "language_codes" not in config["params"] + assert "language" not in config["params"] + + +def test_preview_mllm_rejects_the_singular_language_argument(): + # extra="forbid", so a stale `language=` fails loudly rather than silently + # writing into xAI's field. + with pytest.raises(Exception): + GeminiWalkieTalkie(api_key=API_KEY, language="en-US") + + +# --- Preview wire-shape translation ----------------------------------------- + + +def _preview_mllm(**extra): + config = {"vendor": "gemini", "params": {"model": "walkie-talkie"}} + config.update(extra) + return {"mllm": config} + + +def test_agent_level_greeting_is_retargeted_to_greeting(): + # Agent.to_properties writes agent-level greetings to greeting_message, the + # production spelling. Without this step the preview model never greets. + properties = _preview_mllm(greeting_message="AGENT") + + apply_preview_shape(properties) + + assert properties["mllm"]["greeting"] == "AGENT" + assert "greeting_message" not in properties["mllm"] + + +def test_hand_written_greeting_message_migrates_onto_greeting(): + # Lets an existing MLLM config keep working after only swapping the model. + properties = _preview_mllm(greeting_message="EXISTING") + + apply_preview_shape(properties) + + assert properties["mllm"]["greeting"] == "EXISTING" + assert "greeting_message" not in properties["mllm"] + + +def test_vendor_greeting_wins_over_agent_level(): + properties = _preview_mllm(greeting="VENDOR", greeting_message="AGENT") + + apply_preview_shape(properties) + + assert properties["mllm"]["greeting"] == "VENDOR" + assert "greeting_message" not in properties["mllm"] + + +def test_failure_message_is_left_alone(): + # failure_message is an Agora engine feature, not a Gemini one. + properties = _preview_mllm(failure_message="One moment.") + + apply_preview_shape(properties) + + assert properties["mllm"]["failure_message"] == "One moment." + + +def test_production_mllm_is_left_untouched(): + properties = { + "mllm": { + "vendor": "gemini", + "greeting_message": "PROD", + "params": {"model": "gemini-2.0-flash-live-001"}, + } + } + + apply_preview_shape(properties) + + assert properties["mllm"]["greeting_message"] == "PROD" + assert "greeting" not in properties["mllm"] + + +def test_apply_preview_shape_is_a_no_op_without_mllm(): + properties = {"asr": {"vendor": "gemini"}} + + apply_preview_shape(properties) + + assert properties == {"asr": {"vendor": "gemini"}} + + +def test_started_request_carries_greeting_not_greeting_message(): + # End-to-end: proves apply_preview_shape is wired into start(). + recorder = _Recorder() + client = _preview_client(transport=recorder) + session = ( + Agent(client=client, greeting="AGENT") + .with_mllm(GeminiWalkieTalkie(api_key=API_KEY)) + .create_session(channel="c", agent_uid="1", remote_uids=["100"]) + ) + session.start() + + mllm = json.loads(recorder.requests[0].content)["properties"]["mllm"] + assert mllm["greeting"] == "AGENT" + assert "greeting_message" not in mllm + + +def test_unrecognised_model_on_a_preview_vendor_still_gets_the_greeting_rename(): + # Overriding ``model`` is the documented escape hatch for a Google rename + # that lands ahead of an SDK release, so a name this SDK has never seen is + # reachable by following its own advice. Matching on the model name alone + # made that case fail silently: no rename, and an agent-level greeting would + # land in a field these models ignore. + recorder = _Recorder() + session = ( + Agent(client=_preview_client(transport=recorder), greeting="AGENT") + .with_mllm(GeminiWalkieTalkie(api_key=API_KEY, model="gemini-4.0-live-unreleased")) + .create_session(channel="c", agent_uid="1", remote_uids=["100"]) + ) + session.start() + + mllm = json.loads(recorder.requests[0].content)["properties"]["mllm"] + assert mllm["greeting"] == "AGENT" + assert "greeting_message" not in mllm + + +def test_unrecognised_model_on_a_preview_vendor_still_requires_the_preview_endpoint(): + properties = { + "mllm": GeminiWalkieTalkie(api_key=API_KEY, model="gemini-4.0-live-unreleased").to_config(), + } + + assert required_preview_features(properties) == ["gemini-live"] + + +def test_envelope_check_does_not_catch_a_production_gemini_live_config(): + # GeminiLive leaves params.api_key unset and does not point url at the + # Gemini Developer API host, so widening detection must not retarget + # greeting_message for a GA caller. + properties = { + "mllm": { + "vendor": "gemini", + "api_key": "prod-key", + "url": "wss://generativelanguage.googleapis.com/ws", + "greeting_message": "PROD", + "params": {"model": "gemini-2.0-flash-live-001"}, + } + } + + apply_preview_shape(properties) + + assert properties["mllm"]["greeting_message"] == "PROD" + assert "greeting" not in properties["mllm"] + assert required_preview_features(properties) == [] + + +@pytest.mark.parametrize("cls", [GeminiWalkieTalkie, GeminiCleverChatter]) +def test_rejects_an_empty_api_key(cls): + # Field(...) alone makes the key required but still accepts "", which would + # reach the provider as a blank credential. The TypeScript and Go vendors + # both refuse it at construction, so this one does too. + with pytest.raises(Exception): + cls(api_key="") diff --git a/tests/custom/test_token_credentials.py b/tests/custom/test_token_credentials.py new file mode 100644 index 0000000..b58ffda --- /dev/null +++ b/tests/custom/test_token_credentials.py @@ -0,0 +1,64 @@ +"""Credential validation in the token builders. + +Agora requires the App ID and App Certificate to be exactly 32 characters. The +Python signing path HMACs whatever it is given, so without a check a truncated +or whitespace-padded value produces a well-formed token that the gateway +rejects with an auth error naming neither field. +""" + +import pytest + +from agora_agent.agentkit.token import generate_convo_ai_token, generate_rtc_token + +VALID_APP_ID = "0123456789abcdef0123456789abcdef" +VALID_CERTIFICATE = "fedcba9876543210fedcba9876543210" + + +def test_builds_a_token_when_both_credentials_are_the_right_length(): + token = generate_convo_ai_token( + app_id=VALID_APP_ID, + app_certificate=VALID_CERTIFICATE, + channel_name="demo", + uid=333, + ) + + assert token.startswith("007") + + +@pytest.mark.parametrize( + "app_id,app_certificate,expected", + [ + (VALID_APP_ID, VALID_CERTIFICATE[:31], "app_certificate is 31 characters"), + (VALID_APP_ID, VALID_CERTIFICATE + "\n", "app_certificate is 33 characters"), + ("short-app-id", VALID_CERTIFICATE, "app_id is 12 characters"), + ], +) +def test_rejects_malformed_credentials(app_id, app_certificate, expected): + with pytest.raises(ValueError, match=expected): + generate_convo_ai_token( + app_id=app_id, app_certificate=app_certificate, channel_name="demo", uid=333 + ) + + +def test_names_both_fields_when_both_are_wrong(): + with pytest.raises(ValueError, match="app_id is 5 characters and app_certificate is 10 characters"): + generate_convo_ai_token( + app_id="short", app_certificate="also-short", channel_name="demo", uid=333 + ) + + +def test_never_puts_the_credential_values_in_the_message(): + secret = "super-secret-but-wrong-length" + with pytest.raises(ValueError) as excinfo: + generate_convo_ai_token( + app_id=VALID_APP_ID, app_certificate=secret, channel_name="demo", uid=333 + ) + + assert secret not in str(excinfo.value) + + +def test_guards_the_rtc_builder_too(): + with pytest.raises(ValueError, match="app_certificate is 9 characters"): + generate_rtc_token( + app_id=VALID_APP_ID, app_certificate="too-short", channel="demo", uid=333 + ) From 3914b9d5ae203c488fab67aa065b9198392ee24c Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Mon, 14 Sep 2026 18:34:13 -0700 Subject: [PATCH 02/57] feat: unify Gemini preview MLLM models --- docs/guides/preview-endpoint.md | 13 +-- src/agora_agent/__init__.py | 7 +- src/agora_agent/agentkit/__init__.py | 9 +- src/agora_agent/agentkit/agent.py | 4 +- src/agora_agent/agentkit/preview/__init__.py | 20 ++--- src/agora_agent/agentkit/preview/client.py | 54 ++++++++++-- src/agora_agent/agentkit/preview/vendors.py | 91 +++++++------------- tests/custom/test_agentkit_agent.py | 14 +++ tests/custom/test_preview.py | 29 +++++++ tests/custom/test_preview_gemini_merge.py | 40 +++++++-- 10 files changed, 184 insertions(+), 97 deletions(-) diff --git a/docs/guides/preview-endpoint.md b/docs/guides/preview-endpoint.md index 10988ba..5f4d838 100644 --- a/docs/guides/preview-endpoint.md +++ b/docs/guides/preview-endpoint.md @@ -29,10 +29,13 @@ agent_id = session.start() This session uses the preview base URL and sends `agora-feature: live-models`. A session using `GeminiSTT` uses the client's normal GA regional endpoint without that header. -Use `GeminiWalkieTalkie(api_key=...)` or `GeminiCleverChatter(api_key=..., thinking_level="medium")` with -`with_mllm` for Google's preview MLLMs. Their sessions use the same preview host and send -`agora-feature: gemini-live`. An agent-level greeting is translated to Gemini's `greeting` wire field; -GPT Live retains `greeting_message`. +Use the single `GeminiMLLM(api_key=..., model=...)` class with `with_mllm`. +The current model IDs are `walkie-talkie` and `clever-chatter`; the launch IDs +are `models/gemini-3.8-live` and `models/gemini-3.8-live-extended-thinking`. +Set `thinking_level="medium"` for extended thinking. `GeminiMLLM` sends it +only for `clever-chatter` or the extended-thinking ID. The working walkie +codename remains the default until Google activates the launch IDs. Gemini +sessions send `agora-feature: gemini-live`, while GPT Live retains `live-models`. ## Session-scoped routing @@ -43,7 +46,7 @@ receives private generated clients configured with: - `agora-feature` as the feature gate header. - All custom headers, authentication settings, timeouts, and the supplied `httpx` client from the original client. -The gate header is applied after caller-provided headers, so it cannot be accidentally blanked or replaced. It is +The gate header is applied after caller-provided and per-call headers, so it cannot be accidentally blanked or replaced. It is kept on every request made through that session. Production sessions created from the same client continue using the regional production endpoint. diff --git a/src/agora_agent/__init__.py b/src/agora_agent/__init__.py index e1269da..f17e1d9 100644 --- a/src/agora_agent/__init__.py +++ b/src/agora_agent/__init__.py @@ -48,10 +48,9 @@ GeminiSTT, GeminiSTTModels, GeminiLive, - GeminiWalkieTalkie, - GeminiCleverChatter, - GeminiPreviewMllmModels, - GEMINI_PREVIEW_MLLM_DEFAULTS, + GeminiMLLM, + GeminiMLLMModels, + GEMINI_MLLM_DEFAULT_MODEL, GenericAvatar, GenericTTS, GoogleSTT, diff --git a/src/agora_agent/agentkit/__init__.py b/src/agora_agent/agentkit/__init__.py index 2565b45..46908f9 100644 --- a/src/agora_agent/agentkit/__init__.py +++ b/src/agora_agent/agentkit/__init__.py @@ -154,7 +154,7 @@ OpenAITtsPresetModels, normalize_preset_input, ) -from .preview import OpenAIGPTLive, GeminiCleverChatter, GeminiWalkieTalkie, GeminiPreviewMllmModels, GEMINI_PREVIEW_MLLM_DEFAULTS +from .preview import OpenAIGPTLive, GeminiMLLM, GeminiMLLMModels, GEMINI_MLLM_DEFAULT_MODEL from .vendors import ( AkoolAvatar, AmazonBedrock, @@ -391,10 +391,9 @@ "BaseSTT", "BaseMLLM", "OpenAIGPTLive", - "GeminiWalkieTalkie", - "GeminiCleverChatter", - "GeminiPreviewMllmModels", - "GEMINI_PREVIEW_MLLM_DEFAULTS", + "GeminiMLLM", + "GeminiMLLMModels", + "GEMINI_MLLM_DEFAULT_MODEL", "BaseAvatar", "SampleRate", "ElevenLabsSampleRate", diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index 4f25033..2ea93c7 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -890,7 +890,7 @@ def create_session( if resolved_client is None: raise ValueError("client is required. Pass client=... to Agent(...).") - session_name = name or f"agent-{int(time.time())}" + session_name = name or f"agent-{time.time_ns()}" return AgentSession( client=resolved_client, agent=self, @@ -937,7 +937,7 @@ def create_async_session( if resolved_client is None: raise ValueError("client is required. Pass client=... to Agent(...).") - session_name = name or f"agent-{int(time.time())}" + session_name = name or f"agent-{time.time_ns()}" return AsyncAgentSession( client=resolved_client, agent=self, diff --git a/src/agora_agent/agentkit/preview/__init__.py b/src/agora_agent/agentkit/preview/__init__.py index e73c021..0ead302 100644 --- a/src/agora_agent/agentkit/preview/__init__.py +++ b/src/agora_agent/agentkit/preview/__init__.py @@ -14,16 +14,15 @@ required_preview_features, ) from .vendors import ( - GeminiSTT, - GeminiSTTModels, - OpenAIGPTLive, - GEMINI_PREVIEW_MLLM_DEFAULTS, + GEMINI_MLLM_DEFAULT_MODEL, GEMINI_PREVIEW_MLLM_URL, GEMINI_THINKING_LEVELS, - GeminiCleverChatter, - GeminiPreviewMllmModels, + GeminiMLLM, + GeminiMLLMModels, + GeminiSTT, + GeminiSTTModels, GeminiThinkingLevel, - GeminiWalkieTalkie, + OpenAIGPTLive, ) __all__ = [ @@ -32,13 +31,12 @@ "GeminiSTTModels", "GeminiSTT", "OpenAIGPTLive", - "GEMINI_PREVIEW_MLLM_DEFAULTS", + "GEMINI_MLLM_DEFAULT_MODEL", "GEMINI_PREVIEW_MLLM_URL", "GEMINI_THINKING_LEVELS", - "GeminiCleverChatter", - "GeminiPreviewMllmModels", + "GeminiMLLM", + "GeminiMLLMModels", "GeminiThinkingLevel", - "GeminiWalkieTalkie", "apply_preview_shape", "PreviewFeature", "PreviewFeatures", diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py index 4e3cecf..8fc8831 100644 --- a/src/agora_agent/agentkit/preview/client.py +++ b/src/agora_agent/agentkit/preview/client.py @@ -14,11 +14,11 @@ import typing -from .vendors import GEMINI_PREVIEW_MLLM_URL - +import httpx from ...agent_management.client import AgentManagementClient, AsyncAgentManagementClient from ...agents.client import AgentsClient, AsyncAgentsClient from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .vendors import GEMINI_PREVIEW_MLLM_URL #: Base URL that serves the preview providers. PREVIEW_API_BASE_URL = "https://partner.ai.agora.io/preview/api/conversational-ai-agent" @@ -38,7 +38,7 @@ class PreviewFeatures: vendors on the preview endpoint. """ - #: Deprecated compatibility value. Gemini ASR now uses the production endpoint. + #: Gemini preview MLLM gate. Gemini ASR uses the production endpoint. GEMINI_LIVE = "gemini-live" LIVE_MODELS = "live-models" @@ -46,6 +46,42 @@ class PreviewFeatures: PreviewFeature = str +def _pinned_headers(headers: typing.Any, feature_header: str) -> httpx.Headers: + merged = httpx.Headers(headers) + merged[PREVIEW_FEATURE_HEADER] = feature_header + return merged + + +class _GatedSyncHttpxClient: + """Pin the preview header after generated per-call header overrides.""" + + def __init__(self, inner: httpx.Client, features: typing.Sequence[str]): + self._inner = inner + self._feature_header = ",".join(features) + + def request(self, *args: typing.Any, **kwargs: typing.Any) -> httpx.Response: + kwargs["headers"] = _pinned_headers(kwargs.get("headers"), self._feature_header) + return self._inner.request(*args, **kwargs) + + def stream(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: + kwargs["headers"] = _pinned_headers(kwargs.get("headers"), self._feature_header) + return self._inner.stream(*args, **kwargs) + + +class _GatedAsyncHttpxClient: + def __init__(self, inner: httpx.AsyncClient, features: typing.Sequence[str]): + self._inner = inner + self._feature_header = ",".join(features) + + async def request(self, *args: typing.Any, **kwargs: typing.Any) -> httpx.Response: + kwargs["headers"] = _pinned_headers(kwargs.get("headers"), self._feature_header) + return await self._inner.request(*args, **kwargs) + + def stream(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: + kwargs["headers"] = _pinned_headers(kwargs.get("headers"), self._feature_header) + return self._inner.stream(*args, **kwargs) + + def _preview_headers( features: typing.Sequence[str], headers: typing.Optional[typing.Dict[str, str]], @@ -57,7 +93,10 @@ def _preview_headers( it routes to the production environment, where the preview providers do not exist. Use ``features`` to change the value. """ - merged: typing.Dict[str, str] = dict(headers or {}) + merged: typing.Dict[str, str] = { + key: value for key, value in (headers or {}).items() + if key.lower() != PREVIEW_FEATURE_HEADER + } merged[PREVIEW_FEATURE_HEADER] = ",".join(features) return merged @@ -75,15 +114,16 @@ def create_preview_session_clients( "headers": _preview_headers(features, source.get_custom_headers()), "base_url": PREVIEW_API_BASE_URL, "timeout": source.get_timeout(), - "httpx_client": source.httpx_client.httpx_client, } if isinstance(source, AsyncClientWrapper): + kwargs["httpx_client"] = typing.cast(httpx.AsyncClient, _GatedAsyncHttpxClient(source.httpx_client.httpx_client, features)) async_wrapper = AsyncClientWrapper(**kwargs) return ( AsyncAgentsClient(client_wrapper=async_wrapper), AsyncAgentManagementClient(client_wrapper=async_wrapper), ) if isinstance(source, SyncClientWrapper): + kwargs["httpx_client"] = typing.cast(httpx.Client, _GatedSyncHttpxClient(source.httpx_client.httpx_client, features)) sync_wrapper = SyncClientWrapper(**kwargs) return ( AgentsClient(client_wrapper=sync_wrapper), @@ -103,8 +143,8 @@ def create_preview_session_clients( { "walkie-talkie", "clever-chatter", - "gemini-3.5-live", - "gemini-3.5-live-thinking", + "models/gemini-3.8-live", + "models/gemini-3.8-live-extended-thinking", } ) diff --git a/src/agora_agent/agentkit/preview/vendors.py b/src/agora_agent/agentkit/preview/vendors.py index f8d2a1e..c6d257d 100644 --- a/src/agora_agent/agentkit/preview/vendors.py +++ b/src/agora_agent/agentkit/preview/vendors.py @@ -14,7 +14,7 @@ from ..vendors.base import BaseMLLM, ensure_mcp_transport from ..vendors.mllm import MllmTurnDetectionConfig from ..vendors.stt import GeminiSTT, GeminiSTTModels -from pydantic import ConfigDict, Field +from pydantic import ConfigDict, Field, field_validator from typing_extensions import Literal _OpenAIApiKey = Field(..., min_length=1, description="OpenAI API key") @@ -142,13 +142,12 @@ def to_config(self) -> Dict[str, Any]: __all__ = [ "OpenAIGPTLive", - "GEMINI_PREVIEW_MLLM_DEFAULTS", + "GEMINI_MLLM_DEFAULT_MODEL", "GEMINI_PREVIEW_MLLM_URL", "GEMINI_THINKING_LEVELS", - "GeminiCleverChatter", - "GeminiPreviewMllmModels", + "GeminiMLLM", + "GeminiMLLMModels", "GeminiThinkingLevel", - "GeminiWalkieTalkie", "GeminiSTTModels", "GeminiSTT", ] @@ -160,37 +159,26 @@ def to_config(self) -> Dict[str, Any]: _ApiKey = Field(..., min_length=1, description="Google API key") -class GeminiPreviewMllmModels: +class GeminiMLLMModels: """Preview MLLM model names. - Google is renaming these from codenames to conventional model IDs - (``walkie-talkie`` -> ``gemini-3.5-live``, ``clever-chatter`` -> - ``gemini-3.5-live-thinking``). Both spellings are listed because both are - accepted during the rollout; see :data:`GEMINI_PREVIEW_MLLM_DEFAULTS` for - which one the vendor classes send. + The codenames remain defaults until the new Google IDs launch. The + ``models/`` prefix is part of each 3.8 model ID. """ #: Codename for the low-latency model. Currently the default. WALKIE_TALKIE = "walkie-talkie" - #: Codename for the reasoning model. Currently the default. + #: Codename for the reasoning model. CLEVER_CHATTER = "clever-chatter" - #: Post-rename name for :class:`GeminiWalkieTalkie`. - LIVE_35 = "gemini-3.5-live" - #: Post-rename name for :class:`GeminiCleverChatter`. - LIVE_35_THINKING = "gemini-3.5-live-thinking" + LIVE_38 = "models/gemini-3.8-live" + LIVE_38_EXTENDED_THINKING = "models/gemini-3.8-live-extended-thinking" -#: The model name each preview MLLM class sends by default. +#: The model name the Gemini MLLM sends by default. #: -#: Still the codenames: as of 2026-08-19 the renamed IDs are propagating through -#: Google's fleet and are not live everywhere, while the codenames work. When -#: Google confirms the rename has landed, point these two entries at ``LIVE_35`` -#: / ``LIVE_35_THINKING`` — that is the whole cutover. Callers who need the new -#: names sooner can pass ``model=`` per instance. -GEMINI_PREVIEW_MLLM_DEFAULTS = { - "walkie_talkie": GeminiPreviewMllmModels.WALKIE_TALKIE, - "clever_chatter": GeminiPreviewMllmModels.CLEVER_CHATTER, -} +#: Keep the working codenames until Google activates the 3.8 IDs. Callers can +#: select either new ID using ``model=`` without waiting for an SDK release. +GEMINI_MLLM_DEFAULT_MODEL = GeminiMLLMModels.WALKIE_TALKIE GEMINI_THINKING_LEVELS = ("low", "medium", "high") @@ -201,8 +189,8 @@ class GeminiPreviewMllmModels: GEMINI_PREVIEW_MLLM_URL = "https://generativelanguage.googleapis.com" -class _GeminiPreviewMLLM(BaseMLLM): - """Shared envelope for the preview Gemini MLLMs. +class GeminiMLLM(BaseMLLM): + """Gemini preview MLLM for current codenames and the 3.8 launch IDs. Differs from the production ``GeminiLive`` in two ways the preview gateway requires: ``params.api_key`` is repeated inside ``params``, and @@ -214,6 +202,9 @@ class _GeminiPreviewMLLM(BaseMLLM): api_key: str = _ApiKey model: Optional[str] = Field(default=None, description="Override the preview model name") + thinking_level: Optional[GeminiThinkingLevel] = Field( + default=None, description="Reasoning budget for clever-chatter and extended thinking" + ) voice: Optional[str] = Field(default=None, description="Voice name (e.g., Puck)") language_codes: Optional[List[str]] = Field( default=None, @@ -245,15 +236,18 @@ class _GeminiPreviewMLLM(BaseMLLM): additional_params: Optional[Dict[str, Any]] = Field(default=None) turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None) - #: Set by each concrete subclass. - _default_model: str = "" + _default_model: str = GEMINI_MLLM_DEFAULT_MODEL - def _model_params(self) -> Dict[str, Any]: - """Model-specific params merged after ``voice``.""" - return {} + @field_validator("api_key") + @classmethod + def _validate_api_key(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("GeminiMLLM requires api_key") + return value def to_config(self) -> Dict[str, Any]: - model = self.model if self.model is not None else self._default_model + model = (self.model or "").strip() or self._default_model voice = self.voice if self.voice is not None else "Puck" url = self.url if self.url is not None else GEMINI_PREVIEW_MLLM_URL @@ -261,7 +255,11 @@ def to_config(self) -> Dict[str, Any]: params["api_key"] = self.api_key params["model"] = model params["voice"] = voice - params.update(self._model_params()) + if model in (GeminiMLLMModels.CLEVER_CHATTER, GeminiMLLMModels.LIVE_38_EXTENDED_THINKING): + if self.thinking_level is not None: + params["thinking_level"] = self.thinking_level + else: + params.pop("thinking_level", None) # Plural array, and omitted when unset. The singular ``params.language`` # belongs to xAI Grok in the Agora schema, and the production Gemini Live # provider sends no language field at all. @@ -303,26 +301,3 @@ def to_config(self) -> Dict[str, Any]: config["turn_detection"] = self.turn_detection return config - - -class GeminiWalkieTalkie(_GeminiPreviewMLLM): - """Gemini walkie-talkie MLLM (preview).""" - - _default_model: str = GEMINI_PREVIEW_MLLM_DEFAULTS["walkie_talkie"] - - -class GeminiCleverChatter(_GeminiPreviewMLLM): - """Gemini clever-chatter MLLM (preview). - - ``thinking_level`` is exclusive to this model — it is a separate class - rather than a flag on the walkie-talkie so the distinction is structural. - """ - - thinking_level: Optional[GeminiThinkingLevel] = Field( - default=None, description="Reasoning budget: low, medium, or high" - ) - - _default_model: str = GEMINI_PREVIEW_MLLM_DEFAULTS["clever_chatter"] - - def _model_params(self) -> Dict[str, Any]: - return {} if self.thinking_level is None else {"thinking_level": self.thinking_level} diff --git a/tests/custom/test_agentkit_agent.py b/tests/custom/test_agentkit_agent.py index 63b3c07..d7cf47e 100644 --- a/tests/custom/test_agentkit_agent.py +++ b/tests/custom/test_agentkit_agent.py @@ -50,6 +50,20 @@ def test_generated_core_aliases_are_public(): assert AvatarVendor is not None +def test_default_session_names_do_not_collide_within_one_second(monkeypatch): + import agora_agent.agentkit.agent as agent_module + + monkeypatch.setattr(agent_module.time, "time", lambda: 1_700_000_000.0) + timestamps = iter((1_700_000_000_000_000_001, 1_700_000_000_000_000_002)) + monkeypatch.setattr(agent_module.time, "time_ns", lambda: next(timestamps)) + agent = Agent(test_client()) + options = {"channel": "room", "agent_uid": "1", "remote_uids": ["100"]} + first = agent.create_session(**options) + second = agent.create_async_session(**options) + + assert first._name != second._name + + def test_model_copy_helper_supports_pydantic_v1_copy_api(): copied = Agent._copy_model_update(_CopyOnlyModel(enable_rtm=True), {"data_channel": "rtm"}) # noqa: SLF001 diff --git a/tests/custom/test_preview.py b/tests/custom/test_preview.py index 7f416e6..5aabd27 100644 --- a/tests/custom/test_preview.py +++ b/tests/custom/test_preview.py @@ -140,3 +140,32 @@ def test_preview_client_factory_supports_sync_and_async_clients() -> None: wrapper = generated_client._raw_client._client_wrapper assert wrapper.get_base_url() == PREVIEW_API_BASE_URL assert wrapper.get_custom_headers()[PREVIEW_FEATURE_HEADER] == TEST_FEATURE + + +def test_sync_preview_gate_survives_case_variant_per_call_headers() -> None: + recorder = _Recorder() + client = Agora( + area=Area.US, app_id=APP_ID, app_certificate=APP_CERTIFICATE, + headers={"Agora-Feature": "caller-value"}, + httpx_client=httpx.Client(transport=recorder), + ) + agents, _ = create_preview_session_clients(client, [TEST_FEATURE]) + agents.stop(APP_ID, "agent-1", request_options={"additional_headers": {"AGORA-FEATURE": "wrong", "Authorization": "agora token=fake-token"}}) + assert recorder.requests[0].headers.get_list(PREVIEW_FEATURE_HEADER) == [TEST_FEATURE] + assert recorder.requests[0].headers["Authorization"] == "agora token=fake-token" + assert client._client_wrapper.get_custom_headers()["Agora-Feature"] == "caller-value" + + +@pytest.mark.asyncio +async def test_async_preview_gate_survives_case_variant_per_call_headers() -> None: + recorder = _Recorder() + client = AsyncAgora( + area=Area.US, app_id=APP_ID, app_certificate=APP_CERTIFICATE, + headers={"Agora-Feature": "caller-value"}, + httpx_client=httpx.AsyncClient(transport=recorder), + ) + agents, _ = create_preview_session_clients(client, [TEST_FEATURE]) + await agents.stop(APP_ID, "agent-1", request_options={"additional_headers": {"AGORA-FEATURE": "wrong", "Authorization": "agora token=fake-token"}}) + assert recorder.requests[0].headers.get_list(PREVIEW_FEATURE_HEADER) == [TEST_FEATURE] + assert recorder.requests[0].headers["Authorization"] == "agora token=fake-token" + assert client._client_wrapper.get_custom_headers()["Agora-Feature"] == "caller-value" diff --git a/tests/custom/test_preview_gemini_merge.py b/tests/custom/test_preview_gemini_merge.py index 6328586..7e12a3c 100644 --- a/tests/custom/test_preview_gemini_merge.py +++ b/tests/custom/test_preview_gemini_merge.py @@ -1,15 +1,18 @@ +import pytest +from pydantic import ValidationError + from agora_agent.agentkit.preview import ( - GeminiCleverChatter, - GeminiWalkieTalkie, - PreviewFeatures, + GeminiMLLM, + GeminiMLLMModels, OpenAIGPTLive, + PreviewFeatures, apply_preview_shape, required_preview_features, ) def test_gemini_and_gpt_live_use_their_own_feature_gates(): - for vendor in (GeminiWalkieTalkie(api_key="test-key"), GeminiCleverChatter(api_key="test-key")): + for vendor in (GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.WALKIE_TALKIE), GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.CLEVER_CHATTER)): properties = {"mllm": vendor.to_config()} assert required_preview_features(properties) == [PreviewFeatures.GEMINI_LIVE] assert required_preview_features({"mllm": OpenAIGPTLive(api_key="test-key").to_config()}) == [ @@ -18,8 +21,35 @@ def test_gemini_and_gpt_live_use_their_own_feature_gates(): def test_gemini_greeting_uses_preview_wire_field(): - properties = {"mllm": GeminiWalkieTalkie(api_key="test-key").to_config()} + properties = {"mllm": GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.WALKIE_TALKIE).to_config()} properties["mllm"]["greeting_message"] = "Hello" apply_preview_shape(properties) assert properties["mllm"]["greeting"] == "Hello" assert "greeting_message" not in properties["mllm"] + + +def test_one_gemini_preview_class_supports_codenames_and_38_ids(): + assert GeminiMLLM(api_key="test-key", model=" ").to_config()["params"]["model"] == GeminiMLLMModels.WALKIE_TALKIE + models = ( + (GeminiMLLMModels.WALKIE_TALKIE, "medium"), + (GeminiMLLMModels.CLEVER_CHATTER, "medium"), + (GeminiMLLMModels.LIVE_38, "medium"), + (GeminiMLLMModels.LIVE_38_EXTENDED_THINKING, "medium"), + ) + for model, thinking in models: + config = GeminiMLLM(api_key="test-key", model=model, thinking_level=thinking, + additional_params={"thinking_level": "high"}).to_config() + assert config["params"]["model"] == model + expected_thinking = thinking if model in (GeminiMLLMModels.CLEVER_CHATTER, GeminiMLLMModels.LIVE_38_EXTENDED_THINKING) else None + assert config["params"].get("thinking_level") == expected_thinking + assert required_preview_features({"mllm": config}) == [PreviewFeatures.GEMINI_LIVE] + # Explicit IDs must route even in a hand-written config without the + # SDK vendor's duplicate-key preview envelope. + assert required_preview_features({"mllm": {"vendor": "gemini", "params": {"model": model}}}) == [ + PreviewFeatures.GEMINI_LIVE + ] + + +def test_gemini_mllm_rejects_blank_api_key(): + with pytest.raises(ValidationError, match="GeminiMLLM requires api_key"): + GeminiMLLM(api_key=" ") From a66d0d512859c18faeaf1daac98401ac4074c7cb Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Mon, 14 Sep 2026 21:49:33 -0700 Subject: [PATCH 03/57] fix: declare dynamic package version for uv --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 5b17970..bea7825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [project] name = "agora-agents" +dynamic = ["version"] [tool.poetry] name = "agora-agents" From 115b7debc19a294413bdbef745b5baf114645596 Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Mon, 14 Sep 2026 23:55:07 -0700 Subject: [PATCH 04/57] fix: guard optional preview headers in tests --- tests/custom/test_preview.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/custom/test_preview.py b/tests/custom/test_preview.py index 5aabd27..ca08b8b 100644 --- a/tests/custom/test_preview.py +++ b/tests/custom/test_preview.py @@ -139,7 +139,9 @@ def test_preview_client_factory_supports_sync_and_async_clients() -> None: for generated_client in (sync_agents, sync_management, async_agents, async_management): wrapper = generated_client._raw_client._client_wrapper assert wrapper.get_base_url() == PREVIEW_API_BASE_URL - assert wrapper.get_custom_headers()[PREVIEW_FEATURE_HEADER] == TEST_FEATURE + custom_headers = wrapper.get_custom_headers() + assert custom_headers is not None + assert custom_headers[PREVIEW_FEATURE_HEADER] == TEST_FEATURE def test_sync_preview_gate_survives_case_variant_per_call_headers() -> None: @@ -153,7 +155,9 @@ def test_sync_preview_gate_survives_case_variant_per_call_headers() -> None: agents.stop(APP_ID, "agent-1", request_options={"additional_headers": {"AGORA-FEATURE": "wrong", "Authorization": "agora token=fake-token"}}) assert recorder.requests[0].headers.get_list(PREVIEW_FEATURE_HEADER) == [TEST_FEATURE] assert recorder.requests[0].headers["Authorization"] == "agora token=fake-token" - assert client._client_wrapper.get_custom_headers()["Agora-Feature"] == "caller-value" + custom_headers = client._client_wrapper.get_custom_headers() + assert custom_headers is not None + assert custom_headers["Agora-Feature"] == "caller-value" @pytest.mark.asyncio @@ -168,4 +172,6 @@ async def test_async_preview_gate_survives_case_variant_per_call_headers() -> No await agents.stop(APP_ID, "agent-1", request_options={"additional_headers": {"AGORA-FEATURE": "wrong", "Authorization": "agora token=fake-token"}}) assert recorder.requests[0].headers.get_list(PREVIEW_FEATURE_HEADER) == [TEST_FEATURE] assert recorder.requests[0].headers["Authorization"] == "agora token=fake-token" - assert client._client_wrapper.get_custom_headers()["Agora-Feature"] == "caller-value" + custom_headers = client._client_wrapper.get_custom_headers() + assert custom_headers is not None + assert custom_headers["Agora-Feature"] == "caller-value" From 87557c07c34d16e48858e5ee764d02ecf06c38f5 Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Tue, 15 Sep 2026 08:59:29 -0700 Subject: [PATCH 05/57] Fix Gemini MLLM API key request shape --- docs/guides/preview-endpoint.md | 5 +++-- src/agora_agent/agentkit/preview/client.py | 10 ++++------ src/agora_agent/agentkit/preview/vendors.py | 8 +++----- tests/custom/test_preview_gemini_merge.py | 12 +++++++++++- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/guides/preview-endpoint.md b/docs/guides/preview-endpoint.md index 5f4d838..557ba1c 100644 --- a/docs/guides/preview-endpoint.md +++ b/docs/guides/preview-endpoint.md @@ -34,8 +34,9 @@ The current model IDs are `walkie-talkie` and `clever-chatter`; the launch IDs are `models/gemini-3.8-live` and `models/gemini-3.8-live-extended-thinking`. Set `thinking_level="medium"` for extended thinking. `GeminiMLLM` sends it only for `clever-chatter` or the extended-thinking ID. The working walkie -codename remains the default until Google activates the launch IDs. Gemini -sessions send `agora-feature: gemini-live`, while GPT Live retains `live-models`. +codename remains the default until Google activates the launch IDs. The Gemini +credential is sent once as `mllm.api_key`, never as `mllm.params.api_key`. +Gemini sessions send `agora-feature: gemini-live`, while GPT Live retains `live-models`. ## Session-scoped routing diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py index 8fc8831..9768f25 100644 --- a/src/agora_agent/agentkit/preview/client.py +++ b/src/agora_agent/agentkit/preview/client.py @@ -152,10 +152,9 @@ def create_preview_session_clients( def _has_preview_mllm_envelope(mllm: typing.Mapping[str, typing.Any]) -> bool: """Whether a config carries the envelope the preview MLLM classes emit. - That envelope is the API key repeated inside ``params`` plus a ``url`` on the - Gemini Developer API host. The production ``GeminiLive`` vendor emits - neither — it leaves ``params.api_key`` unset and defaults ``url`` to ``""`` - or a WebSocket endpoint. + That envelope is a top-level ``mllm.api_key`` plus a ``url`` on the Gemini + Developer API host. The production ``GeminiLive`` vendor uses a different + URL (an empty string or WebSocket endpoint). This is the second recognition path, and it exists because keying only off :data:`_PREVIEW_MLLM_MODELS` makes an unrecognised model name fail silently: @@ -165,8 +164,7 @@ def _has_preview_mllm_envelope(mllm: typing.Mapping[str, typing.Any]) -> bool: following this SDK's own advice to override ``model`` when Google renames one ahead of a release, so the failure has to not be silent. """ - params = mllm.get("params") - api_key = params.get("api_key") if isinstance(params, dict) else None + api_key = mllm.get("api_key") url = mllm.get("url") return isinstance(api_key, str) and isinstance(url, str) and url.startswith(GEMINI_PREVIEW_MLLM_URL) diff --git a/src/agora_agent/agentkit/preview/vendors.py b/src/agora_agent/agentkit/preview/vendors.py index c6d257d..13a00c5 100644 --- a/src/agora_agent/agentkit/preview/vendors.py +++ b/src/agora_agent/agentkit/preview/vendors.py @@ -192,10 +192,8 @@ class GeminiMLLMModels: class GeminiMLLM(BaseMLLM): """Gemini preview MLLM for current codenames and the 3.8 launch IDs. - Differs from the production ``GeminiLive`` in two ways the preview - gateway requires: ``params.api_key`` is repeated inside ``params``, and - ``url`` defaults to the Gemini Developer API host rather than a WebSocket - endpoint. + The API key is sent as ``mllm.api_key``. The preview ``url`` defaults to + the Gemini Developer API host rather than a WebSocket endpoint. """ model_config = ConfigDict(extra="forbid") @@ -252,7 +250,7 @@ def to_config(self) -> Dict[str, Any]: url = self.url if self.url is not None else GEMINI_PREVIEW_MLLM_URL params: Dict[str, Any] = dict(self.additional_params or {}) - params["api_key"] = self.api_key + params.pop("api_key", None) params["model"] = model params["voice"] = voice if model in (GeminiMLLMModels.CLEVER_CHATTER, GeminiMLLMModels.LIVE_38_EXTENDED_THINKING): diff --git a/tests/custom/test_preview_gemini_merge.py b/tests/custom/test_preview_gemini_merge.py index 7e12a3c..821a3d9 100644 --- a/tests/custom/test_preview_gemini_merge.py +++ b/tests/custom/test_preview_gemini_merge.py @@ -38,7 +38,9 @@ def test_one_gemini_preview_class_supports_codenames_and_38_ids(): ) for model, thinking in models: config = GeminiMLLM(api_key="test-key", model=model, thinking_level=thinking, - additional_params={"thinking_level": "high"}).to_config() + additional_params={"thinking_level": "high", "api_key": "ignored-key"}).to_config() + assert config["api_key"] == "test-key" + assert "api_key" not in config["params"] assert config["params"]["model"] == model expected_thinking = thinking if model in (GeminiMLLMModels.CLEVER_CHATTER, GeminiMLLMModels.LIVE_38_EXTENDED_THINKING) else None assert config["params"].get("thinking_level") == expected_thinking @@ -53,3 +55,11 @@ def test_one_gemini_preview_class_supports_codenames_and_38_ids(): def test_gemini_mllm_rejects_blank_api_key(): with pytest.raises(ValidationError, match="GeminiMLLM requires api_key"): GeminiMLLM(api_key=" ") + + +def test_unknown_gemini_model_keeps_preview_greeting_without_nested_api_key(): + properties = {"mllm": GeminiMLLM(api_key="test-key", model="future-live-model").to_config()} + properties["mllm"]["greeting_message"] = "Hello" + apply_preview_shape(properties) + assert properties["mllm"]["greeting"] == "Hello" + assert "greeting_message" not in properties["mllm"] From ad4a83d5b7eb7f6288f7cb8079a271bea1f03b68 Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Tue, 15 Sep 2026 09:26:13 -0700 Subject: [PATCH 06/57] Use public Gemini 3.8 model IDs --- docs/guides/preview-endpoint.md | 9 ++++----- src/agora_agent/agentkit/preview/client.py | 2 -- src/agora_agent/agentkit/preview/vendors.py | 18 ++++++------------ tests/custom/test_preview_gemini_merge.py | 14 ++++++-------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/docs/guides/preview-endpoint.md b/docs/guides/preview-endpoint.md index 557ba1c..257c23c 100644 --- a/docs/guides/preview-endpoint.md +++ b/docs/guides/preview-endpoint.md @@ -10,7 +10,7 @@ Some providers may be released through a preview gateway before their production and `AsyncAgentSession` detect registered preview providers from the resolved start request and route the entire session automatically. -OpenAI GPT Live uses the `live-models` feature. Gemini walkie-talkie and clever-chatter MLLMs use `gemini-live`. +OpenAI GPT Live uses the `live-models` feature. Gemini 3.8 MLLMs use `gemini-live`. Gemini STT has graduated to production and uses the normal regional endpoint. Existing imports of `GeminiSTT` and `GeminiSTTModels` from `agora_agent.agentkit.preview` remain supported as compatibility aliases. @@ -30,11 +30,10 @@ This session uses the preview base URL and sends `agora-feature: live-models`. A client's normal GA regional endpoint without that header. Use the single `GeminiMLLM(api_key=..., model=...)` class with `with_mllm`. -The current model IDs are `walkie-talkie` and `clever-chatter`; the launch IDs -are `models/gemini-3.8-live` and `models/gemini-3.8-live-extended-thinking`. +The model IDs are `models/gemini-3.8-live` and +`models/gemini-3.8-live-extended-thinking`; the low-latency ID is the default. Set `thinking_level="medium"` for extended thinking. `GeminiMLLM` sends it -only for `clever-chatter` or the extended-thinking ID. The working walkie -codename remains the default until Google activates the launch IDs. The Gemini +only for the extended-thinking ID. The Gemini credential is sent once as `mllm.api_key`, never as `mllm.params.api_key`. Gemini sessions send `agora-feature: gemini-live`, while GPT Live retains `live-models`. diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py index 9768f25..c11849f 100644 --- a/src/agora_agent/agentkit/preview/client.py +++ b/src/agora_agent/agentkit/preview/client.py @@ -141,8 +141,6 @@ def create_preview_session_clients( _PREVIEW_MLLM_MODELS = frozenset( { - "walkie-talkie", - "clever-chatter", "models/gemini-3.8-live", "models/gemini-3.8-live-extended-thinking", } diff --git a/src/agora_agent/agentkit/preview/vendors.py b/src/agora_agent/agentkit/preview/vendors.py index 13a00c5..ee033df 100644 --- a/src/agora_agent/agentkit/preview/vendors.py +++ b/src/agora_agent/agentkit/preview/vendors.py @@ -162,23 +162,17 @@ def to_config(self) -> Dict[str, Any]: class GeminiMLLMModels: """Preview MLLM model names. - The codenames remain defaults until the new Google IDs launch. The - ``models/`` prefix is part of each 3.8 model ID. + The ``models/`` prefix is part of each model ID. """ - #: Codename for the low-latency model. Currently the default. - WALKIE_TALKIE = "walkie-talkie" - #: Codename for the reasoning model. - CLEVER_CHATTER = "clever-chatter" LIVE_38 = "models/gemini-3.8-live" LIVE_38_EXTENDED_THINKING = "models/gemini-3.8-live-extended-thinking" #: The model name the Gemini MLLM sends by default. #: -#: Keep the working codenames until Google activates the 3.8 IDs. Callers can -#: select either new ID using ``model=`` without waiting for an SDK release. -GEMINI_MLLM_DEFAULT_MODEL = GeminiMLLMModels.WALKIE_TALKIE +#: Low-latency Gemini voice is the default. +GEMINI_MLLM_DEFAULT_MODEL = GeminiMLLMModels.LIVE_38 GEMINI_THINKING_LEVELS = ("low", "medium", "high") @@ -190,7 +184,7 @@ class GeminiMLLMModels: class GeminiMLLM(BaseMLLM): - """Gemini preview MLLM for current codenames and the 3.8 launch IDs. + """Gemini preview MLLM for the two public 3.8 model IDs. The API key is sent as ``mllm.api_key``. The preview ``url`` defaults to the Gemini Developer API host rather than a WebSocket endpoint. @@ -201,7 +195,7 @@ class GeminiMLLM(BaseMLLM): api_key: str = _ApiKey model: Optional[str] = Field(default=None, description="Override the preview model name") thinking_level: Optional[GeminiThinkingLevel] = Field( - default=None, description="Reasoning budget for clever-chatter and extended thinking" + default=None, description="Reasoning budget for extended thinking" ) voice: Optional[str] = Field(default=None, description="Voice name (e.g., Puck)") language_codes: Optional[List[str]] = Field( @@ -253,7 +247,7 @@ def to_config(self) -> Dict[str, Any]: params.pop("api_key", None) params["model"] = model params["voice"] = voice - if model in (GeminiMLLMModels.CLEVER_CHATTER, GeminiMLLMModels.LIVE_38_EXTENDED_THINKING): + if model == GeminiMLLMModels.LIVE_38_EXTENDED_THINKING: if self.thinking_level is not None: params["thinking_level"] = self.thinking_level else: diff --git a/tests/custom/test_preview_gemini_merge.py b/tests/custom/test_preview_gemini_merge.py index 821a3d9..25cb61e 100644 --- a/tests/custom/test_preview_gemini_merge.py +++ b/tests/custom/test_preview_gemini_merge.py @@ -12,7 +12,7 @@ def test_gemini_and_gpt_live_use_their_own_feature_gates(): - for vendor in (GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.WALKIE_TALKIE), GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.CLEVER_CHATTER)): + for vendor in (GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.LIVE_38), GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.LIVE_38_EXTENDED_THINKING)): properties = {"mllm": vendor.to_config()} assert required_preview_features(properties) == [PreviewFeatures.GEMINI_LIVE] assert required_preview_features({"mllm": OpenAIGPTLive(api_key="test-key").to_config()}) == [ @@ -21,18 +21,16 @@ def test_gemini_and_gpt_live_use_their_own_feature_gates(): def test_gemini_greeting_uses_preview_wire_field(): - properties = {"mllm": GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.WALKIE_TALKIE).to_config()} + properties = {"mllm": GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.LIVE_38).to_config()} properties["mllm"]["greeting_message"] = "Hello" apply_preview_shape(properties) assert properties["mllm"]["greeting"] == "Hello" assert "greeting_message" not in properties["mllm"] -def test_one_gemini_preview_class_supports_codenames_and_38_ids(): - assert GeminiMLLM(api_key="test-key", model=" ").to_config()["params"]["model"] == GeminiMLLMModels.WALKIE_TALKIE +def test_one_gemini_preview_class_supports_public_38_ids(): + assert GeminiMLLM(api_key="test-key", model=" ").to_config()["params"]["model"] == GeminiMLLMModels.LIVE_38 models = ( - (GeminiMLLMModels.WALKIE_TALKIE, "medium"), - (GeminiMLLMModels.CLEVER_CHATTER, "medium"), (GeminiMLLMModels.LIVE_38, "medium"), (GeminiMLLMModels.LIVE_38_EXTENDED_THINKING, "medium"), ) @@ -42,11 +40,11 @@ def test_one_gemini_preview_class_supports_codenames_and_38_ids(): assert config["api_key"] == "test-key" assert "api_key" not in config["params"] assert config["params"]["model"] == model - expected_thinking = thinking if model in (GeminiMLLMModels.CLEVER_CHATTER, GeminiMLLMModels.LIVE_38_EXTENDED_THINKING) else None + expected_thinking = thinking if model == GeminiMLLMModels.LIVE_38_EXTENDED_THINKING else None assert config["params"].get("thinking_level") == expected_thinking assert required_preview_features({"mllm": config}) == [PreviewFeatures.GEMINI_LIVE] # Explicit IDs must route even in a hand-written config without the - # SDK vendor's duplicate-key preview envelope. + # SDK vendor's preview envelope. assert required_preview_features({"mllm": {"vendor": "gemini", "params": {"model": model}}}) == [ PreviewFeatures.GEMINI_LIVE ] From 39a3edcb6375944d9fe55008c08686ad4d6f1acb Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Tue, 15 Sep 2026 10:43:02 -0700 Subject: [PATCH 07/57] docs: document Gemini 3.8 MLLM public release --- README.md | 16 ++++++++++++++++ docs/guides/mllm-flow.md | 2 ++ docs/reference/vendors.md | 14 ++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/README.md b/README.md index f250daf..74da7d7 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ Use `with_mllm()` for OpenAI Realtime, Gemini Live, Vertex AI, or xAI Grok. No S ```python from agora_agent import Agent, Agora, Area, OpenAIRealtime +import os import time client = Agora( @@ -206,6 +207,21 @@ session = agent.create_session( session.start() ``` +For Gemini 3.8 Live Extended Thinking, use the same single `GeminiMLLM` class as the regular Live model: + +```python +from agora_agent import GeminiMLLM + +gemini_agent = Agent(client=client).with_mllm(GeminiMLLM( + api_key=os.environ["GOOGLE_API_KEY"], + model="models/gemini-3.8-live-extended-thinking", + thinking_level="medium", + greeting="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/docs/guides/mllm-flow.md b/docs/guides/mllm-flow.md index ae1d036..bcf1c65 100644 --- a/docs/guides/mllm-flow.md +++ b/docs/guides/mllm-flow.md @@ -95,6 +95,8 @@ asyncio.run(main()) ## Gemini Live +Gemini 3.8 Live and Gemini 3.8 Live Extended Thinking use the separate `GeminiMLLM` preview class, not `GeminiLive`. See the [Preview Endpoint guide](./preview-endpoint.md) for the public model IDs, thinking level, and feature header. + Gemini Live uses a Google AI API key: ```python diff --git a/docs/reference/vendors.md b/docs/reference/vendors.md index 76b8328..8811b9a 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -954,6 +954,20 @@ CN Alibaba Cloud Qwen Omni Realtime vendor (`mllm.vendor`: `"qwen_omni"`). Impor | `params` | `Dict[str, Any]` | No | `None` | Additional Qwen Omni parameters | | `turn_detection` | `MllmTurnDetectionConfig` | No | `None` | MLLM turn detection configuration; overrides top-level `turn_detection` | +### `GeminiMLLM` (preview) + +`GeminiMLLM(api_key=..., model=...)` configures either public Gemini 3.8 voice model. A session using this provider routes to the preview gateway with `agora-feature: gemini-live`. See [Preview Endpoint](../guides/preview-endpoint.md). + +| Parameter | Default / wire behavior | +|---|---| +| `api_key` | Required; sent once as `mllm.api_key`. | +| `model` | Defaults to `models/gemini-3.8-live`; also accepts `models/gemini-3.8-live-extended-thinking`. | +| `thinking_level` | `low`, `medium`, or `high`; sent as `mllm.params.thinking_level` only for Extended Thinking. | +| `voice` | Defaults to `Puck`. | +| `instructions` | System prompt in `mllm.params.instructions`. | +| `greeting` | Initial utterance in `mllm.greeting`. | +| `language_codes` | Optional language array in `mllm.params.language_codes`. | + ### `GeminiLive` | Parameter | Type | Required | Default | Description | From 197325ba1c6086195fbb992fd9a9d3d308932c44 Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Tue, 15 Sep 2026 12:16:03 -0700 Subject: [PATCH 08/57] feat: extend GeminiLive for Gemini 3.8 models --- README.md | 8 +- docs/guides/mllm-flow.md | 2 +- docs/guides/preview-endpoint.md | 4 +- docs/reference/vendors.md | 22 +-- src/agora_agent/__init__.py | 3 +- src/agora_agent/agentkit/__init__.py | 5 +- src/agora_agent/agentkit/preview/__init__.py | 6 +- src/agora_agent/agentkit/preview/client.py | 4 +- src/agora_agent/agentkit/preview/vendors.py | 182 +++++++------------ src/agora_agent/agentkit/vendors/mllm.py | 25 ++- tests/custom/test_preview_gemini_merge.py | 26 +-- 11 files changed, 117 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 74da7d7..2e0eaef 100644 --- a/README.md +++ b/README.md @@ -207,16 +207,16 @@ session = agent.create_session( session.start() ``` -For Gemini 3.8 Live Extended Thinking, use the same single `GeminiMLLM` class as the regular Live model: +For Gemini 3.8 Live Extended Thinking, use the same single `GeminiLive` class as the regular Live model: ```python -from agora_agent import GeminiMLLM +from agora_agent import GeminiLive -gemini_agent = Agent(client=client).with_mllm(GeminiMLLM( +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="Hello! Ready to chat.", + greeting_message="Hello! Ready to chat.", )) ``` diff --git a/docs/guides/mllm-flow.md b/docs/guides/mllm-flow.md index bcf1c65..4f4414a 100644 --- a/docs/guides/mllm-flow.md +++ b/docs/guides/mllm-flow.md @@ -95,7 +95,7 @@ asyncio.run(main()) ## Gemini Live -Gemini 3.8 Live and Gemini 3.8 Live Extended Thinking use the separate `GeminiMLLM` preview class, not `GeminiLive`. See the [Preview Endpoint guide](./preview-endpoint.md) for the public model IDs, thinking level, and feature header. +Use `GeminiLive` for the existing Gemini Live models and both Gemini 3.8 models. The 3.8 IDs select the preview route automatically; Extended Thinking also accepts `thinking_level`. See the [Preview Endpoint guide](./preview-endpoint.md). Gemini Live uses a Google AI API key: diff --git a/docs/guides/preview-endpoint.md b/docs/guides/preview-endpoint.md index 257c23c..39148ea 100644 --- a/docs/guides/preview-endpoint.md +++ b/docs/guides/preview-endpoint.md @@ -29,10 +29,10 @@ agent_id = session.start() This session uses the preview base URL and sends `agora-feature: live-models`. A session using `GeminiSTT` uses the client's normal GA regional endpoint without that header. -Use the single `GeminiMLLM(api_key=..., model=...)` class with `with_mllm`. +Use the single `GeminiLive(api_key=..., model=...)` class with `with_mllm`. The model IDs are `models/gemini-3.8-live` and `models/gemini-3.8-live-extended-thinking`; the low-latency ID is the default. -Set `thinking_level="medium"` for extended thinking. `GeminiMLLM` sends it +Set `thinking_level="medium"` for extended thinking. `GeminiLive` sends it only for the extended-thinking ID. The Gemini credential is sent once as `mllm.api_key`, never as `mllm.params.api_key`. Gemini sessions send `agora-feature: gemini-live`, while GPT Live retains `live-models`. diff --git a/docs/reference/vendors.md b/docs/reference/vendors.md index 8811b9a..8e6cebe 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -954,30 +954,20 @@ CN Alibaba Cloud Qwen Omni Realtime vendor (`mllm.vendor`: `"qwen_omni"`). Impor | `params` | `Dict[str, Any]` | No | `None` | Additional Qwen Omni parameters | | `turn_detection` | `MllmTurnDetectionConfig` | No | `None` | MLLM turn detection configuration; overrides top-level `turn_detection` | -### `GeminiMLLM` (preview) - -`GeminiMLLM(api_key=..., model=...)` configures either public Gemini 3.8 voice model. A session using this provider routes to the preview gateway with `agora-feature: gemini-live`. See [Preview Endpoint](../guides/preview-endpoint.md). - -| Parameter | Default / wire behavior | -|---|---| -| `api_key` | Required; sent once as `mllm.api_key`. | -| `model` | Defaults to `models/gemini-3.8-live`; also accepts `models/gemini-3.8-live-extended-thinking`. | -| `thinking_level` | `low`, `medium`, or `high`; sent as `mllm.params.thinking_level` only for Extended Thinking. | -| `voice` | Defaults to `Puck`. | -| `instructions` | System prompt in `mllm.params.instructions`. | -| `greeting` | Initial utterance in `mllm.greeting`. | -| `language_codes` | Optional language array in `mllm.params.language_codes`. | - ### `GeminiLive` +`GeminiLive` supports existing Gemini Live models and both public Gemini 3.8 voice models. The 3.8 IDs select the preview gateway with `agora-feature: gemini-live`; older model IDs keep the production route. See [Preview Endpoint](../guides/preview-endpoint.md). + | Parameter | Type | Required | Default | Description | |---|---|---|---|---| | `api_key` | `str` | Yes | — | Google Gemini API key | -| `model` | `str` | Yes | — | Gemini Live model name | +| `model` | `str` | No | `models/gemini-3.8-live` | Gemini Live model name | +| `thinking_level` | `str` | No | `None` | `low`, `medium`, or `high`; sent only for 3.8 Extended Thinking | +| `language_codes` | `List[str]` | No | `None` | 3.8 language codes in `mllm.params.language_codes` | | `url` | `str` | No | `None` | Custom WebSocket URL | | `instructions` | `str` | No | `None` | System instructions | | `voice` | `str` | No | `None` | Voice name | -| `greeting_message` | `str` | No | `None` | Greeting message | +| `greeting_message` | `str` | No | `None` | Greeting message; sent as `mllm.greeting` for 3.8 models | | `failure_message` | `str` | No | `None` | Message played when the model call fails | | `input_modalities` | `List[str]` | No | `None` | Input modalities | | `output_modalities` | `List[str]` | No | `None` | Output modalities | diff --git a/src/agora_agent/__init__.py b/src/agora_agent/__init__.py index f17e1d9..5a1ad53 100644 --- a/src/agora_agent/__init__.py +++ b/src/agora_agent/__init__.py @@ -48,8 +48,7 @@ GeminiSTT, GeminiSTTModels, GeminiLive, - GeminiMLLM, - GeminiMLLMModels, + GeminiLiveModels, GEMINI_MLLM_DEFAULT_MODEL, GenericAvatar, GenericTTS, diff --git a/src/agora_agent/agentkit/__init__.py b/src/agora_agent/agentkit/__init__.py index 46908f9..924431b 100644 --- a/src/agora_agent/agentkit/__init__.py +++ b/src/agora_agent/agentkit/__init__.py @@ -154,7 +154,7 @@ OpenAITtsPresetModels, normalize_preset_input, ) -from .preview import OpenAIGPTLive, GeminiMLLM, GeminiMLLMModels, GEMINI_MLLM_DEFAULT_MODEL +from .preview import OpenAIGPTLive, GeminiLiveModels, GEMINI_MLLM_DEFAULT_MODEL from .vendors import ( AkoolAvatar, AmazonBedrock, @@ -391,8 +391,7 @@ "BaseSTT", "BaseMLLM", "OpenAIGPTLive", - "GeminiMLLM", - "GeminiMLLMModels", + "GeminiLiveModels", "GEMINI_MLLM_DEFAULT_MODEL", "BaseAvatar", "SampleRate", diff --git a/src/agora_agent/agentkit/preview/__init__.py b/src/agora_agent/agentkit/preview/__init__.py index 0ead302..70240ec 100644 --- a/src/agora_agent/agentkit/preview/__init__.py +++ b/src/agora_agent/agentkit/preview/__init__.py @@ -17,8 +17,7 @@ GEMINI_MLLM_DEFAULT_MODEL, GEMINI_PREVIEW_MLLM_URL, GEMINI_THINKING_LEVELS, - GeminiMLLM, - GeminiMLLMModels, + GeminiLiveModels, GeminiSTT, GeminiSTTModels, GeminiThinkingLevel, @@ -34,8 +33,7 @@ "GEMINI_MLLM_DEFAULT_MODEL", "GEMINI_PREVIEW_MLLM_URL", "GEMINI_THINKING_LEVELS", - "GeminiMLLM", - "GeminiMLLMModels", + "GeminiLiveModels", "GeminiThinkingLevel", "apply_preview_shape", "PreviewFeature", diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py index c11849f..6f496eb 100644 --- a/src/agora_agent/agentkit/preview/client.py +++ b/src/agora_agent/agentkit/preview/client.py @@ -151,8 +151,8 @@ def _has_preview_mllm_envelope(mllm: typing.Mapping[str, typing.Any]) -> bool: """Whether a config carries the envelope the preview MLLM classes emit. That envelope is a top-level ``mllm.api_key`` plus a ``url`` on the Gemini - Developer API host. The production ``GeminiLive`` vendor uses a different - URL (an empty string or WebSocket endpoint). + Developer API host. ``GeminiLive`` configs for older model IDs use a + different URL (an empty string or WebSocket endpoint). This is the second recognition path, and it exists because keying only off :data:`_PREVIEW_MLLM_MODELS` makes an unrecognised model name fail silently: diff --git a/src/agora_agent/agentkit/preview/vendors.py b/src/agora_agent/agentkit/preview/vendors.py index ee033df..1305007 100644 --- a/src/agora_agent/agentkit/preview/vendors.py +++ b/src/agora_agent/agentkit/preview/vendors.py @@ -14,7 +14,7 @@ from ..vendors.base import BaseMLLM, ensure_mcp_transport from ..vendors.mllm import MllmTurnDetectionConfig from ..vendors.stt import GeminiSTT, GeminiSTTModels -from pydantic import ConfigDict, Field, field_validator +from pydantic import ConfigDict, Field from typing_extensions import Literal _OpenAIApiKey = Field(..., min_length=1, description="OpenAI API key") @@ -145,21 +145,15 @@ def to_config(self) -> Dict[str, Any]: "GEMINI_MLLM_DEFAULT_MODEL", "GEMINI_PREVIEW_MLLM_URL", "GEMINI_THINKING_LEVELS", - "GeminiMLLM", - "GeminiMLLMModels", + "GeminiLiveModels", + "build_gemini_preview_config", "GeminiThinkingLevel", "GeminiSTTModels", "GeminiSTT", ] -#: Rejects ``api_key=""``. ``Field(...)`` alone makes the key required but still -#: accepts the empty string, which would reach the provider as a blank -#: credential; the TypeScript and Go vendors both refuse it at construction. -_ApiKey = Field(..., min_length=1, description="Google API key") - - -class GeminiMLLMModels: +class GeminiLiveModels: """Preview MLLM model names. The ``models/`` prefix is part of each model ID. @@ -172,7 +166,7 @@ class GeminiMLLMModels: #: The model name the Gemini MLLM sends by default. #: #: Low-latency Gemini voice is the default. -GEMINI_MLLM_DEFAULT_MODEL = GeminiMLLMModels.LIVE_38 +GEMINI_MLLM_DEFAULT_MODEL = GeminiLiveModels.LIVE_38 GEMINI_THINKING_LEVELS = ("low", "medium", "high") @@ -183,113 +177,59 @@ class GeminiMLLMModels: GEMINI_PREVIEW_MLLM_URL = "https://generativelanguage.googleapis.com" -class GeminiMLLM(BaseMLLM): - """Gemini preview MLLM for the two public 3.8 model IDs. - - The API key is sent as ``mllm.api_key``. The preview ``url`` defaults to - the Gemini Developer API host rather than a WebSocket endpoint. - """ - - model_config = ConfigDict(extra="forbid") - - api_key: str = _ApiKey - model: Optional[str] = Field(default=None, description="Override the preview model name") - thinking_level: Optional[GeminiThinkingLevel] = Field( - default=None, description="Reasoning budget for extended thinking" - ) - voice: Optional[str] = Field(default=None, description="Voice name (e.g., Puck)") - language_codes: Optional[List[str]] = Field( - default=None, - description=( - "Languages the model should work in, sent as params.language_codes. " - "The preview MLLMs take the plural array, not the singular `language` " - "(which belongs to xAI Grok). Omitted from the request when unset." - ), - ) - url: Optional[str] = Field(default=None, description="Gemini API host") - instructions: Optional[str] = Field(default=None, description="System instructions") - transcribe_agent: Optional[bool] = Field(default=None, description="Transcribe agent speech") - transcribe_user: Optional[bool] = Field(default=None, description="Transcribe user speech") - affective_dialog: Optional[bool] = Field(default=None, description="Enable affective dialog") - proactive_audio: Optional[bool] = Field(default=None, description="Enable proactive audio") - http_options: Optional[Dict[str, Any]] = Field(default=None, description="HTTP options") - greeting: Optional[str] = Field( - default=None, - description=( - "Agent greeting, serialized to `greeting`. The preview Gemini models " - "read this spelling, not the `greeting_message` the production " - "providers use." - ), - ) - failure_message: Optional[str] = Field(default=None, description="Message played on failure") - input_modalities: Optional[List[str]] = Field(default=None, description="Input modalities") - output_modalities: Optional[List[str]] = Field(default=None, description="Output modalities") - messages: Optional[List[Dict[str, Any]]] = Field(default=None, description="Conversation messages") - additional_params: Optional[Dict[str, Any]] = Field(default=None) - turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None) - - _default_model: str = GEMINI_MLLM_DEFAULT_MODEL - - @field_validator("api_key") - @classmethod - def _validate_api_key(cls, value: str) -> str: - value = value.strip() - if not value: - raise ValueError("GeminiMLLM requires api_key") - return value - - def to_config(self) -> Dict[str, Any]: - model = (self.model or "").strip() or self._default_model - voice = self.voice if self.voice is not None else "Puck" - url = self.url if self.url is not None else GEMINI_PREVIEW_MLLM_URL - - params: Dict[str, Any] = dict(self.additional_params or {}) - params.pop("api_key", None) - params["model"] = model - params["voice"] = voice - if model == GeminiMLLMModels.LIVE_38_EXTENDED_THINKING: - if self.thinking_level is not None: - params["thinking_level"] = self.thinking_level - else: - params.pop("thinking_level", None) - # Plural array, and omitted when unset. The singular ``params.language`` - # belongs to xAI Grok in the Agora schema, and the production Gemini Live - # provider sends no language field at all. - if self.language_codes is not None: - params["language_codes"] = list(self.language_codes) - - if self.instructions is not None: - params["instructions"] = self.instructions - if self.transcribe_agent is not None: - params["transcribe_agent"] = self.transcribe_agent - if self.transcribe_user is not None: - params["transcribe_user"] = self.transcribe_user - if self.affective_dialog is not None: - params["affective_dialog"] = self.affective_dialog - if self.proactive_audio is not None: - params["proactive_audio"] = self.proactive_audio - if self.http_options is not None: - params["http_options"] = self.http_options - - config: Dict[str, Any] = { - "vendor": "gemini", - "api_key": self.api_key, - "url": url, - "params": params, - } - if self.messages is not None: - config["messages"] = self.messages - # ``greeting``, not ``greeting_message``: the preview Gemini models read - # this spelling. See the preview-endpoint guide. - if self.greeting is not None: - config["greeting"] = self.greeting - if self.failure_message is not None: - config["failure_message"] = self.failure_message - if self.input_modalities is not None: - config["input_modalities"] = self.input_modalities - if self.output_modalities is not None: - config["output_modalities"] = self.output_modalities - if self.turn_detection is not None: - config["turn_detection"] = self.turn_detection - - return config +def build_gemini_preview_config(self: Any) -> Dict[str, Any]: + """Serialize GeminiLive options for the Gemini 3.8 preview gateway.""" + model = (self.model or "").strip() or GEMINI_MLLM_DEFAULT_MODEL + voice = self.voice if self.voice is not None else "Puck" + url = self.url if self.url is not None else GEMINI_PREVIEW_MLLM_URL + + params: Dict[str, Any] = dict(self.additional_params or {}) + params.pop("api_key", None) + params["model"] = model + params["voice"] = voice + if model == GeminiLiveModels.LIVE_38_EXTENDED_THINKING: + if self.thinking_level is not None: + params["thinking_level"] = self.thinking_level + else: + params.pop("thinking_level", None) + # Plural array, and omitted when unset. The singular ``params.language`` + # belongs to xAI Grok in the Agora schema, and the production Gemini Live + # provider sends no language field at all. + if self.language_codes is not None: + params["language_codes"] = list(self.language_codes) + + if self.instructions is not None: + params["instructions"] = self.instructions + if self.transcribe_agent is not None: + params["transcribe_agent"] = self.transcribe_agent + if self.transcribe_user is not None: + params["transcribe_user"] = self.transcribe_user + if self.affective_dialog is not None: + params["affective_dialog"] = self.affective_dialog + if self.proactive_audio is not None: + params["proactive_audio"] = self.proactive_audio + if self.http_options is not None: + params["http_options"] = self.http_options + + config: Dict[str, Any] = { + "vendor": "gemini", + "api_key": self.api_key, + "url": url, + "params": params, + } + if self.messages is not None: + config["messages"] = self.messages + # ``greeting``, not ``greeting_message``: the preview Gemini models read + # this spelling. See the preview-endpoint guide. + if self.greeting_message is not None: + config["greeting"] = self.greeting_message + if self.failure_message is not None: + config["failure_message"] = self.failure_message + if self.input_modalities is not None: + config["input_modalities"] = self.input_modalities + if self.output_modalities is not None: + config["output_modalities"] = self.output_modalities + if self.turn_detection is not None: + config["turn_detection"] = self.turn_detection + + return config diff --git a/src/agora_agent/agentkit/vendors/mllm.py b/src/agora_agent/agentkit/vendors/mllm.py index bb207b7..829124a 100644 --- a/src/agora_agent/agentkit/vendors/mllm.py +++ b/src/agora_agent/agentkit/vendors/mllm.py @@ -1,8 +1,9 @@ from typing import Any, Dict, List, Optional +from typing_extensions import Literal from ...types.mllm_turn_detection import MllmTurnDetection from .base import BaseMLLM -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator MllmTurnDetectionConfig = MllmTurnDetection @@ -261,7 +262,11 @@ class GeminiLiveOptions(BaseModel): model_config = ConfigDict(extra="forbid") api_key: str = Field(..., description="Google API key") - model: str = Field(..., description="Gemini Live model name") + model: str = Field(default="models/gemini-3.8-live", description="Gemini Live model name") + thinking_level: Optional[Literal["low", "medium", "high"]] = Field( + default=None, description="Reasoning budget for the 3.8 extended-thinking model" + ) + language_codes: Optional[List[str]] = Field(default=None, description="Languages for Gemini 3.8") url: Optional[str] = Field(default=None, description="WebSocket URL") instructions: Optional[str] = Field(default=None, description="System instructions") voice: Optional[str] = Field(default=None, description="Voice name") @@ -278,13 +283,27 @@ class GeminiLiveOptions(BaseModel): turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None, description="MLLM turn detection configuration") failure_message: Optional[str] = Field(default=None, description="Message played on failure") + @field_validator("api_key") + @classmethod + def _validate_api_key(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("GeminiLive requires api_key") + return value + class GeminiLive(GeminiLiveOptions, BaseMLLM): def to_config(self) -> Dict[str, Any]: + from ..preview.vendors import GeminiLiveModels, build_gemini_preview_config + + selected_model = self.model.strip() or GeminiLiveModels.LIVE_38 + if selected_model in (GeminiLiveModels.LIVE_38, GeminiLiveModels.LIVE_38_EXTENDED_THINKING): + return build_gemini_preview_config(self) + inner_params: Dict[str, Any] = {} if self.additional_params is not None: inner_params.update(self.additional_params) - inner_params["model"] = self.model + inner_params["model"] = selected_model if self.instructions is not None: inner_params["instructions"] = self.instructions if self.voice is not None: diff --git a/tests/custom/test_preview_gemini_merge.py b/tests/custom/test_preview_gemini_merge.py index 25cb61e..6772d15 100644 --- a/tests/custom/test_preview_gemini_merge.py +++ b/tests/custom/test_preview_gemini_merge.py @@ -2,17 +2,17 @@ from pydantic import ValidationError from agora_agent.agentkit.preview import ( - GeminiMLLM, - GeminiMLLMModels, + GeminiLiveModels, OpenAIGPTLive, PreviewFeatures, apply_preview_shape, required_preview_features, ) +from agora_agent.agentkit.vendors.mllm import GeminiLive def test_gemini_and_gpt_live_use_their_own_feature_gates(): - for vendor in (GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.LIVE_38), GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.LIVE_38_EXTENDED_THINKING)): + for vendor in (GeminiLive(api_key="test-key", model=GeminiLiveModels.LIVE_38), GeminiLive(api_key="test-key", model=GeminiLiveModels.LIVE_38_EXTENDED_THINKING)): properties = {"mllm": vendor.to_config()} assert required_preview_features(properties) == [PreviewFeatures.GEMINI_LIVE] assert required_preview_features({"mllm": OpenAIGPTLive(api_key="test-key").to_config()}) == [ @@ -21,7 +21,7 @@ def test_gemini_and_gpt_live_use_their_own_feature_gates(): def test_gemini_greeting_uses_preview_wire_field(): - properties = {"mllm": GeminiMLLM(api_key="test-key", model=GeminiMLLMModels.LIVE_38).to_config()} + properties = {"mllm": GeminiLive(api_key="test-key", model=GeminiLiveModels.LIVE_38).to_config()} properties["mllm"]["greeting_message"] = "Hello" apply_preview_shape(properties) assert properties["mllm"]["greeting"] == "Hello" @@ -29,18 +29,18 @@ def test_gemini_greeting_uses_preview_wire_field(): def test_one_gemini_preview_class_supports_public_38_ids(): - assert GeminiMLLM(api_key="test-key", model=" ").to_config()["params"]["model"] == GeminiMLLMModels.LIVE_38 + assert GeminiLive(api_key="test-key", model=" ").to_config()["params"]["model"] == GeminiLiveModels.LIVE_38 models = ( - (GeminiMLLMModels.LIVE_38, "medium"), - (GeminiMLLMModels.LIVE_38_EXTENDED_THINKING, "medium"), + (GeminiLiveModels.LIVE_38, "medium"), + (GeminiLiveModels.LIVE_38_EXTENDED_THINKING, "medium"), ) for model, thinking in models: - config = GeminiMLLM(api_key="test-key", model=model, thinking_level=thinking, + config = GeminiLive(api_key="test-key", model=model, thinking_level=thinking, additional_params={"thinking_level": "high", "api_key": "ignored-key"}).to_config() assert config["api_key"] == "test-key" assert "api_key" not in config["params"] assert config["params"]["model"] == model - expected_thinking = thinking if model == GeminiMLLMModels.LIVE_38_EXTENDED_THINKING else None + expected_thinking = thinking if model == GeminiLiveModels.LIVE_38_EXTENDED_THINKING else None assert config["params"].get("thinking_level") == expected_thinking assert required_preview_features({"mllm": config}) == [PreviewFeatures.GEMINI_LIVE] # Explicit IDs must route even in a hand-written config without the @@ -51,12 +51,14 @@ def test_one_gemini_preview_class_supports_public_38_ids(): def test_gemini_mllm_rejects_blank_api_key(): - with pytest.raises(ValidationError, match="GeminiMLLM requires api_key"): - GeminiMLLM(api_key=" ") + with pytest.raises(ValidationError, match="GeminiLive requires api_key"): + GeminiLive(api_key=" ") def test_unknown_gemini_model_keeps_preview_greeting_without_nested_api_key(): - properties = {"mllm": GeminiMLLM(api_key="test-key", model="future-live-model").to_config()} + properties = {"mllm": GeminiLive( + api_key="test-key", model="future-live-model", url="https://generativelanguage.googleapis.com" + ).to_config()} properties["mllm"]["greeting_message"] = "Hello" apply_preview_shape(properties) assert properties["mllm"]["greeting"] == "Hello" From 1b936d45df41112f8bfe12313744ce5251788b5a Mon Sep 17 00:00:00 2001 From: "Hermes (agora)" Date: Tue, 15 Sep 2026 12:21:50 -0700 Subject: [PATCH 09/57] docs: clarify GeminiLive model routing and defaults --- docs/concepts/vendors.md | 2 +- docs/reference/vendors.md | 2 +- src/agora_agent/agentkit/vendors/mllm.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/concepts/vendors.md b/docs/concepts/vendors.md index c093e8d..3ba71f9 100644 --- a/docs/concepts/vendors.md +++ b/docs/concepts/vendors.md @@ -160,7 +160,7 @@ Used with `agent.with_mllm()` for the [MLLM flow](../guides/mllm-flow.md). These | `OpenAIRealtime` | OpenAI Realtime | Global | `api_key`; optional `turn_detection` | | `OpenAIGPTLive` (preview) | OpenAI GPT Live | Global | `api_key`; optional `greeting` | | `AzureOpenAIRealtime` | Azure OpenAI Realtime | Global | `api_key`, `url`, `turn_detection`; optional `max_history` | -| `GeminiLive` | Google Gemini Live API | Global | `api_key`, `model`; optional `turn_detection` | +| `GeminiLive` | Google Gemini Live API | Global | `api_key`; `model` defaults to `models/gemini-3.8-live`. The two 3.8 IDs use preview routing; older IDs use production. | | `VertexAI` | Vertex AI (Gemini Live) | Global | `model`, `project_id`, `location`, `adc_credentials_string`; optional `turn_detection` | | `XaiGrok` | xAI Grok (`mllm.vendor`: `xai`) | Global | `api_key`; optional `voice`, `language`, `sample_rate`, `turn_detection` | | `QwenOmni` | Alibaba Cloud Qwen Omni Realtime | CN | `api_key`, `url`; optional `turn_detection` | diff --git a/docs/reference/vendors.md b/docs/reference/vendors.md index 8e6cebe..d22d705 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -964,7 +964,7 @@ CN Alibaba Cloud Qwen Omni Realtime vendor (`mllm.vendor`: `"qwen_omni"`). Impor | `model` | `str` | No | `models/gemini-3.8-live` | Gemini Live model name | | `thinking_level` | `str` | No | `None` | `low`, `medium`, or `high`; sent only for 3.8 Extended Thinking | | `language_codes` | `List[str]` | No | `None` | 3.8 language codes in `mllm.params.language_codes` | -| `url` | `str` | No | `None` | Custom WebSocket URL | +| `url` | `str` | No | `None` | Custom endpoint; 3.8 defaults to the Gemini Developer API host | | `instructions` | `str` | No | `None` | System instructions | | `voice` | `str` | No | `None` | Voice name | | `greeting_message` | `str` | No | `None` | Greeting message; sent as `mllm.greeting` for 3.8 models | diff --git a/src/agora_agent/agentkit/vendors/mllm.py b/src/agora_agent/agentkit/vendors/mllm.py index 829124a..e0d00a7 100644 --- a/src/agora_agent/agentkit/vendors/mllm.py +++ b/src/agora_agent/agentkit/vendors/mllm.py @@ -267,7 +267,7 @@ class GeminiLiveOptions(BaseModel): default=None, description="Reasoning budget for the 3.8 extended-thinking model" ) language_codes: Optional[List[str]] = Field(default=None, description="Languages for Gemini 3.8") - url: Optional[str] = Field(default=None, description="WebSocket URL") + url: Optional[str] = Field(default=None, description="Endpoint override; Gemini 3.8 defaults to the Developer API host") instructions: Optional[str] = Field(default=None, description="System instructions") voice: Optional[str] = Field(default=None, description="Voice name") affective_dialog: Optional[bool] = Field(default=None, description="Enable affective dialog") From 4f9fc0bb7954e58436241dedf5a55f8fde52bb61 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 12:32:46 +0800 Subject: [PATCH 10/57] feat: add v3 stored-agent API and AgentKit deployment preview --- .fernignore | 3 + README.md | 4 + docs/guides/v3-api-preview.md | 74 +++ src/agora_agent/__init__.py | 5 + src/agora_agent/agentkit/__init__.py | 4 + src/agora_agent/agentkit/agent.py | 25 + src/agora_agent/agentkit/v3.py | 500 +++++++++++++++++ src/agora_agent/pool_client.py | 29 + src/agora_agent/v3/__init__.py | 6 + src/agora_agent/v3/_transport.py | 158 ++++++ src/agora_agent/v3/client.py | 367 ++++++++++++ src/agora_agent/v3/types.py | 807 +++++++++++++++++++++++++++ tests/custom/test_v3_preview.py | 374 +++++++++++++ 13 files changed, 2356 insertions(+) create mode 100644 docs/guides/v3-api-preview.md create mode 100644 src/agora_agent/agentkit/v3.py create mode 100644 src/agora_agent/v3/__init__.py create mode 100644 src/agora_agent/v3/_transport.py create mode 100644 src/agora_agent/v3/client.py create mode 100644 src/agora_agent/v3/types.py create mode 100644 tests/custom/test_v3_preview.py diff --git a/.fernignore b/.fernignore index 3d06feb..25e5a2e 100644 --- a/.fernignore +++ b/.fernignore @@ -31,3 +31,6 @@ requirements.txt .fern/replay.lock .fern/replay.yml .gitattributes + +# Additive v3 preview adapter and typed staging contracts are maintained manually. +src/agora_agent/v3/ diff --git a/README.md b/README.md index 2e0eaef..2ccf6f2 100644 --- a/README.md +++ b/README.md @@ -371,3 +371,7 @@ a proof of concept, but know that we will not be able to merge it as-is. We sugg an issue first to discuss with us! On the other hand, contributions to the README are always very welcome! + +### v3 stored-agent preview + +The additive `client.v3` API and AgentKit `deploy()` / `deploy_async()` save stored agent configurations without changing v2 session behavior. See the [v3 preview guide](docs/guides/v3-api-preview.md) for staging overrides, typed wire responses, ETags, secret references, number assignment, async usage and current backend limitations. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md new file mode 100644 index 0000000..242ce5a --- /dev/null +++ b/docs/guides/v3-api-preview.md @@ -0,0 +1,74 @@ +# v3 stored-agent preview (P1) + +The package version continues to describe SDK compatibility. Existing v2 resources and `Agent.create_session()` remain available. The additive `client.v3` namespace covers all 20 published staging operations: agents and numbers CRUD, secrets CRUD plus individual key writes/deletes, and session list/get. Session writes, outbound calls, analysis helpers and campaigns await backend schemas and are not implemented. + +```python +from agora_agent import Agora, Area, Agent, DeepgramSTT, OpenAI, V3Options, secret_ref + +client = Agora( + area=Area.US, app_id="APP_ID", app_certificate="APP_CERTIFICATE", + # Staging origin only. Supply the gateway's required headers explicitly. + v3=V3Options(base_url="https://YOUR_STAGING_HOST", headers={ + "X-Studio-V2-Gateway-Secret": "GATEWAY_SECRET", + "X-Consumer-Username": "CONSUMER", + }), +) + +client.v3.secrets.create(name="prod", secrets={"openai": "OPENAI_KEY", "deepgram": "DEEPGRAM_KEY"}, + idempotency_key="initial-prod-secrets") +agent = (Agent(client=client) + .with_stt(DeepgramSTT(api_key=secret_ref("prod", "deepgram"), model="nova-3")) + .with_llm(OpenAI(api_key=secret_ref("prod", "openai"), model="gpt-4o-mini", + base_url="https://api.openai.com/v1/chat/completions", + system_messages=[{"role": "system", "content": "Help callers briefly."}])) + .with_variable("resolved", type="bool", extract=True, description="Was the issue resolved?")) +# Add your usual .with_tts(...) configuration for a complete voice pipeline. +support = agent.deploy(name="Support") +print(support.id, support.etag) +support.assign_number("+12025550123", max_call_duration_seconds=900) +support.unassign_number("+12025550123") + +# Save the ID; a later deploy to that ID GETs its ETag then PATCHes. +support = agent.deploy(name="Support", agent_id=support.id) +loaded = type(support).get(client, support.id) +# loaded.delete() deletes the stored configuration. +``` + +Numbers must already exist in the same project. `assign_number` accepts an E.164 string, a number ID (without assuming UUID format), or a v3 number dictionary with `number_id`. E.164 lookup scans cursor pages at 100 items per request and compares formatting-normalized numbers. Assignment GETs the number for its ETag, then PATCHes `inbound.agent`; a 412 triggers one fresh GET and one retry. An ID usually costs two requests, E.164 adds list requests, and a conflict adds two requests. `max_silence_duration_ms` is the other supported call-policy option. Unknown numbers raise `NumberNotFoundError` naming the project. + +## Low-level access, headers and pagination + +Every resource has explicit snake_case methods. `appid` defaults to the owning client's `app_id` and may be overridden per call. Agents, numbers and sessions use `agent_id`, `number_id` and `agent_session_id` respectively. Secret sets use `name`, and individual key operations also take `key`. + +```python +created = client.v3.agents.create(agent_name="Minimal stored agent") +response = client.v3.agents.with_raw_response.get(agent_id=created["agent_id"]) +updated = client.v3.agents.update(agent_id=created["agent_id"], if_match=response.etag, + instructions="New instructions") +print(response.headers.get("Agora-Request-Id"), response.status_code) + +page = client.v3.sessions.list(limit=20, status="ACTIVE", channel="room", + started_after=100, started_before=200) +next_page = client.v3.sessions.list(limit=20, cursor=page["next_cursor"]) if page["has_more"] else None +for number in client.v3.numbers.iter_all(limit=100): + print(number["number_id"]) +# iter_pages() yields complete pages, including count/has_more/next_cursor. + +client.v3.secrets.set_key(name="prod", key="new_key", value="VALUE", if_none_match="*") +``` + +Responses are typed wire dictionaries defined in `agora_agent.v3.types`. Raw responses expose `.data`, `.headers`, `.etag`, `.status_code` and the underlying httpx `.response`. They preserve server pagination metadata and error `{detail, reason}` bodies. `PreconditionFailedError` is an `ApiError` with status/body/headers and `latest()` to GET the current resource (a secret key conflict reloads its set). `latest()` does not repeat the write. Required `If-Match` values are explicit arguments. Omitted optional body fields remain absent; explicit `None` survives as JSON null. PATCH uses the staging contract's `application/json` content type. + +## Async + +Bind an `AsyncAgora` client, use `await agent.deploy_async(...)`, and receive an `AsyncDeployedAgent`. Its `get`, `delete`, `assign_number`, and `unassign_number` methods are awaitable. Low-level methods have identical arguments under `async_client.v3`, including `.with_raw_response`. Use `async for` with `.iter_pages()` / `.iter_all()`, and `await error.latest()` after an async conflict. `deploy()` requires `Agora`; `deploy_async()` requires `AsyncAgora`. + +## Mapping and preview limitations + +`with_variable` is pure and immutable. Variable names match `^[a-z][a-z0-9_]{0,63}$`; types are `str`, `int`, `float`, `bool`, `date`, `datetime`; extraction requires a nonempty description. Existing builder/vendor configuration maps centrally to v3 provider modules, instructions/messages, text/audio greetings, turn detection/interruption, silence settings and filler words. Vendor instructions/greetings take precedence over deprecated Agent fields. API keys become `credential: {mode: "byok", api_key: ...}`; absent keys become `managed`. Provider URLs already serialized by existing vendor classes are preserved, and no additional endpoint defaults are invented. + +Strict mapping raises one `UnsupportedFieldError` listing unmappable fields before any request. These include SAL, advanced features, geofence, labels, RTC configuration, pipeline IDs, and parameters other than `silence_config`. Value-valued `template_variables` cannot be converted into variable definitions; use `with_variable`. Multipart AWS/Vertex credentials, removed avatar vendors, and numeric v2 TTS `skip_patterns` are rejected because no lossless mapping is confirmed. Unsupported deprecated turn settings and greeting policies are also reported rather than silently dropped. Secret references on `create_session`, `create_async_session`, or `to_properties` raise `SecretRefNotSupportedError` before a v2 network request. + +Redeploy recursively nulls stale **readable** mutable configuration fields, including nested provider params, because JSON merge patch does not imply nested replacement. IDs, timestamps, and resolved credential metadata are never echoed. Credentials come from the builder; switching to managed explicitly nulls `api_key`. Write-only keys/headers cannot be reconstructed from GET, so omitted hidden values and mode-switch behavior remain subject to backend merge semantics. Deploy is an ETag-guarded patch, not a guaranteed PUT/replace. Use the low-level raw resource to make targeted patches when retaining hidden credentials matters. Agent conflicts are not retried. All low-level writes disable automatic HTTP retries, even if `max_retries` was requested; staging agent POST has no idempotency protection and numbers currently ignore their declared key. A failed create may have succeeded remotely: inspect resources before manually retrying. + +The shared existing HTTP transport, auth and regional pool are reused; v3 overrides only its origin and headers. `AGORA_AGENTS_V3_BASE_URL` provides the same origin override as `V3Options.base_url` (the explicit option wins). The request path is `/conversational-ai/v3/projects/{appid}`. Production origins/path, all regional availability and Basic-auth support remain unconfirmed. CN deployment is deliberately refused. Staging may require a URL even with managed credentials; supply a backend-approved provider URL through existing vendor configuration. Deferred tools/MCP/custom destinations may be rejected by the server. Number `end_call`, `transfer`, `allowed_ips` and SIP secret references are also currently unsupported by staging. No live staging write was performed for this preview. diff --git a/src/agora_agent/__init__.py b/src/agora_agent/__init__.py index 5a1ad53..557123f 100644 --- a/src/agora_agent/__init__.py +++ b/src/agora_agent/__init__.py @@ -146,3 +146,8 @@ def __getattr__(attr_name: str) -> typing.Any: def __dir__() -> typing.List[str]: return list(__all__) + +from .agentkit.v3 import (DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) +from .v3 import V3Options, PreconditionFailedError + +__all__ = sorted(set(__all__) | {"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 924431b..b1674c7 100644 --- a/src/agora_agent/agentkit/__init__.py +++ b/src/agora_agent/agentkit/__init__.py @@ -478,3 +478,7 @@ "validate_avatar_config", "validate_tts_sample_rate", ] + +from .v3 import (DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) + +__all__ = sorted(set(__all__) | {"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 2ea93c7..56a245c 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -7,6 +7,7 @@ if typing.TYPE_CHECKING: from .agent_session import AgentSession, AsyncAgentSession + from .v3 import DeployedAgent, AsyncDeployedAgent from ..core.domain import Area from ..pool_client import Agora, AsyncAgora @@ -857,6 +858,23 @@ 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 deploy(self, *, name: typing.Optional[str] = None, agent_id: typing.Optional[str] = None) -> "DeployedAgent": + """Create a v3 stored agent, or conditionally redeploy an existing ID.""" + from .v3 import deploy + return deploy(self, name=name, agent_id=agent_id) + + async def deploy_async(self, *, name: typing.Optional[str] = None, agent_id: typing.Optional[str] = None) -> "AsyncDeployedAgent": + """Deploy using a bound AsyncAgora client.""" + from .v3 import deploy_async + return await deploy_async(self, name=name, agent_id=agent_id) + def create_session( self, channel: str, @@ -886,6 +904,8 @@ def create_session( """ from .agent_session import AgentSession + from .v3 import guard_v2 + guard_v2(self.config) resolved_client = self._client if resolved_client is None: raise ValueError("client is required. Pass client=... to Agent(...).") @@ -933,6 +953,8 @@ def create_async_session( """ from .agent_session import AsyncAgentSession + from .v3 import guard_v2 + guard_v2(self.config) resolved_client = self._client if resolved_client is None: raise ValueError("client is required. Pass client=... to Agent(...).") @@ -972,6 +994,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). @@ -1138,6 +1162,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/v3.py b/src/agora_agent/agentkit/v3.py new file mode 100644 index 0000000..99a5954 --- /dev/null +++ b/src/agora_agent/agentkit/v3.py @@ -0,0 +1,500 @@ +"""Stored-agent preview and strict mapping from existing immutable builders.""" +from __future__ import annotations + +import copy +import re +import time +from typing import Any, Dict, Mapping, Optional + +from ..v3 import PreconditionFailedError +from ..v3.types import AgentResponse, NumberResponse + + +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 or hyphens") + if not isinstance(key, str) or not re.fullmatch(pattern, key): + raise ValueError("key must contain 1-64 letters, digits, underscores or hyphens") + return SecretRef(f"$secrets.{secret_set}.{key}") + + +def guard_v2(value): + if isinstance(value, str) and value.startswith("$secrets."): + raise SecretRefNotSupportedError("secret_ref requires 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({(type(v), 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 {}) + keys = [] + for source in (config, params): + for field in ("api_key", "key", "api_subscription_key"): + if field in source: + keys.append(source.pop(field)) + multipart = {"aws_access_key_id", "aws_secret_access_key", "access_key", "secret_key", "adc_credentials_string", "service_account", "credentials"} + errors.extend(category + ".params." + k for k in params if k in multipart) + if len(set(keys)) > 1: + errors.append(category + ".credential (multiple different API keys)") + result = {"credential": {"mode": "byok", "api_key": keys[0]} if keys 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"}, + "mllm": {"vendor", "url", "model", "input_modalities", "output_modalities", "mcp_servers", "headers"}, + "avatar": {"vendor", "url", "enabled"}, + }[category] + # v2 provider serializers keep endpoints and ASR/LLM model choices in params. + for field in ("url", "model", "language"): + 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") + if category == "mllm": + config.pop("enable", None) + for field, value in config.items(): + if field not in allowed: + errors.append(category + "." + field) + else: + result[field] = value + if category == "avatar" and result.get("vendor") in ("sensetime", "spatius"): + errors.append("avatar.vendor." + result["vendor"]) + 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 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): + 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") + realtime = agent._mllm is not None + pipeline = {"mode": "realtime" if realtime else "cascaded"} + if realtime: + for field in ("stt", "llm", "tts"): + if getattr(agent, "_" + field, None) is not None: + errors.append(field + " (realtime pipeline)") + conversational = dump(agent._mllm if realtime else agent._resolve_llm_config()) or {} + if realtime: + realtime_params = conversational.get("params", {}) + if isinstance(realtime_params, dict) and "instructions" in realtime_params: + result["instructions"] = realtime_params.pop("instructions") + messages = conversational.pop("system_messages", None) + if messages is not None: + system = [m["content"] for m in messages if m.get("role") == "system"] + if system: + if any(not isinstance(v, str) for v in system): + errors.append("llm.system_messages.content") + else: + result["instructions"] = "\n".join(system) + non_system = [m for m in messages if m.get("role") != "system"] + if non_system: + result["messages"] = non_system + elif "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 {"on", "delay_ms", "download_timeout_ms", "pcm_sample_rate"}) + 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 + if "mode" in greeting: + # v2 greeting configs mode controls when playback occurs. + result["greeting"]["on"] = greeting["mode"] + result["greeting"]["mode"] = "audio" if greeting_url else "text" + elif greeting: + errors.append("greeting_configs (requires greeting)") + if conversational.pop("template_variables", None): + errors.append(("mllm" if realtime else "llm") + ".template_variables (use with_variable)") + td = conversational.pop("turn_detection", None) if realtime else None + if realtime: + if td: + td = dump(td) + mode = td.pop("mode", td.pop("type", None)) + nested = td.pop("agora_vad_config", {}) + pipeline["turn_detection"] = {"type": mode, **nested, **td} + if agent._turn_detection is not None or agent._interruption is not None: + errors.append("turn_detection (configure realtime vendor turn_detection)") + else: + td = _turn(agent._turn_detection, agent._interruption, errors) + if td: + pipeline["turn_detection"] = td + category = "mllm" if realtime else "llm" + if conversational: + pipeline[category] = module(conversational, category, errors) + for source, target in (("stt", "asr"), ("tts", "tts"), ("avatar", "avatar")): + value = getattr(agent, "_" + source, None) + if value is not None and (not realtime or target == "avatar"): + value = dump(value) + if target == "asr" and agent._turn_detection is not None: + language = dump(agent._turn_detection).get("language") + if language: + value.setdefault("language", language) + pipeline[target] = module(value, target, errors) + if "silence_config" in params: + if realtime: + errors.append("parameters.silence_config (realtime pipeline)") + else: + 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): + """Clear readable stale keys recursively. Never echo IDs or credential metadata.""" + result = copy.deepcopy(desired) + 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]["api_key"] = None + continue # Resolved credentials have read-only metadata and hide API keys. + result[key] = replacement_patch(old, result[key]) + 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("deploy_async requires AsyncAgora" if async_ else "deploy requires Agora") + if getattr(agent._client, "area_scope", None) == "cn": + raise ValueError("v3 deployment in CN is unconfirmed; use a supported global area") + + +def deploy(agent, *, name=None, agent_id=None) -> DeployedAgent: + _require_client(agent, False) + body = build_body(agent, name) + 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 DeployedAgent(agent._client, response) + + +async def deploy_async(agent, *, name=None, agent_id=None) -> AsyncDeployedAgent: + _require_client(agent, True) + body = build_body(agent, name) + 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 AsyncDeployedAgent(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): + policy = {} + 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 + return policy + + +class _DeployedAgentBase: + 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 DeployedAgent(_DeployedAgentBase): + @classmethod + def get(cls, client, agent_id) -> DeployedAgent: + return cls(client, client.v3.agents.with_raw_response.get(agent_id=agent_id)) + + def delete(self): + return self.raw.delete(agent_id=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: + 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) -> NumberResponse: + return self._assign(number, self.id, _policy(max_call_duration_seconds, max_silence_duration_ms)) + + def unassign_number(self, number) -> NumberResponse: + return self._assign(number, None, {}) + + +class AsyncDeployedAgent(_DeployedAgentBase): + @classmethod + async def get(cls, client, agent_id) -> AsyncDeployedAgent: + return cls(client, await client.v3.agents.with_raw_response.get(agent_id=agent_id)) + + async def delete(self): + return await self.raw.delete(agent_id=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: + 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) -> NumberResponse: + return await self._assign(number, self.id, _policy(max_call_duration_seconds, max_silence_duration_ms)) + + async def unassign_number(self, number) -> NumberResponse: + return await self._assign(number, None, {}) diff --git a/src/agora_agent/pool_client.py b/src/agora_agent/pool_client.py index 34a1514..3b1bab8 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -12,6 +12,7 @@ from .agentkit.regional_agent import RegionalAgent from .agentkit.vendors.region import AreaScope, area_to_scope +from .v3 import V3Options, V3Client, AsyncV3Client from .client import Agora as BaseAgora from .client import AsyncAgora as BaseAsyncAgora from .core.api_error import ApiError @@ -223,6 +224,7 @@ def __new__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "CNAgora": ... @typing.overload @@ -240,6 +242,7 @@ def __new__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "GlobalAgoraClient": ... @typing.overload @@ -257,6 +260,7 @@ def __new__( 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: @@ -283,6 +287,7 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -300,6 +305,7 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -317,6 +323,7 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... def __init__( @@ -333,7 +340,9 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ): + self._v3_options = v3 self._pool = Pool(area) self._area = area self.app_id = app_id @@ -375,6 +384,12 @@ def __init__( httpx_client=httpx_client, ) + @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 pool(self) -> Pool: """ @@ -559,6 +574,7 @@ def __new__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "CNAsyncAgora": ... @typing.overload @@ -576,6 +592,7 @@ def __new__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> "GlobalAsyncAgoraClient": ... @typing.overload @@ -593,6 +610,7 @@ def __new__( 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: @@ -619,6 +637,7 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -636,6 +655,7 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... @typing.overload @@ -653,6 +673,7 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ) -> None: ... def __init__( @@ -669,7 +690,9 @@ def __init__( follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, debug: bool = False, + v3: typing.Optional[V3Options] = None, ): + self._v3_options = v3 self._pool = Pool(area) self._area = area self.app_id = app_id @@ -711,6 +734,12 @@ def __init__( httpx_client=httpx_client, ) + @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 pool(self) -> Pool: """ diff --git a/src/agora_agent/v3/__init__.py b/src/agora_agent/v3/__init__.py new file mode 100644 index 0000000..91aac6a --- /dev/null +++ b/src/agora_agent/v3/__init__.py @@ -0,0 +1,6 @@ +"""Additive v3 stored-agent preview. Sessions are read-only.""" +from ._transport import V3Options, RawResponse, PreconditionFailedError +from .client import V3Client, AsyncV3Client +from . import types + +__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..4c81723 --- /dev/null +++ b/src/agora_agent/v3/_transport.py @@ -0,0 +1,158 @@ +"""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, Dict, Generic, Mapping, Optional, TypeVar +from urllib.parse import quote, urlsplit + +import httpx +from ..core.api_error import ApiError +from ..agentkit.token import generate_convo_ai_token + +_UNSET: Any = object() +T = TypeVar("T") + + +@dataclass(frozen=True) +class V3Options: + """Staging overrides. base_url is the origin, without the v3 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") + 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 + base = self.options.base_url or os.environ.get("AGORA_AGENTS_V3_BASE_URL") + if not base: + # The existing pool's base may include the v2 path; v3 uses the same origin. + current = urlsplit(wrapper.get_base_url()) + base = f"{current.scheme}://{current.netloc}" + 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: + raise ValueError("v3 base_url must be an HTTP(S) origin without query or fragment") + base = base.rstrip("/") + 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": + token = generate_convo_ai_token(app_id=self.owner.app_id, + app_certificate=self.owner.app_certificate, channel_name="v3", uid=0) + merged.setdefault("Authorization", f"agora token={token}") + options = dict(request_options or {}) + # 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) + 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=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"] + + +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=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/client.py b/src/agora_agent/v3/client.py new file mode 100644 index 0000000..034edd5 --- /dev/null +++ b/src/agora_agent/v3/client.py @@ -0,0 +1,367 @@ +from __future__ import annotations +from typing import Any, Optional, Dict, List, Union +from typing_extensions import Literal +from ._transport import Resource, AsyncResource, RawResponse +from ._transport import _UNSET, segment +from .types import ( + AgentCreateModel_AudioGreeting, + AgentCreateModel_CascadedPipeline, + AgentCreateModel_FillerWords, + AgentCreateModel_Message, + AgentCreateModel_RealtimePipeline, + AgentCreateModel_TextGreeting, + AgentCreateModel_VariableDefinition, + AgentDeleteResponse, + AgentListResponse, + AgentPatchRequest_FillerWordsPatch, + AgentPatchRequest_GreetingPatch, + AgentPatchRequest_Message, + AgentPatchRequest_PipelinePatch, + AgentPatchRequest_VariableDefinitionPatch, + AgentResponse, + InboundInput, + NumberDeleteResponse, + NumberListResponse, + NumberResponse, + SecretDeleted, + SecretKey, + SecretList, + SecretSet, + SessionList, + SessionResponse, + SipTrunkInput, +) + +class AgentsClient(Resource): + with_raw_response: AgentsRawClient + + def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': + return self.with_raw_response.create(appid=appid, agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, request_options=request_options).data + + def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'AgentListResponse': + return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data + + def get(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentResponse': + return self.with_raw_response.get(appid=appid, agent_id=agent_id, request_options=request_options).data + + def update(self, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': + return self.with_raw_response.update(appid=appid, 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, request_options=request_options).data + + def delete(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentDeleteResponse': + return self.with_raw_response.delete(appid=appid, agent_id=agent_id, request_options=request_options).data + + +class AgentsRawClient(Resource): + def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return self._request('POST', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return self._request('PATCH', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('DELETE', f'/conversational-ai/v3/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 _AgentsClient(owner, options): + client = AgentsClient(owner, options) + client.with_raw_response = AgentsRawClient(owner, options) + return client + +class SecretsClient(Resource): + with_raw_response: SecretsRawClient + + def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': + return self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, name=name, secrets=secrets, request_options=request_options).data + + def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> 'SecretList': + return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data + + def get(self, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> 'SecretSet': + return self.with_raw_response.get(appid=appid, name=name, request_options=request_options).data + + def replace(self, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': + return self.with_raw_response.replace(appid=appid, name=name, if_match=if_match, secrets=secrets, request_options=request_options).data + + def update(self, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> 'SecretSet': + return self.with_raw_response.update(appid=appid, name=name, if_match=if_match, secrets=secrets, request_options=request_options).data + + def delete(self, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> 'SecretDeleted': + return self.with_raw_response.delete(appid=appid, name=name, if_match=if_match, request_options=request_options).data + + def set_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> 'SecretKey': + return self.with_raw_response.set_key(appid=appid, name=name, key=key, if_match=if_match, if_none_match=if_none_match, value=value, request_options=request_options).data + + def delete_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> 'SecretDeleted': + return self.with_raw_response.delete_key(appid=appid, name=name, key=key, if_match=if_match, request_options=request_options).data + + +class SecretsRawClient(Resource): + def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + return self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body={'name': name, 'secrets': secrets}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + return self._request('PUT', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> RawResponse: + return self._request('PATCH', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('DELETE', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('PUT', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + return self._request('DELETE', f'/conversational-ai/v3/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) + +def _SecretsClient(owner, options): + client = SecretsClient(owner, options) + client.with_raw_response = SecretsRawClient(owner, options) + return client + +class NumbersClient(Resource): + with_raw_response: NumbersRawClient + + def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': + return self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options).data + + def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'NumberListResponse': + return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data + + def get(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberResponse': + return self.with_raw_response.get(appid=appid, number_id=number_id, request_options=request_options).data + + def update(self, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': + return self.with_raw_response.update(appid=appid, number_id=number_id, if_match=if_match, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options).data + + def delete(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberDeleteResponse': + return self.with_raw_response.delete(appid=appid, number_id=number_id, request_options=request_options).data + + +class NumbersRawClient(Resource): + def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return self._request('POST', f'/conversational-ai/v3/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={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return self._request('PATCH', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('DELETE', f'/conversational-ai/v3/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 _NumbersClient(owner, options): + client = NumbersClient(owner, options) + client.with_raw_response = NumbersRawClient(owner, options) + return client + +class SessionsClient(Resource): + with_raw_response: SessionsRawClient + + def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> 'SessionList': + return self.with_raw_response.list(appid=appid, channel=channel, status=status, started_after=started_after, started_before=started_before, limit=limit, cursor=cursor, request_options=request_options).data + + def get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> 'SessionResponse': + return self.with_raw_response.get(appid=appid, agent_session_id=agent_session_id, request_options=request_options).data + + +class SessionsRawClient(Resource): + def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> RawResponse: + return self._request('GET', f'/conversational-ai/v3/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 _SessionsClient(owner, options): + client = SessionsClient(owner, options) + client.with_raw_response = SessionsRawClient(owner, options) + return client + +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) + +class AsyncAgentsClient(AsyncResource): + with_raw_response: AsyncAgentsRawClient + + async def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': + return (await self.with_raw_response.create(appid=appid, agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, request_options=request_options)).data + + async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'AgentListResponse': + return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data + + async def get(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentResponse': + return (await self.with_raw_response.get(appid=appid, agent_id=agent_id, request_options=request_options)).data + + async def update(self, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': + return (await self.with_raw_response.update(appid=appid, 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, request_options=request_options)).data + + async def delete(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentDeleteResponse': + return (await self.with_raw_response.delete(appid=appid, agent_id=agent_id, request_options=request_options)).data + + +class AsyncAgentsRawClient(AsyncResource): + async def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('POST', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('PATCH', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('DELETE', f'/conversational-ai/v3/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 _AsyncAgentsClient(owner, options): + client = AsyncAgentsClient(owner, options) + client.with_raw_response = AsyncAgentsRawClient(owner, options) + return client + +class AsyncSecretsClient(AsyncResource): + with_raw_response: AsyncSecretsRawClient + + async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': + return (await self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, name=name, secrets=secrets, request_options=request_options)).data + + async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> 'SecretList': + return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data + + async def get(self, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> 'SecretSet': + return (await self.with_raw_response.get(appid=appid, name=name, request_options=request_options)).data + + async def replace(self, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': + return (await self.with_raw_response.replace(appid=appid, name=name, if_match=if_match, secrets=secrets, request_options=request_options)).data + + async def update(self, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> 'SecretSet': + return (await self.with_raw_response.update(appid=appid, name=name, if_match=if_match, secrets=secrets, request_options=request_options)).data + + async def delete(self, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> 'SecretDeleted': + return (await self.with_raw_response.delete(appid=appid, name=name, if_match=if_match, request_options=request_options)).data + + async def set_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> 'SecretKey': + return (await self.with_raw_response.set_key(appid=appid, name=name, key=key, if_match=if_match, if_none_match=if_none_match, value=value, request_options=request_options)).data + + async def delete_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> 'SecretDeleted': + return (await self.with_raw_response.delete_key(appid=appid, name=name, key=key, if_match=if_match, request_options=request_options)).data + + +class AsyncSecretsRawClient(AsyncResource): + async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + return await self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body={'name': name, 'secrets': secrets}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + return await self._request('PUT', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> RawResponse: + return await self._request('PATCH', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('DELETE', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('PUT', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('DELETE', f'/conversational-ai/v3/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) + +def _AsyncSecretsClient(owner, options): + client = AsyncSecretsClient(owner, options) + client.with_raw_response = AsyncSecretsRawClient(owner, options) + return client + +class AsyncNumbersClient(AsyncResource): + with_raw_response: AsyncNumbersRawClient + + async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': + return (await self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options)).data + + async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'NumberListResponse': + return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data + + async def get(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberResponse': + return (await self.with_raw_response.get(appid=appid, number_id=number_id, request_options=request_options)).data + + async def update(self, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': + return (await self.with_raw_response.update(appid=appid, number_id=number_id, if_match=if_match, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options)).data + + async def delete(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberDeleteResponse': + return (await self.with_raw_response.delete(appid=appid, number_id=number_id, request_options=request_options)).data + + +class AsyncNumbersRawClient(AsyncResource): + async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('POST', f'/conversational-ai/v3/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={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('PATCH', f'/conversational-ai/v3/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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('DELETE', f'/conversational-ai/v3/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 _AsyncNumbersClient(owner, options): + client = AsyncNumbersClient(owner, options) + client.with_raw_response = AsyncNumbersRawClient(owner, options) + return client + +class AsyncSessionsClient(AsyncResource): + with_raw_response: AsyncSessionsRawClient + + async def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> 'SessionList': + return (await self.with_raw_response.list(appid=appid, channel=channel, status=status, started_after=started_after, started_before=started_before, limit=limit, cursor=cursor, request_options=request_options)).data + + async def get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> 'SessionResponse': + return (await self.with_raw_response.get(appid=appid, agent_session_id=agent_session_id, request_options=request_options)).data + + +class AsyncSessionsRawClient(AsyncResource): + async def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('GET', f'/conversational-ai/v3/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 _AsyncSessionsClient(owner, options): + client = AsyncSessionsClient(owner, options) + client.with_raw_response = AsyncSessionsRawClient(owner, options) + return client + +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) diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py new file mode 100644 index 0000000..ce1bc96 --- /dev/null +++ b/src/agora_agent/v3/types.py @@ -0,0 +1,807 @@ +"""Typed staging OpenAPI wire schemas. Dictionaries preserve omitted versus null fields.""" +from __future__ import annotations +from typing import Any, Dict, List, Union +from typing_extensions import TypedDict, Required, Literal + +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[Union['ResolvedCascadedPipeline', 'ResolvedRealtimePipeline'], None] + filler_words: Union['FillerWords', None] + +class AgoraVadTurnDetection(TypedDict, total=False): + type: Required[Literal['agora_vad']] + interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, int, 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 DisabledInterruption(TypedDict, total=False): + mode: Required[Literal['off']] + when_off: Literal['append', 'ignore'] + +class EndCallPolicy(TypedDict, total=False): + on_conversation_complete: bool + on_user_request: bool + on_fax: bool + on_ai_assistant: bool + +ErrorReason = Literal['InvalidRequestBody', 'MissingRequiredField', 'InvalidFieldValue', 'InvalidModuleParameter', 'UnknownSecretReference', 'NotImplemented', 'InvalidPermission', 'ServiceNotEnabled', 'AccountSuspended', 'TaskNotFound', 'ResourceNotFound', 'AgentNotFound', 'NumberAlreadyExists', 'ResourceConflict', 'MethodNotAllowed', 'PayloadTooLarge', 'UnsupportedMediaType', 'TaskConflict', 'IdempotencyKeyConflict', 'SecretSetAlreadyExists', 'SecretSetInUse', 'SecretSetEmpty', 'TaskNotRunning', 'RateLimitExceeded', 'PreconditionFailed', 'PreconditionRequired', 'ResourceQuotaLimitExceeded', 'ConcurrencyLimitExceeded', 'InternalError', 'ServiceUnavailable', 'ResourceAllocationFailed', 'TaskOperationTimeout'] + +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 GeneratedFillerContent(TypedDict, total=False): + mode: Required[Literal['generated']] + instructions: Required[str] + fallback_strategy: Union[Literal['static'], None] + +class InboundCallPolicy(TypedDict, total=False): + max_call_duration_seconds: Union[int, None] + max_silence_duration_ms: Union[int, None] + end_call: Union['EndCallPolicy', 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] + +JsonValue = Any + +class KeywordInterruption(TypedDict, total=False): + mode: Required[Literal['keywords']] + keywords: Required[List[str]] + +class ManualTurnBoundary(TypedDict, total=False): + type: Required[Literal['manual']] + +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 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[Dict[str, 'JsonValue']], None] + tools: Union[List[Dict[str, 'JsonValue']], None] + +class ResolvedMllm(TypedDict, total=False): + credential: Union['ResolvedCredential', None] + vendor: Union[str, None] + model: Union[str, None] + url: Union[str, None] + params: Union[Dict[str, 'JsonValue'], None] + input_modalities: Union[List[str], None] + output_modalities: Union[List[str], None] + mcp_servers: Union[List[Dict[str, 'JsonValue']], None] + +class ResolvedRealtimePipeline(TypedDict, total=False): + mode: Required[Literal['realtime']] + avatar: Union['ResolvedAvatar', None] + turn_detection: Union[Union['AgoraVadTurnDetection', 'ServerVadTurnDetection', 'SemanticVadTurnDetection'], None] + mllm: Union['ResolvedMllm', 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 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] + +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[str]] + 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, None] + pause_state_enabled: Union[bool, None] + +class SemanticTurnStart(TypedDict, total=False): + type: Required[Literal['semantic']] + speaking_interrupt_duration_ms: Union[int, None] + +class SemanticVadTurnDetection(TypedDict, total=False): + type: Required[Literal['semantic_vad']] + eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] + +class ServerVadTurnDetection(TypedDict, total=False): + type: Required[Literal['server_vad']] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, int, None] + idle_timeout_ms: Union[int, None] + start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] + end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] + +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]] + +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 SessionResponse(TypedDict, total=False): + agent_session_id: Required[str] + start_ts: Required[int] + status: Required[Literal['IDLE', 'STARTING', 'RUNNING', 'STOPPING', 'STOPPED', 'FAILED']] + agent_name: str + stop_ts: Union[int, None] + message: Union[str, None] + analysis: Union['SessionAnalysis', None] + +class SessionSummary(TypedDict, total=False): + agent_session_id: Required[str] + start_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: Any + +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'] + +class TextGreeting(TypedDict, total=False): + mode: Required[Literal['text']] + on: Literal['each_join', 'first_join'] + delay_ms: int + text: Required[str] + +class TransferPolicy(TypedDict, total=False): + phone_number: Required[str] + description: Required[str] + +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 AgentCreateModel_AgoraVadTurnDetection(TypedDict, total=False): + type: Required[Literal['agora_vad']] + interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, int, None] + +class AgentCreateModel_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 AgentCreateModel_ByokCredential(TypedDict, total=False): + mode: Required[Literal['byok']] + api_key: Required[str] + +class AgentCreateModel_CascadedPipeline(TypedDict, total=False): + mode: Required[Literal['cascaded']] + avatar: Union['AgentCreateModel_ProviderModule', None] + turn_detection: Union['AgentCreateModel_TurnDetection', None] + asr: Union['AgentCreateModel_ProviderModule', None] + tts: Union['AgentCreateModel_ProviderModule', None] + llm: Union['AgentCreateModel_ProviderModule', None] + silence_config: Union['AgentCreateModel_SilenceConfig', None] + +class AgentCreateModel_DisabledInterruption(TypedDict, total=False): + mode: Required[Literal['off']] + when_off: Literal['append', 'ignore'] + +class AgentCreateModel_FillerTrigger(TypedDict, total=False): + mode: Required[Literal['fixed_time']] + response_wait_ms: Required[int] + +class AgentCreateModel_FillerWords(TypedDict, total=False): + enable: Union[bool, None] + trigger: Union['AgentCreateModel_FillerTrigger', None] + content: Union[Union['AgentCreateModel_StaticFillerContent', 'AgentCreateModel_GeneratedFillerContent'], None] + +class AgentCreateModel_GeneratedFillerContent(TypedDict, total=False): + mode: Required[Literal['generated']] + instructions: Required[str] + fallback_strategy: Union[Literal['static'], None] + +AgentCreateModel_JsonValue = Any + +class AgentCreateModel_KeywordInterruption(TypedDict, total=False): + mode: Required[Literal['keywords']] + keywords: Required[List[str]] + +class AgentCreateModel_ManagedCredential(TypedDict, total=False): + mode: Required[Literal['managed']] + +class AgentCreateModel_ManualTurnBoundary(TypedDict, total=False): + type: Required[Literal['manual']] + +class AgentCreateModel_Message(TypedDict, total=False): + role: Required[Literal['system', 'user', 'assistant']] + content: Required[str] + +class AgentCreateModel_ProviderModule(TypedDict, total=False): + credential: Union[Union['AgentCreateModel_ManagedCredential', 'AgentCreateModel_ByokCredential'], None] + vendor: Union[str, None] + model: Union[str, None] + style: Union[str, None] + language: Union[str, None] + enabled: Union[bool, None] + params: Union[Dict[str, 'AgentCreateModel_JsonValue'], None] + url: 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] + mcp_servers: Union[List[Dict[str, 'AgentCreateModel_JsonValue']], None] + tools: Union[List[Dict[str, 'AgentCreateModel_JsonValue']], None] + skip_patterns: Union[List[str], None] + +class AgentCreateModel_RealtimePipeline(TypedDict, total=False): + mode: Required[Literal['realtime']] + avatar: Union['AgentCreateModel_ProviderModule', None] + turn_detection: Union[Union['AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SemanticVadTurnDetection'], None] + mllm: Union['AgentCreateModel_ProviderModule', None] + +class AgentCreateModel_SemanticTurnEnd(TypedDict, total=False): + type: Required[Literal['semantic']] + silence_duration_ms: Union[int, None] + max_wait_ms: Union[int, None] + pause_state_enabled: Union[bool, None] + +class AgentCreateModel_SemanticTurnStart(TypedDict, total=False): + type: Required[Literal['semantic']] + speaking_interrupt_duration_ms: Union[int, None] + +class AgentCreateModel_SemanticVadTurnDetection(TypedDict, total=False): + type: Required[Literal['semantic_vad']] + eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] + +class AgentCreateModel_ServerVadTurnDetection(TypedDict, total=False): + type: Required[Literal['server_vad']] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, int, None] + idle_timeout_ms: Union[int, None] + start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] + end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] + +class AgentCreateModel_SilenceConfig(TypedDict, total=False): + enabled: Required[bool] + timeout_ms: Required[int] + action: Required[Literal['speak', 'think']] + content: Required[str] + +class AgentCreateModel_SpeechInterruption(TypedDict, total=False): + mode: Required[Literal['start_of_speech']] + +class AgentCreateModel_StaticFillerContent(TypedDict, total=False): + mode: Required[Literal['static']] + phrases: Required[List[str]] + selection_rule: Literal['shuffle', 'round_robin'] + +class AgentCreateModel_TextGreeting(TypedDict, total=False): + mode: Required[Literal['text']] + on: Literal['each_join', 'first_join'] + delay_ms: int + text: Required[str] + +class AgentCreateModel_TurnDetection(TypedDict, total=False): + speech_threshold: Union[float, int, None] + start_of_speech: Union[Union['AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_ManualTurnBoundary'], None] + end_of_speech: Union[Union['AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_ManualTurnBoundary'], None] + interruption: Union[Union['AgentCreateModel_SpeechInterruption', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_DisabledInterruption'], None] + +class AgentCreateModel_VadTurnEnd(TypedDict, total=False): + type: Required[Literal['vad']] + silence_duration_ms: Union[int, None] + +class AgentCreateModel_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 AgentCreateModel_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 AgentCreateModel(TypedDict, total=False): + agent_name: Required[str] + instructions: Union[str, None] + messages: Union[List['AgentCreateModel_Message'], None] + greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] + variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] + pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] + filler_words: Union['AgentCreateModel_FillerWords', None] + +class AgentPatchRequest_CredentialPatch(TypedDict, total=False): + mode: Union[Literal['managed', 'byok'], None] + api_key: Union[str, None] + +class AgentPatchRequest_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 AgentPatchRequest_FillerTriggerPatch(TypedDict, total=False): + mode: Union[Literal['fixed_time'], None] + response_wait_ms: Union[int, None] + +class AgentPatchRequest_FillerWordsPatch(TypedDict, total=False): + enable: Union[bool, None] + trigger: Union['AgentPatchRequest_FillerTriggerPatch', None] + content: Union['AgentPatchRequest_FillerContentPatch', None] + +class AgentPatchRequest_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 AgentPatchRequest_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] + +AgentPatchRequest_JsonValue = Any + +class AgentPatchRequest_Message(TypedDict, total=False): + role: Required[Literal['system', 'user', 'assistant']] + content: Required[str] + +class AgentPatchRequest_PipelinePatch(TypedDict, total=False): + mode: Union[Literal['cascaded', 'realtime'], None] + avatar: Union['AgentPatchRequest_ProviderModulePatch', None] + turn_detection: Union['AgentPatchRequest_PipelineTurnDetectionPatch', None] + asr: Union['AgentPatchRequest_ProviderModulePatch', None] + tts: Union['AgentPatchRequest_ProviderModulePatch', None] + llm: Union['AgentPatchRequest_ProviderModulePatch', None] + mllm: Union['AgentPatchRequest_ProviderModulePatch', None] + silence_config: Union['AgentPatchRequest_SilenceConfigPatch', None] + +class AgentPatchRequest_PipelineTurnDetectionPatch(TypedDict, total=False): + type: Union[Literal['agora_vad', 'server_vad', 'semantic_vad'], None] + speech_threshold: Union[float, int, None] + start_of_speech: Union['AgentPatchRequest_TurnBoundaryPatch', None] + end_of_speech: Union['AgentPatchRequest_TurnBoundaryPatch', None] + interruption: Union['AgentPatchRequest_InterruptionPatch', None] + interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, int, None] + idle_timeout_ms: Union[int, None] + start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] + end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] + eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] + +class AgentPatchRequest_ProviderModulePatch(TypedDict, total=False): + credential: Union['AgentPatchRequest_CredentialPatch', None] + vendor: Union[str, None] + model: Union[str, None] + style: Union[str, None] + language: Union[str, None] + enabled: Union[bool, None] + params: Union[Dict[str, 'AgentPatchRequest_JsonValue'], None] + url: 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] + mcp_servers: Union[List[Dict[str, 'AgentPatchRequest_JsonValue']], None] + tools: Union[List[Dict[str, 'AgentPatchRequest_JsonValue']], None] + skip_patterns: Union[List[str], None] + +class AgentPatchRequest_SilenceConfigPatch(TypedDict, total=False): + enabled: Union[bool, None] + timeout_ms: Union[int, None] + action: Union[Literal['speak', 'think'], None] + content: Union[str, None] + +class AgentPatchRequest_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, None] + pause_state_enabled: Union[bool, None] + +class AgentPatchRequest_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 AgentPatchRequest(TypedDict, total=False): + agent_name: Union[str, None] + instructions: Union[str, None] + messages: Union[List['AgentPatchRequest_Message'], None] + greeting: Union['AgentPatchRequest_GreetingPatch', None] + variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] + pipeline: Union['AgentPatchRequest_PipelinePatch', None] + filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] + +Contract_VariableDefinitions = Dict[str, 'Contract_VariableDefinition'] + +Contract_ModelCredential = Union[Dict[str, Any], Dict[str, Any]] + +class Contract_Llm(TypedDict, total=False): + credential: 'Contract_ModelCredential' + url: str + vendor: str + style: str + model: str + params: Dict[str, Any] + input_modalities: List[str] + output_modalities: List[str] + max_history: int + failure_message: str + mcp_servers: List['Contract_McpServer'] + tools: List['Contract_FunctionTool'] + headers: Dict[str, str] + +class Contract_CascadedPipeline(TypedDict, total=False): + mode: Required[Literal['cascaded']] + avatar: 'Contract_Avatar' + turn_detection: 'Contract_TurnDetection' + asr: 'Contract_Asr' + tts: 'Contract_Tts' + llm: 'Contract_Llm' + silence_config: 'Contract_SilenceConfig' + +Contract_Pipeline = Union['Contract_CascadedPipeline', 'Contract_RealtimePipeline'] + +class Contract_SemanticVadTurnDetection(TypedDict, total=False): + type: Required[Literal['semantic_vad']] + eagerness: Literal['auto', 'low', 'medium', 'high'] + +class Contract_SilenceConfig(TypedDict, total=False): + enabled: Required[bool] + timeout_ms: Required[int] + action: Required[Literal['speak', 'think']] + content: Required[str] + +class Contract_ServerVadTurnDetection(TypedDict, total=False): + type: Required[Literal['server_vad']] + prefix_padding_ms: int + silence_duration_ms: int + threshold: float + idle_timeout_ms: int + start_of_speech_sensitivity: Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'] + end_of_speech_sensitivity: Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'] + +class Contract_McpServer(TypedDict, total=False): + name: Required[str] + endpoint: Required[str] + transport: Required[Literal['streamable_http']] + headers: Dict[str, str] + allowed_tools: List[str] + timeout_ms: int + +class Contract_VariableDefinition(TypedDict, total=False): + type: Required[Literal['str', 'int', 'float', 'bool', 'date', 'datetime']] + required: bool + options: List[Union[str, float, bool]] + extract: bool + description: str + +Contract_Greeting = Union[Dict[str, Any], Dict[str, Any]] + +class Contract_Asr(TypedDict, total=False): + credential: 'Contract_ModelCredential' + url: str + vendor: str + model: str + language: str + params: Dict[str, Any] + +class Contract_FillerWords(TypedDict, total=False): + enable: bool + trigger: Dict[str, Any] + content: Union[Dict[str, Any], Dict[str, Any]] + +Contract_Interruption = Union[Dict[str, Any], Dict[str, Any], Dict[str, Any]] + +Contract_TurnStart = Union[Dict[str, Any], Dict[str, Any], Dict[str, Any]] + +class Contract_FunctionTool(TypedDict, total=False): + type: Required[Literal['function']] + name: Required[str] + description: Required[str] + parameters: Required['Contract_JsonSchema'] + execution: Required['Contract_FunctionToolExecution'] + server: Required['Contract_FunctionToolServer'] + +class Contract_Tts(TypedDict, total=False): + credential: 'Contract_ModelCredential' + url: str + headers: Dict[str, str] + vendor: str + params: Dict[str, Any] + skip_patterns: List[Literal['fullwidth_parentheses', 'lenticular_brackets', 'parentheses', 'square_brackets', 'curly_braces']] + +class Contract_RealtimePipeline(TypedDict, total=False): + mode: Required[Literal['realtime']] + avatar: 'Contract_Avatar' + turn_detection: 'Contract_RealtimeTurnDetection' + mllm: 'Contract_Mllm' + +class Contract_Mllm(TypedDict, total=False): + credential: 'Contract_ModelCredential' + vendor: Required[str] + model: str + url: str + params: Dict[str, Any] + input_modalities: List[Literal['text', 'audio']] + output_modalities: List[Literal['text', 'audio']] + mcp_servers: List['Contract_McpServer'] + headers: Dict[str, str] + +class Contract_Avatar(TypedDict, total=False): + enabled: bool + credential: 'Contract_ModelCredential' + vendor: Literal['akool', 'liveavatar', 'anam', 'generic'] + params: Dict[str, Any] + +Contract_RealtimeTurnDetection = Union['Contract_AgoraVadTurnDetection', 'Contract_ServerVadTurnDetection', 'Contract_SemanticVadTurnDetection'] + +class Contract_FunctionToolServer(TypedDict, total=False): + method: Required[Literal['GET', 'POST']] + url: Required[str] + headers: Dict[str, str] + body: Dict[str, Any] + timeout_ms: int + +Contract_TurnEnd = Union[Dict[str, Any], Dict[str, Any], Dict[str, Any]] + +class Contract_Message(TypedDict, total=False): + role: Required[Literal['system', 'user', 'assistant']] + content: Required[str] + +class Contract_AgentCreateInput(TypedDict, total=False): + agent_name: Required[str] + instructions: str + messages: List['Contract_Message'] + greeting: 'Contract_Greeting' + variables: 'Contract_VariableDefinitions' + pipeline: 'Contract_Pipeline' + filler_words: 'Contract_FillerWords' + +class Contract_TurnDetection(TypedDict, total=False): + speech_threshold: float + start_of_speech: 'Contract_TurnStart' + end_of_speech: 'Contract_TurnEnd' + interruption: 'Contract_Interruption' + +class Contract_AgoraVadTurnDetection(TypedDict, total=False): + type: Required[Literal['agora_vad']] + interrupt_duration_ms: int + prefix_padding_ms: int + silence_duration_ms: int + threshold: float + +class Contract_FunctionToolExecution(TypedDict, total=False): + mode: Required[Literal['sync']] + +Contract_JsonSchema = Union[Dict[str, Any], bool] + +class AgentCreateRequest(TypedDict, total=False): + agent_name: Required[str] + instructions: Union[str, None] + messages: Union[List['AgentCreateModel_Message'], None] + greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] + variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] + pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] + filler_words: Union['AgentCreateModel_FillerWords', None] + +__all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AgoraVadTurnDetection', 'AudioGreeting', 'DisabledInterruption', 'EndCallPolicy', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'GeneratedFillerContent', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JsonValue', 'KeywordInterruption', 'ManualTurnBoundary', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedMllm', 'ResolvedRealtimePipeline', 'ResolvedTts', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SemanticVadTurnDetection', 'ServerVadTurnDetection', 'SessionAnalysis', 'SessionData', 'SessionList', 'SessionResponse', 'SessionSummary', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SpeechInterruption', 'StaticFillerContent', 'TextGreeting', 'TransferPolicy', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_AudioGreeting', 'AgentCreateModel_ByokCredential', 'AgentCreateModel_CascadedPipeline', 'AgentCreateModel_DisabledInterruption', 'AgentCreateModel_FillerTrigger', 'AgentCreateModel_FillerWords', 'AgentCreateModel_GeneratedFillerContent', 'AgentCreateModel_JsonValue', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_ManagedCredential', 'AgentCreateModel_ManualTurnBoundary', 'AgentCreateModel_Message', 'AgentCreateModel_ProviderModule', 'AgentCreateModel_RealtimePipeline', 'AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_SemanticVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SilenceConfig', 'AgentCreateModel_SpeechInterruption', 'AgentCreateModel_StaticFillerContent', 'AgentCreateModel_TextGreeting', 'AgentCreateModel_TurnDetection', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_VariableDefinition', 'AgentCreateModel', 'AgentPatchRequest_CredentialPatch', 'AgentPatchRequest_FillerContentPatch', 'AgentPatchRequest_FillerTriggerPatch', 'AgentPatchRequest_FillerWordsPatch', 'AgentPatchRequest_GreetingPatch', 'AgentPatchRequest_InterruptionPatch', 'AgentPatchRequest_JsonValue', 'AgentPatchRequest_Message', 'AgentPatchRequest_PipelinePatch', 'AgentPatchRequest_PipelineTurnDetectionPatch', 'AgentPatchRequest_ProviderModulePatch', 'AgentPatchRequest_SilenceConfigPatch', 'AgentPatchRequest_TurnBoundaryPatch', 'AgentPatchRequest_VariableDefinitionPatch', 'AgentPatchRequest', 'Contract_VariableDefinitions', 'Contract_ModelCredential', 'Contract_Llm', 'Contract_CascadedPipeline', 'Contract_Pipeline', 'Contract_SemanticVadTurnDetection', 'Contract_SilenceConfig', 'Contract_ServerVadTurnDetection', 'Contract_McpServer', 'Contract_VariableDefinition', 'Contract_Greeting', 'Contract_Asr', 'Contract_FillerWords', 'Contract_Interruption', 'Contract_TurnStart', 'Contract_FunctionTool', 'Contract_Tts', 'Contract_RealtimePipeline', 'Contract_Mllm', 'Contract_Avatar', 'Contract_RealtimeTurnDetection', 'Contract_FunctionToolServer', 'Contract_TurnEnd', 'Contract_Message', 'Contract_AgentCreateInput', 'Contract_TurnDetection', 'Contract_AgoraVadTurnDetection', 'Contract_FunctionToolExecution', 'Contract_JsonSchema', 'AgentCreateRequest'] \ No newline at end of file diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py new file mode 100644 index 0000000..a7c06d0 --- /dev/null +++ b/tests/custom/test_v3_preview.py @@ -0,0 +1,374 @@ +"""Offline contract tests for the additive v3 preview and v2 boundary.""" +import json + +import httpx +import pytest + +from agora_agent import ( + Agent, Agora, AsyncAgora, Area, AsyncDeployedAgent, DeployedAgent, + DeepgramSTT, OpenAI, OpenAITTS, NumberNotFoundError, 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"}, "idempotency_key": "dedupe"}, "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": {"host": "sip.example"}, "idempotency_key": "number-key"}, "POST", "numbers", {"number": "+12025550123", "sip_trunk": {"host": "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), +] + + +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", app_certificate="cert", auth_token="raw-token", + httpx_client=http, v3=V3Options(base_url="https://stage.example/", headers={"Gateway": "secret"}), **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/v3/projects/project/" + suffix).encode() + assert request.headers["Authorization"] == "agora token=raw-token" + assert request.headers["Gateway"] == "secret" + if body is not None: + assert json.loads(request.content) == body + 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_twenty_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_twenty_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_pending_writes_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 not hasattr(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": "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("resource,kwargs", [("agents", {"agent_name": "x"}), ("numbers", {"number": "+1", "sip_trunk": {}})]) +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="tts-key", 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", "api_key": "$secrets.prod.dg"}} + assert body["pipeline"]["llm"]["credential"]["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", "api_key": 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 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", "api_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"))) + for call in (lambda: agent.to_properties(channel="c", agent_uid="1", remote_uids=["*"]), + lambda: agent.create_session(channel="c", agent_uid="1", remote_uids=["*"]), + lambda: agent.create_async_session(channel="c", agent_uid="1", remote_uids=["*"])): + with pytest.raises(SecretRefNotSupportedError, match="requires deploy"): + call() + with pytest.raises(ValueError): + secret_ref("prod.bad", "key") + + +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_staging_environment_override_and_shared_basic_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", customer_id="id", customer_secret="secret", + 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"] == "Basic aWQ6c2VjcmV0" + # 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="project", app_certificate="cert", + 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"] == "project" + + +def test_realtime_instructions_and_audio_greeting_mapping(): + from agora_agent import OpenAIRealtime + agent = Agent(client=make_client(lambda _: pytest.fail("mapping only"))).with_mllm( + OpenAIRealtime(api_key="key", model="gpt-realtime", instructions="Realtime instructions")) + body = build_body(agent) + assert body["instructions"] == "Realtime instructions" + assert "instructions" not in body["pipeline"]["mllm"].get("params", {}) + 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} From 88fc0d73a8a49e85613a91a487f895e292b2bbc1 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 13:38:56 +0800 Subject: [PATCH 11/57] feat: list assigned v3 numbers and document preview workflows --- README.md | 2 +- docs/guides/v3-api-preview.md | 221 +++++++++++++++++++++++++------- docs/index.md | 5 + docs/reference/v3.md | 153 ++++++++++++++++++++++ reference.md | 2 + src/agora_agent/agentkit/v3.py | 16 ++- tests/custom/test_v3_preview.py | 25 ++++ 7 files changed, 377 insertions(+), 47 deletions(-) create mode 100644 docs/reference/v3.md diff --git a/README.md b/README.md index 2ccf6f2..7e11c79 100644 --- a/README.md +++ b/README.md @@ -374,4 +374,4 @@ On the other hand, contributions to the README are always very welcome! ### v3 stored-agent preview -The additive `client.v3` API and AgentKit `deploy()` / `deploy_async()` save stored agent configurations without changing v2 session behavior. See the [v3 preview guide](docs/guides/v3-api-preview.md) for staging overrides, typed wire responses, ETags, secret references, number assignment, async usage and current backend limitations. +The additive `client.v3` API and AgentKit `deploy()` / `deploy_async()` save stored agent configurations without changing v2 session behavior. See the [v3 preview guide](docs/guides/v3-api-preview.md) for a complete staging quick start, deploy updates, secret references, number assignment/listing, async usage and backend limits. The [v3 reference](docs/reference/v3.md) lists all 20 low-level operations and their conditional headers. This protected custom adapter is locally generated from staging OpenAPI, not from the canonical Fern pipeline. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 242ce5a..6253657 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -1,74 +1,205 @@ # v3 stored-agent preview (P1) -The package version continues to describe SDK compatibility. Existing v2 resources and `Agent.create_session()` remain available. The additive `client.v3` namespace covers all 20 published staging operations: agents and numbers CRUD, secrets CRUD plus individual key writes/deletes, and session list/get. Session writes, outbound calls, analysis helpers and campaigns await backend schemas and are not implemented. +Use `Agent.deploy()` to save a reusable configuration and assign it to an existing inbound number. The additive `client.v3` namespace covers all 20 published staging 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 / `DeployedAgent` | 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` | Low-level list and get only | + +Deploying stores configuration; it does not start a session. v3 session writes, outbound calls, AgentKit analysis helpers and campaigns await backend schemas and are not implemented. The SDK package version continues to describe package compatibility independently of the REST API version. + +## Quick start with staging + +Install from this repository's `feat/v3-api-preview` checkout: + +```sh +python -m pip install -e . +``` + +See [installation](../getting-started/installation.md) for prerequisites. v3 is a preview branch feature; these examples do not imply that a released package contains it. Obtain an approved staging origin and gateway credentials, then set: + +```sh +export AGORA_APP_ID='YOUR_APP_ID' +export AGORA_APP_CERTIFICATE='YOUR_APP_CERTIFICATE' +export AGORA_AGENTS_V3_BASE_URL='https://YOUR_STAGING_HOST' +export AGORA_V3_GATEWAY_SECRET='YOUR_GATEWAY_SECRET' +export AGORA_V3_CONSUMER_USERNAME='YOUR_CONSUMER_USERNAME' +export OPENAI_API_KEY='YOUR_OPENAI_KEY' +export DEEPGRAM_API_KEY='YOUR_DEEPGRAM_KEY' +``` + +The base URL is an HTTP(S) **origin**, without a project path, query or fragment. The SDK appends `/conversational-ai/v3/projects/{appid}`. `V3Options(base_url=...)` overrides the environment variable; headers apply only to v3. Existing client authentication still supplies the Authorization header. Production paths, regional availability and Basic-auth acceptance remain unconfirmed; use the supplied staging configuration for evaluation. + +This complete cascaded configuration stores one project secret set, then deploys a stored 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 -from agora_agent import Agora, Area, Agent, DeepgramSTT, OpenAI, V3Options, secret_ref +import os +from agora_agent import ( + Agent, Agora, Area, DeepgramSTT, OpenAI, OpenAITTS, V3Options, secret_ref, +) client = Agora( - area=Area.US, app_id="APP_ID", app_certificate="APP_CERTIFICATE", - # Staging origin only. Supply the gateway's required headers explicitly. - v3=V3Options(base_url="https://YOUR_STAGING_HOST", headers={ - "X-Studio-V2-Gateway-Secret": "GATEWAY_SECRET", - "X-Consumer-Username": "CONSUMER", + area=Area.US, + app_id=os.environ["AGORA_APP_ID"], + app_certificate=os.environ["AGORA_APP_CERTIFICATE"], + v3=V3Options(headers={ + "X-Studio-V2-Gateway-Secret": os.environ["AGORA_V3_GATEWAY_SECRET"], + "X-Consumer-Username": os.environ["AGORA_V3_CONSUMER_USERNAME"], }), ) - -client.v3.secrets.create(name="prod", secrets={"openai": "OPENAI_KEY", "deepgram": "DEEPGRAM_KEY"}, - idempotency_key="initial-prod-secrets") -agent = (Agent(client=client) - .with_stt(DeepgramSTT(api_key=secret_ref("prod", "deepgram"), model="nova-3")) - .with_llm(OpenAI(api_key=secret_ref("prod", "openai"), model="gpt-4o-mini", - base_url="https://api.openai.com/v1/chat/completions", - system_messages=[{"role": "system", "content": "Help callers briefly."}])) - .with_variable("resolved", type="bool", extract=True, description="Was the issue resolved?")) -# Add your usual .with_tts(...) configuration for a complete voice pipeline. +client.v3.secrets.create( + name="support", + secrets={"openai": os.environ["OPENAI_API_KEY"], "deepgram": os.environ["DEEPGRAM_API_KEY"]}, + idempotency_key="support-secrets-initial-provisioning", +) +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.deploy(name="Support") -print(support.id, support.etag) -support.assign_number("+12025550123", max_call_duration_seconds=900) -support.unassign_number("+12025550123") +print(support.id, support.etag) # Persist the ID for the next deploy. +``` + +The example endpoints are existing vendor configuration, not additional v3 defaults. Use backend-approved provider endpoints/models for your staging deployment. Managed mode may also require an explicit provider URL on staging. + +## Redeploy, load and delete + +Pass the saved ID to update the same stored agent. Omitting `agent_id` always creates another configuration; matching names do not provide upsert or deduplication. + +```python +from agora_agent import DeployedAgent -# Save the ID; a later deploy to that ID GETs its ETag then PATCHes. support = agent.deploy(name="Support", agent_id=support.id) -loaded = type(support).get(client, support.id) -# loaded.delete() deletes the stored configuration. +loaded = DeployedAgent.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. +``` + +Redeploy 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 `api_key`. + +This is a merge patch rather than a guaranteed replacement. Write-only keys and headers cannot be reconstructed from GET, and omitted hidden values remain subject to backend merge semantics. Keep intended credentials in the builder or use a targeted [low-level update](../reference/v3.md#agents-five-operations) when retaining existing hidden credentials matters. Agent deploy 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.v3.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") ``` -Numbers must already exist in the same project. `assign_number` accepts an E.164 string, a number ID (without assuming UUID format), or a v3 number dictionary with `number_id`. E.164 lookup scans cursor pages at 100 items per request and compares formatting-normalized numbers. Assignment GETs the number for its ETag, then PATCHes `inbound.agent`; a 412 triggers one fresh GET and one retry. An ID usually costs two requests, E.164 adds list requests, and a conflict adds two requests. `max_silence_duration_ms` is the other supported call-policy option. Unknown numbers raise `NumberNotFoundError` naming the project. +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. -## Low-level access, headers and pagination +`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. -Every resource has explicit snake_case methods. `appid` defaults to the owning client's `app_id` and may be overridden per call. Agents, numbers and sessions use `agent_id`, `number_id` and `agent_session_id` respectively. Secret sets use `name`, and individual key operations also take `key`. +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 -created = client.v3.agents.create(agent_name="Minimal stored agent") -response = client.v3.agents.with_raw_response.get(agent_id=created["agent_id"]) -updated = client.v3.agents.update(agent_id=created["agent_id"], if_match=response.etag, - instructions="New instructions") -print(response.headers.get("Agora-Request-Id"), response.status_code) - -page = client.v3.sessions.list(limit=20, status="ACTIVE", channel="room", - started_after=100, started_before=200) -next_page = client.v3.sessions.list(limit=20, cursor=page["next_cursor"]) if page["has_more"] else None +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.v3.numbers.iter_all(limit=100): print(number["number_id"]) -# iter_pages() yields complete pages, including count/has_more/next_cursor. +``` + +`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 returns normal parsed data rather than a raw response. To obtain the new ETag for a deliberate retry, GET through `.with_raw_response` again. -client.v3.secrets.set_key(name="prod", key="new_key", value="VALUE", if_none_match="*") +## Async applications + +Use `AsyncAgora`, the same low-level keyword arguments, and `await agent.deploy_async(...)`. The deployed handle is `AsyncDeployedAgent`; `get`, `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, AsyncDeployedAgent, 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"], + v3=V3Options(headers={ + "X-Studio-V2-Gateway-Secret": os.environ["AGORA_V3_GATEWAY_SECRET"], + "X-Consumer-Username": os.environ["AGORA_V3_CONSUMER_USERNAME"], + }), + ) + support = await AsyncDeployedAgent.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.deploy_async(name="Support", agent_id=saved_id) +# await support.assign_number("+12025550123") +# await support.unassign_number("+12025550123") +# await support.delete() ``` -Responses are typed wire dictionaries defined in `agora_agent.v3.types`. Raw responses expose `.data`, `.headers`, `.etag`, `.status_code` and the underlying httpx `.response`. They preserve server pagination metadata and error `{detail, reason}` bodies. `PreconditionFailedError` is an `ApiError` with status/body/headers and `latest()` to GET the current resource (a secret key conflict reloads its set). `latest()` does not repeat the write. Required `If-Match` values are explicit arguments. Omitted optional body fields remain absent; explicit `None` survives as JSON null. PATCH uses the staging contract's `application/json` content type. +Use `await error.latest()` after an async 412. `deploy()` requires an `Agora` client; `deploy_async()` requires `AsyncAgora`. Passing the other client kind raises `TypeError` before deployment. + +## Compatibility and backend limits + +Existing v2 resources and `Agent.create_session()` behavior remain available. A v3 `secret_ref` cannot be sent as a v2 API key: `create_session`, `create_async_session` and `to_properties` reject it with `SecretRefNotSupportedError("secret_ref requires deploy()")`. Build a separate configuration with inline credentials for an existing v2 session. Variables configure stored-agent definitions; this phase does not add session-start variable values or an AgentKit extraction-results helper. -## Async +`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. `secret_ref(set_name, key)` validates 1–64 letters, digits, underscores or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. -Bind an `AsyncAgora` client, use `await agent.deploy_async(...)`, and receive an `AsyncDeployedAgent`. Its `get`, `delete`, `assign_number`, and `unassign_number` methods are awaitable. Low-level methods have identical arguments under `async_client.v3`, including `.with_raw_response`. Use `async for` with `.iter_pages()` / `.iter_all()`, and `await error.latest()` after an async conflict. `deploy()` requires `Agora`; `deploy_async()` requires `AsyncAgora`. +The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Turn detection/interruption, supported text/audio greetings, silence settings and filler words are converted to v3 shapes. -## Mapping and preview limitations +| 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 | +| Multipart AWS/Vertex credentials, removed avatar vendors, numeric v2 TTS `skip_patterns`, 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 `end_call`, `transfer`, `allowed_ips`, SIP-password secret references | Currently rejected or unsupported on staging | +| 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 POST has no idempotency key; numbers currently ignore their declared key. An uncertain create outcome requires inspection before manual retry | +| CN and production | AgentKit CN deployment is refused; production path, regional coverage and Basic-auth support remain unconfirmed | +| Session writes, outbound calling, AgentKit analysis and campaigns | No implementation until schemas are published | -`with_variable` is pure and immutable. Variable names match `^[a-z][a-z0-9_]{0,63}$`; types are `str`, `int`, `float`, `bool`, `date`, `datetime`; extraction requires a nonempty description. Existing builder/vendor configuration maps centrally to v3 provider modules, instructions/messages, text/audio greetings, turn detection/interruption, silence settings and filler words. Vendor instructions/greetings take precedence over deprecated Agent fields. API keys become `credential: {mode: "byok", api_key: ...}`; absent keys become `managed`. Provider URLs already serialized by existing vendor classes are preserved, and no additional endpoint defaults are invented. +The adapter shares the existing client's HTTP transport, authentication and current regional-pool origin. The staging override applies only to v3. These docs and offline tests do not establish live backend availability. -Strict mapping raises one `UnsupportedFieldError` listing unmappable fields before any request. These include SAL, advanced features, geofence, labels, RTC configuration, pipeline IDs, and parameters other than `silence_config`. Value-valued `template_variables` cannot be converted into variable definitions; use `with_variable`. Multipart AWS/Vertex credentials, removed avatar vendors, and numeric v2 TTS `skip_patterns` are rejected because no lossless mapping is confirmed. Unsupported deprecated turn settings and greeting policies are also reported rather than silently dropped. Secret references on `create_session`, `create_async_session`, or `to_properties` raise `SecretRefNotSupportedError` before a v2 network request. +## Maintainer provenance -Redeploy recursively nulls stale **readable** mutable configuration fields, including nested provider params, because JSON merge patch does not imply nested replacement. IDs, timestamps, and resolved credential metadata are never echoed. Credentials come from the builder; switching to managed explicitly nulls `api_key`. Write-only keys/headers cannot be reconstructed from GET, so omitted hidden values and mode-switch behavior remain subject to backend merge semantics. Deploy is an ETag-guarded patch, not a guaranteed PUT/replace. Use the low-level raw resource to make targeted patches when retaining hidden credentials matters. Agent conflicts are not retried. All low-level writes disable automatic HTTP retries, even if `max_retries` was requested; staging agent POST has no idempotency protection and numbers currently ignore their declared key. A failed create may have succeeded remotely: inspect resources before manually retrying. +The v3 low-level methods and TypedDict wire schemas were generated locally from the staging OpenAPI reviewed for this preview. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. -The shared existing HTTP transport, auth and regional pool are reused; v3 overrides only its origin and headers. `AGORA_AGENTS_V3_BASE_URL` provides the same origin override as `V3Options.base_url` (the explicit option wins). The request path is `/conversational-ai/v3/projects/{appid}`. Production origins/path, all regional availability and Basic-auth support remain unconfirmed. CN deployment is deliberately refused. Staging may require a URL even with managed credentials; supply a backend-approved provider URL through existing vendor configuration. Deferred tools/MCP/custom destinations may be rejected by the server. Number `end_call`, `transfer`, `allowed_ips` and SIP secret references are also currently unsupported by staging. No live staging write was performed for this preview. +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. diff --git a/docs/index.md b/docs/index.md index c1f8da3..fd8fb9a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,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 +61,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 while existing v2 sessions keep their current lifecycle. Start with the [v3 workflow guide](./guides/v3-api-preview.md), or consult the [complete 20-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/v3.md b/docs/reference/v3.md new file mode 100644 index 0000000..b1aa527 --- /dev/null +++ b/docs/reference/v3.md @@ -0,0 +1,153 @@ +# v3 preview reference + +This reference describes the 20 implemented staging 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 locally generated from staging OpenAPI, 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/v3/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. Pipelines use `mode="cascaded"` with ASR/LLM/TTS or `mode="realtime"` with MLLM. Provider credentials use `mode="managed"` or `mode="byok", api_key=...`; secret references are strings in `api_key`. + +`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/v3/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, idempotency_key=None)` | 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/hyphen syntax; `secret_ref` validates those components locally. + +| Argument | Header | Requirement | +|---|---|---| +| Create `idempotency_key` | `Idempotency-Key` | Optional; honored for secret creation | +| 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/v3/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, idempotency_key=None)` | 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 exposes `idempotency_key` as declared by OpenAPI, but staging currently ignores it; it does not make a repeated create safe. `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: two read operations + +Prefix: `/conversational-ai/v3/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` | + +These 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` and `analysis`; 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. + +`analysis`, when present, exposes extraction status and `result` through the low-level response. This phase has no session start/stop/update/actions, outbound-call methods, or AgentKit `get_analysis`. There is no campaigns namespace. + +## 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. 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 v3-only origin and headers. Origin precedence is explicit option, `AGORA_AGENTS_V3_BASE_URL`, then the origin of the existing regional pool. Only HTTP(S) origins with no path beyond `/`, query or fragment are accepted. The adapter shares HTTP clients, auth and timeouts with the owning `Agora` / `AsyncAgora`. It does not establish production or regional backend availability. + +## AgentKit surface + +| Operation | Synchronous | Async client | +|---|---|---| +| Store/update a builder | `agent.deploy(name=None, agent_id=None)` → `DeployedAgent` | `await agent.deploy_async(...)` → `AsyncDeployedAgent` | +| Load a stored handle | `DeployedAgent.get(client, agent_id)` | `await AsyncDeployedAgent.get(client, agent_id)` | +| Delete | `deployed.delete()` | `await deployed.delete()` | +| Assign | `deployed.assign_number(number, max_call_duration_seconds=None, max_silence_duration_ms=None)` | `await deployed.assign_number(...)` | +| Unassign | `deployed.unassign_number(number)` | `await deployed.unassign_number(number)` | +| Discover assigned numbers | `deployed.list_numbers()` → `List[NumberResponse]` | `await deployed.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 and deploy `name` / `agent_id` are keyword-only. 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. + +`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. diff --git a/reference.md b/reference.md index 55a516e..ad9c30b 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 20 staging operations alongside the existing v2 resources documented below. ## Agents
client.agents.start(...)
diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 99a5954..1da235a 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -4,7 +4,7 @@ import copy import re import time -from typing import Any, Dict, Mapping, Optional +from typing import Any, Dict, List, Mapping, Optional from ..v3 import PreconditionFailedError from ..v3.types import AgentResponse, NumberResponse @@ -412,6 +412,13 @@ def get(cls, client, agent_id) -> DeployedAgent: 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("+"): @@ -460,6 +467,13 @@ async def get(cls, client, agent_id) -> AsyncDeployedAgent: 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("+"): diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index a7c06d0..0ebe1d8 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -372,3 +372,28 @@ def test_realtime_instructions_and_audio_greeting_mapping(): "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} + + +@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 From 6578f09fdf238f3cb365d793a67aa8c7cbebb3bd Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 13:49:06 +0800 Subject: [PATCH 12/57] Fix v3 AgentKit realtime and managed preset mapping --- docs/guides/v3-api-preview.md | 2 +- src/agora_agent/agentkit/v3.py | 72 +++++++++++++++++++++++-------- tests/custom/test_v3_preview.py | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 19 deletions(-) diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 6253657..0846887 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -182,7 +182,7 @@ Existing v2 resources and `Agent.create_session()` behavior remain available. A `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. `secret_ref(set_name, key)` validates 1–64 letters, digits, underscores or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. -The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Turn detection/interruption, supported text/audio greetings, silence settings and filler words are converted to v3 shapes. +The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor messages move to top-level `messages`. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`. | Constraint | Current behavior | |---|---| diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 1da235a..8927457 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -89,6 +89,10 @@ def dump(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) keys = [] for source in (config, params): for field in ("api_key", "key", "api_subscription_key"): @@ -225,19 +229,30 @@ def build_body(agent, name=None): if realtime: realtime_params = conversational.get("params", {}) if isinstance(realtime_params, dict) and "instructions" in realtime_params: - result["instructions"] = realtime_params.pop("instructions") - messages = conversational.pop("system_messages", None) - if messages is not None: - system = [m["content"] for m in messages if m.get("role") == "system"] - if system: - if any(not isinstance(v, str) for v in system): - errors.append("llm.system_messages.content") + instructions = realtime_params.pop("instructions") + if not isinstance(instructions, str): + errors.append("mllm.params.instructions") else: + result["instructions"] = instructions + messages = conversational.pop("messages" if realtime else "system_messages", None) + if messages is not None: + source = "mllm.messages" if realtime else "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 messages if m.get("role") != "system"] - if non_system: - result["messages"] = non_system - elif "instructions" not in result and agent._instructions is not None: + 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) @@ -246,17 +261,22 @@ def build_body(agent, name=None): 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 {"on", "delay_ms", "download_timeout_ms", "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 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 - if "mode" in greeting: - # v2 greeting configs mode controls when playback occurs. - result["greeting"]["on"] = greeting["mode"] - result["greeting"]["mode"] = "audio" if greeting_url else "text" elif greeting: errors.append("greeting_configs (requires greeting)") if conversational.pop("template_variables", None): @@ -266,8 +286,24 @@ def build_body(agent, name=None): if td: td = dump(td) mode = td.pop("mode", td.pop("type", None)) - nested = td.pop("agora_vad_config", {}) - pipeline["turn_detection"] = {"type": mode, **nested, **td} + configurations = {"agora_vad": "agora_vad_config", "server_vad": "server_vad_config", "semantic_vad": "semantic_vad_config"} + allowed = { + "agora_vad": {"interrupt_duration_ms", "prefix_padding_ms", "silence_duration_ms", "threshold"}, + "server_vad": {"prefix_padding_ms", "silence_duration_ms", "threshold", "idle_timeout_ms", "start_of_speech_sensitivity", "end_of_speech_sensitivity"}, + "semantic_vad": {"eagerness"}, + } + if not isinstance(mode, str) or mode not in configurations: + errors.append("mllm.turn_detection.mode") + else: + nested = td.pop(configurations[mode], {}) or {} + if not isinstance(nested, dict): + errors.append("mllm.turn_detection." + configurations[mode]) + nested = {} + direct = {key: td.pop(key) for key in tuple(td) if key in allowed[mode]} + errors.extend("mllm.turn_detection." + key for key in direct if key in nested and direct[key] != nested[key]) + errors.extend("mllm.turn_detection." + key for key in td) + errors.extend("mllm.turn_detection." + key for key in nested if key not in allowed[mode]) + pipeline["turn_detection"] = {"type": mode, **nested, **direct} if agent._turn_detection is not None or agent._interruption is not None: errors.append("turn_detection (configure realtime vendor turn_detection)") else: diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 0ebe1d8..b7ded4c 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -374,6 +374,82 @@ def test_realtime_instructions_and_audio_greeting_mapping(): assert greeting == {"mode": "audio", "url": "https://audio.example/greeting.wav", "text": "Hi", "download_timeout_ms": 1000, "pcm_sample_rate": 16000} +def test_realtime_vendor_messages_use_top_level_and_preserve_instruction_precedence(): + from agora_agent import OpenAIRealtime + client = make_client(lambda _: pytest.fail("pure mapping")) + agent = Agent(client=client, instructions="Deprecated instructions").with_mllm( + OpenAIRealtime( + api_key="key", instructions="Provider instructions", + messages=[{"role": "system", "content": "Explicit system message"}, + {"role": "user", "content": "Hello"}], + ) + ) + body = build_body(agent) + assert body["instructions"] == "Explicit system message" + assert body["messages"] == [{"role": "user", "content": "Hello"}] + assert "messages" not in body["pipeline"]["mllm"] + assert "instructions" not in body["pipeline"]["mllm"].get("params", {}) + + params_only = build_body(Agent(client=client, instructions="Deprecated").with_mllm( + OpenAIRealtime(api_key="key", instructions="Provider", messages=[{"role": "assistant", "content": "Hi"}]) + )) + assert params_only["instructions"] == "Provider" + assert params_only["messages"] == [{"role": "assistant", "content": "Hi"}] + with pytest.raises(UnsupportedFieldError, match=r"mllm.messages\[0\]"): + build_body(Agent(client=client).with_mllm(OpenAIRealtime( + api_key="key", messages=[{"role": "tool", "content": "unsupported"}] + ))) + + +@pytest.mark.parametrize("mode,config,expected", [ + ("agora_vad", {"agora_vad_config": {"threshold": 0.5}}, {"type": "agora_vad", "threshold": 0.5}), + ("server_vad", {"server_vad_config": {"threshold": 0.6, "idle_timeout_ms": 5000}}, + {"type": "server_vad", "threshold": 0.6, "idle_timeout_ms": 5000}), + ("semantic_vad", {"semantic_vad_config": {"eagerness": "high"}}, + {"type": "semantic_vad", "eagerness": "high"}), +]) +def test_real_realtime_vendor_turn_detection_flattens_create_schema_fields(mode, config, expected): + from agora_agent import OpenAIRealtime + from agora_agent.types.mllm_turn_detection import MllmTurnDetection + client = make_client(lambda _: pytest.fail("pure mapping")) + turn = MllmTurnDetection(mode=mode, **config) + body = build_body(Agent(client=client).with_mllm(OpenAIRealtime(api_key="key", turn_detection=turn))) + actual = body["pipeline"]["turn_detection"] + assert actual == expected + + +def test_realtime_turn_detection_rejects_incompatible_config_without_dropping_it(): + from agora_agent import OpenAIRealtime + from agora_agent.types.mllm_turn_detection import MllmTurnDetection + client = make_client(lambda _: pytest.fail("pure mapping")) + turn = MllmTurnDetection(mode="server_vad", semantic_vad_config={"eagerness": "high"}) + with pytest.raises(UnsupportedFieldError, match="semantic_vad_config"): + build_body(Agent(client=client).with_mllm(OpenAIRealtime(api_key="key", turn_detection=turn))) + + +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"})) + + +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_): From 41f415c03fd2aa28792e475b88a6825271b5d476 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 13:51:49 +0800 Subject: [PATCH 13/57] Reject audio-only settings on text greetings --- docs/guides/v3-api-preview.md | 2 +- src/agora_agent/agentkit/v3.py | 4 +++- tests/custom/test_v3_preview.py | 21 +++++++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 0846887..45f7390 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -188,7 +188,7 @@ The central mapper reuses existing vendor configs. Vendor instructions and greet |---|---| | 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 | -| Multipart AWS/Vertex credentials, removed avatar vendors, numeric v2 TTS `skip_patterns`, unmappable deprecated turn/greeting settings | Rejected rather than guessed or silently omitted | +| Multipart AWS/Vertex credentials, removed avatar vendors, 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 `end_call`, `transfer`, `allowed_ips`, SIP-password secret references | Currently rejected or unsupported on staging | | 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 | diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 8927457..b67f3c7 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -4,7 +4,7 @@ import copy import re import time -from typing import Any, Dict, List, Mapping, Optional +from typing import List, Mapping, Optional from ..v3 import PreconditionFailedError from ..v3.types import AgentResponse, NumberResponse @@ -271,6 +271,8 @@ def build_body(agent, name=None): 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: diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index b7ded4c..29720bd 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -5,13 +5,24 @@ import pytest from agora_agent import ( - Agent, Agora, AsyncAgora, Area, AsyncDeployedAgent, DeployedAgent, - DeepgramSTT, OpenAI, OpenAITTS, NumberNotFoundError, PreconditionFailedError, - SecretRefNotSupportedError, UnsupportedFieldError, V3Options, secret_ref, + 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), @@ -434,6 +445,8 @@ def test_greeting_playback_mode_aliases_and_unsupported_mode(): 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 5a6bf60a4dcee4b53a3ec850781c19c9d9790225 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 13:57:55 +0800 Subject: [PATCH 14/57] fix: preserve v3 interaction and provider ASR languages --- docs/guides/v3-api-preview.md | 2 ++ src/agora_agent/agentkit/v3.py | 13 +++++----- tests/custom/test_v3_preview.py | 43 ++++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 45f7390..74b1fd6 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -184,6 +184,8 @@ Existing v2 resources and `Agent.create_session()` behavior remain available. A The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor messages move to top-level `messages`. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`; provider-specific `params.language` remains separate. A configured STT follows v2 interaction-language precedence (the explicit turn language, otherwise `en-US`). An explicit interaction language without an STT vendor uses the existing managed `ares` fallback, without inventing a URL; configure an approved vendor endpoint if staging requires one. 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` | diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index b67f3c7..5d08122 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -111,7 +111,7 @@ def module(config, category, errors): "avatar": {"vendor", "url", "enabled"}, }[category] # v2 provider serializers keep endpoints and ASR/LLM model choices in params. - for field in ("url", "model", "language"): + 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: @@ -317,12 +317,13 @@ def build_body(agent, name=None): pipeline[category] = module(conversational, category, errors) for source, target in (("stt", "asr"), ("tts", "tts"), ("avatar", "avatar")): value = getattr(agent, "_" + source, None) + if target == "asr" and not realtime: + language = agent._field_value(agent._turn_detection, "language") + if value is not None or language is not None: + # Preserve v2 interaction-language precedence and its existing + # managed ASR fallback, without adding ASR to partial builders. + value = agent._resolve_asr_config(agent._resolve_turn_detection_config()) if value is not None and (not realtime or target == "avatar"): - value = dump(value) - if target == "asr" and agent._turn_detection is not None: - language = dump(agent._turn_detection).get("language") - if language: - value.setdefault("language", language) pipeline[target] = module(value, target, errors) if "silence_config" in params: if realtime: diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 29720bd..55eb157 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -174,7 +174,7 @@ def handler(request): 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", "api_key": "$secrets.prod.dg"}} + assert body["pipeline"]["asr"] == {"vendor": "deepgram", "model": "nova-3", "language": "en-US", "credential": {"mode": "byok", "api_key": "$secrets.prod.dg"}} assert body["pipeline"]["llm"]["credential"]["api_key"] == "$secrets.prod.openai" assert "variables" not in build_body(base) assert deployed.raw is client.v3.agents @@ -486,3 +486,44 @@ def handler(request): 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="key", 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 pipeline["asr"]["params"]["language"] == "fr" + assert "language" not in pipeline.get("turn_detection", {}) + v2 = agent.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"] == pipeline["asr"]["params"]["language"] + + +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="key", model="nova-3", language="fr"))) + assert default["pipeline"]["asr"]["language"] == "en-US" + assert default["pipeline"]["asr"]["params"]["language"] == "fr" From 2b18bd7af0ecc79fca152eaa196ade37abacd8a7 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 14:04:09 +0800 Subject: [PATCH 15/57] fix: validate numeric choices and reload conflicts without write conditions --- src/agora_agent/agentkit/v3.py | 2 +- src/agora_agent/v3/__init__.py | 4 ++-- src/agora_agent/v3/_transport.py | 18 ++++++++++++---- src/agora_agent/v3/client.py | 10 +++++---- src/agora_agent/v3/types.py | 5 ++++- tests/custom/test_v3_preview.py | 37 ++++++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 5d08122..37bcc43 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -71,7 +71,7 @@ def variable_definition(name, definition=None, **fields): 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({(type(v), v) for v in options}) != len(options): + 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) diff --git a/src/agora_agent/v3/__init__.py b/src/agora_agent/v3/__init__.py index 91aac6a..9741b1c 100644 --- a/src/agora_agent/v3/__init__.py +++ b/src/agora_agent/v3/__init__.py @@ -1,6 +1,6 @@ """Additive v3 stored-agent preview. Sessions are read-only.""" -from ._transport import V3Options, RawResponse, PreconditionFailedError -from .client import V3Client, AsyncV3Client 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 index 4c81723..9e05326 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -3,12 +3,12 @@ import os from dataclasses import dataclass, field -from typing import Any, Dict, Generic, Mapping, Optional, TypeVar +from typing import Any, Generic, Mapping, Optional, TypeVar from urllib.parse import quote, urlsplit import httpx -from ..core.api_error import ApiError from ..agentkit.token import generate_convo_ai_token +from ..core.api_error import ApiError _UNSET: Any = object() T = TypeVar("T") @@ -105,7 +105,7 @@ def _request(self, method, path, *, body, headers, params, request_options): **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=request_options).data + 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): @@ -127,6 +127,16 @@ def iter_all(self, **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["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: @@ -134,7 +144,7 @@ async def _request(self, method, path, *, body, headers, params, request_options **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=request_options) + 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 diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py index 034edd5..a76666d 100644 --- a/src/agora_agent/v3/client.py +++ b/src/agora_agent/v3/client.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Any, Optional, Dict, List, Union -from typing_extensions import Literal -from ._transport import Resource, AsyncResource, RawResponse -from ._transport import _UNSET, segment + +from typing import Dict, List, Optional, Union + +from ._transport import _UNSET, AsyncResource, RawResponse, Resource, segment from .types import ( AgentCreateModel_AudioGreeting, AgentCreateModel_CascadedPipeline, @@ -31,6 +31,8 @@ SessionResponse, SipTrunkInput, ) +from typing_extensions import Literal + class AgentsClient(Resource): with_raw_response: AgentsRawClient diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index ce1bc96..23af1aa 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -1,7 +1,10 @@ """Typed staging OpenAPI wire schemas. Dictionaries preserve omitted versus null fields.""" from __future__ import annotations + from typing import Any, Dict, List, Union -from typing_extensions import TypedDict, Required, Literal + +from typing_extensions import Literal, Required, TypedDict + class AgentDeleteResponse(TypedDict, total=False): deleted: Required[Literal[True]] diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 55eb157..5d373e0 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -527,3 +527,40 @@ def handler(request): default = build_body(Agent(client=client).with_stt(DeepgramSTT(api_key="key", model="nova-3", language="fr"))) assert default["pipeline"]["asr"]["language"] == "en-US" assert default["pipeline"]["asr"]["params"]["language"] == "fr" + + +@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": "Bearer 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"] == "Bearer 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"' From 3a29bb25a7b701fd97b9e6e9e023671f53c1fae2 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 14:04:36 +0800 Subject: [PATCH 16/57] fix: declare stored agent variable state for type checking --- src/agora_agent/agentkit/agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index 56a245c..97fb7d1 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -389,6 +389,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, From c9a5f098fe4bd33601beaa24a38cf212c41c70ff Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 16:11:55 +0800 Subject: [PATCH 17/57] docs: align Python SDK overview and references with final v3 preview --- README.md | 10 ++++++---- compat/agora-agent-server-sdk/README.md | 2 ++ docs/concepts/agent.md | 2 ++ docs/concepts/architecture.md | 7 ++++++- docs/guides/low-level-api.md | 2 ++ docs/guides/pagination.md | 2 +- docs/guides/v3-api-preview.md | 4 ++-- docs/reference/agent.md | 6 ++++++ docs/reference/client.md | 11 ++++++++--- docs/reference/v3.md | 4 ++-- 10 files changed, 37 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7e11c79..76cb797 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ 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. +## v3 stored-agent preview + +This checkout's additive v3 preview supports stored-agent `deploy()` / `deploy_async()` and redeploy by saved ID; project secrets and `secret_ref()` credentials; immutable `with_variable()` definitions; and number assignment, unassignment, and client-side `list_numbers()` scans. `client.v3` exposes all 20 staging operations with typed dictionaries, raw ETags/headers, conditional writes, and cursor pagination. v3 sessions are read-only; session writes and campaigns are not implemented. Existing v2 sessions and resources retain their current behavior. + +Use this `feat/v3-api-preview` checkout for evaluation; these features are not a claim about the published PyPI release. Start with the [staging workflow guide](docs/guides/v3-api-preview.md) or [complete v3 reference](docs/reference/v3.md). This protected custom adapter was generated locally from staging OpenAPI, not through the canonical Fern pipeline. + ## Install ```sh @@ -371,7 +377,3 @@ a proof of concept, but know that we will not be able to merge it as-is. We sugg an issue first to discuss with us! On the other hand, contributions to the README are always very welcome! - -### v3 stored-agent preview - -The additive `client.v3` API and AgentKit `deploy()` / `deploy_async()` save stored agent configurations without changing v2 session behavior. See the [v3 preview guide](docs/guides/v3-api-preview.md) for a complete staging quick start, deploy updates, secret references, number assignment/listing, async usage and backend limits. The [v3 reference](docs/reference/v3.md) lists all 20 low-level operations and their conditional headers. This protected custom adapter is locally generated from staging OpenAPI, not from the canonical Fern pipeline. diff --git a/compat/agora-agent-server-sdk/README.md b/compat/agora-agent-server-sdk/README.md index 1da36aa..7c3042d 100644 --- a/compat/agora-agent-server-sdk/README.md +++ b/compat/agora-agent-server-sdk/README.md @@ -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) requires the preview checkout and is not implied by installing the compatibility package from PyPI. Existing v2 imports remain available. diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index 6b47972..dde5ae1 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 `deploy()` / `deploy_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..0a09f81 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 `DeployedAgent` / `AsyncDeployedAgent` 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 | +| `DeployedAgent` (preview) | `AsyncDeployedAgent` (preview) | Stored configuration and number assignment calls | ### When to Use Each 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 index 74b1fd6..b933462 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -142,7 +142,7 @@ for number in client.v3.numbers.iter_all(limit=100): `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 returns normal parsed data rather than a raw response. To obtain the new ETag for a deliberate retry, GET through `.with_raw_response` again. +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 per-request authentication/context headers while stripping per-request `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 @@ -180,7 +180,7 @@ Use `await error.latest()` after an async 412. `deploy()` requires an `Agora` cl Existing v2 resources and `Agent.create_session()` behavior remain available. A v3 `secret_ref` cannot be sent as a v2 API key: `create_session`, `create_async_session` and `to_properties` reject it with `SecretRefNotSupportedError("secret_ref requires deploy()")`. Build a separate configuration with inline credentials for an existing v2 session. Variables configure stored-agent definitions; this phase does not add session-start variable values or an AgentKit extraction-results helper. -`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. `secret_ref(set_name, key)` validates 1–64 letters, digits, underscores or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. +`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 or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor messages move to top-level `messages`. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`. diff --git a/docs/reference/agent.md b/docs/reference/agent.md index 50e1915..64efb0f 100644 --- a/docs/reference/agent.md +++ b/docs/reference/agent.md @@ -230,6 +230,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. +## v3 stored-agent preview + +`with_variable(name, definition=None, **fields)` returns a new builder with a typed variable definition. `deploy(name=None, agent_id=None)` stores the configuration with an `Agora` client; `await deploy_async(...)` does so with `AsyncAgora`. Deploy arguments are keyword-only. Passing `agent_id` updates that stored configuration with a fresh ETag instead of creating another one. + +The returned `DeployedAgent` / `AsyncDeployedAgent` supports loading, deletion, number assignment/unassignment, and `list_numbers()` project-page scans. Deployment does not start a session. 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()` diff --git a/docs/reference/client.md b/docs/reference/client.md index 475fc24..9c175a8 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -26,8 +26,8 @@ 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) | +| `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) | | `auth_token` | `str` | No | — | Pre-built raw REST token; SDK sets `Authorization: agora token=` | @@ -36,8 +36,9 @@ client = Agora( | `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` and `app_certificate` are required constructor arguments in every auth mode; the certificate is used for automatic app-credential auth. Supply `customer_id` and `customer_secret` together to select Basic auth. `auth_token` is mutually exclusive with that pair. 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,7 @@ 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. diff --git a/docs/reference/v3.md b/docs/reference/v3.md index b1aa527..220b101 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -129,7 +129,7 @@ Omitted arguments use backend defaults, not client-side substitutions. In partic 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. 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. +`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 per-request authentication/context headers and remove 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. @@ -150,4 +150,4 @@ All low-level writes disable automatic retries. AgentKit number assignment is th The `max_*` assignment options and deploy `name` / `agent_id` are keyword-only. 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. -`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. +`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. From 3755b48a2cf4028d4124e806d772125373e42ead Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 16:39:58 +0800 Subject: [PATCH 18/57] feat: unify AgentKit save workflow and project provisioning --- README.md | 6 +- docs/concepts/agent.md | 2 +- docs/concepts/architecture.md | 4 +- docs/guides/agent-workflow.md | 90 ++++++++++++++++++++++++++ docs/guides/v3-api-preview.md | 49 ++++++++------- docs/index.md | 2 + docs/reference/agent.md | 6 +- docs/reference/client.md | 4 ++ docs/reference/v3.md | 17 ++--- src/agora_agent/__init__.py | 4 +- src/agora_agent/agentkit/__init__.py | 4 +- src/agora_agent/agentkit/agent.py | 22 ++++--- src/agora_agent/agentkit/v3.py | 45 +++++++++---- src/agora_agent/pool_client.py | 21 +++++++ tests/custom/test_v3_preview.py | 94 +++++++++++++++++++++++++++- 15 files changed, 308 insertions(+), 62 deletions(-) create mode 100644 docs/guides/agent-workflow.md diff --git a/README.md b/README.md index 76cb797..f76e419 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ 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. -## v3 stored-agent preview +## Run or save the same agent configuration -This checkout's additive v3 preview supports stored-agent `deploy()` / `deploy_async()` and redeploy by saved ID; project secrets and `secret_ref()` credentials; immutable `with_variable()` definitions; and number assignment, unassignment, and client-side `list_numbers()` scans. `client.v3` exposes all 20 staging operations with typed dictionaries, raw ETags/headers, conditional writes, and cursor pagination. v3 sessions are read-only; session writes and campaigns are not implemented. Existing v2 sessions and resources retain their current behavior. +Use one `Agent` builder to configure providers, then call `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 `secret_ref()` credentials and immutable `with_variable()` definitions for saved configurations. -Use this `feat/v3-api-preview` checkout for evaluation; these features are not a claim about the published PyPI release. Start with the [staging workflow guide](docs/guides/v3-api-preview.md) or [complete v3 reference](docs/reference/v3.md). This protected custom adapter was generated locally from staging OpenAPI, not through the canonical Fern pipeline. +Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 20-operation reference](docs/reference/v3.md) for ETags, cursor pagination and read-only project session queries. `deploy` / `DeployedAgent` remain compatibility names. Saved-agent session starts and campaigns await backend support. ## Install diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index dde5ae1..e84124b 100644 --- a/docs/concepts/agent.md +++ b/docs/concepts/agent.md @@ -8,7 +8,7 @@ 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 `deploy()` / `deploy_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. +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 0a09f81..2390c70 100644 --- a/docs/concepts/architecture.md +++ b/docs/concepts/architecture.md @@ -45,7 +45,7 @@ The auto-generated core provides typed HTTP methods for the existing v2 API endp ### 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 `DeployedAgent` / `AsyncDeployedAgent` for persisted configurations; existing session handles remain v2. See [v3 workflows](../guides/v3-api-preview.md) and [reference](../reference/v3.md). +`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 @@ -55,7 +55,7 @@ The SDK provides two parallel client hierarchies: |---|---|---| | `Agora` | `AsyncAgora` | `httpx.Client` / `httpx.AsyncClient` | | `AgentSession` | `AsyncAgentSession` | Blocking calls / Coroutines | -| `DeployedAgent` (preview) | `AsyncDeployedAgent` (preview) | Stored configuration and number assignment calls | +| `SavedAgent` (preview) | `AsyncSavedAgent` (preview) | Stored configuration and number assignment calls | ### When to Use Each diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md new file mode 100644 index 0000000..62907ef --- /dev/null +++ b/docs/guides/agent-workflow.md @@ -0,0 +1,90 @@ +# 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 require this preview checkout, installed with `python -m pip install -e .` from the repository root. These features are not implied by a published PyPI version. Configure the approved staging origin and gateway headers 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 + +# `client` is the Agora instance configured once in the linked transport setup. +agent = ( + Agent(client=client) + .with_stt(DeepgramSTT(api_key=os.environ["DEEPGRAM_API_KEY"], model="nova-3")) + .with_llm(OpenAI( + api_key=os.environ["OPENAI_API_KEY"], 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=os.environ["OPENAI_API_KEY"], 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=["*"]) +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) +``` + +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. + +Secret references remain server-side persistence inputs. A current session must use inline or managed credentials from its original builder; passing a literal secret reference to session start is rejected before the request. No secret values are recoverable through GET, and the SDK does not implicitly cache or reverse saved credentials. A saved handle does not yet start sessions: the new start API is expected to accept `agent_id`, but its endpoint and complete request contract are still pending. + +## 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=["*"]) + 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. The current transport uses the established session API for running conversations and the stored-agent preview API for persistence/provisioning. 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 read-only project session queries; the existing `client.agents` running-session resource retains its behavior. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index b933462..c91a983 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -1,11 +1,11 @@ -# v3 stored-agent preview (P1) +# Saving agent configurations (preview transport guide) -Use `Agent.deploy()` to save a reusable configuration and assign it to an existing inbound number. The additive `client.v3` namespace covers all 20 published staging operations. See the [complete v3 reference](../reference/v3.md) for method arguments and required headers. +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 all 20 published staging 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 / `DeployedAgent` | Persisted configuration identified by `agent_id` | Create, read, conditional update, delete, inbound-number assignment and number listing | +| 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` | Low-level list and get only | @@ -33,7 +33,7 @@ export DEEPGRAM_API_KEY='YOUR_DEEPGRAM_KEY' The base URL is an HTTP(S) **origin**, without a project path, query or fragment. The SDK appends `/conversational-ai/v3/projects/{appid}`. `V3Options(base_url=...)` overrides the environment variable; headers apply only to v3. Existing client authentication still supplies the Authorization header. Production paths, regional availability and Basic-auth acceptance remain unconfirmed; use the supplied staging configuration for evaluation. -This complete cascaded configuration stores one project secret set, then deploys a stored 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). +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 @@ -50,7 +50,7 @@ client = Agora( "X-Consumer-Username": os.environ["AGORA_V3_CONSUMER_USERNAME"], }), ) -client.v3.secrets.create( +client.secrets.create( name="support", secrets={"openai": os.environ["OPENAI_API_KEY"], "deepgram": os.environ["DEEPGRAM_API_KEY"]}, idempotency_key="support-secrets-initial-provisioning", @@ -71,32 +71,32 @@ agent = ( .with_variable("customer_name", type="str", required=True) .with_variable("resolved", type="bool", extract=True, description="Was the issue resolved?") ) -support = agent.deploy(name="Support") -print(support.id, support.etag) # Persist the ID for the next deploy. +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 mode may also require an explicit provider URL on staging. -## Redeploy, load and delete +## Update, load and delete -Pass the saved ID to update the same stored agent. Omitting `agent_id` always creates another configuration; matching names do not provide upsert or deduplication. +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 DeployedAgent +from agora_agent import SavedAgent -support = agent.deploy(name="Support", agent_id=support.id) -loaded = DeployedAgent.get(client, support.id) +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. ``` -Redeploy 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 `api_key`. +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 `api_key`. -This is a merge patch rather than a guaranteed replacement. Write-only keys and headers cannot be reconstructed from GET, and omitted hidden values remain subject to backend merge semantics. Keep intended credentials in the builder or use a targeted [low-level update](../reference/v3.md#agents-five-operations) when retaining existing hidden credentials matters. Agent deploy conflicts are surfaced without retry. +This is a merge patch rather than a guaranteed replacement. Write-only keys and headers cannot be reconstructed from GET, and omitted hidden values remain subject to backend merge semantics. Keep intended credentials in the builder or use a 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.v3.numbers.create(number=..., sip_trunk={"hostname": ...})`. Number registration details and unsupported staging options are in the [reference](../reference/v3.md#numbers-five-operations). +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, @@ -136,7 +136,7 @@ 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.v3.numbers.iter_all(limit=100): +for number in client.numbers.iter_all(limit=100): print(number["number_id"]) ``` @@ -146,11 +146,11 @@ A 412 raises `PreconditionFailedError`, a subclass of `ApiError`; other HTTP fai ## Async applications -Use `AsyncAgora`, the same low-level keyword arguments, and `await agent.deploy_async(...)`. The deployed handle is `AsyncDeployedAgent`; `get`, `delete`, `assign_number`, `unassign_number` and `list_numbers` are awaitable. `with_variable` and the vendor builders stay local and synchronous. +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, AsyncDeployedAgent, Area, V3Options +from agora_agent import Agent, AsyncAgora, AsyncSavedAgent, Area, V3Options async def inspect_stored_agent(agent_id: str): client = AsyncAgora( @@ -161,24 +161,25 @@ async def inspect_stored_agent(agent_id: str): "X-Consumer-Username": os.environ["AGORA_V3_CONSUMER_USERNAME"], }), ) - support = await AsyncDeployedAgent.get(client, agent_id) + 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.deploy_async(name="Support", agent_id=saved_id) +# 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. `deploy()` requires an `Agora` client; `deploy_async()` requires `AsyncAgora`. Passing the other client kind raises `TypeError` before deployment. +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 -Existing v2 resources and `Agent.create_session()` behavior remain available. A v3 `secret_ref` cannot be sent as a v2 API key: `create_session`, `create_async_session` and `to_properties` reject it with `SecretRefNotSupportedError("secret_ref requires deploy()")`. Build a separate configuration with inline credentials for an existing v2 session. Variables configure stored-agent definitions; this phase does not add session-start variable values or an AgentKit extraction-results helper. +Existing v2 resources and `Agent.create_session()` behavior remain available. A v3 `secret_ref` cannot be sent as a v2 API key: `create_session`, `create_async_session` and `to_properties` reject it with `SecretRefNotSupportedError("secret_ref requires save() or deploy()")`. Use inline or managed credentials for current sessions; their transport cannot dereference persistence-only secret references. Variables configure stored-agent definitions; this phase does not add session-start variable values or an AgentKit extraction-results helper. `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 or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. @@ -205,3 +206,7 @@ The adapter shares the existing client's HTTP transport, authentication and curr The v3 low-level methods and TypedDict wire schemas were generated locally from the staging OpenAPI reviewed for this preview. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. 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. A saved agent has no session-start method until the endpoint and complete contract for starting by `agent_id` are available; create current sessions from the original `Agent` configuration. diff --git a/docs/index.md b/docs/index.md index fd8fb9a..418c47a 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. diff --git a/docs/reference/agent.md b/docs/reference/agent.md index 7d52ec3..cdc2b15 100644 --- a/docs/reference/agent.md +++ b/docs/reference/agent.md @@ -230,11 +230,11 @@ 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. -## v3 stored-agent preview +## Save configurations (preview checkout) -`with_variable(name, definition=None, **fields)` returns a new builder with a typed variable definition. `deploy(name=None, agent_id=None)` stores the configuration with an `Agora` client; `await deploy_async(...)` does so with `AsyncAgora`. Deploy arguments are keyword-only. Passing `agent_id` updates that stored configuration with a fresh ETag instead of creating another one. +`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 `DeployedAgent` / `AsyncDeployedAgent` supports loading, deletion, number assignment/unassignment, and `list_numbers()` project-page scans. Deployment does not start a session. 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. +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()` diff --git a/docs/reference/client.md b/docs/reference/client.md index 9c175a8..c92357c 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -149,3 +149,7 @@ These are lazily initialized on first access. For most use cases, prefer the [Ag ## 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 index 220b101..f43e6b0 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -139,15 +139,18 @@ All low-level writes disable automatic retries. AgentKit number assignment is th | Operation | Synchronous | Async client | |---|---|---| -| Store/update a builder | `agent.deploy(name=None, agent_id=None)` → `DeployedAgent` | `await agent.deploy_async(...)` → `AsyncDeployedAgent` | -| Load a stored handle | `DeployedAgent.get(client, agent_id)` | `await AsyncDeployedAgent.get(client, agent_id)` | -| Delete | `deployed.delete()` | `await deployed.delete()` | -| Assign | `deployed.assign_number(number, max_call_duration_seconds=None, max_silence_duration_ms=None)` | `await deployed.assign_number(...)` | -| Unassign | `deployed.unassign_number(number)` | `await deployed.unassign_number(number)` | -| Discover assigned numbers | `deployed.list_numbers()` → `List[NumberResponse]` | `await deployed.list_numbers()` | +| 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)` | `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 and deploy `name` / `agent_id` are keyword-only. 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. +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` and `client.numbers` are the same resource objects as their `client.v3` counterparts. The advanced namespace remains available for raw transport and project session reads. diff --git a/src/agora_agent/__init__.py b/src/agora_agent/__init__.py index 4155cb1..c933786 100644 --- a/src/agora_agent/__init__.py +++ b/src/agora_agent/__init__.py @@ -155,7 +155,7 @@ def __getattr__(attr_name: str) -> typing.Any: def __dir__() -> typing.List[str]: return list(__all__) -from .agentkit.v3 import (DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) +from .agentkit.v3 import (SavedAgent, AsyncSavedAgent, DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) from .v3 import V3Options, PreconditionFailedError -__all__ = sorted(set(__all__) | {"DeployedAgent", "AsyncDeployedAgent", "SecretRef", "secret_ref", "UnsupportedFieldError", "NumberNotFoundError", "SecretRefNotSupportedError"} | {"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 c792d6e..ebc36b3 100644 --- a/src/agora_agent/agentkit/__init__.py +++ b/src/agora_agent/agentkit/__init__.py @@ -496,6 +496,6 @@ "validate_tts_sample_rate", ] -from .v3 import (DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) +from .v3 import (SavedAgent, AsyncSavedAgent, DeployedAgent, AsyncDeployedAgent, SecretRef, secret_ref, UnsupportedFieldError, NumberNotFoundError, SecretRefNotSupportedError) -__all__ = sorted(set(__all__) | {"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 c2bafc7..bdbdc6e 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -7,7 +7,7 @@ if typing.TYPE_CHECKING: from .agent_session import AgentSession, AsyncAgentSession - from .v3 import DeployedAgent, AsyncDeployedAgent + from .v3 import DeployedAgent, AsyncDeployedAgent, SavedAgent, AsyncSavedAgent from ..core.domain import Area from ..pool_client import Agora, AsyncAgora @@ -870,15 +870,23 @@ def with_variable(self, name: str, definition: typing.Optional[typing.Dict[str, 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": - """Create a v3 stored agent, or conditionally redeploy an existing ID.""" - from .v3 import deploy - return deploy(self, name=name, agent_id=agent_id) + """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": - """Deploy using a bound AsyncAgora client.""" - from .v3 import deploy_async - return await deploy_async(self, name=name, agent_id=agent_id) + """Compatibility alias for save_async().""" + return await self.save_async(name=name, agent_id=agent_id) def create_session( self, diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 37bcc43..cbf6cbe 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -4,11 +4,14 @@ import copy import re import time -from typing import List, Mapping, Optional +from typing import TYPE_CHECKING, List, Mapping, Optional from ..v3 import PreconditionFailedError from ..v3.types import AgentResponse, NumberResponse +if TYPE_CHECKING: + from .agent import Agent + class UnsupportedFieldError(ValueError): def __init__(self, fields): @@ -39,7 +42,7 @@ def secret_ref(secret_set: str, key: str) -> SecretRef: def guard_v2(value): if isinstance(value, str) and value.startswith("$secrets."): - raise SecretRefNotSupportedError("secret_ref requires deploy()") + raise SecretRefNotSupportedError("secret_ref requires save() or deploy()") if isinstance(value, Mapping): for item in value.values(): guard_v2(item) @@ -380,12 +383,12 @@ 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("deploy_async requires AsyncAgora" if async_ else "deploy requires Agora") + raise TypeError("save_async requires AsyncAgora" if async_ else "save requires Agora") if getattr(agent._client, "area_scope", None) == "cn": raise ValueError("v3 deployment in CN is unconfirmed; use a supported global area") -def deploy(agent, *, name=None, agent_id=None) -> DeployedAgent: +def save(agent, *, name=None, agent_id=None) -> SavedAgent: _require_client(agent, False) body = build_body(agent, name) raw = agent._client.v3.agents.with_raw_response @@ -396,10 +399,10 @@ def deploy(agent, *, name=None, agent_id=None) -> DeployedAgent: 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 DeployedAgent(agent._client, response) + return SavedAgent(agent._client, response) -async def deploy_async(agent, *, name=None, agent_id=None) -> AsyncDeployedAgent: +async def save_async(agent, *, name=None, agent_id=None) -> AsyncSavedAgent: _require_client(agent, True) body = build_body(agent, name) raw = agent._client.v3.agents.with_raw_response @@ -409,7 +412,7 @@ async def deploy_async(agent, *, name=None, agent_id=None) -> AsyncDeployedAgent 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 AsyncDeployedAgent(agent._client, response) + return AsyncSavedAgent(agent._client, response) def _normalize(number): @@ -434,7 +437,7 @@ def _policy(max_call_duration_seconds, max_silence_duration_ms): return policy -class _DeployedAgentBase: +class _SavedAgentBase: def __init__(self, client, response): self._client = client self.id: str = response.data["agent_id"] @@ -443,11 +446,17 @@ def __init__(self, client, response): self.data: AgentResponse = response.data self.raw = client.v3.agents -class DeployedAgent(_DeployedAgentBase): +class SavedAgent(_SavedAgentBase): @classmethod - def get(cls, client, agent_id) -> DeployedAgent: + 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) @@ -498,11 +507,17 @@ def unassign_number(self, number) -> NumberResponse: return self._assign(number, None, {}) -class AsyncDeployedAgent(_DeployedAgentBase): +class AsyncSavedAgent(_SavedAgentBase): @classmethod - async def get(cls, client, agent_id) -> AsyncDeployedAgent: + 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) @@ -551,3 +566,9 @@ async def assign_number(self, number, *, max_call_duration_seconds=None, max_sil async def unassign_number(self, number) -> NumberResponse: return await self._assign(number, 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/pool_client.py b/src/agora_agent/pool_client.py index 3b1bab8..ddb0a72 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -13,6 +13,7 @@ from .agentkit.regional_agent import RegionalAgent from .agentkit.vendors.region import AreaScope, area_to_scope from .v3 import V3Options, V3Client, AsyncV3Client +from .v3.client import SecretsClient, NumbersClient, AsyncSecretsClient, AsyncNumbersClient from .client import Agora as BaseAgora from .client import AsyncAgora as BaseAsyncAgora from .core.api_error import ApiError @@ -390,6 +391,16 @@ def v3(self) -> V3Client: self._v3_client = V3Client(self, self._v3_options) return self._v3_client + @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: """ @@ -740,6 +751,16 @@ def v3(self) -> AsyncV3Client: self._v3_client = AsyncV3Client(self, self._v3_options) return self._v3_client + @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: """ diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 5d373e0..1b1d033 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -251,7 +251,7 @@ def test_secret_ref_v2_guard_at_all_boundaries(): for call in (lambda: agent.to_properties(channel="c", agent_uid="1", remote_uids=["*"]), lambda: agent.create_session(channel="c", agent_uid="1", remote_uids=["*"]), lambda: agent.create_async_session(channel="c", agent_uid="1", remote_uids=["*"])): - with pytest.raises(SecretRefNotSupportedError, match="requires deploy"): + with pytest.raises(SecretRefNotSupportedError, match="requires save"): call() with pytest.raises(ValueError): secret_ref("prod.bad", "key") @@ -564,3 +564,95 @@ def handler(request): 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) + 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 agent.save_async(name="Support") + await saved.assign_number(number) + updated = await saved.update(agent.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 = agent.save(name="Support") + saved.assign_number(number) + updated = saved.update(agent.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 not hasattr(saved, "create_session") + assert not hasattr(saved, "create_async_session") + 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(agent, name="Renamed") + else: + renamed = saved.update(agent, 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)) From 984f8ba057627a3c9a7f45ef5795a30c6720f3ec Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 17:00:18 +0800 Subject: [PATCH 19/57] feat: start saved and ephemeral agent sessions through common lifecycle --- README.md | 2 +- docs/guides/agent-workflow.md | 24 +++- docs/guides/v3-api-preview.md | 12 +- docs/index.md | 2 +- docs/reference/v3.md | 19 ++- src/agora_agent/agentkit/agent.py | 26 +++- src/agora_agent/agentkit/agent_session.py | 112 ++++++++++++++++- src/agora_agent/agentkit/session_backend.py | 65 ++++++++++ src/agora_agent/agentkit/v3.py | 20 +++ src/agora_agent/v3/client.py | 42 +++++++ src/agora_agent/v3/types.py | 32 ++++- tests/custom/test_v3_preview.py | 127 ++++++++++++++++++-- 12 files changed, 449 insertions(+), 34 deletions(-) create mode 100644 src/agora_agent/agentkit/session_backend.py diff --git a/README.md b/README.md index f76e419..ce3283b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ and multimodal flows (MLLM) for real-time audio processing. Use one `Agent` builder to configure providers, then call `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 `secret_ref()` credentials and immutable `with_variable()` definitions for saved configurations. -Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 20-operation reference](docs/reference/v3.md) for ETags, cursor pagination and read-only project session queries. `deploy` / `DeployedAgent` remain compatibility names. Saved-agent session starts and campaigns await backend support. +Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 23-operation reference](docs/reference/v3.md) for ETags, cursor pagination, session lifecycle and project queries. `deploy` / `DeployedAgent` remain compatibility names. Saved handles also create sessions by ID; configurations with secret references or variable definitions start ephemeral sessions. Campaign APIs are outside this SDK workflow. ## Install diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 62907ef..7929c97 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -65,7 +65,25 @@ saved = saved.update(referenced) 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. -Secret references remain server-side persistence inputs. A current session must use inline or managed credentials from its original builder; passing a literal secret reference to session start is rejected before the request. No secret values are recoverable through GET, and the SDK does not implicitly cache or reverse saved credentials. A saved handle does not yet start sessions: the new start API is expected to accept `agent_id`, but its endpoint and complete request contract are still pending. +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"], + client_reference="support-call", idle_timeout=60, max_duration=120, + 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). `name` is an alias for `client_reference` in these new sessions; conflicting values are rejected. `labels` is supported only for saved starts. Token, preset, pipeline ID, token-expiry and string-UID overrides are not documented for saved/ephemeral starts and are rejected. For these sessions, `say`, `interrupt`, `think`, `update`, history and turn helpers raise `NotImplementedError` locally; their IDs are never sent to the older control endpoints. `get_info()` returns the project session response, and `.status` normalizes its status to lowercase. ## Async applications @@ -85,6 +103,6 @@ Use `await AsyncSavedAgent.get(client, saved_id)` to load another handle, and aw ## Advanced transport and compatibility -Application code does not select a REST version. The current transport uses the established session API for running conversations and the stored-agent preview API for persistence/provisioning. 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. +Application code does not select a REST version. Ordinary inline configurations retain the established session transport. Configurations with secret references or variable definitions, or the new `client_reference`/`max_duration` options, 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 read-only project session queries; the existing `client.agents` running-session resource retains its behavior. +`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. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index c91a983..d647a7c 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -1,6 +1,6 @@ # 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 all 20 published staging operations. See the [complete v3 reference](../reference/v3.md) for method arguments and required headers. +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 23 staging operations (20 from OpenAPI plus three session writes from refreshed Bruno). See the [complete v3 reference](../reference/v3.md) for method arguments and required headers. | Object or API | Meaning | Available now | |---|---|---| @@ -9,7 +9,7 @@ For the everyday builder → session or saved-agent flow, start with the [agent | v2 `AgentSession` | Running RTC instance, also historically called an agent | Existing `create_session(...).start()` lifecycle | | v3 session | Running instance identified by `agent_session_id` | Low-level list and get only | -Deploying stores configuration; it does not start a session. v3 session writes, outbound calls, AgentKit analysis helpers and campaigns await backend schemas and are not implemented. The SDK package version continues to describe package compatibility independently of the REST API version. +Saving stores configuration; it does not itself start a session. Call `saved.create_session(...).start()` to start by ID. Outbound calls, AgentKit analysis helpers and campaign APIs are outside this SDK workflow. The SDK package version continues to describe package compatibility independently of the REST API version. ## Quick start with staging @@ -179,7 +179,7 @@ Use `await error.latest()` after an async 412. `save()` requires an `Agora` clie ## Compatibility and backend limits -Existing v2 resources and `Agent.create_session()` behavior remain available. A v3 `secret_ref` cannot be sent as a v2 API key: `create_session`, `create_async_session` and `to_properties` reject it with `SecretRefNotSupportedError("secret_ref requires save() or deploy()")`. Use inline or managed credentials for current sessions; their transport cannot dereference persistence-only secret references. Variables configure stored-agent definitions; this phase does not add session-start variable values or an AgentKit extraction-results helper. +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. New session start has no documented variable-value override or AgentKit extraction-results helper. 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 or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. @@ -197,16 +197,16 @@ For cascaded deployment, `with_turn_detection({"language": "de-DE"})` sets `pipe | 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 POST has no idempotency key; numbers currently ignore their declared key. An uncertain create outcome requires inspection before manual retry | | CN and production | AgentKit CN deployment is refused; production path, regional coverage and Basic-auth support remain unconfirmed | -| Session writes, outbound calling, AgentKit analysis and campaigns | No implementation until schemas are published | +| Outbound calling, AgentKit analysis and campaign APIs | Not included in this SDK session workflow | The adapter shares the existing client's HTTP transport, authentication and current regional-pool origin. The staging override applies only to v3. These docs and offline tests do not establish live backend availability. ## Maintainer provenance -The v3 low-level methods and TypedDict wire schemas were generated locally from the staging OpenAPI reviewed for this preview. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. +The original 20 low-level methods and TypedDict wire schemas were generated locally from staging OpenAPI. Three handwritten session write methods and their TypedDict shapes follow the refreshed Bruno `sessions/start`, `start-ephemeral`, and `stop` examples; the older OpenAPI does not contain these writes. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. 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. A saved agent has no session-start method until the endpoint and complete contract for starting by `agent_id` are available; create current sessions from the original `Agent` configuration. +`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. diff --git a/docs/index.md b/docs/index.md index 418c47a..a95886c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,4 +66,4 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on ## v3 stored-agent preview -The additive v3 adapter stores reusable agent configurations while existing v2 sessions keep their current lifecycle. Start with the [v3 workflow guide](./guides/v3-api-preview.md), or consult the [complete 20-operation v3 reference](./reference/v3.md). The preview uses protected custom/local-generated code rather than the canonical Fern pipeline. +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 23-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/v3.md b/docs/reference/v3.md index f43e6b0..1578017 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -1,6 +1,6 @@ # v3 preview reference -This reference describes the 20 implemented staging 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 locally generated from staging OpenAPI, not Fern-generated code; see [maintainer provenance](../guides/v3-api-preview.md#maintainer-provenance). +This reference describes the 23 implemented staging 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 locally generated from staging OpenAPI, not Fern-generated code; see [maintainer provenance](../guides/v3-api-preview.md#maintainer-provenance). ## Shared calling conventions @@ -101,7 +101,7 @@ number = client.v3.numbers.create( # Store number["number_id"] for subsequent conditional updates or assignment. ``` -## Sessions: two read operations +## Sessions: five operations Prefix: `/conversational-ai/v3/projects/{appid}/sessions`. @@ -109,12 +109,18 @@ Prefix: `/conversational-ai/v3/projects/{appid}/sessions`. |---|---|---| | `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, client_reference=None, lifecycle=None, labels=None)` | POST collection | `StartSessionResponse` | +| `start_ephemeral(agent, transport, client_reference=None, lifecycle=None)` | POST `/ephemeral` | `StartSessionResponse` | +| `stop(agent_session_id)` | POST `/{agent_session_id}/stop`, no request body | empty dictionary | -These 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` and `analysis`; a nonunique name does not establish ownership by a stored agent. +The three session writes follow refreshed Bruno examples, not the older GET-only OpenAPI. `transport` is `RtcSessionTransport`: `type="rtc"`, `channel`, `uid`, `subscribe_uids` (strings/list of strings). Optional `SessionLifecycle` fields are integer `idle_timeout_ms` and `max_duration_ms`; the low-level API uses milliseconds. `agent` is `EphemeralSessionAgent`, containing the mapped agent configuration without `agent_name`. Start returns `agent_session_id`, `status` and `created_at`; the timestamp format is unspecified. Bruno start returns lowercase `running`, while retrieve returns uppercase statuses. `labels` is evidenced only for stored starts. No new control endpoints are assumed. + + +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` and `analysis`; 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. -`analysis`, when present, exposes extraction status and `result` through the low-level response. This phase has no session start/stop/update/actions, outbound-call methods, or AgentKit `get_analysis`. There is no campaigns namespace. +`analysis`, when present, exposes extraction status and `result` through the low-level response. Session update/actions, outbound-call methods, AgentKit `get_analysis`, and the campaigns namespace are not included in this workflow. ## Pagination, errors and transport @@ -154,3 +160,8 @@ The `max_*` assignment options, save `name` / `agent_id`, and update `name` are `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` and `client.numbers` 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, remote_uids, name=None, client_reference=None, idle_timeout=None, max_duration=None, labels=None, debug=None, warn=None)` returns `AgentSession`. `AsyncSavedAgent.create_session(...)` and its `create_async_session` alias return `AsyncAgentSession`. Creation is local; call `start()` / `await start()`. `get_info()` and `stop()` use the returned session ID. The same new duration/reference options on builder session creation select ephemeral start, as do secret references or `with_variable` definitions. Durations on handles are seconds. New-backed `.raw` exposes `client.v3.sessions`; `.raw_agent_management` is `None`. diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index bdbdc6e..d87b3f8 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -902,6 +902,9 @@ 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, ) -> "AgentSession": """Create a synchronous session bound to this agent configuration. @@ -917,8 +920,14 @@ def create_session( """ from .agent_session import AgentSession - from .v3 import guard_v2 - guard_v2(self.config) + from .session_backend import create_session, needs_session_backend + if needs_session_backend(self) or max_duration is not None or client_reference is not None or labels is not None: + 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, + 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(...).") @@ -957,6 +966,9 @@ 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, ) -> "AsyncAgentSession": """Create an async session for use with :class:`~agora_agent.AsyncAgora`. @@ -966,8 +978,14 @@ def create_async_session( """ from .agent_session import AsyncAgentSession - from .v3 import guard_v2 - guard_v2(self.config) + from .session_backend import create_session, needs_session_backend + if needs_session_backend(self) or max_duration is not None or client_reference is not None or labels is not None: + 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, + 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(...).") diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index e2b2604..c9adb6f 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -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 @@ -167,12 +169,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.""" @@ -598,6 +600,25 @@ def start(self) -> str: ValueError If avatar/TTS configuration is invalid. """ + if self._session_backend is not None: + if self._status not in ("idle", "stopped", "error"): + raise RuntimeError(f"Cannot start session in {self._status} state") + self._status = "starting" + try: + resource = self._client.v3.sessions + method = resource.start if self._session_backend["stored"] else resource.start_ephemeral + response = method(**self._session_backend["body"]) + 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._status = str(response["status"]).lower() + self._emit("started", {"agent_id": identifier, "agent_session_id": identifier}) + return identifier + except Exception as error: + self._status = "error" + self._emit("error", error) + raise if self._status not in ("idle", "stopped", "error"): raise RuntimeError(f"Cannot start session in {self._status} state") @@ -685,6 +706,20 @@ 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 != "running" or not self._agent_id: + raise RuntimeError(f"Cannot stop session in {self._status} state") + self._status = "stopping" + try: + self._client.v3.sessions.stop(agent_session_id=self._agent_id) + except Exception as error: + if getattr(error, "status_code", None) != 404: + self._status = "error" + self._emit("error", error) + raise + self._status = "stopped" + 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 +765,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 +789,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 +817,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,6 +853,8 @@ def update(self, properties: typing.Any) -> None: properties : UpdateAgentsRequestProperties Partial configuration to update. """ + if self._session_backend is not None: + raise NotImplementedError("update is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot update in {self._status} state") if not self._agent_id: @@ -826,6 +869,8 @@ def update(self, properties: typing.Any) -> None: 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 +880,12 @@ 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") + response = self._client.v3.sessions.get(agent_session_id=self._agent_id) + self._status = str(response["status"]).lower() + return response if not self._agent_id: raise RuntimeError("No agent ID available") @@ -850,6 +901,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 +930,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 +1021,25 @@ async def start(self) -> str: ValueError If avatar/TTS configuration is invalid. """ + if self._session_backend is not None: + if self._status not in ("idle", "stopped", "error"): + raise RuntimeError(f"Cannot start session in {self._status} state") + self._status = "starting" + try: + resource = self._client.v3.sessions + method = resource.start if self._session_backend["stored"] else resource.start_ephemeral + response = await method(**self._session_backend["body"]) + 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._status = str(response["status"]).lower() + self._emit("started", {"agent_id": identifier, "agent_session_id": identifier}) + return identifier + except Exception as error: + self._status = "error" + self._emit("error", error) + raise if self._status not in ("idle", "stopped", "error"): raise RuntimeError(f"Cannot start session in {self._status} state") @@ -1053,6 +1127,20 @@ 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 != "running" or not self._agent_id: + raise RuntimeError(f"Cannot stop session in {self._status} state") + self._status = "stopping" + try: + await self._client.v3.sessions.stop(agent_session_id=self._agent_id) + except Exception as error: + if getattr(error, "status_code", None) != 404: + self._status = "error" + self._emit("error", error) + raise + self._status = "stopped" + 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: @@ -1098,6 +1186,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: @@ -1120,6 +1210,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: @@ -1146,6 +1238,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: @@ -1180,6 +1274,8 @@ async def update(self, properties: typing.Any) -> None: properties : UpdateAgentsRequestProperties Partial configuration to update. """ + if self._session_backend is not None: + raise NotImplementedError("update is not documented for stored or ephemeral sessions") if self._status != "running": raise RuntimeError(f"Cannot update in {self._status} state") if not self._agent_id: @@ -1194,6 +1290,8 @@ async def update(self, properties: typing.Any) -> None: 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") @@ -1203,6 +1301,12 @@ 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") + response = await self._client.v3.sessions.get(agent_session_id=self._agent_id) + self._status = str(response["status"]).lower() + return response if not self._agent_id: raise RuntimeError("No agent ID available") @@ -1218,6 +1322,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") @@ -1245,6 +1351,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/session_backend.py b/src/agora_agent/agentkit/session_backend.py new file mode 100644 index 0000000..c6a8384 --- /dev/null +++ b/src/agora_agent/agentkit/session_backend.py @@ -0,0 +1,65 @@ +"""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 + return bool(getattr(agent, "_v3_variables", {})) or contains(agent.config) + + +def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids, + name=None, client_reference=None, idle_timeout=None, max_duration=None, + labels=None, debug=None, warn=None, **unsupported): + from .agent_session import AgentSession, AsyncAgentSession + from .v3 import build_body + invalid = [key for key, value in unsupported.items() if value is not None] + if invalid: + raise ValueError("Unsupported session options: " + ", ".join(sorted(invalid))) + if not isinstance(channel, str) or not channel.strip(): + raise ValueError("channel must be nonempty") + if 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 for v in remote_uids): + raise ValueError("remote_uids must be a list of nonempty strings") + if name is not None and client_reference is not None and name != client_reference: + raise ValueError("name and client_reference must agree when both are supplied") + body = {"transport": {"type": "rtc", "channel": channel, "uid": agent_uid, + "subscribe_uids": list(remote_uids)}} + reference = client_reference if client_reference is not None else name + if reference is not None: + body["client_reference"] = reference + 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 lifecycle: + body["lifecycle"] = lifecycle + stored = hasattr(owner, "id") + if stored: + body["agent_id"] = owner.id + if labels is not None: + body["labels"] = copy.deepcopy(labels) + else: + if labels is not None: + raise ValueError("labels are not documented for ephemeral sessions") + agent = build_body(owner) + agent.pop("agent_name", None) + body["agent"] = agent + client = owner._client + if client is None: + raise ValueError("client is required") + session_type = AsyncAgentSession if asynchronous else AgentSession + return session_type(client=client, agent=owner, app_id=client.app_id, + name=reference or "", channel=channel, agent_uid=agent_uid, + remote_uids=list(remote_uids), debug=debug, warn=warn, + _session_backend={"stored": stored, "body": copy.deepcopy(body)}) diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index cbf6cbe..2e62e8e 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -447,6 +447,15 @@ def __init__(self, client, response): self.raw = client.v3.agents class SavedAgent(_SavedAgentBase): + def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], + name=None, client_reference=None, idle_timeout=None, + max_duration=None, labels=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, + debug=debug, warn=warn) + @classmethod def get(cls, client, agent_id) -> SavedAgent: return cls(client, client.v3.agents.with_raw_response.get(agent_id=agent_id)) @@ -508,6 +517,17 @@ def unassign_number(self, number) -> NumberResponse: class AsyncSavedAgent(_SavedAgentBase): + def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], + name=None, client_reference=None, idle_timeout=None, + max_duration=None, labels=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, debug=debug, warn=warn) + + create_async_session = create_session + @classmethod async def get(cls, client, agent_id) -> AsyncSavedAgent: return cls(client, await client.v3.agents.with_raw_response.get(agent_id=agent_id)) diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py index a76666d..a21d1df 100644 --- a/src/agora_agent/v3/client.py +++ b/src/agora_agent/v3/client.py @@ -19,17 +19,21 @@ AgentPatchRequest_PipelinePatch, AgentPatchRequest_VariableDefinitionPatch, AgentResponse, + EphemeralSessionAgent, InboundInput, NumberDeleteResponse, NumberListResponse, NumberResponse, + RtcSessionTransport, SecretDeleted, SecretKey, SecretList, SecretSet, + SessionLifecycle, SessionList, SessionResponse, SipTrunkInput, + StartSessionResponse, ) from typing_extensions import Literal @@ -173,6 +177,15 @@ def _NumbersClient(owner, options): return client class SessionsClient(Resource): + def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return self.with_raw_response.start(agent_id=agent_id, transport=transport, client_reference=client_reference, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options).data + + def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return self.with_raw_response.start_ephemeral(agent=agent, transport=transport, client_reference=client_reference, lifecycle=lifecycle, appid=appid, request_options=request_options).data + + def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: + return self.with_raw_response.stop(agent_session_id=agent_session_id, appid=appid, request_options=request_options).data + with_raw_response: SessionsRawClient def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> 'SessionList': @@ -183,6 +196,16 @@ def get(self, *, appid: Optional[str] = None, agent_session_id: str, request_opt class SessionsRawClient(Resource): + def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + segment(agent_id) + return self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body={'agent_id': agent_id, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={}, params={}, request_options=request_options) + + def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + return self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/ephemeral', body={'agent': agent, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={}, params={}, request_options=request_options) + + def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + return self._request('POST', f'/conversational-ai/v3/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) + def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: return self._request('GET', f'/conversational-ai/v3/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) @@ -340,6 +363,15 @@ def _AsyncNumbersClient(owner, options): return client class AsyncSessionsClient(AsyncResource): + async def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return (await self.with_raw_response.start(agent_id=agent_id, transport=transport, client_reference=client_reference, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options)).data + + async def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return (await self.with_raw_response.start_ephemeral(agent=agent, transport=transport, client_reference=client_reference, lifecycle=lifecycle, appid=appid, request_options=request_options)).data + + async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: + return (await self.with_raw_response.stop(agent_session_id=agent_session_id, appid=appid, request_options=request_options)).data + with_raw_response: AsyncSessionsRawClient async def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> 'SessionList': @@ -350,6 +382,16 @@ async def get(self, *, appid: Optional[str] = None, agent_session_id: str, reque class AsyncSessionsRawClient(AsyncResource): + async def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + segment(agent_id) + return await self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body={'agent_id': agent_id, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={}, params={}, request_options=request_options) + + async def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/ephemeral', body={'agent': agent, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={}, params={}, request_options=request_options) + + async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + return await self._request('POST', f'/conversational-ai/v3/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) + async def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: return await self._request('GET', f'/conversational-ai/v3/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) diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index 23af1aa..c063df0 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -807,4 +807,34 @@ class AgentCreateRequest(TypedDict, total=False): pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] filler_words: Union['AgentCreateModel_FillerWords', None] -__all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AgoraVadTurnDetection', 'AudioGreeting', 'DisabledInterruption', 'EndCallPolicy', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'GeneratedFillerContent', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JsonValue', 'KeywordInterruption', 'ManualTurnBoundary', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedMllm', 'ResolvedRealtimePipeline', 'ResolvedTts', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SemanticVadTurnDetection', 'ServerVadTurnDetection', 'SessionAnalysis', 'SessionData', 'SessionList', 'SessionResponse', 'SessionSummary', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SpeechInterruption', 'StaticFillerContent', 'TextGreeting', 'TransferPolicy', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_AudioGreeting', 'AgentCreateModel_ByokCredential', 'AgentCreateModel_CascadedPipeline', 'AgentCreateModel_DisabledInterruption', 'AgentCreateModel_FillerTrigger', 'AgentCreateModel_FillerWords', 'AgentCreateModel_GeneratedFillerContent', 'AgentCreateModel_JsonValue', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_ManagedCredential', 'AgentCreateModel_ManualTurnBoundary', 'AgentCreateModel_Message', 'AgentCreateModel_ProviderModule', 'AgentCreateModel_RealtimePipeline', 'AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_SemanticVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SilenceConfig', 'AgentCreateModel_SpeechInterruption', 'AgentCreateModel_StaticFillerContent', 'AgentCreateModel_TextGreeting', 'AgentCreateModel_TurnDetection', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_VariableDefinition', 'AgentCreateModel', 'AgentPatchRequest_CredentialPatch', 'AgentPatchRequest_FillerContentPatch', 'AgentPatchRequest_FillerTriggerPatch', 'AgentPatchRequest_FillerWordsPatch', 'AgentPatchRequest_GreetingPatch', 'AgentPatchRequest_InterruptionPatch', 'AgentPatchRequest_JsonValue', 'AgentPatchRequest_Message', 'AgentPatchRequest_PipelinePatch', 'AgentPatchRequest_PipelineTurnDetectionPatch', 'AgentPatchRequest_ProviderModulePatch', 'AgentPatchRequest_SilenceConfigPatch', 'AgentPatchRequest_TurnBoundaryPatch', 'AgentPatchRequest_VariableDefinitionPatch', 'AgentPatchRequest', 'Contract_VariableDefinitions', 'Contract_ModelCredential', 'Contract_Llm', 'Contract_CascadedPipeline', 'Contract_Pipeline', 'Contract_SemanticVadTurnDetection', 'Contract_SilenceConfig', 'Contract_ServerVadTurnDetection', 'Contract_McpServer', 'Contract_VariableDefinition', 'Contract_Greeting', 'Contract_Asr', 'Contract_FillerWords', 'Contract_Interruption', 'Contract_TurnStart', 'Contract_FunctionTool', 'Contract_Tts', 'Contract_RealtimePipeline', 'Contract_Mllm', 'Contract_Avatar', 'Contract_RealtimeTurnDetection', 'Contract_FunctionToolServer', 'Contract_TurnEnd', 'Contract_Message', 'Contract_AgentCreateInput', 'Contract_TurnDetection', 'Contract_AgoraVadTurnDetection', 'Contract_FunctionToolExecution', 'Contract_JsonSchema', 'AgentCreateRequest'] \ No newline at end of file +__all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AgoraVadTurnDetection', 'AudioGreeting', 'DisabledInterruption', 'EndCallPolicy', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'GeneratedFillerContent', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JsonValue', 'KeywordInterruption', 'ManualTurnBoundary', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedMllm', 'ResolvedRealtimePipeline', 'ResolvedTts', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SemanticVadTurnDetection', 'ServerVadTurnDetection', 'SessionAnalysis', 'SessionData', 'SessionList', 'SessionResponse', 'SessionSummary', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SpeechInterruption', 'StaticFillerContent', 'TextGreeting', 'TransferPolicy', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_AudioGreeting', 'AgentCreateModel_ByokCredential', 'AgentCreateModel_CascadedPipeline', 'AgentCreateModel_DisabledInterruption', 'AgentCreateModel_FillerTrigger', 'AgentCreateModel_FillerWords', 'AgentCreateModel_GeneratedFillerContent', 'AgentCreateModel_JsonValue', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_ManagedCredential', 'AgentCreateModel_ManualTurnBoundary', 'AgentCreateModel_Message', 'AgentCreateModel_ProviderModule', 'AgentCreateModel_RealtimePipeline', 'AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_SemanticVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SilenceConfig', 'AgentCreateModel_SpeechInterruption', 'AgentCreateModel_StaticFillerContent', 'AgentCreateModel_TextGreeting', 'AgentCreateModel_TurnDetection', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_VariableDefinition', 'AgentCreateModel', 'AgentPatchRequest_CredentialPatch', 'AgentPatchRequest_FillerContentPatch', 'AgentPatchRequest_FillerTriggerPatch', 'AgentPatchRequest_FillerWordsPatch', 'AgentPatchRequest_GreetingPatch', 'AgentPatchRequest_InterruptionPatch', 'AgentPatchRequest_JsonValue', 'AgentPatchRequest_Message', 'AgentPatchRequest_PipelinePatch', 'AgentPatchRequest_PipelineTurnDetectionPatch', 'AgentPatchRequest_ProviderModulePatch', 'AgentPatchRequest_SilenceConfigPatch', 'AgentPatchRequest_TurnBoundaryPatch', 'AgentPatchRequest_VariableDefinitionPatch', 'AgentPatchRequest', 'Contract_VariableDefinitions', 'Contract_ModelCredential', 'Contract_Llm', 'Contract_CascadedPipeline', 'Contract_Pipeline', 'Contract_SemanticVadTurnDetection', 'Contract_SilenceConfig', 'Contract_ServerVadTurnDetection', 'Contract_McpServer', 'Contract_VariableDefinition', 'Contract_Greeting', 'Contract_Asr', 'Contract_FillerWords', 'Contract_Interruption', 'Contract_TurnStart', 'Contract_FunctionTool', 'Contract_Tts', 'Contract_RealtimePipeline', 'Contract_Mllm', 'Contract_Avatar', 'Contract_RealtimeTurnDetection', 'Contract_FunctionToolServer', 'Contract_TurnEnd', 'Contract_Message', 'Contract_AgentCreateInput', 'Contract_TurnDetection', 'Contract_AgoraVadTurnDetection', 'Contract_FunctionToolExecution', 'Contract_JsonSchema', 'AgentCreateRequest'] +# Session write shapes from the refreshed Bruno collection (not the older OpenAPI). +class RtcSessionTransport(TypedDict): + type: Literal['rtc'] + channel: str + uid: str + subscribe_uids: List[str] + + +class SessionLifecycle(TypedDict, total=False): + idle_timeout_ms: int + max_duration_ms: int + + +class EphemeralSessionAgent(TypedDict, total=False): + instructions: str + messages: List[AgentCreateModel_Message] + greeting: Union[AgentCreateModel_TextGreeting, AgentCreateModel_AudioGreeting] + variables: Dict[str, AgentCreateModel_VariableDefinition] + pipeline: Union[AgentCreateModel_CascadedPipeline, AgentCreateModel_RealtimePipeline] + filler_words: AgentCreateModel_FillerWords + + +class StartSessionResponse(TypedDict): + agent_session_id: str + status: str + # Bruno establishes this field but does not specify its timestamp format. + created_at: Any + + +__all__ += ['RtcSessionTransport', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse'] diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 1b1d033..f7df159 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -44,6 +44,9 @@ ("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"]}, "client_reference": "test", "lifecycle": {"max_duration_ms": 120000}}, "POST", "sessions/ephemeral", {"agent": {"instructions": "Help"}, "transport": {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]}, "client_reference": "test", "lifecycle": {"max_duration_ms": 120000}}), + ("sessions", "stop", {"agent_session_id": "session/a"}, "POST", "sessions/session%2Fa/stop", None), ] @@ -63,6 +66,8 @@ def check_request(request, operation): assert request.headers["Gateway"] == "secret" 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] @@ -72,7 +77,7 @@ def check_request(request, operation): @pytest.mark.parametrize("operation", OPERATIONS) -def test_all_twenty_sync_wire_operations(operation): +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"}) @@ -87,7 +92,7 @@ def handler(request): @pytest.mark.asyncio @pytest.mark.parametrize("operation", OPERATIONS) -async def test_all_twenty_async_wire_operations(operation): +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"'}) @@ -98,7 +103,7 @@ def handler(request): assert await getattr(getattr(client.v3, resource), method)(**kwargs) == {"ok": True} -def test_namespace_is_additive_and_pending_writes_absent(): +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") @@ -248,13 +253,10 @@ def test_multipart_credentials_and_numeric_skip_patterns_rejected(): def test_secret_ref_v2_guard_at_all_boundaries(): agent = configured(make_client(lambda _: pytest.fail("must not send"))) - for call in (lambda: agent.to_properties(channel="c", agent_uid="1", remote_uids=["*"]), - lambda: agent.create_session(channel="c", agent_uid="1", remote_uids=["*"]), - lambda: agent.create_async_session(channel="c", agent_uid="1", remote_uids=["*"])): - with pytest.raises(SecretRefNotSupportedError, match="requires save"): - call() - with pytest.raises(ValueError): - secret_ref("prod.bad", "key") + 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=["*"]).status == "idle" + assert agent.create_async_session(channel="c", agent_uid="1", remote_uids=["*"]).status == "idle" def test_number_resolution_pagination_conflict_retry_and_unassign(): @@ -628,8 +630,8 @@ def handler(request): assert updated.id == loaded.id == saved.id == "a" assert updated.name == saved.name == "Support" assert agent.config == snapshot - assert not hasattr(saved, "create_session") - assert not hasattr(saved, "create_async_session") + 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"' @@ -656,3 +658,104 @@ async def test_saved_update_rejects_another_client_before_network(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 body["client_reference"] == "reference" + 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"]["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, client_reference="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", [("token", "token"), ("preset", "test"), ("pipeline_id", "id"), ("expires_in", 60), ("enable_string_uid", False), ("labels", {})]) +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_is_idempotent_and_controls_never_request(async_): + calls = [] + def handler(request): + calls.append(request) + if request.url.path.endswith("/stop"): + return httpx.Response(404, json={"code": "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",)), ("update", ({},)), ("get_history", ()), ("get_turns", ()), ("get_all_turns", ())): + with pytest.raises(NotImplementedError): + result = getattr(session, method)(*args) + if async_: + await result + if async_: + await session.stop() + else: + session.stop() + assert session.status == "stopped" + 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=[], max_duration=seconds) From 06571c64e1211d3cacef1326cde62b558e1cbb7e Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 17:02:07 +0800 Subject: [PATCH 20/57] fix: preserve unrelated session stop failures --- src/agora_agent/agentkit/agent_session.py | 8 ++++++-- tests/custom/test_v3_preview.py | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index c9adb6f..20fc826 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -713,7 +713,9 @@ def stop(self) -> None: try: self._client.v3.sessions.stop(agent_session_id=self._agent_id) except Exception as error: - if getattr(error, "status_code", None) != 404: + body = getattr(error, "body", None) + if not (getattr(error, "status_code", None) == 404 + and isinstance(body, dict) and body.get("reason") == "TaskNotFound"): self._status = "error" self._emit("error", error) raise @@ -1134,7 +1136,9 @@ async def stop(self) -> None: try: await self._client.v3.sessions.stop(agent_session_id=self._agent_id) except Exception as error: - if getattr(error, "status_code", None) != 404: + body = getattr(error, "body", None) + if not (getattr(error, "status_code", None) == 404 + and isinstance(body, dict) and body.get("reason") == "TaskNotFound"): self._status = "error" self._emit("error", error) raise diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index f7df159..3a3705e 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -734,7 +734,7 @@ async def test_new_session_stop_404_is_idempotent_and_controls_never_request(asy def handler(request): calls.append(request) if request.url.path.endswith("/stop"): - return httpx.Response(404, json={"code": "TaskNotFound"}) + 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"]) @@ -759,3 +759,24 @@ def handler(request): 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=[], 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=[]) + 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" From a1a953a9b817759a90d73f04b10d900c2137774e Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Mon, 19 Oct 2026 17:03:02 +0800 Subject: [PATCH 21/57] fix: validate session region and align reference precedence --- docs/guides/agent-workflow.md | 2 +- src/agora_agent/agentkit/session_backend.py | 13 +++++------ tests/custom/test_v3_preview.py | 24 ++++++++++++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 7929c97..7316fa0 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -83,7 +83,7 @@ ephemeral.start() ephemeral.stop() ``` -`idle_timeout` and `max_duration` are seconds (converted to integer milliseconds). `name` is an alias for `client_reference` in these new sessions; conflicting values are rejected. `labels` is supported only for saved starts. Token, preset, pipeline ID, token-expiry and string-UID overrides are not documented for saved/ephemeral starts and are rejected. For these sessions, `say`, `interrupt`, `think`, `update`, history and turn helpers raise `NotImplementedError` locally; their IDs are never sent to the older control endpoints. `get_info()` returns the project session response, and `.status` normalizes its status to lowercase. +`idle_timeout` and `max_duration` are seconds (converted to integer milliseconds). `client_reference` takes precedence over the optional `name` fallback in these new sessions. `labels` is supported only for saved starts. Token, preset, pipeline ID, token-expiry and string-UID overrides are not documented for saved/ephemeral starts and are rejected. For these sessions, `say`, `interrupt`, `think`, `update`, history and turn helpers raise `NotImplementedError` locally; their IDs are never sent to the older control endpoints. `get_info()` returns the project session response, and `.status` normalizes its status to lowercase. ## Async applications diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index c6a8384..91ee449 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -18,8 +18,14 @@ def contains(value): def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids, name=None, client_reference=None, idle_timeout=None, max_duration=None, labels=None, debug=None, warn=None, **unsupported): + from ..pool_client import Agora, AsyncAgora from .agent_session import AgentSession, AsyncAgentSession from .v3 import build_body + client = owner._client + if not isinstance(client, AsyncAgora if asynchronous else Agora): + raise TypeError("Session requires AsyncAgora" if asynchronous else "Session requires Agora") + if getattr(client, "area_scope", None) == "cn": + raise ValueError("Stored and ephemeral sessions in CN are unconfirmed") invalid = [key for key, value in unsupported.items() if value is not None] if invalid: raise ValueError("Unsupported session options: " + ", ".join(sorted(invalid))) @@ -27,10 +33,8 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids raise ValueError("channel must be nonempty") if 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 for v in remote_uids): + 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") - if name is not None and client_reference is not None and name != client_reference: - raise ValueError("name and client_reference must agree when both are supplied") body = {"transport": {"type": "rtc", "channel": channel, "uid": agent_uid, "subscribe_uids": list(remote_uids)}} reference = client_reference if client_reference is not None else name @@ -55,9 +59,6 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids agent = build_body(owner) agent.pop("agent_name", None) body["agent"] = agent - client = owner._client - if client is None: - raise ValueError("client is required") session_type = AsyncAgentSession if asynchronous else AgentSession return session_type(client=client, agent=owner, app_id=client.app_id, name=reference or "", channel=channel, agent_uid=agent_uid, diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 3a3705e..5d93228 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -256,7 +256,7 @@ def test_secret_ref_v2_guard_at_all_boundaries(): 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=["*"]).status == "idle" - assert agent.create_async_session(channel="c", agent_uid="1", remote_uids=["*"]).status == "idle" + assert configured(make_client(lambda _: pytest.fail("must not send"), True)).create_async_session(channel="c", agent_uid="1", remote_uids=["*"]).status == "idle" def test_number_resolution_pagination_conflict_retry_and_unassign(): @@ -780,3 +780,25 @@ def handler(request): 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_new_sessions_reject_cn_before_network(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 + with pytest.raises(ValueError, match="CN"): + method(channel="r", agent_uid="1", remote_uids=["2"]) + + +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", client_reference="explicit") + assert session._session_backend["body"]["client_reference"] == "explicit" + with pytest.raises(ValueError, match="remote_uids"): + create_session(agent, channel="r", agent_uid="1", remote_uids=[" "]) From 50e73490336f55524858e4984b5cbb39fa845e80 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:08:20 -0400 Subject: [PATCH 22/57] feat: align token transport sessions and campaigns with agreed contract --- .fernignore | 5 + src/agora_agent/agentkit/agent.py | 24 +- src/agora_agent/agentkit/agent_session.py | 48 ++-- src/agora_agent/agentkit/session_backend.py | 56 +++- src/agora_agent/agentkit/v3.py | 18 +- src/agora_agent/client.py | 32 +-- src/agora_agent/core/http_client.py | 29 ++ src/agora_agent/core/public_auth.py | 17 ++ src/agora_agent/pool_client.py | 113 ++------ src/agora_agent/v3/_transport.py | 26 +- src/agora_agent/v3/campaigns.py | 134 +++++++++ src/agora_agent/v3/client.py | 144 +++++----- src/agora_agent/v3/session_contract.py | 67 +++++ src/agora_agent/v3/types.py | 179 +++++++++++- tests/custom/test_tts_vendors.py | 3 +- tests/custom/test_v3_preview.py | 296 ++++++++++++++++++-- 16 files changed, 944 insertions(+), 247 deletions(-) create mode 100644 src/agora_agent/core/public_auth.py create mode 100644 src/agora_agent/v3/campaigns.py create mode 100644 src/agora_agent/v3/session_contract.py diff --git a/.fernignore b/.fernignore index 25e5a2e..f421542 100644 --- a/.fernignore +++ b/.fernignore @@ -34,3 +34,8 @@ requirements.txt # 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 diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index d87b3f8..a14a8ca 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -905,6 +905,12 @@ def create_session( 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, ) -> "AgentSession": """Create a synchronous session bound to this agent configuration. @@ -921,11 +927,13 @@ def create_session( from .agent_session import AgentSession from .session_backend import create_session, needs_session_backend - if needs_session_backend(self) or max_duration is not None or client_reference is not None or labels is not None: + if 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)): 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, + 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, enable_string_uid=enable_string_uid, preset=preset, pipeline_id=pipeline_id, expires_in=expires_in, debug=debug, warn=warn) resolved_client = self._client @@ -969,6 +977,12 @@ def create_async_session( 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, ) -> "AsyncAgentSession": """Create an async session for use with :class:`~agora_agent.AsyncAgora`. @@ -979,11 +993,13 @@ def create_async_session( from .agent_session import AsyncAgentSession from .session_backend import create_session, needs_session_backend - if needs_session_backend(self) or max_duration is not None or client_reference is not None or labels is not None: + if 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)): 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, + 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, enable_string_uid=enable_string_uid, preset=preset, pipeline_id=pipeline_id, expires_in=expires_in, debug=debug, warn=warn) resolved_client = self._client diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 20fc826..66d0960 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -601,6 +601,8 @@ def start(self) -> str: 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._status = "starting" @@ -612,7 +614,7 @@ def start(self) -> str: if not isinstance(identifier, str) or not identifier: raise RuntimeError("Session response is missing agent_session_id") self._agent_id = identifier - self._status = str(response["status"]).lower() + self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() self._emit("started", {"agent_id": identifier, "agent_session_id": identifier}) return identifier except Exception as error: @@ -707,18 +709,24 @@ def stop(self) -> None: rather than raising an error. """ if self._session_backend is not None: - if self._status != "running" or not self._agent_id: + if self._status not in ("starting", "running") or not self._agent_id: raise RuntimeError(f"Cannot stop session in {self._status} state") self._status = "stopping" try: self._client.v3.sessions.stop(agent_session_id=self._agent_id) except Exception as error: - body = getattr(error, "body", None) - if not (getattr(error, "status_code", None) == 404 - and isinstance(body, dict) and body.get("reason") == "TaskNotFound"): + if getattr(error, "status_code", None) == 409: + reason = (getattr(error, "body", None) or {}).get("reason") + if reason == "TaskNotRunning": + self._status = "stopped" + elif reason == "TaskConflict": + self._status = "stopping" + else: + self._status = "error" + else: self._status = "error" - self._emit("error", error) - raise + self._emit("error", error) + raise self._status = "stopped" self._emit("stopped", {"agent_id": self._agent_id, "agent_session_id": self._agent_id}) return @@ -886,7 +894,7 @@ def get_info(self) -> typing.Any: if not self._agent_id: raise RuntimeError("No session ID available") response = self._client.v3.sessions.get(agent_session_id=self._agent_id) - self._status = str(response["status"]).lower() + 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") @@ -1024,6 +1032,8 @@ async def start(self) -> str: 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._status = "starting" @@ -1035,7 +1045,7 @@ async def start(self) -> str: if not isinstance(identifier, str) or not identifier: raise RuntimeError("Session response is missing agent_session_id") self._agent_id = identifier - self._status = str(response["status"]).lower() + self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() self._emit("started", {"agent_id": identifier, "agent_session_id": identifier}) return identifier except Exception as error: @@ -1130,18 +1140,24 @@ async def stop(self) -> None: rather than raising an error. """ if self._session_backend is not None: - if self._status != "running" or not self._agent_id: + if self._status not in ("starting", "running") or not self._agent_id: raise RuntimeError(f"Cannot stop session in {self._status} state") self._status = "stopping" try: await self._client.v3.sessions.stop(agent_session_id=self._agent_id) except Exception as error: - body = getattr(error, "body", None) - if not (getattr(error, "status_code", None) == 404 - and isinstance(body, dict) and body.get("reason") == "TaskNotFound"): + if getattr(error, "status_code", None) == 409: + reason = (getattr(error, "body", None) or {}).get("reason") + if reason == "TaskNotRunning": + self._status = "stopped" + elif reason == "TaskConflict": + self._status = "stopping" + else: + self._status = "error" + else: self._status = "error" - self._emit("error", error) - raise + self._emit("error", error) + raise self._status = "stopped" self._emit("stopped", {"agent_id": self._agent_id, "agent_session_id": self._agent_id}) return @@ -1309,7 +1325,7 @@ async def get_info(self) -> typing.Any: if not self._agent_id: raise RuntimeError("No session ID available") response = await self._client.v3.sessions.get(agent_session_id=self._agent_id) - self._status = str(response["status"]).lower() + 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") diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 91ee449..09b1b86 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -12,12 +12,16 @@ def contains(value): if isinstance(value, (list, tuple)): return any(contains(v) for v in value) return False - return bool(getattr(agent, "_v3_variables", {})) or contains(agent.config) + 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, agent_uid, remote_uids, name=None, client_reference=None, idle_timeout=None, max_duration=None, - labels=None, debug=None, warn=None, **unsupported): + labels=None, values=None, idempotency_key=None, token=None, encryption=None, + audio_scenario=None, graceful_stop=None, data_policy=None, debug=None, warn=None, **unsupported): from ..pool_client import Agora, AsyncAgora from .agent_session import AgentSession, AsyncAgentSession from .v3 import build_body @@ -26,6 +30,8 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids raise TypeError("Session requires AsyncAgora" if asynchronous else "Session requires Agora") if getattr(client, "area_scope", None) == "cn": raise ValueError("Stored and ephemeral sessions in CN are unconfirmed") + 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))) @@ -37,28 +43,58 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids raise ValueError("remote_uids must be a list of nonempty strings") body = {"transport": {"type": "rtc", "channel": channel, "uid": agent_uid, "subscribe_uids": list(remote_uids)}} - reference = client_reference if client_reference is not None else name - if reference is not None: - body["client_reference"] = reference + reference = name + for key, value in (("token", token), ("encryption", encryption), ("audio_scenario", audio_scenario)): + if value is not None: + body["transport"][key] = copy.deepcopy(value) + 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 - if labels is not None: - body["labels"] = copy.deepcopy(labels) else: - if labels is not None: - raise ValueError("labels are not documented for ephemeral sessions") - agent = build_body(owner) + from .v3 import dump + params = dump(owner._parameters or {}) + inline = owner._clone() + 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 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 session_type = AsyncAgentSession if asynchronous else AgentSession return session_type(client=client, agent=owner, app_id=client.app_id, name=reference or "", channel=channel, agent_uid=agent_uid, diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 2e62e8e..341267c 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -128,10 +128,12 @@ def module(config, category, errors): errors.append(category + "." + field) else: result[field] = value - if category == "avatar" and result.get("vendor") in ("sensetime", "spatius"): - errors.append("avatar.vendor." + result["vendor"]) 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 keys: + result.pop("url", None) + for key in ("url", "base_url"): + params.pop(key, None) if params: result["params"] = params return result @@ -449,11 +451,14 @@ def __init__(self, client, response): class SavedAgent(_SavedAgentBase): def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], name=None, client_reference=None, idle_timeout=None, - max_duration=None, labels=None, debug=None, warn=None): + max_duration=None, labels=None, values=None, idempotency_key=None, token=None, + encryption=None, audio_scenario=None, graceful_stop=None, data_policy=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, debug=debug, warn=warn) @classmethod @@ -519,12 +524,15 @@ def unassign_number(self, number) -> NumberResponse: class AsyncSavedAgent(_SavedAgentBase): def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], name=None, client_reference=None, idle_timeout=None, - max_duration=None, labels=None, debug=None, warn=None): + max_duration=None, labels=None, values=None, idempotency_key=None, token=None, + encryption=None, audio_scenario=None, graceful_stop=None, data_policy=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, debug=debug, warn=warn) + 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, debug=debug, warn=warn) create_async_session = create_session diff --git a/src/agora_agent/client.py b/src/agora_agent/client.py index 8a981af..f29015f 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,23 @@ 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) + if authorization and not authorization.lower().startswith("agora token="): + raise ValueError("Only Agora token authentication is supported") _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 +143,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 +160,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 +170,23 @@ 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) + if authorization and not authorization.lower().startswith("agora token="): + raise ValueError("Only Agora token authentication is supported") _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/http_client.py b/src/agora_agent/core/http_client.py index e4173f9..d47f337 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -13,6 +13,7 @@ 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 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 +193,13 @@ def request( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: + validate_public_options(request_options) + validate_public_headers(self.base_headers()) + 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("HTTP Basic auth overrides are not supported") base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -289,6 +297,13 @@ def stream( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: + validate_public_options(request_options) + validate_public_headers(self.base_headers()) + 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("HTTP Basic auth overrides are not supported") base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -390,6 +405,13 @@ async def request( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: + validate_public_options(request_options) + validate_public_headers(self.base_headers()) + 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("HTTP Basic auth overrides are not supported") base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") @@ -487,6 +509,13 @@ async def stream( omit: typing.Optional[typing.Any] = None, force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: + validate_public_options(request_options) + validate_public_headers(self.base_headers()) + 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("HTTP Basic auth overrides are not supported") base_url = self.get_base_url(base_url) timeout = ( request_options.get("timeout_in_seconds") diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py new file mode 100644 index 0000000..999ec97 --- /dev/null +++ b/src/agora_agent/core/public_auth.py @@ -0,0 +1,17 @@ +"""Token-only public SDK authentication policy (including transport overrides).""" + + +def validate_public_headers(headers): + for name, value in (headers or {}).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 == "authorization" and value and not str(value).lower().startswith("agora token="): + raise ValueError("Only Agora token authentication is supported") + + +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("Basic credential overrides are not supported") + validate_public_headers((options or {}).get("additional_headers", {})) diff --git a/src/agora_agent/pool_client.py b/src/agora_agent/pool_client.py index ddb0a72..f3026a2 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -2,26 +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 .v3 import V3Options, V3Client, AsyncV3Client -from .v3.client import SecretsClient, NumbersClient, AsyncSecretsClient, AsyncNumbersClient 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"] _DEBUG_LOGGER = logging.getLogger("agora_agent") _AreaT = typing.TypeVar("_AreaT", bound=Area) _GlobalArea = typing_extensions.Literal[Area.US, Area.EU, Area.AP] @@ -109,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]): """ @@ -135,21 +130,10 @@ 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``. headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -186,15 +170,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) @@ -217,8 +200,6 @@ def __new__( area: typing_extensions.Literal[Area.CN], app_id: str, app_certificate: str, - 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, @@ -235,8 +216,6 @@ def __new__( area: _GlobalArea, app_id: str, app_certificate: str, - 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, @@ -253,8 +232,6 @@ def __new__( area: _AreaT, app_id: str, app_certificate: str, - 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, @@ -280,8 +257,6 @@ def __init__( area: typing_extensions.Literal[Area.CN], app_id: str, app_certificate: str, - 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, @@ -298,8 +273,6 @@ def __init__( area: _GlobalArea, app_id: str, app_certificate: str, - 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, @@ -316,8 +289,6 @@ def __init__( area: _AreaT, app_id: str, app_certificate: str, - 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, @@ -333,8 +304,6 @@ def __init__( area: typing.Any, app_id: str, app_certificate: str, - 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, @@ -343,20 +312,15 @@ def __init__( debug: bool = False, v3: typing.Optional[V3Options] = None, ): + from .core.public_auth import validate_public_headers + validate_public_headers(headers) 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: + if auth_token is not None: self.auth_mode = "token" authorization = f"agora token={auth_token}" else: @@ -377,8 +341,6 @@ def __init__( super().__init__( base_url=self._pool.get_current_url(), authorization=authorization, - username="", - password="", headers=headers, timeout=timeout, follow_redirects=follow_redirects, @@ -391,6 +353,11 @@ def v3(self) -> V3Client: 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.""" @@ -511,21 +478,10 @@ 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``. headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -558,15 +514,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", ) """ @@ -577,8 +532,6 @@ def __new__( area: typing_extensions.Literal[Area.CN], app_id: str, app_certificate: str, - 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, @@ -595,8 +548,6 @@ def __new__( area: _GlobalArea, app_id: str, app_certificate: str, - 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, @@ -613,8 +564,6 @@ def __new__( area: _AreaT, app_id: str, app_certificate: str, - 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, @@ -640,8 +589,6 @@ def __init__( area: typing_extensions.Literal[Area.CN], app_id: str, app_certificate: str, - 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, @@ -658,8 +605,6 @@ def __init__( area: _GlobalArea, app_id: str, app_certificate: str, - 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, @@ -676,8 +621,6 @@ def __init__( area: _AreaT, app_id: str, app_certificate: str, - 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, @@ -693,8 +636,6 @@ def __init__( area: typing.Any, app_id: str, app_certificate: str, - 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, @@ -703,20 +644,15 @@ def __init__( debug: bool = False, v3: typing.Optional[V3Options] = None, ): + from .core.public_auth import validate_public_headers + validate_public_headers(headers) 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: + if auth_token is not None: self.auth_mode = "token" authorization = f"agora token={auth_token}" else: @@ -737,8 +673,6 @@ def __init__( super().__init__( base_url=self._pool.get_current_url(), authorization=authorization, - username="", - password="", headers=headers, timeout=timeout, follow_redirects=follow_redirects, @@ -751,6 +685,11 @@ def v3(self) -> AsyncV3Client: 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.""" diff --git a/src/agora_agent/v3/_transport.py b/src/agora_agent/v3/_transport.py index 9e05326..d97cc52 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -9,6 +9,7 @@ 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") @@ -16,7 +17,7 @@ @dataclass(frozen=True) class V3Options: - """Staging overrides. base_url is the origin, without the v3 project path.""" + """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) @@ -66,11 +67,12 @@ def __init__(self, owner: Any, options: Optional[V3Options]): def _arguments(self, method, path, body, headers, params, request_options): wrapper = self.owner._client_wrapper + 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: - # The existing pool's base may include the v2 path; v3 uses the same origin. - current = urlsplit(wrapper.get_base_url()) - base = f"{current.scheme}://{current.netloc}" + 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: raise ValueError("v3 base_url must be an HTTP(S) origin without query or fragment") @@ -82,6 +84,16 @@ def _arguments(self, method, path, body, headers, params, request_options): app_certificate=self.owner.app_certificate, channel_name="v3", uid=0) merged.setdefault("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("Basic credential overrides are not supported") + _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 @@ -97,6 +109,12 @@ def _parse(response): 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): diff --git a/src/agora_agent/v3/campaigns.py b/src/agora_agent/v3/campaigns.py new file mode 100644 index 0000000..e6ba5ab --- /dev/null +++ b/src/agora_agent/v3/campaigns.py @@ -0,0 +1,134 @@ +"""Typed campaign operations; lifecycle semantics follow the engineering reply.""" +from __future__ import annotations + +from typing import List, Optional, Union + +from ._transport import _UNSET, AsyncResource, RawResponse, Resource, segment +from .types import ( + CampaignDeleteResponse, + CampaignLifecycle, + CampaignListResponse, + CampaignResponse, + CampaignTransport, + CampaignTransportPatch, + ContactInput, + ImmediateSchedule, + Pacing, + PacingPatch, + ScheduledSchedule, +) +from typing_extensions import Literal + + +class CampaignsClient(Resource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = CampaignsRawClient(owner, options) + + def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> CampaignResponse: + return self.with_raw_response.create(name=name, agent_id=agent_id, transport=transport, contacts=contacts, schedule=schedule, lifecycle=lifecycle, pacing=pacing, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data + + def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> CampaignListResponse: + return self.with_raw_response.list(status=status, limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options).data + + def get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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) -> CampaignDeleteResponse: + return self.with_raw_response.delete(campaign_id=campaign_id, 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) -> 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) -> 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) -> CampaignResponse: + return self.with_raw_response.cancel(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data + + +class CampaignsRawClient(Resource): + + def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[CampaignResponse]: + return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body={k: v for k, v in {'name': name, 'agent_id': agent_id, 'transport': transport, 'contacts': contacts, 'schedule': schedule, 'lifecycle': lifecycle, 'pacing': pacing}.items() if v is not None}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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 get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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) + + def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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[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) + + +class AsyncCampaignsClient(AsyncResource): + def __init__(self, owner, options): + super().__init__(owner, options) + self.with_raw_response = AsyncCampaignsRawClient(owner, options) + + async def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> CampaignResponse: + return (await self.with_raw_response.create(name=name, agent_id=agent_id, transport=transport, contacts=contacts, schedule=schedule, lifecycle=lifecycle, pacing=pacing, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data + + async def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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 get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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) -> CampaignDeleteResponse: + return (await self.with_raw_response.delete(campaign_id=campaign_id, 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) -> 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) -> 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) -> CampaignResponse: + return (await self.with_raw_response.cancel(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data + + +class AsyncCampaignsRawClient(AsyncResource): + + async def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[CampaignResponse]: + return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body={k: v for k, v in {'name': name, 'agent_id': agent_id, 'transport': transport, 'contacts': contacts, 'schedule': schedule, 'lifecycle': lifecycle, 'pacing': pacing}.items() if v is not None}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + + async def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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 get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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) + + async def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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[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) diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py index a21d1df..ba74008 100644 --- a/src/agora_agent/v3/client.py +++ b/src/agora_agent/v3/client.py @@ -3,6 +3,7 @@ from typing import Dict, List, Optional, Union from ._transport import _UNSET, AsyncResource, RawResponse, Resource, segment +from .campaigns import AsyncCampaignsClient, CampaignsClient from .types import ( AgentCreateModel_AudioGreeting, AgentCreateModel_CascadedPipeline, @@ -19,6 +20,7 @@ AgentPatchRequest_PipelinePatch, AgentPatchRequest_VariableDefinitionPatch, AgentResponse, + DataPolicy, EphemeralSessionAgent, InboundInput, NumberDeleteResponse, @@ -59,19 +61,19 @@ def delete(self, *, appid: Optional[str] = None, agent_id: str, request_options: class AgentsRawClient(Resource): def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return self._request('POST', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + 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, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return self._request('PATCH', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('DELETE', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + 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) def _AgentsClient(owner, options): client = AgentsClient(owner, options) @@ -81,8 +83,8 @@ def _AgentsClient(owner, options): class SecretsClient(Resource): with_raw_response: SecretsRawClient - def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': - return self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, name=name, secrets=secrets, request_options=request_options).data + def create(self, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': + return self.with_raw_response.create(appid=appid, name=name, secrets=secrets, request_options=request_options).data def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> 'SecretList': return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data @@ -107,29 +109,29 @@ def delete_key(self, *, appid: Optional[str] = None, name: str, key: str, if_mat class SecretsRawClient(Resource): - def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: - return self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body={'name': name, 'secrets': secrets}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + def create(self, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body=None, headers={}, params={}, request_options=request_options) + 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, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: - return self._request('PUT', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> RawResponse: - return self._request('PATCH', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('DELETE', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('PUT', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - return self._request('DELETE', f'/conversational-ai/v3/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) + 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) def _SecretsClient(owner, options): client = SecretsClient(owner, options) @@ -139,8 +141,8 @@ def _SecretsClient(owner, options): class NumbersClient(Resource): with_raw_response: NumbersRawClient - def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': - return self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options).data + def create(self, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': + return self.with_raw_response.create(appid=appid, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options).data def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'NumberListResponse': return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data @@ -156,20 +158,20 @@ def delete(self, *, appid: Optional[str] = None, number_id: str, request_options class NumbersRawClient(Resource): - def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return self._request('POST', f'/conversational-ai/v3/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={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + def create(self, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + 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, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return self._request('PATCH', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('DELETE', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + 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) def _NumbersClient(owner, options): client = NumbersClient(owner, options) @@ -177,11 +179,11 @@ def _NumbersClient(owner, options): return client class SessionsClient(Resource): - def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return self.with_raw_response.start(agent_id=agent_id, transport=transport, client_reference=client_reference, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options).data + def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return self.with_raw_response.start(agent_id=agent_id, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options).data - def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return self.with_raw_response.start_ephemeral(agent=agent, transport=transport, client_reference=client_reference, lifecycle=lifecycle, appid=appid, request_options=request_options).data + def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return self.with_raw_response.start_ephemeral(agent=agent, labels=labels, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, appid=appid, request_options=request_options).data def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: return self.with_raw_response.stop(agent_session_id=agent_session_id, appid=appid, request_options=request_options).data @@ -196,21 +198,21 @@ def get(self, *, appid: Optional[str] = None, agent_session_id: str, request_opt class SessionsRawClient(Resource): - def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: segment(agent_id) - return self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body={'agent_id': agent_id, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={}, params={}, request_options=request_options) + 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, 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) - def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - return self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/ephemeral', body={'agent': agent, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={}, params={}, request_options=request_options) + def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, **({'labels': labels} if labels is not None else {}), 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - return self._request('POST', f'/conversational-ai/v3/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) + 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) def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/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) + 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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> RawResponse: - return self._request('GET', f'/conversational-ai/v3/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) + 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 _SessionsClient(owner, options): client = SessionsClient(owner, options) @@ -223,6 +225,7 @@ def __init__(self, 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): with_raw_response: AsyncAgentsRawClient @@ -245,19 +248,19 @@ async def delete(self, *, appid: Optional[str] = None, agent_id: str, request_op class AsyncAgentsRawClient(AsyncResource): async def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('POST', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + 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, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('PATCH', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('DELETE', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/agents/{segment(agent_id)}', body=None, headers={}, params={}, request_options=request_options) + 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) def _AsyncAgentsClient(owner, options): client = AsyncAgentsClient(owner, options) @@ -267,8 +270,8 @@ def _AsyncAgentsClient(owner, options): class AsyncSecretsClient(AsyncResource): with_raw_response: AsyncSecretsRawClient - async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': - return (await self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, name=name, secrets=secrets, request_options=request_options)).data + async def create(self, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': + return (await self.with_raw_response.create(appid=appid, name=name, secrets=secrets, request_options=request_options)).data async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> 'SecretList': return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data @@ -293,29 +296,29 @@ async def delete_key(self, *, appid: Optional[str] = None, name: str, key: str, class AsyncSecretsRawClient(AsyncResource): - async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: - return await self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets', body={'name': name, 'secrets': secrets}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + async def create(self, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/secrets/{segment(name)}', body=None, headers={}, params={}, request_options=request_options) + 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, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: - return await self._request('PUT', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> RawResponse: - return await self._request('PATCH', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('DELETE', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('PUT', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('DELETE', f'/conversational-ai/v3/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) + 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) def _AsyncSecretsClient(owner, options): client = AsyncSecretsClient(owner, options) @@ -325,8 +328,8 @@ def _AsyncSecretsClient(owner, options): class AsyncNumbersClient(AsyncResource): with_raw_response: AsyncNumbersRawClient - async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': - return (await self.with_raw_response.create(appid=appid, idempotency_key=idempotency_key, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options)).data + async def create(self, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': + return (await self.with_raw_response.create(appid=appid, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options)).data async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'NumberListResponse': return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data @@ -342,20 +345,20 @@ async def delete(self, *, appid: Optional[str] = None, number_id: str, request_o class AsyncNumbersRawClient(AsyncResource): - async def create(self, *, appid: Optional[str] = None, idempotency_key: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('POST', f'/conversational-ai/v3/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={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) + async def create(self, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + 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, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('PATCH', f'/conversational-ai/v3/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) + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('DELETE', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/numbers/{segment(number_id)}', body=None, headers={}, params={}, request_options=request_options) + 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) def _AsyncNumbersClient(owner, options): client = AsyncNumbersClient(owner, options) @@ -363,11 +366,11 @@ def _AsyncNumbersClient(owner, options): return client class AsyncSessionsClient(AsyncResource): - async def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return (await self.with_raw_response.start(agent_id=agent_id, transport=transport, client_reference=client_reference, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options)).data + async def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return (await self.with_raw_response.start(agent_id=agent_id, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options)).data - async def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return (await self.with_raw_response.start_ephemeral(agent=agent, transport=transport, client_reference=client_reference, lifecycle=lifecycle, appid=appid, request_options=request_options)).data + async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + return (await self.with_raw_response.start_ephemeral(agent=agent, labels=labels, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, appid=appid, request_options=request_options)).data async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: return (await self.with_raw_response.stop(agent_session_id=agent_session_id, appid=appid, request_options=request_options)).data @@ -382,21 +385,21 @@ async def get(self, *, appid: Optional[str] = None, agent_session_id: str, reque class AsyncSessionsRawClient(AsyncResource): - async def start(self, *, agent_id: str, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + async def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: segment(agent_id) - return await self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions', body={'agent_id': agent_id, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={}, params={}, request_options=request_options) + 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, 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) - async def start_ephemeral(self, *, agent: EphemeralSessionAgent, transport: RtcSessionTransport, client_reference: Optional[str] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('POST', f'/conversational-ai/v3/projects/{segment(self.owner.app_id if appid is None else appid)}/sessions/ephemeral', body={'agent': agent, 'transport': transport, **({'client_reference': client_reference} if client_reference is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={}, params={}, request_options=request_options) + async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, **({'labels': labels} if labels is not None else {}), 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('POST', f'/conversational-ai/v3/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) + 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) async def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/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) + 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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> RawResponse: - return await self._request('GET', f'/conversational-ai/v3/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) + 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) def _AsyncSessionsClient(owner, options): client = AsyncSessionsClient(owner, options) @@ -409,3 +412,4 @@ def __init__(self, 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/session_contract.py b/src/agora_agent/v3/session_contract.py new file mode 100644 index 0000000..d63fcd2 --- /dev/null +++ b/src/agora_agent/v3/session_contract.py @@ -0,0 +1,67 @@ +"""Validation shared by public session requests and AgentKit's internal adapter.""" +import math +import re + + +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"] + _object(transport, {"type", "channel", "uid", "subscribe_uids", "token", "encryption", "audio_scenario"}, "transport") + if transport.get("type") != "rtc": + raise ValueError("Only RTC session transport is currently exposed") + 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 not isinstance(uid, str) or not uid.strip(): + 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 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 != "0" and numeric(uid) != numeric(subscribers[0]): + raise ValueError("agent and subscriber must use the same UID type") + 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") + 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") diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index c063df0..d1016d6 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -809,16 +809,20 @@ class AgentCreateRequest(TypedDict, total=False): __all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AgoraVadTurnDetection', 'AudioGreeting', 'DisabledInterruption', 'EndCallPolicy', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'GeneratedFillerContent', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JsonValue', 'KeywordInterruption', 'ManualTurnBoundary', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedMllm', 'ResolvedRealtimePipeline', 'ResolvedTts', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SemanticVadTurnDetection', 'ServerVadTurnDetection', 'SessionAnalysis', 'SessionData', 'SessionList', 'SessionResponse', 'SessionSummary', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SpeechInterruption', 'StaticFillerContent', 'TextGreeting', 'TransferPolicy', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_AudioGreeting', 'AgentCreateModel_ByokCredential', 'AgentCreateModel_CascadedPipeline', 'AgentCreateModel_DisabledInterruption', 'AgentCreateModel_FillerTrigger', 'AgentCreateModel_FillerWords', 'AgentCreateModel_GeneratedFillerContent', 'AgentCreateModel_JsonValue', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_ManagedCredential', 'AgentCreateModel_ManualTurnBoundary', 'AgentCreateModel_Message', 'AgentCreateModel_ProviderModule', 'AgentCreateModel_RealtimePipeline', 'AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_SemanticVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SilenceConfig', 'AgentCreateModel_SpeechInterruption', 'AgentCreateModel_StaticFillerContent', 'AgentCreateModel_TextGreeting', 'AgentCreateModel_TurnDetection', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_VariableDefinition', 'AgentCreateModel', 'AgentPatchRequest_CredentialPatch', 'AgentPatchRequest_FillerContentPatch', 'AgentPatchRequest_FillerTriggerPatch', 'AgentPatchRequest_FillerWordsPatch', 'AgentPatchRequest_GreetingPatch', 'AgentPatchRequest_InterruptionPatch', 'AgentPatchRequest_JsonValue', 'AgentPatchRequest_Message', 'AgentPatchRequest_PipelinePatch', 'AgentPatchRequest_PipelineTurnDetectionPatch', 'AgentPatchRequest_ProviderModulePatch', 'AgentPatchRequest_SilenceConfigPatch', 'AgentPatchRequest_TurnBoundaryPatch', 'AgentPatchRequest_VariableDefinitionPatch', 'AgentPatchRequest', 'Contract_VariableDefinitions', 'Contract_ModelCredential', 'Contract_Llm', 'Contract_CascadedPipeline', 'Contract_Pipeline', 'Contract_SemanticVadTurnDetection', 'Contract_SilenceConfig', 'Contract_ServerVadTurnDetection', 'Contract_McpServer', 'Contract_VariableDefinition', 'Contract_Greeting', 'Contract_Asr', 'Contract_FillerWords', 'Contract_Interruption', 'Contract_TurnStart', 'Contract_FunctionTool', 'Contract_Tts', 'Contract_RealtimePipeline', 'Contract_Mllm', 'Contract_Avatar', 'Contract_RealtimeTurnDetection', 'Contract_FunctionToolServer', 'Contract_TurnEnd', 'Contract_Message', 'Contract_AgentCreateInput', 'Contract_TurnDetection', 'Contract_AgoraVadTurnDetection', 'Contract_FunctionToolExecution', 'Contract_JsonSchema', 'AgentCreateRequest'] # Session write shapes from the refreshed Bruno collection (not the older OpenAPI). -class RtcSessionTransport(TypedDict): +class RtcSessionTransport(TypedDict, total=False): type: Literal['rtc'] channel: str uid: str subscribe_uids: List[str] + token: str + encryption: RtcEncryption + audio_scenario: Literal["aiserver", "chorus"] class SessionLifecycle(TypedDict, total=False): idle_timeout_ms: int max_duration_ms: int + graceful_stop: GracefulStop class EphemeralSessionAgent(TypedDict, total=False): @@ -830,11 +834,178 @@ class EphemeralSessionAgent(TypedDict, total=False): filler_words: AgentCreateModel_FillerWords -class StartSessionResponse(TypedDict): +class StartSessionResponse(TypedDict, total=False): agent_session_id: str status: str - # Bruno establishes this field but does not specify its timestamp format. - created_at: Any + create_ts: int + # Accepted only during backend rollout. + created_at: int __all__ += ['RtcSessionTransport', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse'] + + +class RtcEncryption(TypedDict, total=False): + mode: 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: str + salt: str + +class GracefulStop(TypedDict, total=False): + enabled: bool + timeout_ms: int + +class DataPolicy(TypedDict, total=False): + retention: Literal["30_days", "none"] + +__all__ += ["RtcEncryption", "GracefulStop", "DataPolicy"] + +# Campaign schemas from the supplied OpenAPI; retry pacing fields remain unconfirmed. + + +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]] + + +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 + + +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 ContactInput(TypedDict, total=False): + phone: Required[str] + values: Dict[str, Union[str, int, float, bool]] + + +class ImmediateSchedule(TypedDict, total=False): + mode: Required[Literal['immediate']] + timezone: str + days: List[CallingDay] + + +class Pacing(TypedDict, total=False): + max_concurrent: int + max_calls_per_second: Union[int, None] + max_attempts: int + retry_backoff_ms: int + + +class PacingPatch(TypedDict, total=False): + max_concurrent: Union[int, None] + max_calls_per_second: Union[int, None] + max_attempts: Union[int, None] + retry_backoff_ms: Union[int, None] + + +class ScheduledSchedule(TypedDict, total=False): + mode: Required[Literal['scheduled']] + timezone: Required[str] + start: Required[str] + end: Required[str] + days: List[CallingDay] + + +class TimeRange(TypedDict, total=False): + start: Required[str] + end: Required[str] + +__all__ += ['CallingDay', 'CampaignCallPolicy', 'CampaignCounts', 'CampaignCreate', 'CampaignDeleteResponse', 'CampaignEndCallPolicy', 'CampaignFailure', 'CampaignLifecycle', 'CampaignListData', 'CampaignListResponse', 'CampaignPatch', 'CampaignResponse', 'CampaignTransferPolicy', 'CampaignTransport', 'CampaignTransportPatch', 'CampaignTransportResponse', 'CampaignVoicemailPolicy', 'ContactInput', 'ImmediateSchedule', 'Pacing', 'PacingPatch', 'ScheduledSchedule', 'TimeRange'] diff --git a/tests/custom/test_tts_vendors.py b/tests/custom/test_tts_vendors.py index 43388bb..466ebe6 100644 --- a/tests/custom/test_tts_vendors.py +++ b/tests/custom/test_tts_vendors.py @@ -356,8 +356,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_preview.py b/tests/custom/test_v3_preview.py index 5d93228..f400fa2 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -29,7 +29,7 @@ ("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"}, "idempotency_key": "dedupe"}, "POST", "secrets", {"name": "prod", "secrets": {"key": "private"}}), + ("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"}}), @@ -37,7 +37,7 @@ ("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": {"host": "sip.example"}, "idempotency_key": "number-key"}, "POST", "numbers", {"number": "+12025550123", "sip_trunk": {"host": "sip.example"}}), + ("numbers", "create", {"number": "+12025550123", "sip_trunk": {"host": "sip.example"}}, "POST", "numbers", {"number": "+12025550123", "sip_trunk": {"host": "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}}), @@ -45,8 +45,16 @@ ("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"]}, "client_reference": "test", "lifecycle": {"max_duration_ms": 120000}}, "POST", "sessions/ephemeral", {"agent": {"instructions": "Help"}, "transport": {"type": "rtc", "channel": "room", "uid": "1", "subscribe_uids": ["2"]}, "client_reference": "test", "lifecycle": {"max_duration_ms": 120000}}), + ("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", "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), ] @@ -55,15 +63,15 @@ def make_client(handler, async_=False, **kwargs): 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", app_certificate="cert", auth_token="raw-token", - httpx_client=http, v3=V3Options(base_url="https://stage.example/", headers={"Gateway": "secret"}), **kwargs) + 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/v3/projects/project/" + suffix).encode() + 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["Gateway"] == "secret" + assert request.headers["X-Test-Context"] == "test" if body is not None: assert json.loads(request.content) == body elif method == "POST": @@ -107,7 +115,7 @@ 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 not hasattr(client.v3, "campaigns") + assert client.campaigns is client.v3.campaigns assert not hasattr(DeployedAgent, "start_call") @@ -255,8 +263,8 @@ 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=["*"]).status == "idle" - assert configured(make_client(lambda _: pytest.fail("must not send"), True)).create_async_session(channel="c", agent_uid="1", remote_uids=["*"]).status == "idle" + 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(): @@ -342,14 +350,14 @@ def handler(request): assert json.loads(calls[-1].content)["instructions"] is None -def test_staging_environment_override_and_shared_basic_auth(monkeypatch): +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", customer_id="id", customer_secret="secret", + 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"] == "Basic aWQ6c2VjcmV0" + 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() @@ -546,13 +554,13 @@ def test_variable_options_distinguish_booleans_strings_and_numbers(): async def test_conflict_latest_preserves_context_without_write_conditions(async_client): calls = [] extra = {"iF-mAtCh": '"old"', "If-None-Match": "*", "Idempotency-Key": "once", - "Authorization": "Bearer override", "X-Custom": "kept"} + "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"] == "Bearer override" + 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) @@ -674,7 +682,7 @@ def handler(request): 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 body["client_reference"] == "reference" + assert "client_reference" not in body if stored: assert body["agent_id"] == "stored-id" assert body["labels"] == {"owner": "test"} @@ -697,7 +705,7 @@ def handler(request): else: owner = configured(client).with_variable("customer", type="str") remote = ["2"] - kwargs = dict(channel="room", agent_uid="1", remote_uids=remote, client_reference="reference", idle_timeout=1.5, max_duration=120) + 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)) @@ -720,7 +728,7 @@ def handler(request): assert len(calls) == 3 -@pytest.mark.parametrize("option,value", [("token", "token"), ("preset", "test"), ("pipeline_id", "id"), ("expires_in", 60), ("enable_string_uid", False), ("labels", {})]) +@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): @@ -729,7 +737,7 @@ def test_ephemeral_rejects_undocumented_options(option, value): @pytest.mark.parametrize("async_", [False, True]) @pytest.mark.asyncio -async def test_new_session_stop_404_is_idempotent_and_controls_never_request(async_): +async def test_new_session_stop_404_propagates_and_controls_never_request(async_): calls = [] def handler(request): calls.append(request) @@ -747,18 +755,20 @@ def handler(request): result = getattr(session, method)(*args) if async_: await result - if async_: - await session.stop() - else: - session.stop() - assert session.status == "stopped" + 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=[], max_duration=seconds) + 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]) @@ -769,7 +779,7 @@ def handler(request): 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=[]) + 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: @@ -798,7 +808,239 @@ def test_new_sessions_reject_cn_before_network(async_, stored): 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", client_reference="explicit") - assert session._session_backend["body"]["client_reference"] == "explicit" + 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" + + +@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_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_removed_basic_credentials_rejected_every_entrypoint(field): + for cls in (Agora, AsyncAgora): + with pytest.raises(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) + + +@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"] + + +@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 From b293816d4a5c8647498ddb330a12181f6530be53 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:08:20 -0400 Subject: [PATCH 23/57] docs: document public token workflow and all 31 operations --- README.md | 2 +- docs/getting-started/authentication.md | 19 +-- docs/guides/agent-workflow.md | 28 +++- docs/guides/v3-api-preview.md | 47 +++---- docs/index.md | 2 +- docs/reference/client.md | 4 +- docs/reference/v3.md | 53 +++++--- reference.md | 173 ++++++++++++++----------- 8 files changed, 185 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index ce3283b..3361af2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ and multimodal flows (MLLM) for real-time audio processing. Use one `Agent` builder to configure providers, then call `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 `secret_ref()` credentials and immutable `with_variable()` definitions for saved configurations. -Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 23-operation reference](docs/reference/v3.md) for ETags, cursor pagination, session lifecycle and project queries. `deploy` / `DeployedAgent` remain compatibility names. Saved handles also create sessions by ID; configurations with secret references or variable definitions start ephemeral sessions. Campaign APIs are outside this SDK workflow. +Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 31-operation reference](docs/reference/v3.md) for ETags, cursor pagination, session lifecycle and project queries. `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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. ## Install diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md index a15f43a..0717524 100644 --- a/docs/getting-started/authentication.md +++ b/docs/getting-started/authentication.md @@ -50,22 +50,9 @@ 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. - -### HTTP Basic Auth - -```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", -) -print(client.auth_mode) # "basic" -``` +The public SDK supports token authentication only: pass a raw `auth_token`, or let the SDK sign tokens from `app_id` and `app_certificate`. Customer ID/secret, client ID/secret and Basic authorization overrides are rejected. REST authentication is separate from the optional RTC transport token. ### Pre-built REST token @@ -81,4 +68,4 @@ client = Agora( print(client.auth_mode) # "token" ``` -`auth_token` is mutually exclusive with `customer_id` / `customer_secret`. +New resources use `https://preview.ai.agora.io/conversational-ai/v1/projects/{appid}`. Do not send internal gateway headers or `X-Studio-Region`. diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 7316fa0..fc6ff0e 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -2,7 +2,7 @@ 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 require this preview checkout, installed with `python -m pip install -e .` from the repository root. These features are not implied by a published PyPI version. Configure the approved staging origin and gateway headers as described in the [transport setup](./v3-api-preview.md#quick-start-with-staging). +Saving and project secrets/numbers require this preview checkout, installed with `python -m pip install -e .` from the repository root. These features are not implied by a published PyPI version. 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 @@ -70,7 +70,7 @@ The same builder can start directly with secret references and variable definiti ```python session = saved.create_session( channel="room", agent_uid="1", remote_uids=["2"], - client_reference="support-call", idle_timeout=60, max_duration=120, + idle_timeout=60, max_duration=120, idempotency_key="support-call", labels={"owner": "support"}, ) session_id = session.start() @@ -83,7 +83,22 @@ ephemeral.start() ephemeral.stop() ``` -`idle_timeout` and `max_duration` are seconds (converted to integer milliseconds). `client_reference` takes precedence over the optional `name` fallback in these new sessions. `labels` is supported only for saved starts. Token, preset, pipeline ID, token-expiry and string-UID overrides are not documented for saved/ephemeral starts and are rejected. For these sessions, `say`, `interrupt`, `think`, `update`, history and turn helpers raise `NotImplementedError` locally; their IDs are never sent to the older control endpoints. `get_info()` returns the project session response, and `.status` normalizes its status to lowercase. +`idle_timeout` and `max_duration` are seconds, converted to integer milliseconds. `values`, `labels`, `token` (RTC only), `encryption`, `audio_scenario`, `graceful_stop`, `data_policy` and `idempotency_key` work for both saved and ephemeral sessions. Exactly one subscriber is required; wildcard subscriptions are unsupported. 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 stored conversation data and post-call extraction; operational-data exceptions remain open. 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. + +Only start, stop and `get_info` are available for new session IDs; say/interrupt/think/update/history/turn helpers fail locally. The proposed session PATCH/action endpoints are not implemented. 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 @@ -103,6 +118,11 @@ Use `await AsyncSavedAgent.get(client, saved_id)` to load another handle, and aw ## 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 the new `client_reference`/`max_duration` options, 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. +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). diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index d647a7c..91b4b5f 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -1,15 +1,15 @@ # 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 23 staging operations (20 from OpenAPI plus three session writes from refreshed Bruno). See the [complete v3 reference](../reference/v3.md) for method arguments and required headers. +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 31 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` | Low-level list and get only | +| 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. Outbound calls, AgentKit analysis helpers and campaign APIs are outside this SDK workflow. The SDK package version continues to describe package compatibility independently of the REST API version. +Saving stores configuration; it does not itself start a session. Call `saved.create_session(...).start()` to start by ID. The low-level campaign API is available; high-level outbound-call helpers and AgentKit analysis helpers are not included. The SDK package version continues to describe package compatibility independently of the REST API version. ## Quick start with staging @@ -19,19 +19,16 @@ Install from this repository's `feat/v3-api-preview` checkout: python -m pip install -e . ``` -See [installation](../getting-started/installation.md) for prerequisites. v3 is a preview branch feature; these examples do not imply that a released package contains it. Obtain an approved staging origin and gateway credentials, then set: +See [installation](../getting-started/installation.md) for prerequisites. v3 is a preview branch feature; these examples do not imply that a released package contains it. 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 AGORA_AGENTS_V3_BASE_URL='https://YOUR_STAGING_HOST' -export AGORA_V3_GATEWAY_SECRET='YOUR_GATEWAY_SECRET' -export AGORA_V3_CONSUMER_USERNAME='YOUR_CONSUMER_USERNAME' export OPENAI_API_KEY='YOUR_OPENAI_KEY' export DEEPGRAM_API_KEY='YOUR_DEEPGRAM_KEY' ``` -The base URL is an HTTP(S) **origin**, without a project path, query or fragment. The SDK appends `/conversational-ai/v3/projects/{appid}`. `V3Options(base_url=...)` overrides the environment variable; headers apply only to v3. Existing client authentication still supplies the Authorization header. Production paths, regional availability and Basic-auth acceptance remain unconfirmed; use the supplied staging configuration for evaluation. +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 is token-only: a prebuilt `auth_token` or App ID/App Certificate signing. Internal gateway headers, `X-Studio-Region` and Basic authorization overrides are rejected. Region/failover guarantees remain open; AgentKit refuses CN for these resources. 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). @@ -45,15 +42,10 @@ client = Agora( area=Area.US, app_id=os.environ["AGORA_APP_ID"], app_certificate=os.environ["AGORA_APP_CERTIFICATE"], - v3=V3Options(headers={ - "X-Studio-V2-Gateway-Secret": os.environ["AGORA_V3_GATEWAY_SECRET"], - "X-Consumer-Username": os.environ["AGORA_V3_CONSUMER_USERNAME"], - }), ) client.secrets.create( name="support", secrets={"openai": os.environ["OPENAI_API_KEY"], "deepgram": os.environ["DEEPGRAM_API_KEY"]}, - idempotency_key="support-secrets-initial-provisioning", ) agent = ( Agent(client=client) @@ -75,7 +67,7 @@ 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 mode may also require an explicit provider URL on staging. +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 @@ -92,7 +84,7 @@ print(loaded.id, loaded.name, loaded.etag, loaded.data) 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 `api_key`. -This is a merge patch rather than a guaranteed replacement. Write-only keys and headers cannot be reconstructed from GET, and omitted hidden values remain subject to backend merge semantics. Keep intended credentials in the builder or use a targeted [low-level update](../reference/v3.md#agents-five-operations) when retaining existing hidden credentials matters. Saved-agent update conflicts are surfaced without retry. +The reply marks full-replacement PATCH behavior as **Planned**, so the current SDK conservatively keeps this conditional merge-compatible update until backend support is confirmed. It does not assume planned replacement or name-based upsert. Write-only keys and headers cannot be reconstructed from GET, and omitted hidden values remain subject to backend merge semantics. Keep intended credentials in the builder or use a 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 @@ -156,10 +148,6 @@ 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"], - v3=V3Options(headers={ - "X-Studio-V2-Gateway-Secret": os.environ["AGORA_V3_GATEWAY_SECRET"], - "X-Consumer-Username": os.environ["AGORA_V3_CONSUMER_USERNAME"], - }), ) support = await AsyncSavedAgent.get(client, agent_id) numbers = await support.list_numbers() @@ -185,28 +173,35 @@ Existing resources and ordinary inline `Agent.create_session()` behavior remain The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor messages move to top-level `messages`. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`; provider-specific `params.language` remains separate. A configured STT follows v2 interaction-language precedence (the explicit turn language, otherwise `en-US`). An explicit interaction language without an STT vendor uses the existing managed `ares` fallback, without inventing a URL; configure an approved vendor endpoint if staging requires one. A partial builder with neither STT nor an interaction language keeps ASR absent. +For cascaded deployment, `with_turn_detection({"language": "de-DE"})` sets `pipeline.asr.language`; provider-specific `params.language` remains separate. A configured STT follows v2 interaction-language precedence (the explicit turn language, otherwise `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 | -| Multipart AWS/Vertex credentials, removed avatar vendors, numeric v2 TTS `skip_patterns`, audio-only settings on text greetings, unmappable deprecated turn/greeting settings | Rejected rather than guessed or silently omitted | +| Multipart AWS/Vertex 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 `end_call`, `transfer`, `allowed_ips`, SIP-password secret references | Currently rejected or unsupported on staging | | 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 POST has no idempotency key; numbers currently ignore their declared key. An uncertain create outcome requires inspection before manual retry | -| CN and production | AgentKit CN deployment is refused; production path, regional coverage and Basic-auth support remain unconfirmed | -| Outbound calling, AgentKit analysis and campaign APIs | Not included in this SDK session workflow | +| 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; regional availability/failover remain open, and CN is refused | +| Session controls and analysis helpers | Proposed PATCH/actions and AgentKit analysis helpers are not implemented | -The adapter shares the existing client's HTTP transport, authentication and current regional-pool origin. The staging override applies only to v3. These docs and offline tests do not establish live backend availability. +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 original 20 low-level methods and TypedDict wire schemas were generated locally from staging OpenAPI. Three handwritten session write methods and their TypedDict shapes follow the refreshed Bruno `sessions/start`, `start-ephemeral`, and `stop` examples; the older OpenAPI does not contain these writes. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. +Wire schemas and methods are locally maintained from the supplied OpenAPI and engineering reply. The reply takes precedence where it corrects routing, authentication policy, session idempotency, status and timestamps. Campaign schema retry fields remain unconfirmed; no retry algorithm is implemented. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. 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. + + +## 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 a95886c..8cda4f0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,4 +66,4 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on ## 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 23-operation v3 reference](./reference/v3.md). The preview uses protected custom/local-generated code rather than the canonical Fern pipeline. +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 31-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/client.md b/docs/reference/client.md index c92357c..51418be 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -28,8 +28,6 @@ client = Agora( | `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) | | `auth_token` | `str` | No | — | Pre-built raw REST token; SDK sets `Authorization: agora token=` | | `headers` | `Dict[str, str]` | No | `None` | Additional headers sent with every request | | `timeout` | `float` | No | `60` | Request timeout in seconds | @@ -38,7 +36,7 @@ client = Agora( | `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) | -*`app_id` and `app_certificate` are required constructor arguments in every auth mode; the certificate is used for automatic app-credential auth. Supply `customer_id` and `customer_secret` together to select Basic auth. `auth_token` is mutually exclusive with that pair. +`app_id` and `app_certificate` remain required constructor arguments. Omit `auth_token` to sign REST tokens automatically, or supply a raw prebuilt token. Basic credentials are not exposed. 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. diff --git a/docs/reference/v3.md b/docs/reference/v3.md index 1578017..566b3cf 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -1,6 +1,6 @@ # v3 preview reference -This reference describes the 23 implemented staging 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 locally generated from staging OpenAPI, not Fern-generated code; see [maintainer provenance](../guides/v3-api-preview.md#maintainer-provenance). +This reference describes the 31 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 @@ -25,7 +25,7 @@ The raw form returns `RawResponse`: `.data` is the normal result; `.headers` is ## Agents: five operations -Prefix: `/conversational-ai/v3/projects/{appid}/agents`. A stored configuration's identifier is `agent_id`; it is not a v2 running-session ID. +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 | |---|---|---| @@ -41,11 +41,11 @@ Prefix: `/conversational-ai/v3/projects/{appid}/agents`. A stored configuration' ## Secrets: eight operations -Prefix: `/conversational-ai/v3/projects/{appid}/secrets`. Set names and keys are distinct path components. Secret values are write-only. +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, idempotency_key=None)` | POST collection | `SecretSet` | +| `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` | @@ -58,7 +58,6 @@ Prefix: `/conversational-ai/v3/projects/{appid}/secrets`. Set names and keys are | Argument | Header | Requirement | |---|---|---| -| Create `idempotency_key` | `Idempotency-Key` | Optional; honored for secret creation | | 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 | @@ -77,17 +76,17 @@ client.v3.secrets.update(name="support", if_match=raw.etag, secrets={"new_key": ## Numbers: five operations -Prefix: `/conversational-ai/v3/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. +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, idempotency_key=None)` | POST collection | `NumberResponse` | +| `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 exposes `idempotency_key` as declared by OpenAPI, but staging currently ignores it; it does not make a repeated create safe. `order` accepts `create_ts.asc` or `create_ts.desc`. +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. @@ -103,24 +102,27 @@ number = client.v3.numbers.create( ## Sessions: five operations -Prefix: `/conversational-ai/v3/projects/{appid}/sessions`. +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, client_reference=None, lifecycle=None, labels=None)` | POST collection | `StartSessionResponse` | -| `start_ephemeral(agent, transport, client_reference=None, lifecycle=None)` | POST `/ephemeral` | `StartSessionResponse` | +| `start(agent_id, transport, values=None, labels=None, lifecycle=None, data_policy=None, idempotency_key=None)` | POST collection | `StartSessionResponse` | +| `start_ephemeral(agent, transport, values=None, labels=None, lifecycle=None, data_policy=None, idempotency_key=None)` | POST `/ephemeral` | `StartSessionResponse` | | `stop(agent_session_id)` | POST `/{agent_session_id}/stop`, no request body | empty dictionary | -The three session writes follow refreshed Bruno examples, not the older GET-only OpenAPI. `transport` is `RtcSessionTransport`: `type="rtc"`, `channel`, `uid`, `subscribe_uids` (strings/list of strings). Optional `SessionLifecycle` fields are integer `idle_timeout_ms` and `max_duration_ms`; the low-level API uses milliseconds. `agent` is `EphemeralSessionAgent`, containing the mapped agent configuration without `agent_name`. Start returns `agent_session_id`, `status` and `created_at`; the timestamp format is unspecified. Bruno start returns lowercase `running`, while retrieve returns uppercase statuses. `labels` is evidenced only for stored starts. No new control endpoints are assumed. +Session requests follow the supplied OpenAPI plus the engineering reply corrections. `RtcSessionTransport` requires `type="rtc"`, `channel`, `uid` and exactly one non-wildcard `subscribe_uids` entry. 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. Only RTC transport is exposed by these SDK methods. +`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. No session PATCH/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` and `analysis`; 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. -`analysis`, when present, exposes extraction status and `result` through the low-level response. Session update/actions, outbound-call methods, AgentKit `get_analysis`, and the campaigns namespace are not included in this workflow. +`analysis`, when present, exposes extraction status and `result` through the low-level response. Session update/actions, outbound-call methods, AgentKit `get_analysis`, are not included in this workflow. ## Pagination, errors and transport @@ -159,9 +161,30 @@ The `max_*` assignment options, save `name` / `agent_id`, and update `name` are `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` and `client.numbers` are the same resource objects as their `client.v3` counterparts. The advanced namespace remains available for raw transport and project session reads. +`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, remote_uids, name=None, client_reference=None, idle_timeout=None, max_duration=None, labels=None, debug=None, warn=None)` returns `AgentSession`. `AsyncSavedAgent.create_session(...)` and its `create_async_session` alias return `AsyncAgentSession`. Creation is local; call `start()` / `await start()`. `get_info()` and `stop()` use the returned session ID. The same new duration/reference options on builder session creation select ephemeral start, as do secret references or `with_variable` definitions. Durations on handles are seconds. New-backed `.raw` exposes `client.v3.sessions`; `.raw_agent_management` is `None`. +`SavedAgent.create_session(*, channel, agent_uid, remote_uids, 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, 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=None, pacing=None, 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–5,000 contacts per campaign. 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 fields are schema-typed, but `max_attempts` and `retry_backoff_ms` remain unconfirmed; do not rely on built-in retries. + +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. diff --git a/reference.md b/reference.md index ad9c30b..241f6af 100644 --- a/reference.md +++ b/reference.md @@ -1,6 +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 20 staging operations alongside the existing v2 resources documented below. +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(...)
@@ -29,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, @@ -40,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", @@ -184,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", @@ -317,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", @@ -400,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", @@ -485,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", @@ -582,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", @@ -663,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", @@ -776,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", @@ -893,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", @@ -982,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", @@ -1135,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", @@ -1268,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", @@ -1393,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", @@ -1474,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", @@ -1556,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() @@ -1618,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", @@ -1757,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", @@ -1832,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", @@ -1904,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", From f6d6cfee5bbebfd5dcfa3ecac0342b29a5444413 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:13:22 -0400 Subject: [PATCH 24/57] feat: expose typed telephony transport for low-level session starts --- docs/reference/v3.md | 2 +- src/agora_agent/v3/client.py | 18 ++--- src/agora_agent/v3/session_contract.py | 95 +++++++++++++++++++------- src/agora_agent/v3/types.py | 34 +++++++-- tests/custom/test_v3_preview.py | 27 ++++++++ 5 files changed, 135 insertions(+), 41 deletions(-) diff --git a/docs/reference/v3.md b/docs/reference/v3.md index 566b3cf..c8e21be 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -112,7 +112,7 @@ Prefix: `/conversational-ai/v1/projects/{appid}/sessions`. | `start_ephemeral(agent, transport, values=None, labels=None, lifecycle=None, data_policy=None, idempotency_key=None)` | POST `/ephemeral` | `StartSessionResponse` | | `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`, `uid` and exactly one non-wildcard `subscribe_uids` entry. 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. Only RTC transport is exposed by these SDK methods. +Session requests follow the supplied OpenAPI plus the engineering reply corrections. `RtcSessionTransport` requires `type="rtc"`, `channel`, `uid` and exactly one non-wildcard `subscribe_uids` entry. 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. AgentKit session creation remains RTC-oriented; no high-level outbound-call helper is added. Stop-to-hangup/ringing behavior remains open. `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`. diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py index ba74008..bedae24 100644 --- a/src/agora_agent/v3/client.py +++ b/src/agora_agent/v3/client.py @@ -26,7 +26,6 @@ NumberDeleteResponse, NumberListResponse, NumberResponse, - RtcSessionTransport, SecretDeleted, SecretKey, SecretList, @@ -34,6 +33,7 @@ SessionLifecycle, SessionList, SessionResponse, + SessionTransport, SipTrunkInput, StartSessionResponse, ) @@ -179,10 +179,10 @@ def _NumbersClient(owner, options): return client class SessionsClient(Resource): - def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + def start(self, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: return self.with_raw_response.start(agent_id=agent_id, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options).data - def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: return self.with_raw_response.start_ephemeral(agent=agent, labels=labels, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, appid=appid, request_options=request_options).data def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: @@ -198,11 +198,11 @@ def get(self, *, appid: Optional[str] = None, agent_session_id: str, request_opt class SessionsRawClient(Resource): - def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + def start(self, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: segment(agent_id) 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, 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) - def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: 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, **({'labels': labels} if labels is not None else {}), 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: @@ -366,10 +366,10 @@ def _AsyncNumbersClient(owner, options): return client class AsyncSessionsClient(AsyncResource): - async def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + async def start(self, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: return (await self.with_raw_response.start(agent_id=agent_id, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options)).data - async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: + async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: return (await self.with_raw_response.start_ephemeral(agent=agent, labels=labels, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, appid=appid, request_options=request_options)).data async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: @@ -385,11 +385,11 @@ async def get(self, *, appid: Optional[str] = None, agent_session_id: str, reque class AsyncSessionsRawClient(AsyncResource): - async def start(self, *, agent_id: str, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + async def start(self, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: segment(agent_id) 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, 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) - async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: RtcSessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: 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, **({'labels': labels} if labels is not None else {}), 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) async def stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: diff --git a/src/agora_agent/v3/session_contract.py b/src/agora_agent/v3/session_contract.py index d63fcd2..22508bd 100644 --- a/src/agora_agent/v3/session_contract.py +++ b/src/agora_agent/v3/session_contract.py @@ -1,6 +1,7 @@ """Validation shared by public session requests and AgentKit's internal adapter.""" import math import re +import uuid def _object(value, allowed, field): @@ -16,31 +17,14 @@ def _integer(value, minimum, maximum, field): def validate_session(body): _object(body, {"agent_id", "agent", "transport", "values", "labels", "lifecycle", "data_policy"}, "session") transport = body["transport"] - _object(transport, {"type", "channel", "uid", "subscribe_uids", "token", "encryption", "audio_scenario"}, "transport") - if transport.get("type") != "rtc": - raise ValueError("Only RTC session transport is currently exposed") - 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 not isinstance(uid, str) or not uid.strip(): - 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 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 != "0" and numeric(uid) != numeric(subscribers[0]): - raise ValueError("agent and subscriber must use the same UID type") - 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") + 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") @@ -65,3 +49,64 @@ def validate_session(body): 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"}, "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 not isinstance(uid, str) or not uid.strip(): + 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 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 != "0" and numeric(uid) != numeric(subscribers[0]): + raise ValueError("agent and subscriber must use the same UID type") + 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"}, "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 index d1016d6..728132c 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -810,10 +810,10 @@ class AgentCreateRequest(TypedDict, total=False): __all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AgoraVadTurnDetection', 'AudioGreeting', 'DisabledInterruption', 'EndCallPolicy', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'GeneratedFillerContent', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JsonValue', 'KeywordInterruption', 'ManualTurnBoundary', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedMllm', 'ResolvedRealtimePipeline', 'ResolvedTts', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SemanticVadTurnDetection', 'ServerVadTurnDetection', 'SessionAnalysis', 'SessionData', 'SessionList', 'SessionResponse', 'SessionSummary', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SpeechInterruption', 'StaticFillerContent', 'TextGreeting', 'TransferPolicy', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_AudioGreeting', 'AgentCreateModel_ByokCredential', 'AgentCreateModel_CascadedPipeline', 'AgentCreateModel_DisabledInterruption', 'AgentCreateModel_FillerTrigger', 'AgentCreateModel_FillerWords', 'AgentCreateModel_GeneratedFillerContent', 'AgentCreateModel_JsonValue', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_ManagedCredential', 'AgentCreateModel_ManualTurnBoundary', 'AgentCreateModel_Message', 'AgentCreateModel_ProviderModule', 'AgentCreateModel_RealtimePipeline', 'AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_SemanticVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SilenceConfig', 'AgentCreateModel_SpeechInterruption', 'AgentCreateModel_StaticFillerContent', 'AgentCreateModel_TextGreeting', 'AgentCreateModel_TurnDetection', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_VariableDefinition', 'AgentCreateModel', 'AgentPatchRequest_CredentialPatch', 'AgentPatchRequest_FillerContentPatch', 'AgentPatchRequest_FillerTriggerPatch', 'AgentPatchRequest_FillerWordsPatch', 'AgentPatchRequest_GreetingPatch', 'AgentPatchRequest_InterruptionPatch', 'AgentPatchRequest_JsonValue', 'AgentPatchRequest_Message', 'AgentPatchRequest_PipelinePatch', 'AgentPatchRequest_PipelineTurnDetectionPatch', 'AgentPatchRequest_ProviderModulePatch', 'AgentPatchRequest_SilenceConfigPatch', 'AgentPatchRequest_TurnBoundaryPatch', 'AgentPatchRequest_VariableDefinitionPatch', 'AgentPatchRequest', 'Contract_VariableDefinitions', 'Contract_ModelCredential', 'Contract_Llm', 'Contract_CascadedPipeline', 'Contract_Pipeline', 'Contract_SemanticVadTurnDetection', 'Contract_SilenceConfig', 'Contract_ServerVadTurnDetection', 'Contract_McpServer', 'Contract_VariableDefinition', 'Contract_Greeting', 'Contract_Asr', 'Contract_FillerWords', 'Contract_Interruption', 'Contract_TurnStart', 'Contract_FunctionTool', 'Contract_Tts', 'Contract_RealtimePipeline', 'Contract_Mllm', 'Contract_Avatar', 'Contract_RealtimeTurnDetection', 'Contract_FunctionToolServer', 'Contract_TurnEnd', 'Contract_Message', 'Contract_AgentCreateInput', 'Contract_TurnDetection', 'Contract_AgoraVadTurnDetection', 'Contract_FunctionToolExecution', 'Contract_JsonSchema', 'AgentCreateRequest'] # Session write shapes from the refreshed Bruno collection (not the older OpenAPI). class RtcSessionTransport(TypedDict, total=False): - type: Literal['rtc'] - channel: str - uid: str - subscribe_uids: List[str] + type: Required[Literal['rtc']] + channel: Required[str] + uid: Required[str] + subscribe_uids: Required[List[str]] token: str encryption: RtcEncryption audio_scenario: Literal["aiserver", "chorus"] @@ -846,8 +846,8 @@ class StartSessionResponse(TypedDict, total=False): class RtcEncryption(TypedDict, total=False): - mode: 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: str + 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: str class GracefulStop(TypedDict, total=False): @@ -1009,3 +1009,25 @@ class TimeRange(TypedDict, total=False): end: Required[str] __all__ += ['CallingDay', 'CampaignCallPolicy', 'CampaignCounts', 'CampaignCreate', 'CampaignDeleteResponse', 'CampaignEndCallPolicy', 'CampaignFailure', 'CampaignLifecycle', 'CampaignListData', 'CampaignListResponse', 'CampaignPatch', 'CampaignResponse', 'CampaignTransferPolicy', 'CampaignTransport', 'CampaignTransportPatch', 'CampaignTransportResponse', 'CampaignVoicemailPolicy', 'ContactInput', 'ImmediateSchedule', 'Pacing', 'PacingPatch', 'ScheduledSchedule', 'TimeRange'] + + +class VoicemailPolicy(TypedDict): + mode: Literal["continue", "hangup"] + +class CallPolicy(TypedDict, total=False): + max_ring_duration_ms: Union[int, None] + max_call_duration_seconds: Union[int, None] + max_silence_duration_ms: Union[int, None] + voicemail: Union[VoicemailPolicy, None] + end_call: Union[EndCallPolicy, None] + transfer: Union[TransferPolicy, None] + +TelephonyTransport = TypedDict("TelephonyTransport", { + "type": Required[Literal["telephony"]], + "from": Required[str], + "to": Required[str], + "call_policy": Union[CallPolicy, None], +}, total=False) + +SessionTransport = Union[RtcSessionTransport, TelephonyTransport] +__all__ += ["VoicemailPolicy", "CallPolicy", "TelephonyTransport", "SessionTransport"] diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index f400fa2..2655f15 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1044,3 +1044,30 @@ def handler(request): 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) From bcb5986ea2e9eb951899e66c5eea11225a501e49 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:18:24 -0400 Subject: [PATCH 25/57] fix: preserve avatar credentials and reject credential-bearing origins --- src/agora_agent/agentkit/v3.py | 7 ++++++- src/agora_agent/v3/_transport.py | 2 +- tests/custom/test_v3_preview.py | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 341267c..6ed4ce1 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -97,8 +97,13 @@ def module(config, category, errors): if preset_model is not None: params.setdefault("model", preset_model) keys = [] + credential_fields = ["api_key", "key", "api_subscription_key"] + if category == "avatar" and config.get("vendor") == "sensetime": + credential_fields.append("app_key") + if category == "avatar" and config.get("vendor") == "spatius": + credential_fields.append("spatius_api_key") for source in (config, params): - for field in ("api_key", "key", "api_subscription_key"): + for field in credential_fields: if field in source: keys.append(source.pop(field)) multipart = {"aws_access_key_id", "aws_secret_access_key", "access_key", "secret_key", "adc_credentials_string", "service_account", "credentials"} diff --git a/src/agora_agent/v3/_transport.py b/src/agora_agent/v3/_transport.py index d97cc52..d915577 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -74,7 +74,7 @@ def _arguments(self, method, path, body, headers, params, request_options): 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: + 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("/") merged = dict(self.options.headers) diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 2655f15..4a322da 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1071,3 +1071,39 @@ def test_lowlevel_telephony_rejects_invalid_source_destination_and_policy(transp 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) + agent.save(name="Avatar") + body = json.loads(calls[0].content)["pipeline"]["avatar"] + assert body["vendor"] == vendor + assert body["credential"] == {"mode": "byok", "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 From ca74b7b41646b7abf6a9f71aca49154c3f933327 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:21:31 -0400 Subject: [PATCH 26/57] fix: sign token-backed legacy requests from app credentials --- .fernignore | 1 + src/agora_agent/core/client_wrapper.py | 5 +++-- src/agora_agent/pool_client.py | 8 ++++++++ tests/custom/test_v3_preview.py | 26 ++++++++++++++++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.fernignore b/.fernignore index f421542..f67c441 100644 --- a/.fernignore +++ b/.fernignore @@ -39,3 +39,4 @@ src/agora_agent/v3/ 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 diff --git a/src/agora_agent/core/client_wrapper.py b/src/agora_agent/core/client_wrapper.py index ad3aa18..93ad9aa 100644 --- a/src/agora_agent/core/client_wrapper.py +++ b/src/agora_agent/core/client_wrapper.py @@ -18,6 +18,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 @@ -32,8 +33,8 @@ def get_headers(self) -> typing.Dict[str, str]: "X-Fern-SDK-Version": "v2.12.0", **(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/pool_client.py b/src/agora_agent/pool_client.py index f3026a2..8b3d36d 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -346,6 +346,10 @@ def __init__( follow_redirects=follow_redirects, httpx_client=httpx_client, ) + if auth_token is None: + self._client_wrapper._authorization_supplier = lambda: "agora token=" + generate_convo_ai_token( + app_id=self.app_id, app_certificate=self.app_certificate, channel_name="", uid=0, + ) @property def v3(self) -> V3Client: @@ -678,6 +682,10 @@ def __init__( follow_redirects=follow_redirects, httpx_client=httpx_client, ) + if auth_token is None: + self._client_wrapper._authorization_supplier = lambda: "agora token=" + generate_convo_ai_token( + app_id=self.app_id, app_certificate=self.app_certificate, channel_name="", uid=0, + ) @property def v3(self) -> AsyncV3Client: diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 4a322da..0141d2b 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -373,12 +373,12 @@ 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="project", app_certificate="cert", + 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"] == "project" + assert generated[0]["app_id"] == "0" * 32 def test_realtime_instructions_and_audio_greeting_mapping(): @@ -1107,3 +1107,25 @@ async def test_public_origin_rejects_embedded_credentials(async_, 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"] From 2acc4678babdccfb0a95c8138396f5c7098fa5a6 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:28:27 -0400 Subject: [PATCH 27/57] fix: complete RTC auth parity and unconditional conflict reloads --- docs/getting-started/authentication.md | 5 +- docs/guides/agent-workflow.md | 4 +- docs/guides/v3-api-preview.md | 2 +- docs/reference/client.md | 4 +- docs/reference/v3.md | 2 +- src/agora_agent/agentkit/agent.py | 195 +++++++++++++------- src/agora_agent/agentkit/session_backend.py | 10 +- src/agora_agent/agentkit/v3.py | 8 +- src/agora_agent/core/http_client.py | 27 ++- src/agora_agent/core/public_auth.py | 7 +- src/agora_agent/pool_client.py | 44 +++-- src/agora_agent/v3/_transport.py | 2 + src/agora_agent/v3/session_contract.py | 10 +- src/agora_agent/v3/types.py | 8 + tests/custom/test_v3_preview.py | 100 ++++++++++ 15 files changed, 322 insertions(+), 106 deletions(-) diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md index 0717524..9efae3c 100644 --- a/docs/getting-started/authentication.md +++ b/docs/getting-started/authentication.md @@ -62,10 +62,11 @@ Pass the raw token string. The SDK sets `Authorization: agora token= client = Agora( area=Area.US, app_id="your-app-id", - app_certificate="your-app-certificate", - auth_token="your-raw-token", + auth_token="your-raw-token", # No certificate needed for prebuilt REST auth. ) print(client.auth_mode) # "token" ``` New resources use `https://preview.ai.agora.io/conversational-ai/v1/projects/{appid}`. Do not send internal gateway headers or `X-Studio-Region`. + +A client with only a REST `auth_token` 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`. diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index fc6ff0e..bd91fc0 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -83,7 +83,7 @@ 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` and `idempotency_key` work for both saved and ephemeral sessions. Exactly one subscriber is required; wildcard subscriptions are unsupported. Agent/subscriber UID types must match. The agreed contract permits string UIDs; numeric-only validation may remain during backend rollout. +`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 subscriber is required; wildcard subscriptions are unsupported. 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. @@ -126,3 +126,5 @@ Application code does not select a REST version. Ordinary inline configurations ## 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/v3-api-preview.md b/docs/guides/v3-api-preview.md index 91b4b5f..84620f2 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -134,7 +134,7 @@ for number in client.numbers.iter_all(limit=100): `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 per-request authentication/context headers while stripping per-request `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. +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 diff --git a/docs/reference/client.md b/docs/reference/client.md index 51418be..032cbc1 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -27,7 +27,7 @@ client = Agora( |---|---|---|---|---| | `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) | +| `app_certificate` | `str` | Unless `auth_token` supplied | `None` | Agora App Certificate for signing | | `auth_token` | `str` | No | — | Pre-built raw REST token; SDK sets `Authorization: agora token=` | | `headers` | `Dict[str, str]` | No | `None` | Additional headers sent with every request | | `timeout` | `float` | No | `60` | Request timeout in seconds | @@ -36,7 +36,7 @@ client = Agora( | `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) | -`app_id` and `app_certificate` remain required constructor arguments. Omit `auth_token` to sign REST tokens automatically, or supply a raw prebuilt token. Basic credentials are not exposed. +`app_id` is required; `app_certificate` is required only when `auth_token` is omitted. Omit `auth_token` to sign REST tokens automatically, or supply a raw prebuilt token. Basic credentials are not exposed. 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. diff --git a/docs/reference/v3.md b/docs/reference/v3.md index c8e21be..0e78172 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -166,7 +166,7 @@ The `max_*` assignment options, save `name` / `agent_id`, and update `name` are ### Session handles -`SavedAgent.create_session(*, channel, agent_uid, remote_uids, 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, 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`. +`SavedAgent.create_session(*, channel, agent_uid, remote_uids, 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 diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index a14a8ca..b166eb0 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -2,92 +2,149 @@ import time import typing -import typing_extensions import warnings +import typing_extensions + if typing.TYPE_CHECKING: - from .agent_session import AgentSession, AsyncAgentSession - from .v3 import DeployedAgent, AsyncDeployedAgent, SavedAgent, AsyncSavedAgent 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_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_interruption import StartAgentsRequestPropertiesInterruption -from ..agents.types.start_agents_request_properties_interruption_mode import StartAgentsRequestPropertiesInterruptionMode +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_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 ..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_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 ..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 @@ -258,7 +315,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", @@ -911,6 +968,7 @@ def create_session( 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. @@ -925,15 +983,14 @@ 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 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)): + if 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, + 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 @@ -983,6 +1040,7 @@ def create_async_session( 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`. @@ -991,15 +1049,14 @@ 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 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)): + if 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, + 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 diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 09b1b86..9e3f430 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -21,10 +21,10 @@ def contains(value): def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids, 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, debug=None, warn=None, **unsupported): + audio_scenario=None, graceful_stop=None, data_policy=None, geofence=None, debug=None, warn=None, **unsupported): from ..pool_client import Agora, AsyncAgora from .agent_session import AgentSession, AsyncAgentSession - from .v3 import build_body + 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") @@ -44,6 +44,10 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids body = {"transport": {"type": "rtc", "channel": channel, "uid": agent_uid, "subscribe_uids": list(remote_uids)}} reference = name + 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) @@ -64,9 +68,9 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids if stored: body["agent_id"] = owner.id else: - from .v3 import dump params = dump(owner._parameters or {}) inline = owner._clone() + inline._geofence = 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") diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 6ed4ce1..7c25ca9 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -457,13 +457,13 @@ class SavedAgent(_SavedAgentBase): def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], 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, debug=None, warn=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, + audio_scenario=audio_scenario, graceful_stop=graceful_stop, data_policy=data_policy, geofence=geofence, debug=debug, warn=warn) @classmethod @@ -530,14 +530,14 @@ class AsyncSavedAgent(_SavedAgentBase): def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], 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, debug=None, warn=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, debug=debug, warn=warn) + data_policy=data_policy, geofence=geofence, debug=debug, warn=warn) create_async_session = create_session diff --git a/src/agora_agent/core/http_client.py b/src/agora_agent/core/http_client.py index d47f337..384fb5b 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -218,7 +218,7 @@ 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( @@ -431,7 +431,7 @@ 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( @@ -570,3 +570,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 index 999ec97..26be1ac 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -1,12 +1,15 @@ """Token-only public SDK authentication policy (including transport overrides).""" +import re def validate_public_headers(headers): - for name, value in (headers or {}).items(): + 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 == "authorization" and value and not str(value).lower().startswith("agora token="): + if lower == "authorization" and value and not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE): raise ValueError("Only Agora token authentication is supported") diff --git a/src/agora_agent/pool_client.py b/src/agora_agent/pool_client.py index 8b3d36d..1ab2479 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -199,7 +199,7 @@ 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -215,7 +215,7 @@ def __new__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -231,7 +231,7 @@ def __new__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -256,7 +256,7 @@ 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -272,7 +272,7 @@ def __init__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -288,7 +288,7 @@ def __init__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -303,7 +303,7 @@ def __init__( *, area: typing.Any, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -314,6 +314,10 @@ def __init__( ): from .core.public_auth import validate_public_headers validate_public_headers(headers) + if auth_token is not None and (not isinstance(auth_token, str) or not auth_token.strip()): + raise ValueError("auth_token must be a nonempty raw token") + if auth_token is None and (not isinstance(app_certificate, str) or not app_certificate.strip()): + raise ValueError("app_certificate is required unless auth_token is supplied") self._v3_options = v3 self._pool = Pool(area) self._area = area @@ -348,7 +352,7 @@ def __init__( ) if auth_token is None: self._client_wrapper._authorization_supplier = lambda: "agora token=" + generate_convo_ai_token( - app_id=self.app_id, app_certificate=self.app_certificate, channel_name="", uid=0, + app_id=self.app_id, app_certificate=typing.cast(str, self.app_certificate), channel_name="", uid=0, ) @property @@ -447,7 +451,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, ) @@ -535,7 +539,7 @@ 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -551,7 +555,7 @@ def __new__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -567,7 +571,7 @@ def __new__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -592,7 +596,7 @@ 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -608,7 +612,7 @@ def __init__( *, area: _GlobalArea, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -624,7 +628,7 @@ def __init__( *, area: _AreaT, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -639,7 +643,7 @@ def __init__( *, area: typing.Any, app_id: str, - app_certificate: str, + app_certificate: typing.Optional[str] = None, auth_token: typing.Optional[str] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -650,6 +654,10 @@ def __init__( ): from .core.public_auth import validate_public_headers validate_public_headers(headers) + if auth_token is not None and (not isinstance(auth_token, str) or not auth_token.strip()): + raise ValueError("auth_token must be a nonempty raw token") + if auth_token is None and (not isinstance(app_certificate, str) or not app_certificate.strip()): + raise ValueError("app_certificate is required unless auth_token is supplied") self._v3_options = v3 self._pool = Pool(area) self._area = area @@ -684,7 +692,7 @@ def __init__( ) if auth_token is None: self._client_wrapper._authorization_supplier = lambda: "agora token=" + generate_convo_ai_token( - app_id=self.app_id, app_certificate=self.app_certificate, channel_name="", uid=0, + app_id=self.app_id, app_certificate=typing.cast(str, self.app_certificate), channel_name="", uid=0, ) @property @@ -783,7 +791,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/_transport.py b/src/agora_agent/v3/_transport.py index d915577..178833f 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -77,6 +77,7 @@ def _arguments(self, method, path, body, headers, params, request_options): 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": @@ -148,6 +149,7 @@ def iter_all(self, **kwargs): 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 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"} diff --git a/src/agora_agent/v3/session_contract.py b/src/agora_agent/v3/session_contract.py index 22508bd..27f782e 100644 --- a/src/agora_agent/v3/session_contract.py +++ b/src/agora_agent/v3/session_contract.py @@ -52,7 +52,7 @@ def validate_session(body): def _validate_rtc_transport(transport): - _object(transport, {"type", "channel", "uid", "subscribe_uids", "token", "encryption", "audio_scenario"}, "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") @@ -64,6 +64,14 @@ def _validate_rtc_transport(transport): numeric = lambda value: re.fullmatch(r"[0-9]+", value) is not None if 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"}: diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index 728132c..6ebd287 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -816,6 +816,7 @@ class RtcSessionTransport(TypedDict, total=False): subscribe_uids: Required[List[str]] token: str encryption: RtcEncryption + geofence: RtcGeofence audio_scenario: Literal["aiserver", "chorus"] @@ -1031,3 +1032,10 @@ class CallPolicy(TypedDict, total=False): SessionTransport = Union[RtcSessionTransport, TelephonyTransport] __all__ += ["VoicemailPolicy", "CallPolicy", "TelephonyTransport", "SessionTransport"] + + +class RtcGeofence(TypedDict, total=False): + area: Required[Literal["global", "north_america", "europe", "asia", "india", "japan"]] + exclude_area: Literal["north_america", "europe", "asia", "india", "japan"] + +__all__ += ["RtcGeofence"] diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 0141d2b..281bee3 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1129,3 +1129,103 @@ def sign(**kwargs): 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" From 057eb79c70a2c63ae11a38181aa9720929e676f5 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:30:19 -0400 Subject: [PATCH 28/57] fix: preserve non-object session stop conflict errors --- src/agora_agent/agentkit/agent_session.py | 6 ++++-- tests/custom/test_v3_preview.py | 24 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 66d0960..893260a 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -716,7 +716,8 @@ def stop(self) -> None: self._client.v3.sessions.stop(agent_session_id=self._agent_id) except Exception as error: if getattr(error, "status_code", None) == 409: - reason = (getattr(error, "body", None) or {}).get("reason") + 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": @@ -1147,7 +1148,8 @@ async def stop(self) -> None: await self._client.v3.sessions.stop(agent_session_id=self._agent_id) except Exception as error: if getattr(error, "status_code", None) == 409: - reason = (getattr(error, "body", None) or {}).get("reason") + 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": diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 281bee3..41ca241 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1229,3 +1229,27 @@ def handler(request): 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" From 5e758ebf307574d4e3b151b8663da42506adaf66 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:33:43 -0400 Subject: [PATCH 29/57] fix: reject zero subscriber UIDs and note current preview rollout --- changelog.md | 4 ++++ docs/guides/agent-workflow.md | 2 +- docs/reference/v3.md | 4 ++-- src/agora_agent/v3/session_contract.py | 2 +- tests/custom/test_v3_preview.py | 12 ++++++++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 7542307..8885d50 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## Unreleased 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.12.0] — 2026-10-07 ### Added diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index bd91fc0..ba6c153 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -83,7 +83,7 @@ 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 subscriber is required; wildcard subscriptions are unsupported. Agent/subscriber UID types must match. The agreed contract permits string UIDs; numeric-only validation may remain during backend rollout. +`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. diff --git a/docs/reference/v3.md b/docs/reference/v3.md index 0e78172..05e237b 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -112,7 +112,7 @@ Prefix: `/conversational-ai/v1/projects/{appid}/sessions`. | `start_ephemeral(agent, transport, values=None, labels=None, lifecycle=None, data_policy=None, idempotency_key=None)` | POST `/ephemeral` | `StartSessionResponse` | | `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`, `uid` and exactly one non-wildcard `subscribe_uids` entry. 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. AgentKit session creation remains RTC-oriented; no high-level outbound-call helper is added. Stop-to-hangup/ringing behavior remains open. +Session requests follow the supplied OpenAPI plus the engineering reply corrections. `RtcSessionTransport` requires `type="rtc"`, `channel`, `uid` and exactly one non-wildcard, nonzero `subscribe_uids` entry; agent UID `0` alone 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. AgentKit session creation remains RTC-oriented; no high-level outbound-call helper is added. Stop-to-hangup/ringing behavior remains open. `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`. @@ -141,7 +141,7 @@ All four resources offer `list()`, `iter_pages(**list_arguments)` and `iter_all( 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 v3-only origin and headers. Origin precedence is explicit option, `AGORA_AGENTS_V3_BASE_URL`, then the origin of the existing regional pool. Only HTTP(S) origins with no path beyond `/`, query or fragment are accepted. The adapter shares HTTP clients, auth and timeouts with the owning `Agora` / `AsyncAgora`. It does not establish production or regional backend availability. +`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 diff --git a/src/agora_agent/v3/session_contract.py b/src/agora_agent/v3/session_contract.py index 27f782e..c3e431f 100644 --- a/src/agora_agent/v3/session_contract.py +++ b/src/agora_agent/v3/session_contract.py @@ -59,7 +59,7 @@ def _validate_rtc_transport(transport): subscribers = transport.get("subscribe_uids") if not isinstance(uid, str) or not uid.strip(): 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 subscribers[0] == "*": + if not isinstance(subscribers, list) or len(subscribers) != 1 or not isinstance(subscribers[0], str) or not subscribers[0].strip() 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 != "0" and numeric(uid) != numeric(subscribers[0]): diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 41ca241..409f34d 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1253,3 +1253,15 @@ def handler(request): 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 From 5e2d35f5621d96ac5248890d76626d50d18b4451 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 07:34:46 -0400 Subject: [PATCH 30/57] fix: reject explicit blank session and campaign idempotency keys --- src/agora_agent/agentkit/session_backend.py | 2 ++ src/agora_agent/core/public_auth.py | 2 ++ tests/custom/test_v3_preview.py | 27 +++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 9e3f430..4c825ec 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -30,6 +30,8 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids raise TypeError("Session requires AsyncAgora" if asynchronous else "Session requires Agora") if getattr(client, "area_scope", None) == "cn": raise ValueError("Stored and ephemeral sessions in CN are unconfirmed") + if idempotency_key is not None and (not isinstance(idempotency_key, str) or not idempotency_key.strip()): + raise ValueError("idempotency_key must be a nonempty string") 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] diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py index 26be1ac..06bc231 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -9,6 +9,8 @@ def validate_public_headers(headers): 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 not value.strip()): + raise ValueError("Idempotency-Key must be a nonempty string") if lower == "authorization" and value and not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE): raise ValueError("Only Agora token authentication is supported") diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 409f34d..aa0942a 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1265,3 +1265,30 @@ async def test_subscriber_zero_rejected_even_when_agent_zero_is_allowed(async_, 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) From f747b40e45aeccaeff19631bd74c2c23213d3a98 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 08:01:41 -0400 Subject: [PATCH 31/57] fix: harden v3 session and transport edge cases --- src/agora_agent/agentkit/agent_session.py | 57 +++++++- src/agora_agent/agentkit/session_backend.py | 3 + src/agora_agent/core/http_client.py | 11 +- src/agora_agent/core/public_auth.py | 4 +- src/agora_agent/v3/_transport.py | 7 + tests/custom/test_v3_preview.py | 154 +++++++++++++++++++- 6 files changed, 221 insertions(+), 15 deletions(-) diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 893260a..7cf9b37 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -133,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]] ] = {} @@ -605,6 +610,9 @@ def start(self) -> str: 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 @@ -615,10 +623,14 @@ def start(self) -> str: raise RuntimeError("Session response is missing agent_session_id") self._agent_id = identifier 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 Exception as error: + 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"): @@ -711,10 +723,12 @@ def stop(self) -> None: 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 Exception as error: + 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 @@ -726,9 +740,13 @@ def stop(self) -> None: 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": @@ -894,8 +912,13 @@ def get_info(self) -> typing.Any: if self._session_backend is not None: if not self._agent_id: raise RuntimeError("No session ID available") - response = self._client.v3.sessions.get(agent_session_id=self._agent_id) - self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() + 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") @@ -1037,6 +1060,9 @@ async def start(self) -> str: 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 @@ -1047,10 +1073,14 @@ async def start(self) -> str: raise RuntimeError("Session response is missing agent_session_id") self._agent_id = identifier 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 Exception as error: + 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"): @@ -1143,10 +1173,12 @@ async def stop(self) -> None: 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 Exception as error: + 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 @@ -1158,9 +1190,13 @@ async def stop(self) -> None: 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": @@ -1326,8 +1362,13 @@ async def get_info(self) -> typing.Any: if self._session_backend is not None: if not self._agent_id: raise RuntimeError("No session ID available") - response = await self._client.v3.sessions.get(agent_session_id=self._agent_id) - self._status = "error" if str(response["status"]).upper() == "FAILED" else str(response["status"]).lower() + 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") diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 4c825ec..253e130 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -45,6 +45,8 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids raise ValueError("remote_uids must be a list of nonempty strings") body = {"transport": {"type": "rtc", "channel": channel, "uid": agent_uid, "subscribe_uids": list(remote_uids)}} + if labels is None: + labels = getattr(owner, "_labels", None) reference = name fence = geofence if geofence is not None else getattr(owner, "_geofence", None) if fence is not None: @@ -73,6 +75,7 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids 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") diff --git a/src/agora_agent/core/http_client.py b/src/agora_agent/core/http_client.py index 384fb5b..39d433e 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -194,7 +194,10 @@ def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: validate_public_options(request_options) - validate_public_headers(self.base_headers()) + # 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. + validate_public_headers(self.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", {})) @@ -298,7 +301,7 @@ def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: validate_public_options(request_options) - validate_public_headers(self.base_headers()) + validate_public_headers(self.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", {})) @@ -406,7 +409,7 @@ async def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: validate_public_options(request_options) - validate_public_headers(self.base_headers()) + validate_public_headers(self.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", {})) @@ -510,7 +513,7 @@ async def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: validate_public_options(request_options) - validate_public_headers(self.base_headers()) + validate_public_headers(self.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", {})) diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py index 06bc231..213ae8d 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -2,7 +2,7 @@ import re -def validate_public_headers(headers): +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: @@ -11,7 +11,7 @@ def validate_public_headers(headers): 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 not value.strip()): raise ValueError("Idempotency-Key must be a nonempty string") - if lower == "authorization" and value and not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE): + if lower == "authorization" and value is not None and not (allow_empty_authorization and value == "") and not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE): raise ValueError("Only Agora token authentication is supported") diff --git a/src/agora_agent/v3/_transport.py b/src/agora_agent/v3/_transport.py index 178833f..91c6bfa 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -47,6 +47,10 @@ def latest(self): 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="") @@ -150,6 +154,9 @@ 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"} diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index aa0942a..366fac4 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1,5 +1,7 @@ """Offline contract tests for the additive v3 preview and v2 boundary.""" +import asyncio import json +import threading import httpx import pytest @@ -121,7 +123,7 @@ def test_namespace_is_additive_and_out_of_scope_features_absent(): 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": "a", "appid": ""}): + 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): @@ -142,6 +144,31 @@ def handler(request): 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": {"unexpected": "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)["unexpected"] == "write-only" + assert calls[1].method == "GET" + assert calls[1].content == b"" + + @pytest.mark.parametrize("resource,kwargs", [("agents", {"agent_name": "x"}), ("numbers", {"number": "+1", "sip_trunk": {}})]) def test_nonidempotent_create_is_never_retried(resource, kwargs): calls = [] @@ -855,6 +882,81 @@ def handler(request): 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.parametrize("async_", [False, True]) @pytest.mark.parametrize("reason,expected", [("TaskNotRunning", "stopped"), ("TaskConflict", "stopping"), ("Other", "error")]) @pytest.mark.asyncio @@ -875,6 +977,35 @@ def handler(request): 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_): @@ -915,6 +1046,20 @@ def test_forbidden_headers_rejected_in_all_new_transport_layers(header, value): 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) + + @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 @@ -943,6 +1088,13 @@ def test_builder_retention_farewell_mapping_uses_session_envelope(): 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 From 52ea4f462e64e325f116e0bb553c4a81d006f005 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 08:02:12 -0400 Subject: [PATCH 32/57] test: cover concurrent v3 session lifecycle reads --- tests/custom/test_v3_preview.py | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 366fac4..4cf574a 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -957,6 +957,77 @@ async def handler(request): 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 From 2acbd2a16658e74450311efd281894ef3c13bd4d Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 09:18:57 -0400 Subject: [PATCH 33/57] fix: validate effective token headers on raw clients --- src/agora_agent/agentkit/preview/client.py | 2 + src/agora_agent/core/http_client.py | 83 +++++++++++----------- src/agora_agent/core/public_auth.py | 18 +++++ tests/custom/test_gemini_tts.py | 1 + tests/custom/test_v3_preview.py | 8 +++ 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/agora_agent/agentkit/preview/client.py b/src/agora_agent/agentkit/preview/client.py index e917b1c..a6dfae3 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/core/http_client.py b/src/agora_agent/core/http_client.py index 39d433e..272af95 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -13,7 +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 validate_public_headers, validate_public_options +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 @@ -197,12 +202,19 @@ def request( # 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. - validate_public_headers(self.base_headers(), allow_empty_authorization=True) + 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("HTTP Basic auth overrides are not supported") + 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") @@ -224,15 +236,7 @@ def 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( @@ -301,12 +305,19 @@ def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: validate_public_options(request_options) - validate_public_headers(self.base_headers(), allow_empty_authorization=True) + 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("HTTP Basic auth overrides are not supported") + 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") @@ -328,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( @@ -409,12 +412,19 @@ async def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: validate_public_options(request_options) - validate_public_headers(self.base_headers(), allow_empty_authorization=True) + 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("HTTP Basic auth overrides are not supported") + 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") @@ -437,15 +447,7 @@ async def 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( @@ -513,12 +515,19 @@ async def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: validate_public_options(request_options) - validate_public_headers(self.base_headers(), allow_empty_authorization=True) + 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("HTTP Basic auth overrides are not supported") + 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") @@ -540,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( diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py index 213ae8d..c32f9f1 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -20,3 +20,21 @@ def validate_public_options(options): if forbidden.intersection(options or {}): raise ValueError("Basic credential overrides are not supported") validate_public_headers((options or {}).get("additional_headers", {})) + + +def validate_effective_public_headers(*sources): + """Validate the case-insensitive merged headers that will reach HTTPX.""" + validate_public_headers(merge_public_headers(*sources)) + + +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/tests/custom/test_gemini_tts.py b/tests/custom/test_gemini_tts.py index c3a491a..6478334 100644 --- a/tests/custom/test_gemini_tts.py +++ b/tests/custom/test_gemini_tts.py @@ -117,6 +117,7 @@ def test_preview_gate_survives_raw_per_call_headers(header): session.raw.get("0" * 32, "agent-1", request_options={"additional_headers": headers}) for req in requests: assert req.headers.get_list("agora-feature") == ["gemini-live"] + assert req.headers["Authorization"].startswith("agora token=") assert requests[-1].headers["x-custom"] == "preserved" assert headers[header] == "" assert client._client_wrapper.get_custom_headers()[header] == "caller-value" diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 4cf574a..9a1da06 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1129,6 +1129,14 @@ def test_blank_authorization_overrides_rejected_before_request(): 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("resource,kwargs", [("secrets", {"name": "s", "secrets": {"k": "v"}}), ("numbers", {"number": "+12025550123", "sip_trunk": {"hostname": "sip.example"}})]) From b1aff347d293abb488fed4a3020ae4267043332c Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 09:21:42 -0400 Subject: [PATCH 34/57] fix: require effective token authorization on requests --- src/agora_agent/core/public_auth.py | 5 ++++- tests/custom/test_v3_preview.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py index c32f9f1..cc1d086 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -24,7 +24,10 @@ def validate_public_options(options): def validate_effective_public_headers(*sources): """Validate the case-insensitive merged headers that will reach HTTPX.""" - validate_public_headers(merge_public_headers(*sources)) + merged = merge_public_headers(*sources) + validate_public_headers(merged) + if not any(name.lower() == "authorization" for name in merged): + raise ValueError("Agora token authorization is required") def merge_public_headers(*sources): diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 9a1da06..c4a8d84 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1139,6 +1139,26 @@ def test_blank_authorization_overrides_rejected_before_request(): 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 From bdf40b51f1905781da737162798494088f9c5b68 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 09:23:11 -0400 Subject: [PATCH 35/57] fix: reject custom HTTP authentication handlers --- src/agora_agent/core/http_client.py | 16 ++++++++-------- tests/custom/test_v3_preview.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/agora_agent/core/http_client.py b/src/agora_agent/core/http_client.py index 272af95..584615a 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -207,8 +207,8 @@ def request( 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("HTTP Basic auth overrides are not supported") + if getattr(transport_client, "auth", None) is not None: + raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") request_headers = merge_public_headers( base_headers, headers or {}, @@ -310,8 +310,8 @@ def stream( 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("HTTP Basic auth overrides are not supported") + if getattr(transport_client, "auth", None) is not None: + raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") request_headers = merge_public_headers( base_headers, headers or {}, @@ -417,8 +417,8 @@ async def request( 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("HTTP Basic auth overrides are not supported") + if getattr(transport_client, "auth", None) is not None: + raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") request_headers = merge_public_headers( base_headers, headers or {}, @@ -520,8 +520,8 @@ async def stream( 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("HTTP Basic auth overrides are not supported") + if getattr(transport_client, "auth", None) is not None: + raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") request_headers = merge_public_headers( base_headers, headers or {}, diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index c4a8d84..7123f48 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1195,10 +1195,16 @@ def test_builder_labels_map_to_new_session_labels(): @pytest.mark.parametrize("async_", [False, True]) -@pytest.mark.parametrize("override", ["header", "basic_client", "credentials"]) +@pytest.mark.parametrize("override", ["header", "basic_client", "custom_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 {})) + class BearerAuth(httpx.Auth): + def auth_flow(self, request): + request.headers["Authorization"] = "Bearer injected" + yield request + + auth = httpx.BasicAuth("id", "secret") if override == "basic_client" else BearerAuth() if override == "custom_client" else None + http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(lambda _: pytest.fail("must not send")), **({"auth": auth} if auth is not None 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): From 434843ce73afd93c54d572aeed308e38dbec68fc Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 09:23:34 -0400 Subject: [PATCH 36/57] Revert "fix: reject custom HTTP authentication handlers" This reverts commit bdf40b51f1905781da737162798494088f9c5b68. --- src/agora_agent/core/http_client.py | 16 ++++++++-------- tests/custom/test_v3_preview.py | 10 ++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/agora_agent/core/http_client.py b/src/agora_agent/core/http_client.py index 584615a..272af95 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -207,8 +207,8 @@ def request( validate_public_headers(headers) transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) validate_public_headers(getattr(transport_client, "headers", {})) - if getattr(transport_client, "auth", None) is not None: - raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("HTTP Basic auth overrides are not supported") request_headers = merge_public_headers( base_headers, headers or {}, @@ -310,8 +310,8 @@ def stream( validate_public_headers(headers) transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) validate_public_headers(getattr(transport_client, "headers", {})) - if getattr(transport_client, "auth", None) is not None: - raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("HTTP Basic auth overrides are not supported") request_headers = merge_public_headers( base_headers, headers or {}, @@ -417,8 +417,8 @@ async def request( validate_public_headers(headers) transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) validate_public_headers(getattr(transport_client, "headers", {})) - if getattr(transport_client, "auth", None) is not None: - raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("HTTP Basic auth overrides are not supported") request_headers = merge_public_headers( base_headers, headers or {}, @@ -520,8 +520,8 @@ async def stream( validate_public_headers(headers) transport_client = getattr(self.httpx_client, "_inner", self.httpx_client) validate_public_headers(getattr(transport_client, "headers", {})) - if getattr(transport_client, "auth", None) is not None: - raise ValueError("HTTP auth overrides are not supported; use Agora token authorization") + if isinstance(getattr(transport_client, "auth", None), httpx.BasicAuth): + raise ValueError("HTTP Basic auth overrides are not supported") request_headers = merge_public_headers( base_headers, headers or {}, diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index 7123f48..c4a8d84 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1195,16 +1195,10 @@ def test_builder_labels_map_to_new_session_labels(): @pytest.mark.parametrize("async_", [False, True]) -@pytest.mark.parametrize("override", ["header", "basic_client", "custom_client", "credentials"]) +@pytest.mark.parametrize("override", ["header", "basic_client", "credentials"]) @pytest.mark.asyncio async def test_legacy_resource_token_policy_at_http_boundary(async_, override): - class BearerAuth(httpx.Auth): - def auth_flow(self, request): - request.headers["Authorization"] = "Bearer injected" - yield request - - auth = httpx.BasicAuth("id", "secret") if override == "basic_client" else BearerAuth() if override == "custom_client" else None - http = (httpx.AsyncClient if async_ else httpx.Client)(transport=httpx.MockTransport(lambda _: pytest.fail("must not send")), **({"auth": auth} if auth is not None else {})) + 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): From c06aa28d92ec29075b495e8dbf228e8629f36f5b Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 15:04:20 -0400 Subject: [PATCH 37/57] Align preview SDK with corrected shared release contract --- .fernignore | 4 + README.md | 4 +- changelog.md | 7 + docs/guides/agent-workflow.md | 20 +- docs/guides/v3-api-preview.md | 22 +- docs/reference/v3.md | 37 +- openapi.json | 15887 ++++++++++++++++++ openapi.yaml | 9596 +++++++++++ scripts/generate-v3-preview.py | 178 + src/agora_agent/agentkit/agent.py | 16 +- src/agora_agent/agentkit/agent_session.py | 12 +- src/agora_agent/agentkit/session_backend.py | 48 +- src/agora_agent/agentkit/v3.py | 84 +- src/agora_agent/core/public_auth.py | 4 +- src/agora_agent/v3/__init__.py | 2 +- src/agora_agent/v3/_transport.py | 6 + src/agora_agent/v3/campaigns.py | 136 +- src/agora_agent/v3/client.py | 544 +- src/agora_agent/v3/contract.py | 153 + src/agora_agent/v3/openapi.json | 15887 ++++++++++++++++++ src/agora_agent/v3/session_contract.py | 8 +- src/agora_agent/v3/types.py | 1358 +- tests/custom/test_v3_latest_contract.py | 157 + tests/custom/test_v3_preview.py | 64 +- 24 files changed, 43073 insertions(+), 1161 deletions(-) create mode 100644 openapi.json create mode 100644 openapi.yaml create mode 100644 scripts/generate-v3-preview.py create mode 100644 src/agora_agent/v3/contract.py create mode 100644 src/agora_agent/v3/openapi.json create mode 100644 tests/custom/test_v3_latest_contract.py diff --git a/.fernignore b/.fernignore index f67c441..673a106 100644 --- a/.fernignore +++ b/.fernignore @@ -40,3 +40,7 @@ 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/README.md b/README.md index 3361af2..7c48810 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ 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_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 `secret_ref()` credentials and immutable `with_variable()` definitions for saved configurations. +Use one `Agent` builder to configure providers, then call `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. See the [daily agent workflow](docs/guides/agent-workflow.md), [persistence transport guide](docs/guides/v3-api-preview.md), and [advanced 31-operation reference](docs/reference/v3.md) for ETags, cursor pagination, session lifecycle and project queries. `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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. +Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. ## Install diff --git a/changelog.md b/changelog.md index 8885d50..1a79b9d 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## Unreleased preview contract alignment + +- Shared root OpenAPI JSON/YAML, local reproducible wire generator, sync/async/raw session PATCH, and restored realtime MLLM contracts. +- 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. + ## Unreleased 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. diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index ba6c153..93fe687 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -6,25 +6,27 @@ Saving and project secrets/numbers require this preview checkout, installed with ```python import os -from agora_agent import Agent, DeepgramSTT, OpenAI, OpenAITTS, SavedAgent +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=os.environ["DEEPGRAM_API_KEY"], model="nova-3")) + .with_stt(DeepgramSTT(api_key=secret_ref("support", "deepgram"), model="nova-3")) .with_llm(OpenAI( - api_key=os.environ["OPENAI_API_KEY"], model="gpt-4o-mini", + 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=os.environ["OPENAI_API_KEY"], model="tts-1", voice="alloy", + 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=["*"]) +session = agent.create_session(channel="room", agent_uid="1", remote_uids=["2"]) session_id = session.start() # session.stop() @@ -63,6 +65,8 @@ referenced = agent.with_llm(OpenAI( 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. @@ -96,9 +100,9 @@ session = saved.create_session( ) ``` -Retention `none` disables stored conversation data and post-call extraction; operational-data exceptions remain open. 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. +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. -Only start, stop and `get_info` are available for new session IDs; say/interrupt/think/update/history/turn helpers fail locally. The proposed session PATCH/action endpoints are not implemented. 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. +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; 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 @@ -106,7 +110,7 @@ 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=["*"]) + 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")) diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 84620f2..6a6b486 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -1,6 +1,6 @@ # 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 31 documented operations, including eight campaign operations. See the [complete v3 reference](../reference/v3.md) for method arguments and required headers. +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 | |---|---|---| @@ -82,9 +82,9 @@ 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 `api_key`. +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. -The reply marks full-replacement PATCH behavior as **Planned**, so the current SDK conservatively keeps this conditional merge-compatible update until backend support is confirmed. It does not assume planned replacement or name-based upsert. Write-only keys and headers cannot be reconstructed from GET, and omitted hidden values remain subject to backend merge semantics. Keep intended credentials in the builder or use a targeted [low-level update](../reference/v3.md#agents-five-operations) when retaining existing hidden credentials matters. Saved-agent update conflicts are surfaced without retry. +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 @@ -167,11 +167,11 @@ Use `await error.latest()` after an async 412. `save()` requires an `Agora` clie ## Compatibility and backend limits -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. New session start has no documented variable-value override or AgentKit extraction-results helper. See the [daily workflow](./agent-workflow.md) for session options and unsupported controls. +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; the current response contract does not expose an analysis-result field or an AgentKit extraction-results helper. 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 or hyphens per component and returns `$secrets..`. It creates a reference, not a secret value. +`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. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor messages move to top-level `messages`. Provider API keys become `{mode: "byok", api_key: ...}`, while absent keys select `managed`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`. +The central mapper reuses existing vendor configs. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor 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`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`; provider-specific `params.language` remains separate. A configured STT follows v2 interaction-language precedence (the explicit turn language, otherwise `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. @@ -179,19 +179,19 @@ For cascaded deployment, `with_turn_detection({"language": "de-DE"})` sets `pipe |---|---| | 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 | -| Multipart AWS/Vertex credentials, numeric v2 TTS `skip_patterns`, audio-only settings on text greetings, unmappable deprecated turn/greeting settings | Rejected rather than guessed or silently omitted | +| 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 `end_call`, `transfer`, `allowed_ips`, SIP-password secret references | Currently rejected or unsupported on staging | +| 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; regional availability/failover remain open, and CN is refused | -| Session controls and analysis helpers | Proposed PATCH/actions and AgentKit analysis helpers are not implemented | +| 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 active LLM/MLLM 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 -Wire schemas and methods are locally maintained from the supplied OpenAPI and engineering reply. The reply takes precedence where it corrects routing, authentication policy, session idempotency, status and timestamps. Campaign schema retry fields remain unconfirmed; no retry algorithm is implemented. They are protected custom code under `src/agora_agent/v3`, with a handwritten shared-transport adapter and AgentKit mapping. **They are not output from the repository's Fern generation pipeline.** This Python checkout does not contain the local generator or a vendored OpenAPI snapshot, so a reproducible generation workflow is not currently checked in. +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. diff --git a/docs/reference/v3.md b/docs/reference/v3.md index 05e237b..f0e3f93 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -1,6 +1,6 @@ # v3 preview reference -This reference describes the 31 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). +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 @@ -35,7 +35,7 @@ Prefix: `/conversational-ai/v1/projects/{appid}/agents`. A stored configuration' | `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. Pipelines use `mode="cascaded"` with ASR/LLM/TTS or `mode="realtime"` with MLLM. Provider credentials use `mode="managed"` or `mode="byok", api_key=...`; secret references are strings in `api_key`. +`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. Pipelines use `mode="cascaded"` with ASR/LLM/TTS or `mode="realtime"` with MLLM. 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. @@ -54,7 +54,7 @@ Prefix: `/conversational-ai/v1/projects/{appid}/secrets`. Set names and keys are | `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/hyphen syntax; `secret_ref` validates those components locally. +`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 | |---|---|---| @@ -100,7 +100,7 @@ number = client.v3.numbers.create( # Store number["number_id"] for subsequent conditional updates or assignment. ``` -## Sessions: five operations +## Sessions: six operations Prefix: `/conversational-ai/v1/projects/{appid}/sessions`. @@ -108,21 +108,22 @@ Prefix: `/conversational-ai/v1/projects/{appid}/sessions`. |---|---|---| | `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=None, labels=None, lifecycle=None, data_policy=None, idempotency_key=None)` | POST collection | `StartSessionResponse` | -| `start_ephemeral(agent, transport, values=None, labels=None, lifecycle=None, data_policy=None, idempotency_key=None)` | POST `/ephemeral` | `StartSessionResponse` | +| `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`, `uid` and exactly one non-wildcard, nonzero `subscribe_uids` entry; agent UID `0` alone 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. AgentKit session creation remains RTC-oriented; no high-level outbound-call helper is added. Stop-to-hangup/ringing behavior remains open. +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. AgentKit session creation remains RTC-oriented; no high-level outbound-call helper is added. Stop-to-hangup/ringing behavior remains open. `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. No session PATCH/action endpoints are exposed. +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 active LLM/MLLM 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` and `analysis`; a nonunique name does not establish ownership by a stored agent. +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`; the current contract has no `analysis` result field; 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. -`analysis`, when present, exposes extraction status and `result` through the low-level response. Session update/actions, outbound-call methods, AgentKit `get_analysis`, are not included in this workflow. +No extraction-result field is promised by the current session response contract. Unknown rollout fields remain readable in returned dictionaries. AgentKit analysis and outbound-call helpers are not provided. ## Pagination, errors and transport @@ -137,7 +138,7 @@ Omitted arguments use backend defaults, not client-side substitutions. In partic 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 per-request authentication/context headers and remove 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. +`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. @@ -166,7 +167,7 @@ The `max_*` assignment options, save `name` / `agent_id`, and update `name` are ### Session handles -`SavedAgent.create_session(*, channel, agent_uid, remote_uids, 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`. +`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 @@ -174,7 +175,7 @@ The `max_*` assignment options, save `name` / `agent_id`, and update `name` are | Method | HTTP | Result | |---|---|---| -| `create(name, agent_id, transport, contacts, schedule, lifecycle=None, pacing=None, idempotency_key=None)` | POST collection | `CampaignResponse` | +| `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` | @@ -185,6 +186,14 @@ The `max_*` assignment options, save `name` / `agent_id`, and update `name` are 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–5,000 contacts per campaign. 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 fields are schema-typed, but `max_attempts` and `retry_backoff_ms` remain unconfirmed; do not rely on built-in retries. +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": {...}}}}}`; use `mllm` instead of `llm` for realtime. Do not send both modules. 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. The existing legacy update path remains unchanged. + +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. diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..4e3a43d --- /dev/null +++ b/openapi.json @@ -0,0 +1,15887 @@ +{ + "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 the existing Agora token system using Authorization: agora token=." + }, + "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" + }, + { + "$ref": "#/components/schemas/ResolvedRealtimePipeline" + }, + { + "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" + }, + { + "$ref": "#/components/schemas/RealtimePipelineInput" + }, + { + "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" + } + }, + "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": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$" + }, + "type": "array", + "title": "Keys" + }, + "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" + }, + "mllm": { + "$ref": "#/components/schemas/SessionModulePatch", + "title": "Mllm", + "description": "Complete params replacement for the active realtime MLLM module. Credentials and provider selection are not mutable." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionPipelinePatch", + "not": { + "required": [ + "llm", + "mllm" + ] + } + }, + "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" + } + }, + "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" + }, + { + "$ref": "#/components/schemas/AgentRequest_RealtimePipelineInput" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Cascaded or realtime pipeline. Omission or null selects the existing managed cascaded defaults. A realtime pipeline explicitly sets mode to realtime." + }, + "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_PipelinePatch" + }, + { + "type": "null" + } + ], + "default": null, + "description": "JSON Merge Patch of the stored pipeline. Omitted mode retains the stored mode. The merged result must validate against the corresponding cascaded or realtime input; switching mode requires deleting incompatible module fields." + }, + "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" + }, + "AgoraVadTurnDetection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "agora_vad" + }, + "interrupt_duration_ms": { + "type": "integer", + "minimum": 0 + }, + "prefix_padding_ms": { + "type": "integer", + "minimum": 0 + }, + "silence_duration_ms": { + "type": "integer", + "minimum": 0 + }, + "threshold": { + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "AgentRequest_AgoraVadTurnDetectionPatch": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "agora_vad" + }, + { + "type": "null" + } + ] + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "threshold": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ServerVadTurnDetection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "server_vad" + }, + "prefix_padding_ms": { + "type": "integer", + "minimum": 0 + }, + "silence_duration_ms": { + "type": "integer", + "minimum": 0 + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "OpenAI Realtime API only." + }, + "idle_timeout_ms": { + "type": "integer", + "minimum": 0, + "description": "OpenAI Realtime API only." + }, + "start_of_speech_sensitivity": { + "type": "string", + "enum": [ + "START_SENSITIVITY_HIGH", + "START_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + }, + "end_of_speech_sensitivity": { + "type": "string", + "enum": [ + "END_SENSITIVITY_HIGH", + "END_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "AgentRequest_ServerVadTurnDetectionPatch": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "server_vad" + }, + { + "type": "null" + } + ] + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "OpenAI Realtime API only." + }, + { + "type": "null" + } + ] + }, + "idle_timeout_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "description": "OpenAI Realtime API only." + }, + { + "type": "null" + } + ] + }, + "start_of_speech_sensitivity": { + "anyOf": [ + { + "type": "string", + "enum": [ + "START_SENSITIVITY_HIGH", + "START_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + }, + { + "type": "null" + } + ] + }, + "end_of_speech_sensitivity": { + "anyOf": [ + { + "type": "string", + "enum": [ + "END_SENSITIVITY_HIGH", + "END_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "SemanticVadTurnDetection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "semantic_vad" + }, + "eagerness": { + "type": "string", + "enum": [ + "auto", + "low", + "medium", + "high" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "AgentRequest_SemanticVadTurnDetectionPatch": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "semantic_vad" + }, + { + "type": "null" + } + ] + }, + "eagerness": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auto", + "low", + "medium", + "high" + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "RealtimeTurnDetection": { + "oneOf": [ + { + "$ref": "#/components/schemas/AgoraVadTurnDetection" + }, + { + "$ref": "#/components/schemas/ServerVadTurnDetection" + }, + { + "$ref": "#/components/schemas/SemanticVadTurnDetection" + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "AgentRequest_RealtimeTurnDetectionPatch": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AgoraVadTurnDetectionPatch" + }, + { + "$ref": "#/components/schemas/AgentRequest_ServerVadTurnDetectionPatch" + }, + { + "$ref": "#/components/schemas/AgentRequest_SemanticVadTurnDetectionPatch" + } + ], + "description": "JSON Merge Patch of realtime turn detection. Omitted type retains the stored discriminator; the merged result must validate against RealtimeTurnDetection." + }, + "AgentRequest_MllmInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "properties": { + "credential": { + "properties": { + "fields": { + "additionalProperties": { + "type": "string", + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + } + } + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mcp Servers" + }, + "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": { + "type": "string" + }, + "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" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers", + "writeOnly": true + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + } + }, + "title": "AgentRequest_MllmInput", + "type": "object", + "required": [ + "vendor" + ], + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "MllmInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "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": { + "type": "string" + }, + "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" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "writeOnly": true + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "MllmInput", + "required": [ + "vendor" + ], + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "AgentRequest_MllmPatch": { + "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" + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmCredentialPatch" + }, + { + "type": "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" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers", + "writeOnly": true + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + } + }, + "title": "AgentRequest_MllmPatch", + "type": "object", + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "ResolvedMllm": { + "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" + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "mcp_servers": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/McpServer" + } + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedMllm", + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "AgentRequest_MllmCredentialPatch": { + "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": [ + { + "type": "object", + "maxProperties": 8, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "title": "PublicCredentialPatch", + "type": "object" + }, + "AgentRequest_RealtimePipelineInput": { + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "const": "realtime" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/RealtimeTurnDetection" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmInput" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "mode" + ], + "title": "AgentRequest_RealtimePipelineInput", + "type": "object" + }, + "RealtimePipelineInput": { + "properties": { + "mode": { + "type": "string", + "const": "realtime" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarInput" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/RealtimeTurnDetection" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/MllmInput" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "RealtimePipelineInput" + }, + "ResolvedRealtimePipeline": { + "properties": { + "mode": { + "type": "string", + "const": "realtime" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedAvatar" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/RealtimeTurnDetection" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedMllm" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ResolvedRealtimePipeline" + }, + "AgentRequest_RealtimePipelinePatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "type": "string", + "const": "realtime" + }, + { + "type": "null" + } + ] + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmPatch" + }, + { + "type": "null" + } + ] + } + }, + "title": "AgentRequest_RealtimePipelinePatch", + "type": "object" + }, + "AgentRequest_PipelinePatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "cascaded", + "realtime" + ] + }, + { + "type": "null" + } + ] + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + }, + { + "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + }, + { + "type": "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 + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmPatch" + }, + { + "type": "null" + } + ] + } + }, + "title": "PipelinePatch", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "mode": { + "const": "realtime" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "properties": { + "asr": { + "type": "null" + }, + "llm": { + "type": "null" + }, + "tts": { + "type": "null" + }, + "silence_config": { + "type": "null" + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + }, + { + "type": "null" + } + ] + } + } + } + }, + { + "if": { + "properties": { + "mode": { + "const": "cascaded" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "properties": { + "mllm": { + "type": "null" + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + }, + { + "type": "null" + } + ] + } + } + } + } + ] + } + }, + "securitySchemes": { + "AgoraTokenAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "An Agora token produced by the existing token system. Send the complete header value: agora token=. SDKs accept a supplied token or generate one from App ID and App Certificate; Customer ID/Customer Secret authentication is not exposed." + } + }, + "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": [] + } + ] +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..d50d95b --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,9596 @@ +--- +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 the existing Agora token system using Authorization: agora token=.' +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" + - "$ref": "#/components/schemas/ResolvedRealtimePipeline" + - 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" + - "$ref": "#/components/schemas/RealtimePipelineInput" + - 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 + 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: + type: string + maxLength: 64 + minLength: 1 + pattern: "^[A-Za-z0-9_]+$" + type: array + title: Keys + 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 + mllm: + "$ref": "#/components/schemas/SessionModulePatch" + title: Mllm + description: Complete params replacement for the active realtime MLLM module. + Credentials and provider selection are not mutable. + additionalProperties: false + type: object + title: SessionPipelinePatch + not: + required: + - llm + - mllm + 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 + 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" + - "$ref": "#/components/schemas/AgentRequest_RealtimePipelineInput" + - type: 'null' + default: + description: Cascaded or realtime pipeline. Omission or null selects the + existing managed cascaded defaults. A realtime pipeline explicitly sets + mode to realtime. + 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_PipelinePatch" + - type: 'null' + default: + description: JSON Merge Patch of the stored pipeline. Omitted mode retains + the stored mode. The merged result must validate against the corresponding + cascaded or realtime input; switching mode requires deleting incompatible + module fields. + 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 + AgoraVadTurnDetection: + type: object + properties: + type: + type: string + const: agora_vad + interrupt_duration_ms: + type: integer + minimum: 0 + prefix_padding_ms: + type: integer + minimum: 0 + silence_duration_ms: + type: integer + minimum: 0 + threshold: + type: number + exclusiveMinimum: 0 + exclusiveMaximum: 1 + required: + - type + additionalProperties: false + AgentRequest_AgoraVadTurnDetectionPatch: + type: object + properties: + type: + anyOf: + - type: string + const: agora_vad + - type: 'null' + interrupt_duration_ms: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + prefix_padding_ms: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + silence_duration_ms: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + threshold: + anyOf: + - type: number + exclusiveMinimum: 0 + exclusiveMaximum: 1 + - type: 'null' + additionalProperties: false + ServerVadTurnDetection: + type: object + properties: + type: + type: string + const: server_vad + prefix_padding_ms: + type: integer + minimum: 0 + silence_duration_ms: + type: integer + minimum: 0 + threshold: + type: number + minimum: 0 + maximum: 1 + description: OpenAI Realtime API only. + idle_timeout_ms: + type: integer + minimum: 0 + description: OpenAI Realtime API only. + start_of_speech_sensitivity: + type: string + enum: + - START_SENSITIVITY_HIGH + - START_SENSITIVITY_LOW + description: Gemini Live only. + end_of_speech_sensitivity: + type: string + enum: + - END_SENSITIVITY_HIGH + - END_SENSITIVITY_LOW + description: Gemini Live only. + required: + - type + additionalProperties: false + AgentRequest_ServerVadTurnDetectionPatch: + type: object + properties: + type: + anyOf: + - type: string + const: server_vad + - type: 'null' + prefix_padding_ms: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + silence_duration_ms: + anyOf: + - type: integer + minimum: 0 + - type: 'null' + threshold: + anyOf: + - type: number + minimum: 0 + maximum: 1 + description: OpenAI Realtime API only. + - type: 'null' + idle_timeout_ms: + anyOf: + - type: integer + minimum: 0 + description: OpenAI Realtime API only. + - type: 'null' + start_of_speech_sensitivity: + anyOf: + - type: string + enum: + - START_SENSITIVITY_HIGH + - START_SENSITIVITY_LOW + description: Gemini Live only. + - type: 'null' + end_of_speech_sensitivity: + anyOf: + - type: string + enum: + - END_SENSITIVITY_HIGH + - END_SENSITIVITY_LOW + description: Gemini Live only. + - type: 'null' + additionalProperties: false + SemanticVadTurnDetection: + type: object + properties: + type: + type: string + const: semantic_vad + eagerness: + type: string + enum: + - auto + - low + - medium + - high + required: + - type + additionalProperties: false + AgentRequest_SemanticVadTurnDetectionPatch: + type: object + properties: + type: + anyOf: + - type: string + const: semantic_vad + - type: 'null' + eagerness: + anyOf: + - type: string + enum: + - auto + - low + - medium + - high + - type: 'null' + additionalProperties: false + RealtimeTurnDetection: + oneOf: + - "$ref": "#/components/schemas/AgoraVadTurnDetection" + - "$ref": "#/components/schemas/ServerVadTurnDetection" + - "$ref": "#/components/schemas/SemanticVadTurnDetection" + discriminator: + propertyName: type + AgentRequest_RealtimeTurnDetectionPatch: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AgoraVadTurnDetectionPatch" + - "$ref": "#/components/schemas/AgentRequest_ServerVadTurnDetectionPatch" + - "$ref": "#/components/schemas/AgentRequest_SemanticVadTurnDetectionPatch" + description: JSON Merge Patch of realtime turn detection. Omitted type retains + the stored discriminator; the merged result must validate against RealtimeTurnDetection. + AgentRequest_MllmInput: + additionalProperties: false + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + required: + - mode + type: object + required: + - credential + - properties: + credential: + properties: + fields: + additionalProperties: + type: string + pattern: "^\\$secrets\\.[^.]+\\.[^.]+$" + properties: + mcp_servers: + anyOf: + - items: + "$ref": "#/components/schemas/AgentRequest_McpServer" + type: array + - type: 'null' + default: + title: Mcp Servers + 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: + type: string + 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 + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + default: + title: Headers + writeOnly: true + input_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + output_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + title: AgentRequest_MllmInput + type: object + required: + - vendor + description: Realtime MLLM module. Credentials use credential.fields; saved-agent + writes require secret references, while ephemeral input also permits direct + values. Provider settings remain in params. + MllmInput: + allOf: + - if: + properties: + url: + type: string + required: + - url + then: + properties: + credential: + properties: + mode: + const: byok + type: object + required: + - mode + required: + - credential + properties: + mcp_servers: + anyOf: + - items: + "$ref": "#/components/schemas/McpServer" + type: array + - type: 'null' + title: Mcp Servers + 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: + type: string + 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 + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + title: Headers + writeOnly: true + input_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + output_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + additionalProperties: false + type: object + title: MllmInput + required: + - vendor + description: Realtime MLLM module. Credentials use credential.fields; saved-agent + writes require secret references, while ephemeral input also permits direct + values. Provider settings remain in params. + AgentRequest_MllmPatch: + 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 + credential: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_MllmCredentialPatch" + - type: 'null' + 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 + headers: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + default: + title: Headers + writeOnly: true + input_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + output_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + title: AgentRequest_MllmPatch + type: object + description: Realtime MLLM module. Credentials use credential.fields; saved-agent + writes require secret references, while ephemeral input also permits direct + values. Provider settings remain in params. + ResolvedMllm: + 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 + input_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + output_modalities: + anyOf: + - type: array + items: + type: string + enum: + - text + - audio + - type: 'null' + mcp_servers: + anyOf: + - type: array + items: + "$ref": "#/components/schemas/McpServer" + - type: 'null' + title: Mcp Servers + additionalProperties: false + type: object + title: ResolvedMllm + description: Realtime MLLM module. Credentials use credential.fields; saved-agent + writes require secret references, while ephemeral input also permits direct + values. Provider settings remain in params. + AgentRequest_MllmCredentialPatch: + 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: + - type: object + maxProperties: 8 + additionalProperties: + anyOf: + - type: string + pattern: "^\\$secrets\\.[^.]+\\.[^.]+$" + - type: 'null' + - type: 'null' + title: PublicCredentialPatch + type: object + AgentRequest_RealtimePipelineInput: + additionalProperties: false + properties: + mode: + type: string + const: realtime + avatar: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarInput" + - type: 'null' + default: + turn_detection: + anyOf: + - "$ref": "#/components/schemas/RealtimeTurnDetection" + - type: 'null' + mllm: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_MllmInput" + - type: 'null' + required: + - mode + title: AgentRequest_RealtimePipelineInput + type: object + RealtimePipelineInput: + properties: + mode: + type: string + const: realtime + avatar: + anyOf: + - "$ref": "#/components/schemas/AvatarInput" + - type: 'null' + turn_detection: + anyOf: + - "$ref": "#/components/schemas/RealtimeTurnDetection" + - type: 'null' + mllm: + anyOf: + - "$ref": "#/components/schemas/MllmInput" + - type: 'null' + additionalProperties: false + type: object + required: + - mode + title: RealtimePipelineInput + ResolvedRealtimePipeline: + properties: + mode: + type: string + const: realtime + avatar: + anyOf: + - "$ref": "#/components/schemas/ResolvedAvatar" + - type: 'null' + turn_detection: + anyOf: + - "$ref": "#/components/schemas/RealtimeTurnDetection" + - type: 'null' + mllm: + anyOf: + - "$ref": "#/components/schemas/ResolvedMllm" + - type: 'null' + additionalProperties: false + type: object + required: + - mode + title: ResolvedRealtimePipeline + AgentRequest_RealtimePipelinePatch: + additionalProperties: false + properties: + mode: + anyOf: + - type: string + const: realtime + - type: 'null' + avatar: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + - type: 'null' + default: + turn_detection: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + - type: 'null' + mllm: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_MllmPatch" + - type: 'null' + title: AgentRequest_RealtimePipelinePatch + type: object + AgentRequest_PipelinePatch: + additionalProperties: false + properties: + mode: + anyOf: + - type: string + enum: + - cascaded + - realtime + - type: 'null' + avatar: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + - type: 'null' + default: + turn_detection: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + - type: 'null' + 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: + mllm: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_MllmPatch" + - type: 'null' + title: PipelinePatch + type: object + allOf: + - if: + properties: + mode: + const: realtime + required: + - mode + then: + properties: + asr: + type: 'null' + llm: + type: 'null' + tts: + type: 'null' + silence_config: + type: 'null' + turn_detection: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + - type: 'null' + - if: + properties: + mode: + const: cascaded + required: + - mode + then: + properties: + mllm: + type: 'null' + turn_detection: + anyOf: + - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + - type: 'null' + securitySchemes: + AgoraTokenAuth: + type: apiKey + in: header + name: Authorization + description: 'An Agora token produced by the existing token system. Send the + complete header value: agora token=. SDKs accept a supplied token or + generate one from App ID and App Certificate; Customer ID/Customer Secret + authentication is not exposed.' + 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: [] diff --git a/scripts/generate-v3-preview.py b/scripts/generate-v3-preview.py new file mode 100644 index 0000000..6b2cede --- /dev/null +++ b/scripts/generate-v3-preview.py @@ -0,0 +1,178 @@ +"""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 + +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 fields) + "}" if 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/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index b166eb0..298f8ff 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -948,8 +948,8 @@ async def deploy_async(self, *, name: typing.Optional[str] = None, agent_id: typ 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, @@ -984,7 +984,9 @@ def create_session( """ from .agent_session import AgentSession from .session_backend import create_session, needs_session_backend - if 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)): + 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, @@ -1020,8 +1022,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, @@ -1050,7 +1052,9 @@ def create_async_session( """ from .agent_session import AsyncAgentSession from .session_backend import create_session, needs_session_backend - if 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)): + 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, diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 7cf9b37..01a5731 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -883,7 +883,11 @@ def update(self, properties: typing.Any) -> None: Partial configuration to update. """ if self._session_backend is not None: - raise NotImplementedError("update is not documented for stored or ephemeral sessions") + 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: @@ -1333,7 +1337,11 @@ async def update(self, properties: typing.Any) -> None: Partial configuration to update. """ if self._session_backend is not None: - raise NotImplementedError("update is not documented for stored or ephemeral sessions") + 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: diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 253e130..ef74777 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -28,10 +28,9 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids client = owner._client if not isinstance(client, AsyncAgora if asynchronous else Agora): raise TypeError("Session requires AsyncAgora" if asynchronous else "Session requires Agora") - if getattr(client, "area_scope", None) == "cn": - raise ValueError("Stored and ephemeral sessions in CN are unconfirmed") - if idempotency_key is not None and (not isinstance(idempotency_key, str) or not idempotency_key.strip()): - raise ValueError("idempotency_key must be a nonempty string") + 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] @@ -39,12 +38,13 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids raise ValueError("Unsupported session options: " + ", ".join(sorted(invalid))) if not isinstance(channel, str) or not channel.strip(): raise ValueError("channel must be nonempty") - if not isinstance(agent_uid, str) or not agent_uid.strip(): + 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, "uid": agent_uid, - "subscribe_uids": list(remote_uids)}} + body = {"transport": {"type": "rtc", "channel": channel, "subscribe_uids": list(remote_uids)}} + if agent_uid is not None: + body["transport"]["uid"] = agent_uid if labels is None: labels = getattr(owner, "_labels", None) reference = name @@ -109,3 +109,37 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids name=reference or "", channel=channel, agent_uid=agent_uid, remote_uids=list(remote_uids), debug=debug, warn=warn, _session_backend={"stored": stored, "body": copy.deepcopy(body)}) + + +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", "mllm"}: + raise ValueError("Session update supports token, messages, llm.params and mllm.params only") + result = {} + if "token" in value: + result["transport"] = {"token": value["token"]} + overrides = {} + if "messages" in value: + overrides["messages"] = value["messages"] + pipeline = {} + for category in ("llm", "mllm"): + if category not in value: + continue + module = value[category] + allowed = {"params", "system_messages"} if category == "llm" else {"params"} + if not isinstance(module, dict) or set(module) - allowed: + raise ValueError("Session update supports only provider params and llm.system_messages") + if "system_messages" in module: + if "messages" in overrides and overrides["messages"] != module["system_messages"]: + raise ValueError("Supply messages or llm.system_messages, not conflicting values") + overrides["messages"] = module["system_messages"] + if "params" in module: + pipeline[category] = {"params": module["params"]} + if pipeline: + overrides["pipeline"] = pipeline + if overrides: + result["agent"] = {"overrides": overrides} + validate_write("PATCH", "/conversational-ai/v1/projects/project/sessions/session", result) + return result diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 7c25ca9..da3aa23 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -32,11 +32,11 @@ class SecretRef(str): def secret_ref(secret_set: str, key: str) -> SecretRef: - pattern = r"[A-Za-z0-9_-]{1,64}" + 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 or hyphens") + 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 or hyphens") + raise ValueError("key must contain 1-64 letters, digits, underscores") return SecretRef(f"$secrets.{secret_set}.{key}") @@ -96,21 +96,39 @@ def module(config, category, errors): preset_model = config.pop("_minimax_preset_model", None) if preset_model is not None: params.setdefault("model", preset_model) - keys = [] - credential_fields = ["api_key", "key", "api_subscription_key"] - if category == "avatar" and config.get("vendor") == "sensetime": - credential_fields.append("app_key") - if category == "avatar" and config.get("vendor") == "spatius": - credential_fields.append("spatius_api_key") + if category == "llm" and config.get("style") == "bedrock": + config.setdefault("vendor", "amazon") + if "region" in config: + params.setdefault("region", config.pop("region")) + vendor = config.get("vendor") + 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 credential_fields: - if field in source: - keys.append(source.pop(field)) - multipart = {"aws_access_key_id", "aws_secret_access_key", "access_key", "secret_key", "adc_credentials_string", "service_account", "credentials"} - errors.extend(category + ".params." + k for k in params if k in multipart) - if len(set(keys)) > 1: - errors.append(category + ".credential (multiple different API keys)") - result = {"credential": {"mode": "byok", "api_key": keys[0]} if keys else {"mode": "managed"}} + 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"}, @@ -135,7 +153,7 @@ def module(config, category, errors): 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 keys: + if not fields: result.pop("url", None) for key in ("url", "base_url"): params.pop(key, None) @@ -365,18 +383,32 @@ def build_body(agent, name=None): _MUTABLE = {"agent_name", "instructions", "messages", "greeting", "variables", "pipeline", "filler_words"} -def replacement_patch(previous, desired): +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]["api_key"] = None + result[key]["fields"] = None continue # Resolved credentials have read-only metadata and hide API keys. - result[key] = replacement_patch(old, result[key]) + result[key] = replacement_patch(old, result[key], key if key in {"asr", "llm", "tts", "mllm", "avatar"} else category) return result @@ -391,13 +423,13 @@ def _require_client(agent, async_): expected = AsyncAgora if async_ else Agora if not isinstance(agent._client, expected): raise TypeError("save_async requires AsyncAgora" if async_ else "save requires Agora") - if getattr(agent._client, "area_scope", None) == "cn": - raise ValueError("v3 deployment in CN is unconfirmed; use a supported global area") 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) @@ -412,6 +444,8 @@ def save(agent, *, name=None, agent_id=None) -> SavedAgent: 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) @@ -454,7 +488,7 @@ def __init__(self, client, response): self.raw = client.v3.agents class SavedAgent(_SavedAgentBase): - def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], + 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): @@ -527,7 +561,7 @@ def unassign_number(self, number) -> NumberResponse: class AsyncSavedAgent(_SavedAgentBase): - def create_session(self, *, channel: str, agent_uid: str, remote_uids: List[str], + 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): diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py index cc1d086..88743e0 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -9,8 +9,8 @@ def validate_public_headers(headers, *, allow_empty_authorization=False): 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 not value.strip()): - raise ValueError("Idempotency-Key must be a nonempty string") + 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 == "") and not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE): raise ValueError("Only Agora token authentication is supported") diff --git a/src/agora_agent/v3/__init__.py b/src/agora_agent/v3/__init__.py index 9741b1c..5868df4 100644 --- a/src/agora_agent/v3/__init__.py +++ b/src/agora_agent/v3/__init__.py @@ -1,4 +1,4 @@ -"""Additive v3 stored-agent preview. Sessions are read-only.""" +"""Stored-agent, session lifecycle and campaign preview resources.""" from . import types from ._transport import PreconditionFailedError, RawResponse, V3Options from .client import AsyncV3Client, V3Client diff --git a/src/agora_agent/v3/_transport.py b/src/agora_agent/v3/_transport.py index 91c6bfa..9b883ca 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -71,6 +71,12 @@ def __init__(self, owner: Any, options: Optional[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 {})}) diff --git a/src/agora_agent/v3/campaigns.py b/src/agora_agent/v3/campaigns.py index e6ba5ab..3b8d7f2 100644 --- a/src/agora_agent/v3/campaigns.py +++ b/src/agora_agent/v3/campaigns.py @@ -1,134 +1,4 @@ -"""Typed campaign operations; lifecycle semantics follow the engineering reply.""" -from __future__ import annotations +"""Compatibility re-exports of generated campaign resources.""" +from .client import AsyncCampaignsClient, CampaignsClient -from typing import List, Optional, Union - -from ._transport import _UNSET, AsyncResource, RawResponse, Resource, segment -from .types import ( - CampaignDeleteResponse, - CampaignLifecycle, - CampaignListResponse, - CampaignResponse, - CampaignTransport, - CampaignTransportPatch, - ContactInput, - ImmediateSchedule, - Pacing, - PacingPatch, - ScheduledSchedule, -) -from typing_extensions import Literal - - -class CampaignsClient(Resource): - def __init__(self, owner, options): - super().__init__(owner, options) - self.with_raw_response = CampaignsRawClient(owner, options) - - def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> CampaignResponse: - return self.with_raw_response.create(name=name, agent_id=agent_id, transport=transport, contacts=contacts, schedule=schedule, lifecycle=lifecycle, pacing=pacing, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data - - def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> CampaignListResponse: - return self.with_raw_response.list(status=status, limit=limit, cursor=cursor, order=order, appid=appid, request_options=request_options).data - - def get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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) -> CampaignDeleteResponse: - return self.with_raw_response.delete(campaign_id=campaign_id, 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) -> 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) -> 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) -> CampaignResponse: - return self.with_raw_response.cancel(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options).data - - -class CampaignsRawClient(Resource): - - def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[CampaignResponse]: - return self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body={k: v for k, v in {'name': name, 'agent_id': agent_id, 'transport': transport, 'contacts': contacts, 'schedule': schedule, 'lifecycle': lifecycle, 'pacing': pacing}.items() if v is not None}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) - - def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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 get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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) - - def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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[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) - - -class AsyncCampaignsClient(AsyncResource): - def __init__(self, owner, options): - super().__init__(owner, options) - self.with_raw_response = AsyncCampaignsRawClient(owner, options) - - async def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> CampaignResponse: - return (await self.with_raw_response.create(name=name, agent_id=agent_id, transport=transport, contacts=contacts, schedule=schedule, lifecycle=lifecycle, pacing=pacing, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data - - async def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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 get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> 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) -> CampaignDeleteResponse: - return (await self.with_raw_response.delete(campaign_id=campaign_id, 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) -> 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) -> 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) -> CampaignResponse: - return (await self.with_raw_response.cancel(campaign_id=campaign_id, idempotency_key=idempotency_key, appid=appid, request_options=request_options)).data - - -class AsyncCampaignsRawClient(AsyncResource): - - async def create(self, *, name: str, agent_id: str, transport: CampaignTransport, contacts: List[ContactInput], schedule: Union[ImmediateSchedule, ScheduledSchedule], lifecycle: Optional[CampaignLifecycle] = None, pacing: Optional[Pacing] = None, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[CampaignResponse]: - return await self._request('POST', f'/conversational-ai/v1/projects/{segment(self.owner.app_id if appid is None else appid)}/campaigns', body={k: v for k, v in {'name': name, 'agent_id': agent_id, 'transport': transport, 'contacts': contacts, 'schedule': schedule, 'lifecycle': lifecycle, 'pacing': pacing}.items() if v is not None}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_options) - - async def list(self, *, status: Optional[Literal["scheduled", "running", "paused", "completed", "canceled", "failed"]] = None, limit: Optional[int] = None, cursor: Optional[str] = None, order: Optional[Literal["create_ts.asc", "create_ts.desc"]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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 get(self, *, campaign_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[CampaignLifecycle, None] = _UNSET, pacing: Union[PacingPatch, None] = _UNSET, schedule: Union[ImmediateSchedule, ScheduledSchedule, None] = _UNSET, transport: Union[CampaignTransportPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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) - - async def pause(self, *, campaign_id: str, idempotency_key: Optional[str] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[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[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[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) +__all__ = ["CampaignsClient", "AsyncCampaignsClient"] diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py index bedae24..be2f696 100644 --- a/src/agora_agent/v3/client.py +++ b/src/agora_agent/v3/client.py @@ -1,415 +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 .campaigns import AsyncCampaignsClient, CampaignsClient -from .types import ( - AgentCreateModel_AudioGreeting, - AgentCreateModel_CascadedPipeline, - AgentCreateModel_FillerWords, - AgentCreateModel_Message, - AgentCreateModel_RealtimePipeline, - AgentCreateModel_TextGreeting, - AgentCreateModel_VariableDefinition, - AgentDeleteResponse, - AgentListResponse, - AgentPatchRequest_FillerWordsPatch, - AgentPatchRequest_GreetingPatch, - AgentPatchRequest_Message, - AgentPatchRequest_PipelinePatch, - AgentPatchRequest_VariableDefinitionPatch, - AgentResponse, - DataPolicy, - EphemeralSessionAgent, - InboundInput, - NumberDeleteResponse, - NumberListResponse, - NumberResponse, - SecretDeleted, - SecretKey, - SecretList, - SecretSet, - SessionLifecycle, - SessionList, - SessionResponse, - SessionTransport, - SipTrunkInput, - StartSessionResponse, -) from typing_extensions import Literal class AgentsClient(Resource): - with_raw_response: AgentsRawClient - - def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': - return self.with_raw_response.create(appid=appid, agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, request_options=request_options).data - - def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'AgentListResponse': - return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data + 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, T.AgentRequest_RealtimePipelineInput, 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 get(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentResponse': - return self.with_raw_response.get(appid=appid, agent_id=agent_id, 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 update(self, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': - return self.with_raw_response.update(appid=appid, 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, 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 delete(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentDeleteResponse': - return self.with_raw_response.delete(appid=appid, agent_id=agent_id, 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_PipelinePatch, 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, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, T.AgentRequest_RealtimePipelineInput, 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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_PipelinePatch, 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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) -def _AgentsClient(owner, options): - client = AgentsClient(owner, options) - client.with_raw_response = AgentsRawClient(owner, options) - return client - class SecretsClient(Resource): - with_raw_response: SecretsRawClient - - def create(self, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': - return self.with_raw_response.create(appid=appid, name=name, secrets=secrets, request_options=request_options).data - - def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> 'SecretList': - return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data + 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 get(self, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> 'SecretSet': - return self.with_raw_response.get(appid=appid, name=name, 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 replace(self, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': - return self.with_raw_response.replace(appid=appid, name=name, if_match=if_match, secrets=secrets, 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 update(self, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> 'SecretSet': - return self.with_raw_response.update(appid=appid, name=name, if_match=if_match, secrets=secrets, 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 delete(self, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> 'SecretDeleted': - return self.with_raw_response.delete(appid=appid, name=name, if_match=if_match, 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 set_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> 'SecretKey': - return self.with_raw_response.set_key(appid=appid, name=name, key=key, if_match=if_match, if_none_match=if_none_match, value=value, 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 delete_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> 'SecretDeleted': - return self.with_raw_response.delete_key(appid=appid, name=name, key=key, if_match=if_match, 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, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + 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) -def _SecretsClient(owner, options): - client = SecretsClient(owner, options) - client.with_raw_response = SecretsRawClient(owner, options) - return client - class NumbersClient(Resource): - with_raw_response: NumbersRawClient - - def create(self, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': - return self.with_raw_response.create(appid=appid, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options).data - - def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'NumberListResponse': - return self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options).data + 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 get(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberResponse': - return self.with_raw_response.get(appid=appid, number_id=number_id, 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 update(self, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': - return self.with_raw_response.update(appid=appid, number_id=number_id, if_match=if_match, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, 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 delete(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberDeleteResponse': - return self.with_raw_response.delete(appid=appid, number_id=number_id, 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, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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) -def _NumbersClient(owner, options): - client = NumbersClient(owner, options) - client.with_raw_response = NumbersRawClient(owner, options) - return client - class SessionsClient(Resource): - def start(self, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return self.with_raw_response.start(agent_id=agent_id, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options).data - - def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return self.with_raw_response.start_ephemeral(agent=agent, labels=labels, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, appid=appid, request_options=request_options).data + 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 stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: - return self.with_raw_response.stop(agent_session_id=agent_session_id, 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 - with_raw_response: SessionsRawClient + 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 list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> 'SessionList': - return self.with_raw_response.list(appid=appid, channel=channel, status=status, started_after=started_after, started_before=started_before, limit=limit, cursor=cursor, 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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> 'SessionResponse': - return self.with_raw_response.get(appid=appid, agent_session_id=agent_session_id, 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, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - segment(agent_id) - 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, 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_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) -> 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, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - 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, **({'labels': labels} if labels is not None else {}), 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, 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 stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + 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) - def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: - 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) +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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> RawResponse: - 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 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 _SessionsClient(owner, options): - client = SessionsClient(owner, options) - client.with_raw_response = SessionsRawClient(owner, options) - return client + 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.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): - with_raw_response: AsyncAgentsRawClient - - async def create(self, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': - return (await self.with_raw_response.create(appid=appid, agent_name=agent_name, instructions=instructions, messages=messages, greeting=greeting, variables=variables, pipeline=pipeline, filler_words=filler_words, request_options=request_options)).data - - async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'AgentListResponse': - return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data + 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, T.AgentRequest_RealtimePipelineInput, 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 get(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentResponse': - return (await self.with_raw_response.get(appid=appid, agent_id=agent_id, 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 update(self, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> 'AgentResponse': - return (await self.with_raw_response.update(appid=appid, 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, 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 delete(self, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> 'AgentDeleteResponse': - return (await self.with_raw_response.delete(appid=appid, agent_id=agent_id, 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_PipelinePatch, 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, *, appid: Optional[str] = None, agent_name: str, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentCreateModel_Message'], None] = _UNSET, greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] = _UNSET, variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] = _UNSET, pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] = _UNSET, filler_words: Union['AgentCreateModel_FillerWords', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, T.AgentRequest_RealtimePipelineInput, 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, agent_id: str, if_match: str, agent_name: Union[str, None] = _UNSET, instructions: Union[str, None] = _UNSET, messages: Union[List['AgentPatchRequest_Message'], None] = _UNSET, greeting: Union['AgentPatchRequest_GreetingPatch', None] = _UNSET, variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] = _UNSET, pipeline: Union['AgentPatchRequest_PipelinePatch', None] = _UNSET, filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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_PipelinePatch, 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, *, appid: Optional[str] = None, agent_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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) -def _AsyncAgentsClient(owner, options): - client = AsyncAgentsClient(owner, options) - client.with_raw_response = AsyncAgentsRawClient(owner, options) - return client - class AsyncSecretsClient(AsyncResource): - with_raw_response: AsyncSecretsRawClient - - async def create(self, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': - return (await self.with_raw_response.create(appid=appid, name=name, secrets=secrets, request_options=request_options)).data - - async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> 'SecretList': - return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data + 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 get(self, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> 'SecretSet': - return (await self.with_raw_response.get(appid=appid, name=name, 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 replace(self, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> 'SecretSet': - return (await self.with_raw_response.replace(appid=appid, name=name, if_match=if_match, secrets=secrets, 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 update(self, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> 'SecretSet': - return (await self.with_raw_response.update(appid=appid, name=name, if_match=if_match, secrets=secrets, 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 delete(self, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> 'SecretDeleted': - return (await self.with_raw_response.delete(appid=appid, name=name, if_match=if_match, 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 set_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> 'SecretKey': - return (await self.with_raw_response.set_key(appid=appid, name=name, key=key, if_match=if_match, if_none_match=if_none_match, value=value, 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 delete_key(self, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> 'SecretDeleted': - return (await self.with_raw_response.delete_key(appid=appid, name=name, key=key, if_match=if_match, 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, *, appid: Optional[str] = None, name: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.asc', 'create_ts.desc']] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, if_match: str, secrets: Dict[str, str], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, if_match: Optional[str] = None, secrets: Dict[str, Union[str, None]], request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, if_match: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, if_none_match: Optional[str] = None, value: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, name: str, key: str, if_match: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + 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) -def _AsyncSecretsClient(owner, options): - client = AsyncSecretsClient(owner, options) - client.with_raw_response = AsyncSecretsRawClient(owner, options) - return client - class AsyncNumbersClient(AsyncResource): - with_raw_response: AsyncNumbersRawClient - - async def create(self, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': - return (await self.with_raw_response.create(appid=appid, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, request_options=request_options)).data - - async def list(self, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> 'NumberListResponse': - return (await self.with_raw_response.list(appid=appid, limit=limit, cursor=cursor, order=order, request_options=request_options)).data + 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 get(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberResponse': - return (await self.with_raw_response.get(appid=appid, number_id=number_id, 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 update(self, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> 'NumberResponse': - return (await self.with_raw_response.update(appid=appid, number_id=number_id, if_match=if_match, number=number, description=description, sip_trunk=sip_trunk, inbound=inbound, 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 delete(self, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> 'NumberDeleteResponse': - return (await self.with_raw_response.delete(appid=appid, number_id=number_id, 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, *, appid: Optional[str] = None, number: str, description: Union[str, None] = _UNSET, sip_trunk: 'SipTrunkInput', inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[Union[str, None]] = None, order: Optional[Literal['create_ts.desc', 'create_ts.asc']] = None, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, number_id: str, if_match: str, number: Union[str, None] = _UNSET, description: Union[str, None] = _UNSET, sip_trunk: Union['SipTrunkInput', None] = _UNSET, inbound: Union['InboundInput', None] = _UNSET, request_options: Optional[dict] = None) -> RawResponse: + 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, *, appid: Optional[str] = None, number_id: str, request_options: Optional[dict] = None) -> RawResponse: + 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) -def _AsyncNumbersClient(owner, options): - client = AsyncNumbersClient(owner, options) - client.with_raw_response = AsyncNumbersRawClient(owner, options) - return client - class AsyncSessionsClient(AsyncResource): - async def start(self, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return (await self.with_raw_response.start(agent_id=agent_id, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, labels=labels, appid=appid, request_options=request_options)).data - - async def start_ephemeral(self, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> StartSessionResponse: - return (await self.with_raw_response.start_ephemeral(agent=agent, labels=labels, transport=transport, values=values, idempotency_key=idempotency_key, data_policy=data_policy, lifecycle=lifecycle, appid=appid, request_options=request_options)).data + 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 stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> dict: - return (await self.with_raw_response.stop(agent_session_id=agent_session_id, 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 - with_raw_response: AsyncSessionsRawClient + 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 list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> 'SessionList': - return (await self.with_raw_response.list(appid=appid, channel=channel, status=status, started_after=started_after, started_before=started_before, limit=limit, cursor=cursor, 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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> 'SessionResponse': - return (await self.with_raw_response.get(appid=appid, agent_session_id=agent_session_id, 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, *, agent_id: str, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, labels: Optional[Dict[str, str]] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - segment(agent_id) - 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, 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {}), **({'labels': labels} if labels is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, request_options=request_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) -> 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, *, agent: EphemeralSessionAgent, labels: Optional[Dict[str, str]] = None, transport: SessionTransport, values: Optional[Dict[str, Union[str, int, float, bool]]] = None, idempotency_key: Optional[str] = None, data_policy: Optional[DataPolicy] = None, lifecycle: Optional[SessionLifecycle] = None, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: - 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, **({'labels': labels} if labels is not None else {}), 'transport': transport, **({'values': values} if values is not None else {}), **({'data_policy': data_policy} if data_policy is not None else {}), **({'lifecycle': lifecycle} if lifecycle is not None else {})}, headers={'Idempotency-Key': idempotency_key}, params={}, 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 stop(self, *, agent_session_id: str, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse: + 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) - async def list(self, *, appid: Optional[str] = None, 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, request_options: Optional[dict] = None) -> RawResponse: - 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) +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 get(self, *, appid: Optional[str] = None, agent_session_id: str, request_options: Optional[dict] = None) -> RawResponse: - 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 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) -def _AsyncSessionsClient(owner, options): - client = AsyncSessionsClient(owner, options) - client.with_raw_response = AsyncSessionsRawClient(owner, options) - return client + 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.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..09c2a26 --- /dev/null +++ b/src/agora_agent/v3/contract.py @@ -0,0 +1,153 @@ +"""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 "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"}.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..4e3a43d --- /dev/null +++ b/src/agora_agent/v3/openapi.json @@ -0,0 +1,15887 @@ +{ + "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 the existing Agora token system using Authorization: agora token=." + }, + "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" + }, + { + "$ref": "#/components/schemas/ResolvedRealtimePipeline" + }, + { + "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" + }, + { + "$ref": "#/components/schemas/RealtimePipelineInput" + }, + { + "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" + } + }, + "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": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$" + }, + "type": "array", + "title": "Keys" + }, + "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" + }, + "mllm": { + "$ref": "#/components/schemas/SessionModulePatch", + "title": "Mllm", + "description": "Complete params replacement for the active realtime MLLM module. Credentials and provider selection are not mutable." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SessionPipelinePatch", + "not": { + "required": [ + "llm", + "mllm" + ] + } + }, + "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" + } + }, + "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" + }, + { + "$ref": "#/components/schemas/AgentRequest_RealtimePipelineInput" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Cascaded or realtime pipeline. Omission or null selects the existing managed cascaded defaults. A realtime pipeline explicitly sets mode to realtime." + }, + "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_PipelinePatch" + }, + { + "type": "null" + } + ], + "default": null, + "description": "JSON Merge Patch of the stored pipeline. Omitted mode retains the stored mode. The merged result must validate against the corresponding cascaded or realtime input; switching mode requires deleting incompatible module fields." + }, + "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" + }, + "AgoraVadTurnDetection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "agora_vad" + }, + "interrupt_duration_ms": { + "type": "integer", + "minimum": 0 + }, + "prefix_padding_ms": { + "type": "integer", + "minimum": 0 + }, + "silence_duration_ms": { + "type": "integer", + "minimum": 0 + }, + "threshold": { + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "AgentRequest_AgoraVadTurnDetectionPatch": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "agora_vad" + }, + { + "type": "null" + } + ] + }, + "interrupt_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "threshold": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ServerVadTurnDetection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "server_vad" + }, + "prefix_padding_ms": { + "type": "integer", + "minimum": 0 + }, + "silence_duration_ms": { + "type": "integer", + "minimum": 0 + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "OpenAI Realtime API only." + }, + "idle_timeout_ms": { + "type": "integer", + "minimum": 0, + "description": "OpenAI Realtime API only." + }, + "start_of_speech_sensitivity": { + "type": "string", + "enum": [ + "START_SENSITIVITY_HIGH", + "START_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + }, + "end_of_speech_sensitivity": { + "type": "string", + "enum": [ + "END_SENSITIVITY_HIGH", + "END_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "AgentRequest_ServerVadTurnDetectionPatch": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "server_vad" + }, + { + "type": "null" + } + ] + }, + "prefix_padding_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "silence_duration_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "threshold": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "OpenAI Realtime API only." + }, + { + "type": "null" + } + ] + }, + "idle_timeout_ms": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "description": "OpenAI Realtime API only." + }, + { + "type": "null" + } + ] + }, + "start_of_speech_sensitivity": { + "anyOf": [ + { + "type": "string", + "enum": [ + "START_SENSITIVITY_HIGH", + "START_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + }, + { + "type": "null" + } + ] + }, + "end_of_speech_sensitivity": { + "anyOf": [ + { + "type": "string", + "enum": [ + "END_SENSITIVITY_HIGH", + "END_SENSITIVITY_LOW" + ], + "description": "Gemini Live only." + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "SemanticVadTurnDetection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "semantic_vad" + }, + "eagerness": { + "type": "string", + "enum": [ + "auto", + "low", + "medium", + "high" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "AgentRequest_SemanticVadTurnDetectionPatch": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "semantic_vad" + }, + { + "type": "null" + } + ] + }, + "eagerness": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auto", + "low", + "medium", + "high" + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "RealtimeTurnDetection": { + "oneOf": [ + { + "$ref": "#/components/schemas/AgoraVadTurnDetection" + }, + { + "$ref": "#/components/schemas/ServerVadTurnDetection" + }, + { + "$ref": "#/components/schemas/SemanticVadTurnDetection" + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "AgentRequest_RealtimeTurnDetectionPatch": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AgoraVadTurnDetectionPatch" + }, + { + "$ref": "#/components/schemas/AgentRequest_ServerVadTurnDetectionPatch" + }, + { + "$ref": "#/components/schemas/AgentRequest_SemanticVadTurnDetectionPatch" + } + ], + "description": "JSON Merge Patch of realtime turn detection. Omitted type retains the stored discriminator; the merged result must validate against RealtimeTurnDetection." + }, + "AgentRequest_MllmInput": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + }, + "required": [ + "credential" + ] + } + }, + { + "properties": { + "credential": { + "properties": { + "fields": { + "additionalProperties": { + "type": "string", + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + } + } + } + } + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/AgentRequest_McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mcp Servers" + }, + "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": { + "type": "string" + }, + "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" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers", + "writeOnly": true + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + } + }, + "title": "AgentRequest_MllmInput", + "type": "object", + "required": [ + "vendor" + ], + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "MllmInput": { + "allOf": [ + { + "if": { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "then": { + "properties": { + "credential": { + "properties": { + "mode": { + "const": "byok" + } + }, + "type": "object", + "required": [ + "mode" + ] + } + }, + "required": [ + "credential" + ] + } + } + ], + "properties": { + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/McpServer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "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": { + "type": "string" + }, + "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" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "writeOnly": true + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "title": "MllmInput", + "required": [ + "vendor" + ], + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "AgentRequest_MllmPatch": { + "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" + }, + "credential": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmCredentialPatch" + }, + { + "type": "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" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers", + "writeOnly": true + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + } + }, + "title": "AgentRequest_MllmPatch", + "type": "object", + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "ResolvedMllm": { + "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" + }, + "input_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "output_modalities": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "audio" + ] + } + }, + { + "type": "null" + } + ] + }, + "mcp_servers": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/McpServer" + } + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ResolvedMllm", + "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." + }, + "AgentRequest_MllmCredentialPatch": { + "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": [ + { + "type": "object", + "maxProperties": 8, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "title": "PublicCredentialPatch", + "type": "object" + }, + "AgentRequest_RealtimePipelineInput": { + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "const": "realtime" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarInput" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/RealtimeTurnDetection" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmInput" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "mode" + ], + "title": "AgentRequest_RealtimePipelineInput", + "type": "object" + }, + "RealtimePipelineInput": { + "properties": { + "mode": { + "type": "string", + "const": "realtime" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AvatarInput" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/RealtimeTurnDetection" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/MllmInput" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "RealtimePipelineInput" + }, + "ResolvedRealtimePipeline": { + "properties": { + "mode": { + "type": "string", + "const": "realtime" + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedAvatar" + }, + { + "type": "null" + } + ] + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/RealtimeTurnDetection" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResolvedMllm" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "mode" + ], + "title": "ResolvedRealtimePipeline" + }, + "AgentRequest_RealtimePipelinePatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "type": "string", + "const": "realtime" + }, + { + "type": "null" + } + ] + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + }, + { + "type": "null" + } + ] + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmPatch" + }, + { + "type": "null" + } + ] + } + }, + "title": "AgentRequest_RealtimePipelinePatch", + "type": "object" + }, + "AgentRequest_PipelinePatch": { + "additionalProperties": false, + "properties": { + "mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "cascaded", + "realtime" + ] + }, + { + "type": "null" + } + ] + }, + "avatar": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_AvatarPatch" + }, + { + "type": "null" + } + ], + "default": null + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + }, + { + "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + }, + { + "type": "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 + }, + "mllm": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_MllmPatch" + }, + { + "type": "null" + } + ] + } + }, + "title": "PipelinePatch", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "mode": { + "const": "realtime" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "properties": { + "asr": { + "type": "null" + }, + "llm": { + "type": "null" + }, + "tts": { + "type": "null" + }, + "silence_config": { + "type": "null" + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" + }, + { + "type": "null" + } + ] + } + } + } + }, + { + "if": { + "properties": { + "mode": { + "const": "cascaded" + } + }, + "required": [ + "mode" + ] + }, + "then": { + "properties": { + "mllm": { + "type": "null" + }, + "turn_detection": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" + }, + { + "type": "null" + } + ] + } + } + } + } + ] + } + }, + "securitySchemes": { + "AgoraTokenAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "An Agora token produced by the existing token system. Send the complete header value: agora token=. SDKs accept a supplied token or generate one from App ID and App Certificate; Customer ID/Customer Secret authentication is not exposed." + } + }, + "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": [] + } + ] +} diff --git a/src/agora_agent/v3/session_contract.py b/src/agora_agent/v3/session_contract.py index c3e431f..97d641f 100644 --- a/src/agora_agent/v3/session_contract.py +++ b/src/agora_agent/v3/session_contract.py @@ -57,12 +57,12 @@ def _validate_rtc_transport(transport): raise ValueError("channel must contain 1-256 characters") uid = transport.get("uid") subscribers = transport.get("subscribe_uids") - if not isinstance(uid, str) or not uid.strip(): + 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 (subscribers[0] == "*" or re.fullmatch(r"0+", subscribers[0])): + 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 != "0" and numeric(uid) != numeric(subscribers[0]): + 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"] @@ -108,7 +108,7 @@ def _validate_telephony_transport(transport): 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"}, "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: diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index 6ebd287..cfee764 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -1,4 +1,4 @@ -"""Typed staging OpenAPI wire schemas. Dictionaries preserve omitted versus null fields.""" +"""Generated wire types from root openapi.json; constraints live in the shared contract.""" from __future__ import annotations from typing import Any, Dict, List, Union @@ -11,7 +11,7 @@ class AgentDeleteResponse(TypedDict, total=False): class AgentListData(TypedDict, total=False): count: Required[int] - list: Required[List['AgentListItem']] + list: Required[List[AgentListItem]] class AgentListItem(TypedDict, total=False): agent_id: Required[str] @@ -20,7 +20,7 @@ class AgentListItem(TypedDict, total=False): update_ts: Required[int] class AgentListResponse(TypedDict, total=False): - data: Required['AgentListData'] + data: Required[AgentListData] has_more: Required[bool] next_cursor: Required[Union[str, None]] @@ -30,18 +30,19 @@ class AgentResponse(TypedDict, total=False): 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[Union['ResolvedCascadedPipeline', 'ResolvedRealtimePipeline'], None] - filler_words: Union['FillerWords', None] - -class AgoraVadTurnDetection(TypedDict, total=False): - type: Required[Literal['agora_vad']] - interrupt_duration_ms: Union[int, None] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, int, None] + messages: Union[List[Message], None] + greeting: Union[Union[TextGreeting, AudioGreeting], None] + variables: Union[Dict[str, VariableDefinition], None] + pipeline: Union[ResolvedCascadedPipeline, ResolvedRealtimePipeline, 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']] @@ -52,21 +53,137 @@ class AudioGreeting(TypedDict, total=False): download_timeout_ms: int pcm_sample_rate: int -class DisabledInterruption(TypedDict, total=False): - mode: Required[Literal['off']] - when_off: Literal['append', 'ignore'] +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 EndCallPolicy(TypedDict, total=False): +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]] -ErrorReason = Literal['InvalidRequestBody', 'MissingRequiredField', 'InvalidFieldValue', 'InvalidModuleParameter', 'UnknownSecretReference', 'NotImplemented', 'InvalidPermission', 'ServiceNotEnabled', 'AccountSuspended', 'TaskNotFound', 'ResourceNotFound', 'AgentNotFound', 'NumberAlreadyExists', 'ResourceConflict', 'MethodNotAllowed', 'PayloadTooLarge', 'UnsupportedMediaType', 'TaskConflict', 'IdempotencyKeyConflict', 'SecretSetAlreadyExists', 'SecretSetInUse', 'SecretSetEmpty', 'TaskNotRunning', 'RateLimitExceeded', 'PreconditionFailed', 'PreconditionRequired', 'ResourceQuotaLimitExceeded', 'ConcurrencyLimitExceeded', 'InternalError', 'ServiceUnavailable', 'ResourceAllocationFailed', 'TaskOperationTimeout'] +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'] + reason: Required[ErrorReason] class FillerTrigger(TypedDict, total=False): mode: Required[Literal['fixed_time']] @@ -74,37 +191,83 @@ class FillerTrigger(TypedDict, total=False): class FillerWords(TypedDict, total=False): enable: Union[bool, None] - trigger: Union['FillerTrigger', None] - content: Union[Union['StaticFillerContent', 'GeneratedFillerContent'], 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['EndCallPolicy', None] - transfer: Union['TransferPolicy', 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] + call_policy: Union[InboundCallPolicy, None] class InboundOutput(TypedDict, total=False): agent: Required[Union[str, None]] - call_policy: Union['InboundCallPolicy', None] - -JsonValue = Any + 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] @@ -112,99 +275,132 @@ class Message(TypedDict, total=False): class NumberCreate(TypedDict, total=False): number: Required[str] description: Union[str, None] - sip_trunk: Required['SipTrunkInput'] - inbound: Union['InboundInput', 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']] + list: Required[List[NumberResponse]] class NumberListResponse(TypedDict, total=False): - data: Required['NumberListData'] + 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] + 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] + 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, RealtimePipelineInput, 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] + 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] + params: Union[Dict[str, JsonValue], None] class ResolvedAvatar(TypedDict, total=False): enabled: Union[bool, None] - credential: Union['ResolvedCredential', None] + credential: Union[ResolvedCredential, None] vendor: Union[str, None] - params: Union[Dict[str, 'JsonValue'], 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] + 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] + 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] + 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[Dict[str, 'JsonValue']], None] - tools: Union[List[Dict[str, 'JsonValue']], None] - -class ResolvedMllm(TypedDict, total=False): - credential: Union['ResolvedCredential', None] - vendor: Union[str, None] - model: Union[str, None] - url: Union[str, None] - params: Union[Dict[str, 'JsonValue'], None] - input_modalities: Union[List[str], None] - output_modalities: Union[List[str], None] - mcp_servers: Union[List[Dict[str, 'JsonValue']], None] - -class ResolvedRealtimePipeline(TypedDict, total=False): - mode: Required[Literal['realtime']] - avatar: Union['ResolvedAvatar', None] - turn_detection: Union[Union['AgoraVadTurnDetection', 'ServerVadTurnDetection', 'SemanticVadTurnDetection'], None] - mllm: Union['ResolvedMllm', None] + mcp_servers: Union[List[McpServer], None] + tools: Union[List[FunctionTool], None] class ResolvedTts(TypedDict, total=False): - credential: Union['ResolvedCredential', None] + credential: Union[ResolvedCredential, None] url: Union[str, None] vendor: Union[str, None] model: Union[str, None] - params: Union[Dict[str, 'JsonValue'], 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]] @@ -219,13 +415,13 @@ class SecretKeyWrite(TypedDict, total=False): value: Required[str] class SecretList(TypedDict, total=False): - data: Required['SecretPage'] + 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']] + list: Required[List[SecretSet]] class SecretReplace(TypedDict, total=False): secrets: Required[Dict[str, str]] @@ -242,56 +438,77 @@ class SecretUpdate(TypedDict, total=False): class SemanticTurnEnd(TypedDict, total=False): type: Required[Literal['semantic']] silence_duration_ms: Union[int, None] - max_wait_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 SemanticVadTurnDetection(TypedDict, total=False): - type: Required[Literal['semantic_vad']] - eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] +class SessionAgentOverridesPatch(TypedDict, total=False): + messages: List[Message] + pipeline: SessionPipelinePatch -class ServerVadTurnDetection(TypedDict, total=False): - type: Required[Literal['server_vad']] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, int, None] - idle_timeout_ms: Union[int, None] - start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] - end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] +class SessionAgentPatch(TypedDict, total=False): + overrides: SessionAgentOverridesPatch -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]] +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']] + list: Required[List[SessionSummary]] class SessionList(TypedDict, total=False): - data: Required['SessionData'] + 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 + mllm: 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']] - agent_name: str 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] @@ -307,8 +524,8 @@ class SipTrunkInput(TypedDict, total=False): hostname: Required[str] port: Union[int, None] transport: Literal['udp', 'tcp', 'tls'] - auth: Union['SipAuth', None] - allowed_ips: Any + auth: Union[SipAuth, None] + allowed_ips: Union[List[str], None] class SipTrunkOutput(TypedDict, total=False): name: Union[str, None] @@ -325,21 +542,45 @@ class StaticFillerContent(TypedDict, total=False): 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] + 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']] @@ -358,14 +599,44 @@ class VariableDefinition(TypedDict, total=False): extract: bool description: Union[str, None] -class AgentCreateModel_AgoraVadTurnDetection(TypedDict, total=False): - type: Required[Literal['agora_vad']] - interrupt_duration_ms: Union[int, None] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, int, 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, AgentRequest_RealtimePipelineInput, 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_PipelinePatch, 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 AgentCreateModel_AudioGreeting(TypedDict, total=False): +class AgentRequest_AudioGreeting(TypedDict, total=False): mode: Required[Literal['audio']] on: Literal['each_join', 'first_join'] delay_ms: int @@ -374,307 +645,288 @@ class AgentCreateModel_AudioGreeting(TypedDict, total=False): download_timeout_ms: int pcm_sample_rate: int -class AgentCreateModel_ByokCredential(TypedDict, total=False): - mode: Required[Literal['byok']] - api_key: Required[str] +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 AgentCreateModel_CascadedPipeline(TypedDict, total=False): +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['AgentCreateModel_ProviderModule', None] - turn_detection: Union['AgentCreateModel_TurnDetection', None] - asr: Union['AgentCreateModel_ProviderModule', None] - tts: Union['AgentCreateModel_ProviderModule', None] - llm: Union['AgentCreateModel_ProviderModule', None] - silence_config: Union['AgentCreateModel_SilenceConfig', None] - -class AgentCreateModel_DisabledInterruption(TypedDict, total=False): + 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 AgentCreateModel_FillerTrigger(TypedDict, total=False): +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 AgentCreateModel_FillerWords(TypedDict, total=False): +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['AgentCreateModel_FillerTrigger', None] - content: Union[Union['AgentCreateModel_StaticFillerContent', 'AgentCreateModel_GeneratedFillerContent'], None] + trigger: Union[AgentRequest_FillerTriggerPatch, None] + content: Union[AgentRequest_FillerContentPatch, None] -class AgentCreateModel_GeneratedFillerContent(TypedDict, total=False): +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] -AgentCreateModel_JsonValue = Any +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 AgentCreateModel_KeywordInterruption(TypedDict, total=False): +class AgentRequest_KeywordInterruption(TypedDict, total=False): mode: Required[Literal['keywords']] keywords: Required[List[str]] -class AgentCreateModel_ManagedCredential(TypedDict, total=False): - mode: Required[Literal['managed']] - -class AgentCreateModel_ManualTurnBoundary(TypedDict, total=False): - type: Required[Literal['manual']] - -class AgentCreateModel_Message(TypedDict, total=False): - role: Required[Literal['system', 'user', 'assistant']] - content: Required[str] - -class AgentCreateModel_ProviderModule(TypedDict, total=False): - credential: Union[Union['AgentCreateModel_ManagedCredential', 'AgentCreateModel_ByokCredential'], None] +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] - model: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] style: Union[str, None] - language: Union[str, None] - enabled: Union[bool, None] - params: Union[Dict[str, 'AgentCreateModel_JsonValue'], 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] - mcp_servers: Union[List[Dict[str, 'AgentCreateModel_JsonValue']], None] - tools: Union[List[Dict[str, 'AgentCreateModel_JsonValue']], None] - skip_patterns: Union[List[str], None] -class AgentCreateModel_RealtimePipeline(TypedDict, total=False): - mode: Required[Literal['realtime']] - avatar: Union['AgentCreateModel_ProviderModule', None] - turn_detection: Union[Union['AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SemanticVadTurnDetection'], None] - mllm: Union['AgentCreateModel_ProviderModule', None] +class AgentRequest_ManagedCredential(TypedDict, total=False): + mode: Required[Literal['managed']] + +class AgentRequest_ManualTurnBoundary(TypedDict, total=False): + type: Required[Literal['manual']] -class AgentCreateModel_SemanticTurnEnd(TypedDict, total=False): +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, None] + max_wait_ms: Union[int, Literal[-1], None] pause_state_enabled: Union[bool, None] -class AgentCreateModel_SemanticTurnStart(TypedDict, total=False): +class AgentRequest_SemanticTurnStart(TypedDict, total=False): type: Required[Literal['semantic']] speaking_interrupt_duration_ms: Union[int, None] -class AgentCreateModel_SemanticVadTurnDetection(TypedDict, total=False): - type: Required[Literal['semantic_vad']] - eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] - -class AgentCreateModel_ServerVadTurnDetection(TypedDict, total=False): - type: Required[Literal['server_vad']] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, int, None] - idle_timeout_ms: Union[int, None] - start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] - end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] - -class AgentCreateModel_SilenceConfig(TypedDict, total=False): +class AgentRequest_SilenceConfig(TypedDict, total=False): enabled: Required[bool] timeout_ms: Required[int] action: Required[Literal['speak', 'think']] content: Required[str] -class AgentCreateModel_SpeechInterruption(TypedDict, total=False): +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 AgentCreateModel_StaticFillerContent(TypedDict, total=False): +class AgentRequest_StaticFillerContent(TypedDict, total=False): mode: Required[Literal['static']] phrases: Required[List[str]] selection_rule: Literal['shuffle', 'round_robin'] -class AgentCreateModel_TextGreeting(TypedDict, total=False): +class AgentRequest_TextGreeting(TypedDict, total=False): mode: Required[Literal['text']] on: Literal['each_join', 'first_join'] delay_ms: int text: Required[str] -class AgentCreateModel_TurnDetection(TypedDict, total=False): +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['AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_ManualTurnBoundary'], None] - end_of_speech: Union[Union['AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_ManualTurnBoundary'], None] - interruption: Union[Union['AgentCreateModel_SpeechInterruption', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_DisabledInterruption'], 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 AgentCreateModel_VadTurnEnd(TypedDict, total=False): +class AgentRequest_VadTurnEnd(TypedDict, total=False): type: Required[Literal['vad']] silence_duration_ms: Union[int, None] -class AgentCreateModel_VadTurnStart(TypedDict, total=False): +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 AgentCreateModel_VariableDefinition(TypedDict, total=False): +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 AgentCreateModel(TypedDict, total=False): - agent_name: Required[str] - instructions: Union[str, None] - messages: Union[List['AgentCreateModel_Message'], None] - greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] - variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] - pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] - filler_words: Union['AgentCreateModel_FillerWords', None] - -class AgentPatchRequest_CredentialPatch(TypedDict, total=False): - mode: Union[Literal['managed', 'byok'], None] - api_key: Union[str, None] - -class AgentPatchRequest_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 AgentPatchRequest_FillerTriggerPatch(TypedDict, total=False): - mode: Union[Literal['fixed_time'], None] - response_wait_ms: Union[int, None] - -class AgentPatchRequest_FillerWordsPatch(TypedDict, total=False): - enable: Union[bool, None] - trigger: Union['AgentPatchRequest_FillerTriggerPatch', None] - content: Union['AgentPatchRequest_FillerContentPatch', None] - -class AgentPatchRequest_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 AgentPatchRequest_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] - -AgentPatchRequest_JsonValue = Any - -class AgentPatchRequest_Message(TypedDict, total=False): - role: Required[Literal['system', 'user', 'assistant']] - content: Required[str] - -class AgentPatchRequest_PipelinePatch(TypedDict, total=False): - mode: Union[Literal['cascaded', 'realtime'], None] - avatar: Union['AgentPatchRequest_ProviderModulePatch', None] - turn_detection: Union['AgentPatchRequest_PipelineTurnDetectionPatch', None] - asr: Union['AgentPatchRequest_ProviderModulePatch', None] - tts: Union['AgentPatchRequest_ProviderModulePatch', None] - llm: Union['AgentPatchRequest_ProviderModulePatch', None] - mllm: Union['AgentPatchRequest_ProviderModulePatch', None] - silence_config: Union['AgentPatchRequest_SilenceConfigPatch', None] - -class AgentPatchRequest_PipelineTurnDetectionPatch(TypedDict, total=False): - type: Union[Literal['agora_vad', 'server_vad', 'semantic_vad'], None] - speech_threshold: Union[float, int, None] - start_of_speech: Union['AgentPatchRequest_TurnBoundaryPatch', None] - end_of_speech: Union['AgentPatchRequest_TurnBoundaryPatch', None] - interruption: Union['AgentPatchRequest_InterruptionPatch', None] - interrupt_duration_ms: Union[int, None] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, int, None] - idle_timeout_ms: Union[int, None] - start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] - end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] - eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] - -class AgentPatchRequest_ProviderModulePatch(TypedDict, total=False): - credential: Union['AgentPatchRequest_CredentialPatch', None] - vendor: Union[str, None] - model: Union[str, None] - style: Union[str, None] - language: Union[str, None] - enabled: Union[bool, None] - params: Union[Dict[str, 'AgentPatchRequest_JsonValue'], None] - url: 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] - mcp_servers: Union[List[Dict[str, 'AgentPatchRequest_JsonValue']], None] - tools: Union[List[Dict[str, 'AgentPatchRequest_JsonValue']], None] - skip_patterns: Union[List[str], None] - -class AgentPatchRequest_SilenceConfigPatch(TypedDict, total=False): - enabled: Union[bool, None] - timeout_ms: Union[int, None] - action: Union[Literal['speak', 'think'], None] - content: Union[str, None] - -class AgentPatchRequest_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, None] - pause_state_enabled: Union[bool, None] - -class AgentPatchRequest_VariableDefinitionPatch(TypedDict, total=False): +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 AgentPatchRequest(TypedDict, total=False): - agent_name: Union[str, None] - instructions: Union[str, None] - messages: Union[List['AgentPatchRequest_Message'], None] - greeting: Union['AgentPatchRequest_GreetingPatch', None] - variables: Union[Dict[str, Union['AgentPatchRequest_VariableDefinitionPatch', None]], None] - pipeline: Union['AgentPatchRequest_PipelinePatch', None] - filler_words: Union['AgentPatchRequest_FillerWordsPatch', None] - -Contract_VariableDefinitions = Dict[str, 'Contract_VariableDefinition'] - -Contract_ModelCredential = Union[Dict[str, Any], Dict[str, Any]] - -class Contract_Llm(TypedDict, total=False): - credential: 'Contract_ModelCredential' - url: str - vendor: str - style: str - model: str - params: Dict[str, Any] - input_modalities: List[str] - output_modalities: List[str] - max_history: int - failure_message: str - mcp_servers: List['Contract_McpServer'] - tools: List['Contract_FunctionTool'] - headers: Dict[str, str] - -class Contract_CascadedPipeline(TypedDict, total=False): - mode: Required[Literal['cascaded']] - avatar: 'Contract_Avatar' - turn_detection: 'Contract_TurnDetection' - asr: 'Contract_Asr' - tts: 'Contract_Tts' - llm: 'Contract_Llm' - silence_config: 'Contract_SilenceConfig' +class SessionEndCallPolicy(TypedDict, total=False): + on_conversation_complete: bool + on_user_request: bool + on_fax: bool + on_ai_assistant: bool + on_voicemail: bool -Contract_Pipeline = Union['Contract_CascadedPipeline', 'Contract_RealtimePipeline'] +class InboundEndCallPolicy(TypedDict, total=False): + on_conversation_complete: bool + on_user_request: bool + on_voicemail: bool + on_ai_assistant: bool + on_fax: bool -class Contract_SemanticVadTurnDetection(TypedDict, total=False): - type: Required[Literal['semantic_vad']] - eagerness: Literal['auto', 'low', 'medium', 'high'] +class AgoraVadTurnDetection(TypedDict, total=False): + type: Required[Literal['agora_vad']] + interrupt_duration_ms: int + prefix_padding_ms: int + silence_duration_ms: int + threshold: float -class Contract_SilenceConfig(TypedDict, total=False): - enabled: Required[bool] - timeout_ms: Required[int] - action: Required[Literal['speak', 'think']] - content: Required[str] +class AgentRequest_AgoraVadTurnDetectionPatch(TypedDict, total=False): + type: Union[Literal['agora_vad'], None] + interrupt_duration_ms: Union[int, None] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, None] -class Contract_ServerVadTurnDetection(TypedDict, total=False): +class ServerVadTurnDetection(TypedDict, total=False): type: Required[Literal['server_vad']] prefix_padding_ms: int silence_duration_ms: int @@ -683,359 +935,129 @@ class Contract_ServerVadTurnDetection(TypedDict, total=False): start_of_speech_sensitivity: Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'] end_of_speech_sensitivity: Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'] -class Contract_McpServer(TypedDict, total=False): - name: Required[str] - endpoint: Required[str] - transport: Required[Literal['streamable_http']] - headers: Dict[str, str] - allowed_tools: List[str] - timeout_ms: int - -class Contract_VariableDefinition(TypedDict, total=False): - type: Required[Literal['str', 'int', 'float', 'bool', 'date', 'datetime']] - required: bool - options: List[Union[str, float, bool]] - extract: bool - description: str - -Contract_Greeting = Union[Dict[str, Any], Dict[str, Any]] - -class Contract_Asr(TypedDict, total=False): - credential: 'Contract_ModelCredential' - url: str - vendor: str - model: str - language: str - params: Dict[str, Any] - -class Contract_FillerWords(TypedDict, total=False): - enable: bool - trigger: Dict[str, Any] - content: Union[Dict[str, Any], Dict[str, Any]] - -Contract_Interruption = Union[Dict[str, Any], Dict[str, Any], Dict[str, Any]] +class AgentRequest_ServerVadTurnDetectionPatch(TypedDict, total=False): + type: Union[Literal['server_vad'], None] + prefix_padding_ms: Union[int, None] + silence_duration_ms: Union[int, None] + threshold: Union[float, None] + idle_timeout_ms: Union[int, None] + start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] + end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] -Contract_TurnStart = Union[Dict[str, Any], Dict[str, Any], Dict[str, Any]] +class SemanticVadTurnDetection(TypedDict, total=False): + type: Required[Literal['semantic_vad']] + eagerness: Literal['auto', 'low', 'medium', 'high'] -class Contract_FunctionTool(TypedDict, total=False): - type: Required[Literal['function']] - name: Required[str] - description: Required[str] - parameters: Required['Contract_JsonSchema'] - execution: Required['Contract_FunctionToolExecution'] - server: Required['Contract_FunctionToolServer'] - -class Contract_Tts(TypedDict, total=False): - credential: 'Contract_ModelCredential' - url: str - headers: Dict[str, str] - vendor: str - params: Dict[str, Any] - skip_patterns: List[Literal['fullwidth_parentheses', 'lenticular_brackets', 'parentheses', 'square_brackets', 'curly_braces']] - -class Contract_RealtimePipeline(TypedDict, total=False): - mode: Required[Literal['realtime']] - avatar: 'Contract_Avatar' - turn_detection: 'Contract_RealtimeTurnDetection' - mllm: 'Contract_Mllm' +class AgentRequest_SemanticVadTurnDetectionPatch(TypedDict, total=False): + type: Union[Literal['semantic_vad'], None] + eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] -class Contract_Mllm(TypedDict, total=False): - credential: 'Contract_ModelCredential' +class AgentRequest_MllmInput(TypedDict, total=False): + mcp_servers: Union[List[AgentRequest_McpServer], None] + credential: Union[Union[AgentRequest_ManagedCredential, AgentRequest_PublicByokCredential], None] vendor: Required[str] - model: str - url: str - params: Dict[str, Any] - input_modalities: List[Literal['text', 'audio']] - output_modalities: List[Literal['text', 'audio']] - mcp_servers: List['Contract_McpServer'] - headers: Dict[str, str] - -class Contract_Avatar(TypedDict, total=False): - enabled: bool - credential: 'Contract_ModelCredential' - vendor: Literal['akool', 'liveavatar', 'anam', 'generic'] - params: Dict[str, Any] - -Contract_RealtimeTurnDetection = Union['Contract_AgoraVadTurnDetection', 'Contract_ServerVadTurnDetection', 'Contract_SemanticVadTurnDetection'] - -class Contract_FunctionToolServer(TypedDict, total=False): - method: Required[Literal['GET', 'POST']] - url: Required[str] - headers: Dict[str, str] - body: Dict[str, Any] - timeout_ms: int - -Contract_TurnEnd = Union[Dict[str, Any], Dict[str, Any], Dict[str, Any]] - -class Contract_Message(TypedDict, total=False): - role: Required[Literal['system', 'user', 'assistant']] - content: Required[str] - -class Contract_AgentCreateInput(TypedDict, total=False): - agent_name: Required[str] - instructions: str - messages: List['Contract_Message'] - greeting: 'Contract_Greeting' - variables: 'Contract_VariableDefinitions' - pipeline: 'Contract_Pipeline' - filler_words: 'Contract_FillerWords' - -class Contract_TurnDetection(TypedDict, total=False): - speech_threshold: float - start_of_speech: 'Contract_TurnStart' - end_of_speech: 'Contract_TurnEnd' - interruption: 'Contract_Interruption' - -class Contract_AgoraVadTurnDetection(TypedDict, total=False): - type: Required[Literal['agora_vad']] - interrupt_duration_ms: int - prefix_padding_ms: int - silence_duration_ms: int - threshold: float - -class Contract_FunctionToolExecution(TypedDict, total=False): - mode: Required[Literal['sync']] - -Contract_JsonSchema = Union[Dict[str, Any], bool] - -class AgentCreateRequest(TypedDict, total=False): - agent_name: Required[str] - instructions: Union[str, None] - messages: Union[List['AgentCreateModel_Message'], None] - greeting: Union[Union['AgentCreateModel_TextGreeting', 'AgentCreateModel_AudioGreeting'], None] - variables: Union[Dict[str, 'AgentCreateModel_VariableDefinition'], None] - pipeline: Union[Union['AgentCreateModel_CascadedPipeline', 'AgentCreateModel_RealtimePipeline'], None] - filler_words: Union['AgentCreateModel_FillerWords', None] - -__all__ = ['AgentDeleteResponse', 'AgentListData', 'AgentListItem', 'AgentListResponse', 'AgentResponse', 'AgoraVadTurnDetection', 'AudioGreeting', 'DisabledInterruption', 'EndCallPolicy', 'ErrorReason', 'ErrorResponse', 'FillerTrigger', 'FillerWords', 'GeneratedFillerContent', 'InboundCallPolicy', 'InboundInput', 'InboundOutput', 'JsonValue', 'KeywordInterruption', 'ManualTurnBoundary', 'Message', 'NumberCreate', 'NumberDeleteResponse', 'NumberListData', 'NumberListResponse', 'NumberPatch', 'NumberResponse', 'ResolvedAsr', 'ResolvedAvatar', 'ResolvedCascadedPipeline', 'ResolvedCredential', 'ResolvedLlm', 'ResolvedMllm', 'ResolvedRealtimePipeline', 'ResolvedTts', 'SecretCreate', 'SecretDeleted', 'SecretKey', 'SecretKeyWrite', 'SecretList', 'SecretPage', 'SecretReplace', 'SecretSet', 'SecretUpdate', 'SemanticTurnEnd', 'SemanticTurnStart', 'SemanticVadTurnDetection', 'ServerVadTurnDetection', 'SessionAnalysis', 'SessionData', 'SessionList', 'SessionResponse', 'SessionSummary', 'SilenceConfig', 'SipAuth', 'SipTrunkInput', 'SipTrunkOutput', 'SpeechInterruption', 'StaticFillerContent', 'TextGreeting', 'TransferPolicy', 'TurnDetection', 'VadTurnEnd', 'VadTurnStart', 'VariableDefinition', 'AgentCreateModel_AgoraVadTurnDetection', 'AgentCreateModel_AudioGreeting', 'AgentCreateModel_ByokCredential', 'AgentCreateModel_CascadedPipeline', 'AgentCreateModel_DisabledInterruption', 'AgentCreateModel_FillerTrigger', 'AgentCreateModel_FillerWords', 'AgentCreateModel_GeneratedFillerContent', 'AgentCreateModel_JsonValue', 'AgentCreateModel_KeywordInterruption', 'AgentCreateModel_ManagedCredential', 'AgentCreateModel_ManualTurnBoundary', 'AgentCreateModel_Message', 'AgentCreateModel_ProviderModule', 'AgentCreateModel_RealtimePipeline', 'AgentCreateModel_SemanticTurnEnd', 'AgentCreateModel_SemanticTurnStart', 'AgentCreateModel_SemanticVadTurnDetection', 'AgentCreateModel_ServerVadTurnDetection', 'AgentCreateModel_SilenceConfig', 'AgentCreateModel_SpeechInterruption', 'AgentCreateModel_StaticFillerContent', 'AgentCreateModel_TextGreeting', 'AgentCreateModel_TurnDetection', 'AgentCreateModel_VadTurnEnd', 'AgentCreateModel_VadTurnStart', 'AgentCreateModel_VariableDefinition', 'AgentCreateModel', 'AgentPatchRequest_CredentialPatch', 'AgentPatchRequest_FillerContentPatch', 'AgentPatchRequest_FillerTriggerPatch', 'AgentPatchRequest_FillerWordsPatch', 'AgentPatchRequest_GreetingPatch', 'AgentPatchRequest_InterruptionPatch', 'AgentPatchRequest_JsonValue', 'AgentPatchRequest_Message', 'AgentPatchRequest_PipelinePatch', 'AgentPatchRequest_PipelineTurnDetectionPatch', 'AgentPatchRequest_ProviderModulePatch', 'AgentPatchRequest_SilenceConfigPatch', 'AgentPatchRequest_TurnBoundaryPatch', 'AgentPatchRequest_VariableDefinitionPatch', 'AgentPatchRequest', 'Contract_VariableDefinitions', 'Contract_ModelCredential', 'Contract_Llm', 'Contract_CascadedPipeline', 'Contract_Pipeline', 'Contract_SemanticVadTurnDetection', 'Contract_SilenceConfig', 'Contract_ServerVadTurnDetection', 'Contract_McpServer', 'Contract_VariableDefinition', 'Contract_Greeting', 'Contract_Asr', 'Contract_FillerWords', 'Contract_Interruption', 'Contract_TurnStart', 'Contract_FunctionTool', 'Contract_Tts', 'Contract_RealtimePipeline', 'Contract_Mllm', 'Contract_Avatar', 'Contract_RealtimeTurnDetection', 'Contract_FunctionToolServer', 'Contract_TurnEnd', 'Contract_Message', 'Contract_AgentCreateInput', 'Contract_TurnDetection', 'Contract_AgoraVadTurnDetection', 'Contract_FunctionToolExecution', 'Contract_JsonSchema', 'AgentCreateRequest'] -# Session write shapes from the refreshed Bruno collection (not the older OpenAPI). -class RtcSessionTransport(TypedDict, total=False): - type: Required[Literal['rtc']] - channel: Required[str] - uid: Required[str] - subscribe_uids: Required[List[str]] - token: str - encryption: RtcEncryption - geofence: RtcGeofence - audio_scenario: Literal["aiserver", "chorus"] - - -class SessionLifecycle(TypedDict, total=False): - idle_timeout_ms: int - max_duration_ms: int - graceful_stop: GracefulStop - - -class EphemeralSessionAgent(TypedDict, total=False): - instructions: str - messages: List[AgentCreateModel_Message] - greeting: Union[AgentCreateModel_TextGreeting, AgentCreateModel_AudioGreeting] - variables: Dict[str, AgentCreateModel_VariableDefinition] - pipeline: Union[AgentCreateModel_CascadedPipeline, AgentCreateModel_RealtimePipeline] - filler_words: AgentCreateModel_FillerWords - - -class StartSessionResponse(TypedDict, total=False): - agent_session_id: str - status: str - create_ts: int - # Accepted only during backend rollout. - created_at: int - - -__all__ += ['RtcSessionTransport', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse'] - - -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: str - -class GracefulStop(TypedDict, total=False): - enabled: bool - timeout_ms: int - -class DataPolicy(TypedDict, total=False): - retention: Literal["30_days", "none"] - -__all__ += ["RtcEncryption", "GracefulStop", "DataPolicy"] - -# Campaign schemas from the supplied OpenAPI; retry pacing fields remain unconfirmed. - - -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]] - - -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 - - -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 ContactInput(TypedDict, total=False): - phone: Required[str] - values: Dict[str, Union[str, int, float, bool]] - - -class ImmediateSchedule(TypedDict, total=False): - mode: Required[Literal['immediate']] - timezone: str - days: List[CallingDay] - - -class Pacing(TypedDict, total=False): - max_concurrent: int - max_calls_per_second: Union[int, None] - max_attempts: int - retry_backoff_ms: int - - -class PacingPatch(TypedDict, total=False): - max_concurrent: Union[int, None] - max_calls_per_second: Union[int, None] - max_attempts: Union[int, None] - retry_backoff_ms: Union[int, None] - - -class ScheduledSchedule(TypedDict, total=False): - mode: Required[Literal['scheduled']] - timezone: Required[str] - start: Required[str] - end: Required[str] - days: List[CallingDay] - + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + model: Union[str, None] + headers: Union[Dict[str, str], None] + input_modalities: Union[List[Literal['text', 'audio']], None] + output_modalities: Union[List[Literal['text', 'audio']], None] -class TimeRange(TypedDict, total=False): - start: Required[str] - end: Required[str] +class MllmInput(TypedDict, total=False): + mcp_servers: Union[List[McpServer], None] + credential: Union[Union[ManagedCredential, PublicByokCredential], None] + vendor: Required[str] + params: Union[JSONObject, None] + url: Union[str, None] + model: Union[str, None] + headers: Union[Dict[str, str], None] + input_modalities: Union[List[Literal['text', 'audio']], None] + output_modalities: Union[List[Literal['text', 'audio']], None] -__all__ += ['CallingDay', 'CampaignCallPolicy', 'CampaignCounts', 'CampaignCreate', 'CampaignDeleteResponse', 'CampaignEndCallPolicy', 'CampaignFailure', 'CampaignLifecycle', 'CampaignListData', 'CampaignListResponse', 'CampaignPatch', 'CampaignResponse', 'CampaignTransferPolicy', 'CampaignTransport', 'CampaignTransportPatch', 'CampaignTransportResponse', 'CampaignVoicemailPolicy', 'ContactInput', 'ImmediateSchedule', 'Pacing', 'PacingPatch', 'ScheduledSchedule', 'TimeRange'] +class AgentRequest_MllmPatch(TypedDict, total=False): + mcp_servers: Union[List[AgentRequest_McpServer], None] + credential: Union[AgentRequest_MllmCredentialPatch, None] + vendor: Union[str, None] + params: Union[AgentRequest_JSONObject, None] + url: Union[str, None] + model: Union[str, None] + headers: Union[Dict[str, str], None] + input_modalities: Union[List[Literal['text', 'audio']], None] + output_modalities: Union[List[Literal['text', 'audio']], None] +class ResolvedMllm(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] + input_modalities: Union[List[Literal['text', 'audio']], None] + output_modalities: Union[List[Literal['text', 'audio']], None] + mcp_servers: Union[List[McpServer], None] -class VoicemailPolicy(TypedDict): - mode: Literal["continue", "hangup"] +class AgentRequest_MllmCredentialPatch(TypedDict, total=False): + mode: Union[Literal['managed', 'byok'], None] + fields: Union[Dict[str, Union[str, None]], None] -class CallPolicy(TypedDict, total=False): - max_ring_duration_ms: Union[int, None] - max_call_duration_seconds: Union[int, None] - max_silence_duration_ms: Union[int, None] - voicemail: Union[VoicemailPolicy, None] - end_call: Union[EndCallPolicy, None] - transfer: Union[TransferPolicy, None] +class AgentRequest_RealtimePipelineInput(TypedDict, total=False): + mode: Required[Literal['realtime']] + avatar: Union[AgentRequest_AvatarInput, None] + turn_detection: Union[RealtimeTurnDetection, None] + mllm: Union[AgentRequest_MllmInput, None] -TelephonyTransport = TypedDict("TelephonyTransport", { - "type": Required[Literal["telephony"]], - "from": Required[str], - "to": Required[str], - "call_policy": Union[CallPolicy, None], -}, total=False) +class RealtimePipelineInput(TypedDict, total=False): + mode: Required[Literal['realtime']] + avatar: Union[AvatarInput, None] + turn_detection: Union[RealtimeTurnDetection, None] + mllm: Union[MllmInput, None] -SessionTransport = Union[RtcSessionTransport, TelephonyTransport] -__all__ += ["VoicemailPolicy", "CallPolicy", "TelephonyTransport", "SessionTransport"] +class ResolvedRealtimePipeline(TypedDict, total=False): + mode: Required[Literal['realtime']] + avatar: Union[ResolvedAvatar, None] + turn_detection: Union[RealtimeTurnDetection, None] + mllm: Union[ResolvedMllm, None] +class AgentRequest_RealtimePipelinePatch(TypedDict, total=False): + mode: Union[Literal['realtime'], None] + avatar: Union[AgentRequest_AvatarPatch, None] + turn_detection: Union[AgentRequest_RealtimeTurnDetectionPatch, None] + mllm: Union[AgentRequest_MllmPatch, None] +class AgentRequest_PipelinePatch(TypedDict, total=False): + mode: Union[Literal['cascaded', 'realtime'], None] + avatar: Union[AgentRequest_AvatarPatch, None] + turn_detection: Union[AgentRequest_CascadedTurnDetectionPatch, AgentRequest_RealtimeTurnDetectionPatch, None] + asr: Union[AgentRequest_AsrPatch, None] + tts: Union[AgentRequest_TtsPatch, None] + llm: Union[AgentRequest_LlmPatch, None] + silence_config: Union[AgentRequest_SilenceConfigPatch, None] + mllm: Union[AgentRequest_MllmPatch, 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 +RealtimeTurnDetection = Union[AgoraVadTurnDetection, ServerVadTurnDetection, SemanticVadTurnDetection] +AgentRequest_RealtimeTurnDetectionPatch = Union[AgentRequest_AgoraVadTurnDetectionPatch, AgentRequest_ServerVadTurnDetectionPatch, AgentRequest_SemanticVadTurnDetectionPatch] + +# 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__ += ["RtcGeofence"] +__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', 'AgoraVadTurnDetection', 'AgentRequest_AgoraVadTurnDetectionPatch', 'ServerVadTurnDetection', 'AgentRequest_ServerVadTurnDetectionPatch', 'SemanticVadTurnDetection', 'AgentRequest_SemanticVadTurnDetectionPatch', 'RealtimeTurnDetection', 'AgentRequest_RealtimeTurnDetectionPatch', 'AgentRequest_MllmInput', 'MllmInput', 'AgentRequest_MllmPatch', 'ResolvedMllm', 'AgentRequest_MllmCredentialPatch', 'AgentRequest_RealtimePipelineInput', 'RealtimePipelineInput', 'ResolvedRealtimePipeline', 'AgentRequest_RealtimePipelinePatch', 'AgentRequest_PipelinePatch', 'AgentCreateModel', 'AgentPatchRequest', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse', 'RtcGeofence', 'RtcSessionTransport', 'SessionTransport'] diff --git a/tests/custom/test_v3_latest_contract.py b/tests/custom/test_v3_latest_contract.py new file mode 100644 index 0000000..9d744a4 --- /dev/null +++ b/tests/custom/test_v3_latest_contract.py @@ -0,0 +1,157 @@ +"""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, OpenAIRealtime, 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_mllm(OpenAIRealtime(api_key="inline", model="gpt-realtime")) + 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"]["mllm"]["credential"] == {"mode": "byok", "fields": {"api_key": "inline"}} + properties = {"token": "renewed", "messages": [{"role": "user", "content": "Hi"}], "mllm": {"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": {"mllm": properties["mllm"]}}}} + assert properties == before + for invalid in ({"llm": {"params": {}}, "mllm": {"params": {}}}, {"llm": {"params": {"url": "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"} diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index c4a8d84..f6c4feb 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -39,7 +39,7 @@ ("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": {"host": "sip.example"}}, "POST", "numbers", {"number": "+12025550123", "sip_trunk": {"host": "sip.example"}}), + ("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}}), @@ -48,6 +48,7 @@ ("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": {"mllm": {"params": {"temperature": 0.2}}}}}}, "PATCH", "sessions/s", {"transport": {"token": "new-token"}, "agent": {"overrides": {"pipeline": {"mllm": {"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), @@ -155,7 +156,7 @@ def handler(request): with pytest.raises(PreconditionFailedError) as caught: result = client.v3.agents.update( agent_id="a", if_match='"old"', instructions="updated", - request_options={"additional_body_parameters": {"unexpected": "write-only"}}, + request_options={"additional_body_parameters": {"messages": [{"role": "user", "content": "write-only"}]}}, ) if async_: await result @@ -164,12 +165,12 @@ def handler(request): latest = await latest assert latest == {"agent_id": "a"} assert calls[0].method == "PATCH" - assert json.loads(calls[0].content)["unexpected"] == "write-only" + 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": "+1", "sip_trunk": {}})]) +@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): @@ -197,7 +198,7 @@ def configured(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="tts-key", base_url="https://api.openai.com/v1/audio/speech", voice="alloy", model="tts-1"))) + .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(): @@ -214,8 +215,8 @@ def handler(request): 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", "language": "en-US", "credential": {"mode": "byok", "api_key": "$secrets.prod.dg"}} - assert body["pipeline"]["llm"]["credential"]["api_key"] == "$secrets.prod.openai" + assert body["pipeline"]["asr"] == {"vendor": "deepgram", "model": "nova-3", "language": "en-US", "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"') @@ -237,7 +238,7 @@ def handler(request): 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", "api_key": None} + assert patch["pipeline"]["llm"]["credential"] == {"mode": "managed", "fields": None} def test_real_turn_interruption_greeting_and_silence_mapping(): @@ -277,12 +278,12 @@ def test_strict_unsupported_fields_are_aggregated_before_network(): 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 any("aws_secret_access_key" in x for x in 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", "api_key": "key"} + assert result["credential"] == {"mode": "byok", "fields": {"key": "key"}} assert result["params"] == {"region": "eastus"} @@ -533,7 +534,7 @@ 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="key", model="nova-3", language="fr")) + .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") @@ -543,7 +544,7 @@ def handler(request): assert pipeline["asr"]["language"] == "de-DE" assert pipeline["asr"]["params"]["language"] == "fr" assert "language" not in pipeline.get("turn_detection", {}) - v2 = agent.to_properties(channel="room", agent_uid="1", remote_uids=["*"], token="rtc", + 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"] == pipeline["asr"]["params"]["language"] @@ -561,7 +562,7 @@ def handler(request): 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="key", model="nova-3", language="fr"))) + 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"] == "en-US" assert default["pipeline"]["asr"]["params"]["language"] == "fr" @@ -642,14 +643,15 @@ def handler(request): .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 agent.save_async(name="Support") + saved = await saved_config.save_async(name="Support") await saved.assign_number(number) - updated = await saved.update(agent.with_greeting("Updated")) + updated = await saved.update(saved_config.with_greeting("Updated")) assert isinstance(saved, AsyncSavedAgent) loaded = await AsyncSavedAgent.get(client, saved.id) else: @@ -657,9 +659,9 @@ def handler(request): 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 = agent.save(name="Support") + saved = saved_config.save(name="Support") saved.assign_number(number) - updated = saved.update(agent.with_greeting("Updated")) + 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" @@ -672,9 +674,9 @@ def handler(request): assert patch.headers["If-Match"] == '"current"' assert json.loads(patch.content)["agent_name"] == "Support" if async_: - renamed = await saved.update(agent, name="Renamed") + renamed = await saved.update(saved_config, name="Renamed") else: - renamed = saved.update(agent, name="Renamed") + renamed = saved.update(saved_config, name="Renamed") assert renamed.name == "Renamed" assert saved.name == "Support" @@ -717,7 +719,7 @@ def handler(request): else: assert path.endswith("/sessions/ephemeral") assert "agent_name" not in body["agent"] - assert body["agent"]["pipeline"]["llm"]["credential"]["api_key"] == "$secrets.prod.openai" + 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 "")) @@ -777,7 +779,7 @@ def handler(request): await session.start() else: session.start() - for method, args in (("interrupt", ()), ("think", ("test",)), ("update", ({},)), ("get_history", ()), ("get_turns", ()), ("get_all_turns", ())): + 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_: @@ -821,15 +823,15 @@ def handler(request): @pytest.mark.parametrize("async_", [False, True]) @pytest.mark.parametrize("stored", [False, True]) -def test_new_sessions_reject_cn_before_network(async_, stored): +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 - with pytest.raises(ValueError, match="CN"): - method(channel="r", agent_uid="1", remote_uids=["2"]) + session = method(channel="r", agent_uid="1", remote_uids=["2"]) + assert session.status == "idle" def test_new_sessions_reference_precedence_and_whitespace_uids(): @@ -1338,10 +1340,16 @@ def handler(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) - agent.save(name="Avatar") - body = json.loads(calls[0].content)["pipeline"]["avatar"] + 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", "api_key": key} + 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" @@ -1541,5 +1549,5 @@ async def test_explicit_blank_idempotency_key_rejected_before_http(async_, key, @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"): + 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) From d776c69bf6a20128cba0aabbfc7391a7e11a6797 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 15:04:58 -0400 Subject: [PATCH 38/57] Explicitly package preview contract snapshot and source handoff --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3e1797e..81bc17e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,12 @@ 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", From 946fec6a139d5bb8f838d4f9146814787acecd0d Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 15:05:34 -0400 Subject: [PATCH 39/57] Validate conditional provider credential constraints from contract --- src/agora_agent/v3/contract.py | 9 +++++++++ tests/custom/test_v3_latest_contract.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/agora_agent/v3/contract.py b/src/agora_agent/v3/contract.py index 09c2a26..4f3f36f 100644 --- a/src/agora_agent/v3/contract.py +++ b/src/agora_agent/v3/contract.py @@ -46,6 +46,15 @@ def validate(value, schema, path="body"): 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) diff --git a/tests/custom/test_v3_latest_contract.py b/tests/custom/test_v3_latest_contract.py index 9d744a4..134e4f6 100644 --- a/tests/custom/test_v3_latest_contract.py +++ b/tests/custom/test_v3_latest_contract.py @@ -155,3 +155,12 @@ async def test_no_body_operations_do_not_accept_hidden_write_body(async_): 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"]}}) From 642b86054d7e651bf5d754de0ff624a16fb9d37f Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 15:07:08 -0400 Subject: [PATCH 40/57] Reject runtime endpoint aliases consistently across SDKs --- src/agora_agent/v3/contract.py | 2 +- tests/custom/test_v3_latest_contract.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agora_agent/v3/contract.py b/src/agora_agent/v3/contract.py index 4f3f36f..b4fe29a 100644 --- a/src/agora_agent/v3/contract.py +++ b/src/agora_agent/v3/contract.py @@ -157,6 +157,6 @@ def validate_write(method, path, body): 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"}.intersection(module.get("params", {})): + if {"url", "base_url", "api_base_url", "endpoint"}.intersection(module.get("params", {})): raise ValueError("Session update cannot change endpoint aliases") return diff --git a/tests/custom/test_v3_latest_contract.py b/tests/custom/test_v3_latest_contract.py index 134e4f6..e35a730 100644 --- a/tests/custom/test_v3_latest_contract.py +++ b/tests/custom/test_v3_latest_contract.py @@ -60,7 +60,7 @@ def handler(request): assert requests[-1].method == "PATCH" assert json.loads(requests[-1].content) == {"transport": {"token": "renewed"}, "agent": {"overrides": {"messages": properties["messages"], "pipeline": {"mllm": properties["mllm"]}}}} assert properties == before - for invalid in ({"llm": {"params": {}}, "mllm": {"params": {}}}, {"llm": {"params": {"url": "https://other"}}}, {"token": None}, {"instructions": "no"}): + for invalid in ({"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) From afec2f80bf30a9ede8df83ecaa8244fab9579a43 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 15:07:45 -0400 Subject: [PATCH 41/57] Keep runtime message updates independent of session transport --- docs/guides/agent-workflow.md | 2 +- docs/reference/v3.md | 2 +- src/agora_agent/agentkit/agent_session.py | 6 ++-- src/agora_agent/agentkit/session_backend.py | 19 +++++++++-- tests/custom/test_v3_latest_contract.py | 35 +++++++++++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 93fe687..505a37d 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -102,7 +102,7 @@ session = saved.create_session( 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; 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. +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 diff --git a/docs/reference/v3.md b/docs/reference/v3.md index f0e3f93..35a736a 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -194,6 +194,6 @@ After calls begin, only pacing changes are supported; schedule, lifecycle and ca 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": {...}}}}}`; use `mllm` instead of `llm` for realtime. Do not send both modules. 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. The existing legacy update path remains unchanged. +Session PATCH accepts `transport={"token": "..."}` and/or `agent={"overrides": {"messages": [...], "pipeline": {"llm": {"params": {...}}}}}`; use `mllm` instead of `llm` for realtime. Do not send both modules. 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. diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 01a5731..7ad50da 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -893,10 +893,11 @@ def update(self, properties: typing.Any) -> None: 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(), ) @@ -1347,10 +1348,11 @@ async def update(self, properties: typing.Any) -> None: 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(), ) diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index ef74777..cad2bc2 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -132,8 +132,8 @@ def update_body(properties): if not isinstance(module, dict) or set(module) - allowed: raise ValueError("Session update supports only provider params and llm.system_messages") if "system_messages" in module: - if "messages" in overrides and overrides["messages"] != module["system_messages"]: - raise ValueError("Supply messages or llm.system_messages, not conflicting values") + if "messages" in overrides: + raise ValueError("Supply messages or llm.system_messages, not both") overrides["messages"] = module["system_messages"] if "params" in module: pipeline[category] = {"params": module["params"]} @@ -143,3 +143,18 @@ def update_body(properties): 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 diff --git a/tests/custom/test_v3_latest_contract.py b/tests/custom/test_v3_latest_contract.py index e35a730..0192ecf 100644 --- a/tests/custom/test_v3_latest_contract.py +++ b/tests/custom/test_v3_latest_contract.py @@ -164,3 +164,38 @@ def test_same_vendor_alternate_credential_form_clears_hidden_key(): 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_realtime_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}, "mllm": {"params": {"temperature": 0.2}}}) == {"agent": {"overrides": {"messages": messages, "pipeline": {"mllm": {"params": {"temperature": 0.2}}}}}} + with pytest.raises(ValueError): + update_body({"messages": messages, "llm": {"system_messages": messages}}) From c1d7ba48098161908f9600982b3150523bf2126d Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 15:08:33 -0400 Subject: [PATCH 42/57] Update documentation navigation for session PATCH operation --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 8cda4f0..63d8a32 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,4 +66,4 @@ The Agora Conversational AI Python SDK lets you build voice-powered AI agents on ## 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 31-operation v3 reference](./reference/v3.md). The preview uses protected custom/local-generated code rather than the canonical Fern pipeline. +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. From 8edae82099f9db6b00ca2e9d7b41900887b2c4f8 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Tue, 27 Oct 2026 21:44:05 -0400 Subject: [PATCH 43/57] fix: limit preview pipelines to cascaded agents --- README.md | 2 +- changelog.md | 3 +- docs/guides/v3-api-preview.md | 8 +- docs/reference/v3.md | 6 +- openapi.json | 1392 +------------------ openapi.yaml | 754 +--------- src/agora_agent/agentkit/session_backend.py | 18 +- src/agora_agent/agentkit/v3.py | 74 +- src/agora_agent/v3/client.py | 16 +- src/agora_agent/v3/openapi.json | 1392 +------------------ src/agora_agent/v3/types.py | 134 +- tests/custom/test_v3_latest_contract.py | 68 +- tests/custom/test_v3_preview.py | 63 +- 13 files changed, 118 insertions(+), 3812 deletions(-) diff --git a/README.md b/README.md index 7c48810..f706621 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ and multimodal flows (MLLM) for real-time audio processing. Use one `Agent` builder to configure providers, then call `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. Existing session behavior is preserved. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. +Saving and project provisioning require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. ## Install diff --git a/changelog.md b/changelog.md index 1a79b9d..0983baf 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## Unreleased preview contract alignment -- Shared root OpenAPI JSON/YAML, local reproducible wire generator, sync/async/raw session PATCH, and restored realtime MLLM contracts. +- 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. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 6a6b486..d682e85 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -28,7 +28,7 @@ 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 is token-only: a prebuilt `auth_token` or App ID/App Certificate signing. Internal gateway headers, `X-Studio-Region` and Basic authorization overrides are rejected. Region/failover guarantees remain open; AgentKit refuses CN for these resources. +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 is token-only: a prebuilt `auth_token` or App ID/App Certificate signing. Internal gateway headers, `X-Studio-Region` and Basic authorization overrides are rejected. Current rollout availability is described in the [release notes](../../changelog.md#unreleased-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). @@ -167,11 +167,13 @@ Use `await error.latest()` after an async 412. `save()` requires an `Agora` clie ## 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; the current response contract does not expose an analysis-result field or an AgentKit extraction-results helper. 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. Vendor instructions and greetings take precedence over deprecated Agent fields; realtime `params.instructions` moves to top-level instructions, and supported vendor 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`. Realtime Agora, server and semantic VAD settings, supported text/audio greetings, silence settings and filler words are converted to v3 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`. +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`; provider-specific `params.language` remains separate. A configured STT follows v2 interaction-language precedence (the explicit turn language, otherwise `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. @@ -185,7 +187,7 @@ For cascaded deployment, `with_turn_detection({"language": "de-DE"})` sets `pipe | 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 active LLM/MLLM params only. Say/interrupt/think/history/turn and extraction-result helpers are unavailable on new session IDs | +| 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. diff --git a/docs/reference/v3.md b/docs/reference/v3.md index 35a736a..8d69a05 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -35,7 +35,7 @@ Prefix: `/conversational-ai/v1/projects/{appid}/agents`. A stored configuration' | `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. Pipelines use `mode="cascaded"` with ASR/LLM/TTS or `mode="realtime"` with MLLM. 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. +`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. @@ -117,7 +117,7 @@ Session requests follow the supplied OpenAPI plus the engineering reply correcti `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 active LLM/MLLM params; no speculative action endpoints are exposed. +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`; the current contract has no `analysis` result field; a nonunique name does not establish ownership by a stored agent. @@ -194,6 +194,6 @@ After calls begin, only pacing changes are supported; schedule, lifecycle and ca 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": {...}}}}}`; use `mllm` instead of `llm` for realtime. Do not send both modules. 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 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. diff --git a/openapi.json b/openapi.json index 4e3a43d..dc43782 100644 --- a/openapi.json +++ b/openapi.json @@ -4819,9 +4819,6 @@ { "$ref": "#/components/schemas/ResolvedCascadedPipeline" }, - { - "$ref": "#/components/schemas/ResolvedRealtimePipeline" - }, { "type": "null" } @@ -7631,9 +7628,6 @@ { "$ref": "#/components/schemas/CascadedPipelineInput" }, - { - "$ref": "#/components/schemas/RealtimePipelineInput" - }, { "type": "null" } @@ -8936,22 +8930,11 @@ "llm": { "$ref": "#/components/schemas/SessionModulePatch", "title": "Llm" - }, - "mllm": { - "$ref": "#/components/schemas/SessionModulePatch", - "title": "Mllm", - "description": "Complete params replacement for the active realtime MLLM module. Credentials and provider selection are not mutable." } }, "additionalProperties": false, "type": "object", - "title": "SessionPipelinePatch", - "not": { - "required": [ - "llm", - "mllm" - ] - } + "title": "SessionPipelinePatch" }, "SessionResponse": { "properties": { @@ -10470,15 +10453,12 @@ { "$ref": "#/components/schemas/AgentRequest_CascadedPipelineInput" }, - { - "$ref": "#/components/schemas/AgentRequest_RealtimePipelineInput" - }, { "type": "null" } ], "default": null, - "description": "Cascaded or realtime pipeline. Omission or null selects the existing managed cascaded defaults. A realtime pipeline explicitly sets mode to realtime." + "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": [ @@ -10578,14 +10558,13 @@ "pipeline": { "anyOf": [ { - "$ref": "#/components/schemas/AgentRequest_PipelinePatch" + "$ref": "#/components/schemas/AgentRequest_CascadedPipelinePatch" }, { "type": "null" } ], - "default": null, - "description": "JSON Merge Patch of the stored pipeline. Omitted mode retains the stored mode. The merged result must validate against the corresponding cascaded or realtime input; switching mode requires deleting incompatible module fields." + "default": null }, "filler_words": { "anyOf": [ @@ -14474,1369 +14453,6 @@ "additionalProperties": false, "type": "object", "title": "EndCallPolicy" - }, - "AgoraVadTurnDetection": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "agora_vad" - }, - "interrupt_duration_ms": { - "type": "integer", - "minimum": 0 - }, - "prefix_padding_ms": { - "type": "integer", - "minimum": 0 - }, - "silence_duration_ms": { - "type": "integer", - "minimum": 0 - }, - "threshold": { - "type": "number", - "exclusiveMinimum": 0, - "exclusiveMaximum": 1 - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AgentRequest_AgoraVadTurnDetectionPatch": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "agora_vad" - }, - { - "type": "null" - } - ] - }, - "interrupt_duration_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "prefix_padding_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "silence_duration_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "threshold": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0, - "exclusiveMaximum": 1 - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ServerVadTurnDetection": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "server_vad" - }, - "prefix_padding_ms": { - "type": "integer", - "minimum": 0 - }, - "silence_duration_ms": { - "type": "integer", - "minimum": 0 - }, - "threshold": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "OpenAI Realtime API only." - }, - "idle_timeout_ms": { - "type": "integer", - "minimum": 0, - "description": "OpenAI Realtime API only." - }, - "start_of_speech_sensitivity": { - "type": "string", - "enum": [ - "START_SENSITIVITY_HIGH", - "START_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - }, - "end_of_speech_sensitivity": { - "type": "string", - "enum": [ - "END_SENSITIVITY_HIGH", - "END_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AgentRequest_ServerVadTurnDetectionPatch": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "server_vad" - }, - { - "type": "null" - } - ] - }, - "prefix_padding_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "silence_duration_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "threshold": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "OpenAI Realtime API only." - }, - { - "type": "null" - } - ] - }, - "idle_timeout_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0, - "description": "OpenAI Realtime API only." - }, - { - "type": "null" - } - ] - }, - "start_of_speech_sensitivity": { - "anyOf": [ - { - "type": "string", - "enum": [ - "START_SENSITIVITY_HIGH", - "START_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - }, - { - "type": "null" - } - ] - }, - "end_of_speech_sensitivity": { - "anyOf": [ - { - "type": "string", - "enum": [ - "END_SENSITIVITY_HIGH", - "END_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "SemanticVadTurnDetection": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "semantic_vad" - }, - "eagerness": { - "type": "string", - "enum": [ - "auto", - "low", - "medium", - "high" - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AgentRequest_SemanticVadTurnDetectionPatch": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "semantic_vad" - }, - { - "type": "null" - } - ] - }, - "eagerness": { - "anyOf": [ - { - "type": "string", - "enum": [ - "auto", - "low", - "medium", - "high" - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "RealtimeTurnDetection": { - "oneOf": [ - { - "$ref": "#/components/schemas/AgoraVadTurnDetection" - }, - { - "$ref": "#/components/schemas/ServerVadTurnDetection" - }, - { - "$ref": "#/components/schemas/SemanticVadTurnDetection" - } - ], - "discriminator": { - "propertyName": "type" - } - }, - "AgentRequest_RealtimeTurnDetectionPatch": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AgoraVadTurnDetectionPatch" - }, - { - "$ref": "#/components/schemas/AgentRequest_ServerVadTurnDetectionPatch" - }, - { - "$ref": "#/components/schemas/AgentRequest_SemanticVadTurnDetectionPatch" - } - ], - "description": "JSON Merge Patch of realtime turn detection. Omitted type retains the stored discriminator; the merged result must validate against RealtimeTurnDetection." - }, - "AgentRequest_MllmInput": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "url": { - "type": "string" - } - }, - "required": [ - "url" - ] - }, - "then": { - "properties": { - "credential": { - "properties": { - "mode": { - "const": "byok" - } - }, - "required": [ - "mode" - ], - "type": "object" - } - }, - "required": [ - "credential" - ] - } - }, - { - "properties": { - "credential": { - "properties": { - "fields": { - "additionalProperties": { - "type": "string", - "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" - } - } - } - } - } - } - ], - "properties": { - "mcp_servers": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/AgentRequest_McpServer" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Mcp Servers" - }, - "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": { - "type": "string" - }, - "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" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Headers", - "writeOnly": true - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - } - }, - "title": "AgentRequest_MllmInput", - "type": "object", - "required": [ - "vendor" - ], - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "MllmInput": { - "allOf": [ - { - "if": { - "properties": { - "url": { - "type": "string" - } - }, - "required": [ - "url" - ] - }, - "then": { - "properties": { - "credential": { - "properties": { - "mode": { - "const": "byok" - } - }, - "type": "object", - "required": [ - "mode" - ] - } - }, - "required": [ - "credential" - ] - } - } - ], - "properties": { - "mcp_servers": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/McpServer" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Mcp Servers" - }, - "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": { - "type": "string" - }, - "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" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Headers", - "writeOnly": true - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "type": "object", - "title": "MllmInput", - "required": [ - "vendor" - ], - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "AgentRequest_MllmPatch": { - "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" - }, - "credential": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmCredentialPatch" - }, - { - "type": "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" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Headers", - "writeOnly": true - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - } - }, - "title": "AgentRequest_MllmPatch", - "type": "object", - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "ResolvedMllm": { - "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" - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "mcp_servers": { - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/components/schemas/McpServer" - } - }, - { - "type": "null" - } - ], - "title": "Mcp Servers" - } - }, - "additionalProperties": false, - "type": "object", - "title": "ResolvedMllm", - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "AgentRequest_MllmCredentialPatch": { - "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": [ - { - "type": "object", - "maxProperties": 8, - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - } - }, - "title": "PublicCredentialPatch", - "type": "object" - }, - "AgentRequest_RealtimePipelineInput": { - "additionalProperties": false, - "properties": { - "mode": { - "type": "string", - "const": "realtime" - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AvatarInput" - }, - { - "type": "null" - } - ], - "default": null - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/RealtimeTurnDetection" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmInput" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "mode" - ], - "title": "AgentRequest_RealtimePipelineInput", - "type": "object" - }, - "RealtimePipelineInput": { - "properties": { - "mode": { - "type": "string", - "const": "realtime" - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AvatarInput" - }, - { - "type": "null" - } - ] - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/RealtimeTurnDetection" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/MllmInput" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "mode" - ], - "title": "RealtimePipelineInput" - }, - "ResolvedRealtimePipeline": { - "properties": { - "mode": { - "type": "string", - "const": "realtime" - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/ResolvedAvatar" - }, - { - "type": "null" - } - ] - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/RealtimeTurnDetection" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/ResolvedMllm" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "mode" - ], - "title": "ResolvedRealtimePipeline" - }, - "AgentRequest_RealtimePipelinePatch": { - "additionalProperties": false, - "properties": { - "mode": { - "anyOf": [ - { - "type": "string", - "const": "realtime" - }, - { - "type": "null" - } - ] - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" - }, - { - "type": "null" - } - ], - "default": null - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmPatch" - }, - { - "type": "null" - } - ] - } - }, - "title": "AgentRequest_RealtimePipelinePatch", - "type": "object" - }, - "AgentRequest_PipelinePatch": { - "additionalProperties": false, - "properties": { - "mode": { - "anyOf": [ - { - "type": "string", - "enum": [ - "cascaded", - "realtime" - ] - }, - { - "type": "null" - } - ] - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" - }, - { - "type": "null" - } - ], - "default": null - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" - }, - { - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - }, - { - "type": "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 - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmPatch" - }, - { - "type": "null" - } - ] - } - }, - "title": "PipelinePatch", - "type": "object", - "allOf": [ - { - "if": { - "properties": { - "mode": { - "const": "realtime" - } - }, - "required": [ - "mode" - ] - }, - "then": { - "properties": { - "asr": { - "type": "null" - }, - "llm": { - "type": "null" - }, - "tts": { - "type": "null" - }, - "silence_config": { - "type": "null" - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - }, - { - "type": "null" - } - ] - } - } - } - }, - { - "if": { - "properties": { - "mode": { - "const": "cascaded" - } - }, - "required": [ - "mode" - ] - }, - "then": { - "properties": { - "mllm": { - "type": "null" - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" - }, - { - "type": "null" - } - ] - } - } - } - } - ] } }, "securitySchemes": { diff --git a/openapi.yaml b/openapi.yaml index d50d95b..774381c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3028,7 +3028,6 @@ components: pipeline: anyOf: - "$ref": "#/components/schemas/ResolvedCascadedPipeline" - - "$ref": "#/components/schemas/ResolvedRealtimePipeline" - type: 'null' filler_words: anyOf: @@ -4730,7 +4729,6 @@ components: pipeline: anyOf: - "$ref": "#/components/schemas/CascadedPipelineInput" - - "$ref": "#/components/schemas/RealtimePipelineInput" - type: 'null' filler_words: anyOf: @@ -5520,18 +5518,9 @@ components: llm: "$ref": "#/components/schemas/SessionModulePatch" title: Llm - mllm: - "$ref": "#/components/schemas/SessionModulePatch" - title: Mllm - description: Complete params replacement for the active realtime MLLM module. - Credentials and provider selection are not mutable. additionalProperties: false type: object title: SessionPipelinePatch - not: - required: - - llm - - mllm SessionResponse: properties: agent_session_id: @@ -6488,12 +6477,10 @@ components: pipeline: anyOf: - "$ref": "#/components/schemas/AgentRequest_CascadedPipelineInput" - - "$ref": "#/components/schemas/AgentRequest_RealtimePipelineInput" - type: 'null' default: - description: Cascaded or realtime pipeline. Omission or null selects the - existing managed cascaded defaults. A realtime pipeline explicitly sets - mode to realtime. + 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" @@ -6544,13 +6531,9 @@ components: title: Variables pipeline: anyOf: - - "$ref": "#/components/schemas/AgentRequest_PipelinePatch" + - "$ref": "#/components/schemas/AgentRequest_CascadedPipelinePatch" - type: 'null' default: - description: JSON Merge Patch of the stored pipeline. Omitted mode retains - the stored mode. The merged result must validate against the corresponding - cascaded or realtime input; switching mode requires deleting incompatible - module fields. filler_words: anyOf: - "$ref": "#/components/schemas/AgentRequest_FillerWordsPatch" @@ -8820,737 +8803,6 @@ components: additionalProperties: false type: object title: EndCallPolicy - AgoraVadTurnDetection: - type: object - properties: - type: - type: string - const: agora_vad - interrupt_duration_ms: - type: integer - minimum: 0 - prefix_padding_ms: - type: integer - minimum: 0 - silence_duration_ms: - type: integer - minimum: 0 - threshold: - type: number - exclusiveMinimum: 0 - exclusiveMaximum: 1 - required: - - type - additionalProperties: false - AgentRequest_AgoraVadTurnDetectionPatch: - type: object - properties: - type: - anyOf: - - type: string - const: agora_vad - - type: 'null' - interrupt_duration_ms: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - prefix_padding_ms: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - silence_duration_ms: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - threshold: - anyOf: - - type: number - exclusiveMinimum: 0 - exclusiveMaximum: 1 - - type: 'null' - additionalProperties: false - ServerVadTurnDetection: - type: object - properties: - type: - type: string - const: server_vad - prefix_padding_ms: - type: integer - minimum: 0 - silence_duration_ms: - type: integer - minimum: 0 - threshold: - type: number - minimum: 0 - maximum: 1 - description: OpenAI Realtime API only. - idle_timeout_ms: - type: integer - minimum: 0 - description: OpenAI Realtime API only. - start_of_speech_sensitivity: - type: string - enum: - - START_SENSITIVITY_HIGH - - START_SENSITIVITY_LOW - description: Gemini Live only. - end_of_speech_sensitivity: - type: string - enum: - - END_SENSITIVITY_HIGH - - END_SENSITIVITY_LOW - description: Gemini Live only. - required: - - type - additionalProperties: false - AgentRequest_ServerVadTurnDetectionPatch: - type: object - properties: - type: - anyOf: - - type: string - const: server_vad - - type: 'null' - prefix_padding_ms: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - silence_duration_ms: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - threshold: - anyOf: - - type: number - minimum: 0 - maximum: 1 - description: OpenAI Realtime API only. - - type: 'null' - idle_timeout_ms: - anyOf: - - type: integer - minimum: 0 - description: OpenAI Realtime API only. - - type: 'null' - start_of_speech_sensitivity: - anyOf: - - type: string - enum: - - START_SENSITIVITY_HIGH - - START_SENSITIVITY_LOW - description: Gemini Live only. - - type: 'null' - end_of_speech_sensitivity: - anyOf: - - type: string - enum: - - END_SENSITIVITY_HIGH - - END_SENSITIVITY_LOW - description: Gemini Live only. - - type: 'null' - additionalProperties: false - SemanticVadTurnDetection: - type: object - properties: - type: - type: string - const: semantic_vad - eagerness: - type: string - enum: - - auto - - low - - medium - - high - required: - - type - additionalProperties: false - AgentRequest_SemanticVadTurnDetectionPatch: - type: object - properties: - type: - anyOf: - - type: string - const: semantic_vad - - type: 'null' - eagerness: - anyOf: - - type: string - enum: - - auto - - low - - medium - - high - - type: 'null' - additionalProperties: false - RealtimeTurnDetection: - oneOf: - - "$ref": "#/components/schemas/AgoraVadTurnDetection" - - "$ref": "#/components/schemas/ServerVadTurnDetection" - - "$ref": "#/components/schemas/SemanticVadTurnDetection" - discriminator: - propertyName: type - AgentRequest_RealtimeTurnDetectionPatch: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_AgoraVadTurnDetectionPatch" - - "$ref": "#/components/schemas/AgentRequest_ServerVadTurnDetectionPatch" - - "$ref": "#/components/schemas/AgentRequest_SemanticVadTurnDetectionPatch" - description: JSON Merge Patch of realtime turn detection. Omitted type retains - the stored discriminator; the merged result must validate against RealtimeTurnDetection. - AgentRequest_MllmInput: - additionalProperties: false - allOf: - - if: - properties: - url: - type: string - required: - - url - then: - properties: - credential: - properties: - mode: - const: byok - required: - - mode - type: object - required: - - credential - - properties: - credential: - properties: - fields: - additionalProperties: - type: string - pattern: "^\\$secrets\\.[^.]+\\.[^.]+$" - properties: - mcp_servers: - anyOf: - - items: - "$ref": "#/components/schemas/AgentRequest_McpServer" - type: array - - type: 'null' - default: - title: Mcp Servers - 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: - type: string - 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 - headers: - anyOf: - - additionalProperties: - type: string - type: object - - type: 'null' - default: - title: Headers - writeOnly: true - input_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - output_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - title: AgentRequest_MllmInput - type: object - required: - - vendor - description: Realtime MLLM module. Credentials use credential.fields; saved-agent - writes require secret references, while ephemeral input also permits direct - values. Provider settings remain in params. - MllmInput: - allOf: - - if: - properties: - url: - type: string - required: - - url - then: - properties: - credential: - properties: - mode: - const: byok - type: object - required: - - mode - required: - - credential - properties: - mcp_servers: - anyOf: - - items: - "$ref": "#/components/schemas/McpServer" - type: array - - type: 'null' - title: Mcp Servers - 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: - type: string - 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 - headers: - anyOf: - - additionalProperties: - type: string - type: object - - type: 'null' - title: Headers - writeOnly: true - input_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - output_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - additionalProperties: false - type: object - title: MllmInput - required: - - vendor - description: Realtime MLLM module. Credentials use credential.fields; saved-agent - writes require secret references, while ephemeral input also permits direct - values. Provider settings remain in params. - AgentRequest_MllmPatch: - 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 - credential: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_MllmCredentialPatch" - - type: 'null' - 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 - headers: - anyOf: - - additionalProperties: - type: string - type: object - - type: 'null' - default: - title: Headers - writeOnly: true - input_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - output_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - title: AgentRequest_MllmPatch - type: object - description: Realtime MLLM module. Credentials use credential.fields; saved-agent - writes require secret references, while ephemeral input also permits direct - values. Provider settings remain in params. - ResolvedMllm: - 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 - input_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - output_modalities: - anyOf: - - type: array - items: - type: string - enum: - - text - - audio - - type: 'null' - mcp_servers: - anyOf: - - type: array - items: - "$ref": "#/components/schemas/McpServer" - - type: 'null' - title: Mcp Servers - additionalProperties: false - type: object - title: ResolvedMllm - description: Realtime MLLM module. Credentials use credential.fields; saved-agent - writes require secret references, while ephemeral input also permits direct - values. Provider settings remain in params. - AgentRequest_MllmCredentialPatch: - 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: - - type: object - maxProperties: 8 - additionalProperties: - anyOf: - - type: string - pattern: "^\\$secrets\\.[^.]+\\.[^.]+$" - - type: 'null' - - type: 'null' - title: PublicCredentialPatch - type: object - AgentRequest_RealtimePipelineInput: - additionalProperties: false - properties: - mode: - type: string - const: realtime - avatar: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_AvatarInput" - - type: 'null' - default: - turn_detection: - anyOf: - - "$ref": "#/components/schemas/RealtimeTurnDetection" - - type: 'null' - mllm: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_MllmInput" - - type: 'null' - required: - - mode - title: AgentRequest_RealtimePipelineInput - type: object - RealtimePipelineInput: - properties: - mode: - type: string - const: realtime - avatar: - anyOf: - - "$ref": "#/components/schemas/AvatarInput" - - type: 'null' - turn_detection: - anyOf: - - "$ref": "#/components/schemas/RealtimeTurnDetection" - - type: 'null' - mllm: - anyOf: - - "$ref": "#/components/schemas/MllmInput" - - type: 'null' - additionalProperties: false - type: object - required: - - mode - title: RealtimePipelineInput - ResolvedRealtimePipeline: - properties: - mode: - type: string - const: realtime - avatar: - anyOf: - - "$ref": "#/components/schemas/ResolvedAvatar" - - type: 'null' - turn_detection: - anyOf: - - "$ref": "#/components/schemas/RealtimeTurnDetection" - - type: 'null' - mllm: - anyOf: - - "$ref": "#/components/schemas/ResolvedMllm" - - type: 'null' - additionalProperties: false - type: object - required: - - mode - title: ResolvedRealtimePipeline - AgentRequest_RealtimePipelinePatch: - additionalProperties: false - properties: - mode: - anyOf: - - type: string - const: realtime - - type: 'null' - avatar: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" - - type: 'null' - default: - turn_detection: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - - type: 'null' - mllm: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_MllmPatch" - - type: 'null' - title: AgentRequest_RealtimePipelinePatch - type: object - AgentRequest_PipelinePatch: - additionalProperties: false - properties: - mode: - anyOf: - - type: string - enum: - - cascaded - - realtime - - type: 'null' - avatar: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" - - type: 'null' - default: - turn_detection: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" - - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - - type: 'null' - 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: - mllm: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_MllmPatch" - - type: 'null' - title: PipelinePatch - type: object - allOf: - - if: - properties: - mode: - const: realtime - required: - - mode - then: - properties: - asr: - type: 'null' - llm: - type: 'null' - tts: - type: 'null' - silence_config: - type: 'null' - turn_detection: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - - type: 'null' - - if: - properties: - mode: - const: cascaded - required: - - mode - then: - properties: - mllm: - type: 'null' - turn_detection: - anyOf: - - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" - - type: 'null' securitySchemes: AgoraTokenAuth: type: apiKey diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index cad2bc2..72f1380 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -115,30 +115,24 @@ 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", "mllm"}: - raise ValueError("Session update supports token, messages, llm.params and mllm.params only") + 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"] - pipeline = {} - for category in ("llm", "mllm"): - if category not in value: - continue - module = value[category] - allowed = {"params", "system_messages"} if category == "llm" else {"params"} - if not isinstance(module, dict) or set(module) - allowed: + 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: - pipeline[category] = {"params": module["params"]} - if pipeline: - overrides["pipeline"] = pipeline + overrides["pipeline"] = {"llm": {"params": module["params"]}} if overrides: result["agent"] = {"overrides": overrides} validate_write("PATCH", "/conversational-ai/v1/projects/project/sessions/session", result) diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index da3aa23..a333c0b 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -133,7 +133,6 @@ def module(config, category, errors): "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"}, - "mllm": {"vendor", "url", "model", "input_modalities", "output_modalities", "mcp_servers", "headers"}, "avatar": {"vendor", "url", "enabled"}, }[category] # v2 provider serializers keep endpoints and ASR/LLM model choices in params. @@ -144,8 +143,6 @@ def module(config, category, errors): result["url"] = params.pop("base_url") if category == "avatar" and "enable" in config: config["enabled"] = config.pop("enable") - if category == "mllm": - config.pop("enable", None) for field, value in config.items(): if field not in allowed: errors.append(category + "." + field) @@ -238,6 +235,8 @@ def _turn(config, interruption, errors): 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: @@ -247,24 +246,11 @@ def build_body(agent, name=None): 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") - realtime = agent._mllm is not None - pipeline = {"mode": "realtime" if realtime else "cascaded"} - if realtime: - for field in ("stt", "llm", "tts"): - if getattr(agent, "_" + field, None) is not None: - errors.append(field + " (realtime pipeline)") - conversational = dump(agent._mllm if realtime else agent._resolve_llm_config()) or {} - if realtime: - realtime_params = conversational.get("params", {}) - if isinstance(realtime_params, dict) and "instructions" in realtime_params: - instructions = realtime_params.pop("instructions") - if not isinstance(instructions, str): - errors.append("mllm.params.instructions") - else: - result["instructions"] = instructions - messages = conversational.pop("messages" if realtime else "system_messages", None) + pipeline = {"mode": "cascaded"} + conversational = dump(agent._resolve_llm_config()) or {} + messages = conversational.pop("system_messages", None) if messages is not None: - source = "mllm.messages" if realtime else "llm.system_messages" + source = "llm.system_messages" if not isinstance(messages, list): errors.append(source) else: @@ -310,54 +296,24 @@ def build_body(agent, name=None): elif greeting: errors.append("greeting_configs (requires greeting)") if conversational.pop("template_variables", None): - errors.append(("mllm" if realtime else "llm") + ".template_variables (use with_variable)") - td = conversational.pop("turn_detection", None) if realtime else None - if realtime: - if td: - td = dump(td) - mode = td.pop("mode", td.pop("type", None)) - configurations = {"agora_vad": "agora_vad_config", "server_vad": "server_vad_config", "semantic_vad": "semantic_vad_config"} - allowed = { - "agora_vad": {"interrupt_duration_ms", "prefix_padding_ms", "silence_duration_ms", "threshold"}, - "server_vad": {"prefix_padding_ms", "silence_duration_ms", "threshold", "idle_timeout_ms", "start_of_speech_sensitivity", "end_of_speech_sensitivity"}, - "semantic_vad": {"eagerness"}, - } - if not isinstance(mode, str) or mode not in configurations: - errors.append("mllm.turn_detection.mode") - else: - nested = td.pop(configurations[mode], {}) or {} - if not isinstance(nested, dict): - errors.append("mllm.turn_detection." + configurations[mode]) - nested = {} - direct = {key: td.pop(key) for key in tuple(td) if key in allowed[mode]} - errors.extend("mllm.turn_detection." + key for key in direct if key in nested and direct[key] != nested[key]) - errors.extend("mllm.turn_detection." + key for key in td) - errors.extend("mllm.turn_detection." + key for key in nested if key not in allowed[mode]) - pipeline["turn_detection"] = {"type": mode, **nested, **direct} - if agent._turn_detection is not None or agent._interruption is not None: - errors.append("turn_detection (configure realtime vendor turn_detection)") - else: - td = _turn(agent._turn_detection, agent._interruption, errors) - if td: - pipeline["turn_detection"] = td - category = "mllm" if realtime else "llm" + 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[category] = module(conversational, category, errors) + pipeline["llm"] = module(conversational, "llm", errors) for source, target in (("stt", "asr"), ("tts", "tts"), ("avatar", "avatar")): value = getattr(agent, "_" + source, None) - if target == "asr" and not realtime: + if target == "asr": language = agent._field_value(agent._turn_detection, "language") if value is not None or language is not None: # Preserve v2 interaction-language precedence and its existing # managed ASR fallback, without adding ASR to partial builders. value = agent._resolve_asr_config(agent._resolve_turn_detection_config()) - if value is not None and (not realtime or target == "avatar"): + if value is not None: pipeline[target] = module(value, target, errors) if "silence_config" in params: - if realtime: - errors.append("parameters.silence_config (realtime pipeline)") - else: - pipeline["silence_config"] = {"enabled": True, **params["silence_config"]} + 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")): @@ -408,7 +364,7 @@ def replacement_patch(previous, desired, category=None): 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", "mllm", "avatar"} else category) + result[key] = replacement_patch(old, result[key], key if key in {"asr", "llm", "tts", "avatar"} else category) return result diff --git a/src/agora_agent/v3/client.py b/src/agora_agent/v3/client.py index be2f696..67ab947 100644 --- a/src/agora_agent/v3/client.py +++ b/src/agora_agent/v3/client.py @@ -12,7 +12,7 @@ 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, T.AgentRequest_RealtimePipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + 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: @@ -21,14 +21,14 @@ def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None] 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_PipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + 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, T.AgentRequest_RealtimePipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + 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]: @@ -37,7 +37,7 @@ def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, None] 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_PipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + 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]: @@ -237,7 +237,7 @@ 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, T.AgentRequest_RealtimePipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + 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: @@ -246,14 +246,14 @@ async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, 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_PipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> T.AgentResponse: + 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, T.AgentRequest_RealtimePipelineInput, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWords, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + 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]: @@ -262,7 +262,7 @@ async def list(self, *, limit: Optional[int] = None, cursor: Optional[Union[str, 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_PipelinePatch, None] = _UNSET, filler_words: Union[T.AgentRequest_FillerWordsPatch, None] = _UNSET, appid: Optional[str] = None, request_options: Optional[dict] = None) -> RawResponse[T.AgentResponse]: + 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]: diff --git a/src/agora_agent/v3/openapi.json b/src/agora_agent/v3/openapi.json index 4e3a43d..dc43782 100644 --- a/src/agora_agent/v3/openapi.json +++ b/src/agora_agent/v3/openapi.json @@ -4819,9 +4819,6 @@ { "$ref": "#/components/schemas/ResolvedCascadedPipeline" }, - { - "$ref": "#/components/schemas/ResolvedRealtimePipeline" - }, { "type": "null" } @@ -7631,9 +7628,6 @@ { "$ref": "#/components/schemas/CascadedPipelineInput" }, - { - "$ref": "#/components/schemas/RealtimePipelineInput" - }, { "type": "null" } @@ -8936,22 +8930,11 @@ "llm": { "$ref": "#/components/schemas/SessionModulePatch", "title": "Llm" - }, - "mllm": { - "$ref": "#/components/schemas/SessionModulePatch", - "title": "Mllm", - "description": "Complete params replacement for the active realtime MLLM module. Credentials and provider selection are not mutable." } }, "additionalProperties": false, "type": "object", - "title": "SessionPipelinePatch", - "not": { - "required": [ - "llm", - "mllm" - ] - } + "title": "SessionPipelinePatch" }, "SessionResponse": { "properties": { @@ -10470,15 +10453,12 @@ { "$ref": "#/components/schemas/AgentRequest_CascadedPipelineInput" }, - { - "$ref": "#/components/schemas/AgentRequest_RealtimePipelineInput" - }, { "type": "null" } ], "default": null, - "description": "Cascaded or realtime pipeline. Omission or null selects the existing managed cascaded defaults. A realtime pipeline explicitly sets mode to realtime." + "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": [ @@ -10578,14 +10558,13 @@ "pipeline": { "anyOf": [ { - "$ref": "#/components/schemas/AgentRequest_PipelinePatch" + "$ref": "#/components/schemas/AgentRequest_CascadedPipelinePatch" }, { "type": "null" } ], - "default": null, - "description": "JSON Merge Patch of the stored pipeline. Omitted mode retains the stored mode. The merged result must validate against the corresponding cascaded or realtime input; switching mode requires deleting incompatible module fields." + "default": null }, "filler_words": { "anyOf": [ @@ -14474,1369 +14453,6 @@ "additionalProperties": false, "type": "object", "title": "EndCallPolicy" - }, - "AgoraVadTurnDetection": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "agora_vad" - }, - "interrupt_duration_ms": { - "type": "integer", - "minimum": 0 - }, - "prefix_padding_ms": { - "type": "integer", - "minimum": 0 - }, - "silence_duration_ms": { - "type": "integer", - "minimum": 0 - }, - "threshold": { - "type": "number", - "exclusiveMinimum": 0, - "exclusiveMaximum": 1 - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AgentRequest_AgoraVadTurnDetectionPatch": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "agora_vad" - }, - { - "type": "null" - } - ] - }, - "interrupt_duration_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "prefix_padding_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "silence_duration_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "threshold": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0, - "exclusiveMaximum": 1 - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "ServerVadTurnDetection": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "server_vad" - }, - "prefix_padding_ms": { - "type": "integer", - "minimum": 0 - }, - "silence_duration_ms": { - "type": "integer", - "minimum": 0 - }, - "threshold": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "OpenAI Realtime API only." - }, - "idle_timeout_ms": { - "type": "integer", - "minimum": 0, - "description": "OpenAI Realtime API only." - }, - "start_of_speech_sensitivity": { - "type": "string", - "enum": [ - "START_SENSITIVITY_HIGH", - "START_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - }, - "end_of_speech_sensitivity": { - "type": "string", - "enum": [ - "END_SENSITIVITY_HIGH", - "END_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AgentRequest_ServerVadTurnDetectionPatch": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "server_vad" - }, - { - "type": "null" - } - ] - }, - "prefix_padding_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "silence_duration_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ] - }, - "threshold": { - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "OpenAI Realtime API only." - }, - { - "type": "null" - } - ] - }, - "idle_timeout_ms": { - "anyOf": [ - { - "type": "integer", - "minimum": 0, - "description": "OpenAI Realtime API only." - }, - { - "type": "null" - } - ] - }, - "start_of_speech_sensitivity": { - "anyOf": [ - { - "type": "string", - "enum": [ - "START_SENSITIVITY_HIGH", - "START_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - }, - { - "type": "null" - } - ] - }, - "end_of_speech_sensitivity": { - "anyOf": [ - { - "type": "string", - "enum": [ - "END_SENSITIVITY_HIGH", - "END_SENSITIVITY_LOW" - ], - "description": "Gemini Live only." - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "SemanticVadTurnDetection": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "semantic_vad" - }, - "eagerness": { - "type": "string", - "enum": [ - "auto", - "low", - "medium", - "high" - ] - } - }, - "required": [ - "type" - ], - "additionalProperties": false - }, - "AgentRequest_SemanticVadTurnDetectionPatch": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "semantic_vad" - }, - { - "type": "null" - } - ] - }, - "eagerness": { - "anyOf": [ - { - "type": "string", - "enum": [ - "auto", - "low", - "medium", - "high" - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "RealtimeTurnDetection": { - "oneOf": [ - { - "$ref": "#/components/schemas/AgoraVadTurnDetection" - }, - { - "$ref": "#/components/schemas/ServerVadTurnDetection" - }, - { - "$ref": "#/components/schemas/SemanticVadTurnDetection" - } - ], - "discriminator": { - "propertyName": "type" - } - }, - "AgentRequest_RealtimeTurnDetectionPatch": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AgoraVadTurnDetectionPatch" - }, - { - "$ref": "#/components/schemas/AgentRequest_ServerVadTurnDetectionPatch" - }, - { - "$ref": "#/components/schemas/AgentRequest_SemanticVadTurnDetectionPatch" - } - ], - "description": "JSON Merge Patch of realtime turn detection. Omitted type retains the stored discriminator; the merged result must validate against RealtimeTurnDetection." - }, - "AgentRequest_MllmInput": { - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "url": { - "type": "string" - } - }, - "required": [ - "url" - ] - }, - "then": { - "properties": { - "credential": { - "properties": { - "mode": { - "const": "byok" - } - }, - "required": [ - "mode" - ], - "type": "object" - } - }, - "required": [ - "credential" - ] - } - }, - { - "properties": { - "credential": { - "properties": { - "fields": { - "additionalProperties": { - "type": "string", - "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" - } - } - } - } - } - } - ], - "properties": { - "mcp_servers": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/AgentRequest_McpServer" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Mcp Servers" - }, - "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": { - "type": "string" - }, - "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" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Headers", - "writeOnly": true - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - } - }, - "title": "AgentRequest_MllmInput", - "type": "object", - "required": [ - "vendor" - ], - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "MllmInput": { - "allOf": [ - { - "if": { - "properties": { - "url": { - "type": "string" - } - }, - "required": [ - "url" - ] - }, - "then": { - "properties": { - "credential": { - "properties": { - "mode": { - "const": "byok" - } - }, - "type": "object", - "required": [ - "mode" - ] - } - }, - "required": [ - "credential" - ] - } - } - ], - "properties": { - "mcp_servers": { - "anyOf": [ - { - "items": { - "$ref": "#/components/schemas/McpServer" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Mcp Servers" - }, - "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": { - "type": "string" - }, - "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" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Headers", - "writeOnly": true - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "type": "object", - "title": "MllmInput", - "required": [ - "vendor" - ], - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "AgentRequest_MllmPatch": { - "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" - }, - "credential": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmCredentialPatch" - }, - { - "type": "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" - }, - "headers": { - "anyOf": [ - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Headers", - "writeOnly": true - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - } - }, - "title": "AgentRequest_MllmPatch", - "type": "object", - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "ResolvedMllm": { - "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" - }, - "input_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "output_modalities": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio" - ] - } - }, - { - "type": "null" - } - ] - }, - "mcp_servers": { - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/components/schemas/McpServer" - } - }, - { - "type": "null" - } - ], - "title": "Mcp Servers" - } - }, - "additionalProperties": false, - "type": "object", - "title": "ResolvedMllm", - "description": "Realtime MLLM module. Credentials use credential.fields; saved-agent writes require secret references, while ephemeral input also permits direct values. Provider settings remain in params." - }, - "AgentRequest_MllmCredentialPatch": { - "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": [ - { - "type": "object", - "maxProperties": 8, - "additionalProperties": { - "anyOf": [ - { - "type": "string", - "pattern": "^\\$secrets\\.[^.]+\\.[^.]+$" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - } - }, - "title": "PublicCredentialPatch", - "type": "object" - }, - "AgentRequest_RealtimePipelineInput": { - "additionalProperties": false, - "properties": { - "mode": { - "type": "string", - "const": "realtime" - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AvatarInput" - }, - { - "type": "null" - } - ], - "default": null - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/RealtimeTurnDetection" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmInput" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "mode" - ], - "title": "AgentRequest_RealtimePipelineInput", - "type": "object" - }, - "RealtimePipelineInput": { - "properties": { - "mode": { - "type": "string", - "const": "realtime" - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AvatarInput" - }, - { - "type": "null" - } - ] - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/RealtimeTurnDetection" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/MllmInput" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "mode" - ], - "title": "RealtimePipelineInput" - }, - "ResolvedRealtimePipeline": { - "properties": { - "mode": { - "type": "string", - "const": "realtime" - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/ResolvedAvatar" - }, - { - "type": "null" - } - ] - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/RealtimeTurnDetection" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/ResolvedMllm" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "mode" - ], - "title": "ResolvedRealtimePipeline" - }, - "AgentRequest_RealtimePipelinePatch": { - "additionalProperties": false, - "properties": { - "mode": { - "anyOf": [ - { - "type": "string", - "const": "realtime" - }, - { - "type": "null" - } - ] - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" - }, - { - "type": "null" - } - ], - "default": null - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - }, - { - "type": "null" - } - ] - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmPatch" - }, - { - "type": "null" - } - ] - } - }, - "title": "AgentRequest_RealtimePipelinePatch", - "type": "object" - }, - "AgentRequest_PipelinePatch": { - "additionalProperties": false, - "properties": { - "mode": { - "anyOf": [ - { - "type": "string", - "enum": [ - "cascaded", - "realtime" - ] - }, - { - "type": "null" - } - ] - }, - "avatar": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_AvatarPatch" - }, - { - "type": "null" - } - ], - "default": null - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" - }, - { - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - }, - { - "type": "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 - }, - "mllm": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_MllmPatch" - }, - { - "type": "null" - } - ] - } - }, - "title": "PipelinePatch", - "type": "object", - "allOf": [ - { - "if": { - "properties": { - "mode": { - "const": "realtime" - } - }, - "required": [ - "mode" - ] - }, - "then": { - "properties": { - "asr": { - "type": "null" - }, - "llm": { - "type": "null" - }, - "tts": { - "type": "null" - }, - "silence_config": { - "type": "null" - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_RealtimeTurnDetectionPatch" - }, - { - "type": "null" - } - ] - } - } - } - }, - { - "if": { - "properties": { - "mode": { - "const": "cascaded" - } - }, - "required": [ - "mode" - ] - }, - "then": { - "properties": { - "mllm": { - "type": "null" - }, - "turn_detection": { - "anyOf": [ - { - "$ref": "#/components/schemas/AgentRequest_CascadedTurnDetectionPatch" - }, - { - "type": "null" - } - ] - } - } - } - } - ] } }, "securitySchemes": { diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index cfee764..c308a80 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -33,7 +33,7 @@ class AgentResponse(TypedDict, total=False): messages: Union[List[Message], None] greeting: Union[Union[TextGreeting, AudioGreeting], None] variables: Union[Dict[str, VariableDefinition], None] - pipeline: Union[ResolvedCascadedPipeline, ResolvedRealtimePipeline, None] + pipeline: Union[ResolvedCascadedPipeline, None] filler_words: Union[FillerWords, None] class AsrInput(TypedDict, total=False): @@ -317,7 +317,7 @@ class PublicAgentInput(TypedDict, total=False): messages: Union[List[Message], None] greeting: Union[Union[TextGreeting, AudioGreeting], None] variables: Union[Dict[str, VariableDefinition], None] - pipeline: Union[CascadedPipelineInput, RealtimePipelineInput, None] + pipeline: Union[CascadedPipelineInput, None] filler_words: Union[FillerWords, None] class PublicByokCredential(TypedDict, total=False): @@ -478,7 +478,6 @@ class SessionPatch(TypedDict, total=False): class SessionPipelinePatch(TypedDict, total=False): llm: SessionModulePatch - mllm: SessionModulePatch class SessionResponse(TypedDict, total=False): agent_session_id: Required[str] @@ -608,7 +607,7 @@ class AgentRequest_AgentCreateInput(TypedDict, total=False): 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, AgentRequest_RealtimePipelineInput, None] + pipeline: Union[AgentRequest_CascadedPipelineInput, None] filler_words: Union[AgentRequest_FillerWords, None] class AgentRequest_AgentPatch(TypedDict, total=False): @@ -617,7 +616,7 @@ class AgentRequest_AgentPatch(TypedDict, total=False): messages: Union[List[AgentRequest_Message], None] greeting: Union[AgentRequest_GreetingPatch, None] variables: Union[Dict[str, Union[AgentRequest_VariableDefinitionPatch, None]], None] - pipeline: Union[AgentRequest_PipelinePatch, None] + pipeline: Union[AgentRequest_CascadedPipelinePatch, None] filler_words: Union[AgentRequest_FillerWordsPatch, None] class AgentRequest_AsrInput(TypedDict, total=False): @@ -912,127 +911,6 @@ class InboundEndCallPolicy(TypedDict, total=False): on_ai_assistant: bool on_fax: bool -class AgoraVadTurnDetection(TypedDict, total=False): - type: Required[Literal['agora_vad']] - interrupt_duration_ms: int - prefix_padding_ms: int - silence_duration_ms: int - threshold: float - -class AgentRequest_AgoraVadTurnDetectionPatch(TypedDict, total=False): - type: Union[Literal['agora_vad'], None] - interrupt_duration_ms: Union[int, None] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, None] - -class ServerVadTurnDetection(TypedDict, total=False): - type: Required[Literal['server_vad']] - prefix_padding_ms: int - silence_duration_ms: int - threshold: float - idle_timeout_ms: int - start_of_speech_sensitivity: Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'] - end_of_speech_sensitivity: Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'] - -class AgentRequest_ServerVadTurnDetectionPatch(TypedDict, total=False): - type: Union[Literal['server_vad'], None] - prefix_padding_ms: Union[int, None] - silence_duration_ms: Union[int, None] - threshold: Union[float, None] - idle_timeout_ms: Union[int, None] - start_of_speech_sensitivity: Union[Literal['START_SENSITIVITY_HIGH', 'START_SENSITIVITY_LOW'], None] - end_of_speech_sensitivity: Union[Literal['END_SENSITIVITY_HIGH', 'END_SENSITIVITY_LOW'], None] - -class SemanticVadTurnDetection(TypedDict, total=False): - type: Required[Literal['semantic_vad']] - eagerness: Literal['auto', 'low', 'medium', 'high'] - -class AgentRequest_SemanticVadTurnDetectionPatch(TypedDict, total=False): - type: Union[Literal['semantic_vad'], None] - eagerness: Union[Literal['auto', 'low', 'medium', 'high'], None] - -class AgentRequest_MllmInput(TypedDict, total=False): - mcp_servers: Union[List[AgentRequest_McpServer], None] - credential: Union[Union[AgentRequest_ManagedCredential, AgentRequest_PublicByokCredential], None] - vendor: Required[str] - params: Union[AgentRequest_JSONObject, None] - url: Union[str, None] - model: Union[str, None] - headers: Union[Dict[str, str], None] - input_modalities: Union[List[Literal['text', 'audio']], None] - output_modalities: Union[List[Literal['text', 'audio']], None] - -class MllmInput(TypedDict, total=False): - mcp_servers: Union[List[McpServer], None] - credential: Union[Union[ManagedCredential, PublicByokCredential], None] - vendor: Required[str] - params: Union[JSONObject, None] - url: Union[str, None] - model: Union[str, None] - headers: Union[Dict[str, str], None] - input_modalities: Union[List[Literal['text', 'audio']], None] - output_modalities: Union[List[Literal['text', 'audio']], None] - -class AgentRequest_MllmPatch(TypedDict, total=False): - mcp_servers: Union[List[AgentRequest_McpServer], None] - credential: Union[AgentRequest_MllmCredentialPatch, None] - vendor: Union[str, None] - params: Union[AgentRequest_JSONObject, None] - url: Union[str, None] - model: Union[str, None] - headers: Union[Dict[str, str], None] - input_modalities: Union[List[Literal['text', 'audio']], None] - output_modalities: Union[List[Literal['text', 'audio']], None] - -class ResolvedMllm(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] - input_modalities: Union[List[Literal['text', 'audio']], None] - output_modalities: Union[List[Literal['text', 'audio']], None] - mcp_servers: Union[List[McpServer], None] - -class AgentRequest_MllmCredentialPatch(TypedDict, total=False): - mode: Union[Literal['managed', 'byok'], None] - fields: Union[Dict[str, Union[str, None]], None] - -class AgentRequest_RealtimePipelineInput(TypedDict, total=False): - mode: Required[Literal['realtime']] - avatar: Union[AgentRequest_AvatarInput, None] - turn_detection: Union[RealtimeTurnDetection, None] - mllm: Union[AgentRequest_MllmInput, None] - -class RealtimePipelineInput(TypedDict, total=False): - mode: Required[Literal['realtime']] - avatar: Union[AvatarInput, None] - turn_detection: Union[RealtimeTurnDetection, None] - mllm: Union[MllmInput, None] - -class ResolvedRealtimePipeline(TypedDict, total=False): - mode: Required[Literal['realtime']] - avatar: Union[ResolvedAvatar, None] - turn_detection: Union[RealtimeTurnDetection, None] - mllm: Union[ResolvedMllm, None] - -class AgentRequest_RealtimePipelinePatch(TypedDict, total=False): - mode: Union[Literal['realtime'], None] - avatar: Union[AgentRequest_AvatarPatch, None] - turn_detection: Union[AgentRequest_RealtimeTurnDetectionPatch, None] - mllm: Union[AgentRequest_MllmPatch, None] - -class AgentRequest_PipelinePatch(TypedDict, total=False): - mode: Union[Literal['cascaded', 'realtime'], None] - avatar: Union[AgentRequest_AvatarPatch, None] - turn_detection: Union[AgentRequest_CascadedTurnDetectionPatch, AgentRequest_RealtimeTurnDetectionPatch, None] - asr: Union[AgentRequest_AsrPatch, None] - tts: Union[AgentRequest_TtsPatch, None] - llm: Union[AgentRequest_LlmPatch, None] - silence_config: Union[AgentRequest_SilenceConfigPatch, None] - mllm: Union[AgentRequest_MllmPatch, 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'] @@ -1044,8 +922,6 @@ class AgentRequest_PipelinePatch(TypedDict, total=False): AgentRequest_SkipPattern = Literal['fullwidth_parentheses', 'lenticular_brackets', 'parentheses', 'square_brackets', 'curly_braces'] AgentCreateInput = AgentRequest_AgentCreateInput AgentPatch = AgentRequest_AgentPatch -RealtimeTurnDetection = Union[AgoraVadTurnDetection, ServerVadTurnDetection, SemanticVadTurnDetection] -AgentRequest_RealtimeTurnDetectionPatch = Union[AgentRequest_AgoraVadTurnDetectionPatch, AgentRequest_ServerVadTurnDetectionPatch, AgentRequest_SemanticVadTurnDetectionPatch] # Compatibility names retained for the additive preview namespace. AgentCreateModel = AgentCreateInput @@ -1060,4 +936,4 @@ 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', 'AgoraVadTurnDetection', 'AgentRequest_AgoraVadTurnDetectionPatch', 'ServerVadTurnDetection', 'AgentRequest_ServerVadTurnDetectionPatch', 'SemanticVadTurnDetection', 'AgentRequest_SemanticVadTurnDetectionPatch', 'RealtimeTurnDetection', 'AgentRequest_RealtimeTurnDetectionPatch', 'AgentRequest_MllmInput', 'MllmInput', 'AgentRequest_MllmPatch', 'ResolvedMllm', 'AgentRequest_MllmCredentialPatch', 'AgentRequest_RealtimePipelineInput', 'RealtimePipelineInput', 'ResolvedRealtimePipeline', 'AgentRequest_RealtimePipelinePatch', 'AgentRequest_PipelinePatch', 'AgentCreateModel', 'AgentPatchRequest', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse', 'RtcGeofence', 'RtcSessionTransport', 'SessionTransport'] +__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', 'AgentCreateModel', 'AgentPatchRequest', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse', 'RtcGeofence', 'RtcSessionTransport', 'SessionTransport'] diff --git a/tests/custom/test_v3_latest_contract.py b/tests/custom/test_v3_latest_contract.py index 0192ecf..a6cc86b 100644 --- a/tests/custom/test_v3_latest_contract.py +++ b/tests/custom/test_v3_latest_contract.py @@ -7,7 +7,7 @@ import pytest from test_v3_preview import make_client -from agora_agent import Agent, OpenAI, OpenAIRealtime, secret_ref +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 @@ -44,23 +44,23 @@ 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_mllm(OpenAIRealtime(api_key="inline", model="gpt-realtime")) + 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"]["mllm"]["credential"] == {"mode": "byok", "fields": {"api_key": "inline"}} - properties = {"token": "renewed", "messages": [{"role": "user", "content": "Hi"}], "mllm": {"params": {"temperature": 0.4}}} + 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": {"mllm": properties["mllm"]}}}} + assert json.loads(requests[-1].content) == {"transport": {"token": "renewed"}, "agent": {"overrides": {"messages": properties["messages"], "pipeline": {"llm": properties["llm"]}}}} assert properties == before - for invalid in ({"llm": {"params": {}}, "mllm": {"params": {}}}, {"llm": {"params": {"url": "https://other"}}}, {"mllm": {"params": {"endpoint": "https://other"}}}, {"token": None}, {"instructions": "no"}): + 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) @@ -193,9 +193,61 @@ def handler(request): await result -def test_realtime_params_with_legacy_messages_spelling_is_supported(): +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}, "mllm": {"params": {"temperature": 0.2}}}) == {"agent": {"overrides": {"messages": messages, "pipeline": {"mllm": {"params": {"temperature": 0.2}}}}}} + 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 index f6c4feb..fae1e38 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -48,7 +48,7 @@ ("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": {"mllm": {"params": {"temperature": 0.2}}}}}}, "PATCH", "sessions/s", {"transport": {"token": "new-token"}, "agent": {"overrides": {"pipeline": {"mllm": {"params": {"temperature": 0.2}}}}}}), + ("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), @@ -409,13 +409,7 @@ def token(**kwargs): assert generated[0]["app_id"] == "0" * 32 -def test_realtime_instructions_and_audio_greeting_mapping(): - from agora_agent import OpenAIRealtime - agent = Agent(client=make_client(lambda _: pytest.fail("mapping only"))).with_mllm( - OpenAIRealtime(api_key="key", model="gpt-realtime", instructions="Realtime instructions")) - body = build_body(agent) - assert body["instructions"] == "Realtime instructions" - assert "instructions" not in body["pipeline"]["mllm"].get("params", {}) +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}} @@ -423,59 +417,6 @@ def test_realtime_instructions_and_audio_greeting_mapping(): assert greeting == {"mode": "audio", "url": "https://audio.example/greeting.wav", "text": "Hi", "download_timeout_ms": 1000, "pcm_sample_rate": 16000} -def test_realtime_vendor_messages_use_top_level_and_preserve_instruction_precedence(): - from agora_agent import OpenAIRealtime - client = make_client(lambda _: pytest.fail("pure mapping")) - agent = Agent(client=client, instructions="Deprecated instructions").with_mllm( - OpenAIRealtime( - api_key="key", instructions="Provider instructions", - messages=[{"role": "system", "content": "Explicit system message"}, - {"role": "user", "content": "Hello"}], - ) - ) - body = build_body(agent) - assert body["instructions"] == "Explicit system message" - assert body["messages"] == [{"role": "user", "content": "Hello"}] - assert "messages" not in body["pipeline"]["mllm"] - assert "instructions" not in body["pipeline"]["mllm"].get("params", {}) - - params_only = build_body(Agent(client=client, instructions="Deprecated").with_mllm( - OpenAIRealtime(api_key="key", instructions="Provider", messages=[{"role": "assistant", "content": "Hi"}]) - )) - assert params_only["instructions"] == "Provider" - assert params_only["messages"] == [{"role": "assistant", "content": "Hi"}] - with pytest.raises(UnsupportedFieldError, match=r"mllm.messages\[0\]"): - build_body(Agent(client=client).with_mllm(OpenAIRealtime( - api_key="key", messages=[{"role": "tool", "content": "unsupported"}] - ))) - - -@pytest.mark.parametrize("mode,config,expected", [ - ("agora_vad", {"agora_vad_config": {"threshold": 0.5}}, {"type": "agora_vad", "threshold": 0.5}), - ("server_vad", {"server_vad_config": {"threshold": 0.6, "idle_timeout_ms": 5000}}, - {"type": "server_vad", "threshold": 0.6, "idle_timeout_ms": 5000}), - ("semantic_vad", {"semantic_vad_config": {"eagerness": "high"}}, - {"type": "semantic_vad", "eagerness": "high"}), -]) -def test_real_realtime_vendor_turn_detection_flattens_create_schema_fields(mode, config, expected): - from agora_agent import OpenAIRealtime - from agora_agent.types.mllm_turn_detection import MllmTurnDetection - client = make_client(lambda _: pytest.fail("pure mapping")) - turn = MllmTurnDetection(mode=mode, **config) - body = build_body(Agent(client=client).with_mllm(OpenAIRealtime(api_key="key", turn_detection=turn))) - actual = body["pipeline"]["turn_detection"] - assert actual == expected - - -def test_realtime_turn_detection_rejects_incompatible_config_without_dropping_it(): - from agora_agent import OpenAIRealtime - from agora_agent.types.mllm_turn_detection import MllmTurnDetection - client = make_client(lambda _: pytest.fail("pure mapping")) - turn = MllmTurnDetection(mode="server_vad", semantic_vad_config={"eagerness": "high"}) - with pytest.raises(UnsupportedFieldError, match="semantic_vad_config"): - build_body(Agent(client=client).with_mllm(OpenAIRealtime(api_key="key", turn_detection=turn))) - - 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")): From 9df8284a7c3f4e47510252fec0ff1fb3ddf899bc Mon Sep 17 00:00:00 2001 From: seymourtang Date: Wed, 28 Oct 2026 21:38:54 +0800 Subject: [PATCH 44/57] docs: release v3.0.0-alpha.1 changelog --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 0983baf..3074b01 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). -## Unreleased preview contract alignment +## [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. @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - 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. -## Unreleased preview rollout +### 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. From e5f35ae79edeb6408c3152ad8e1f1f8d97376eca Mon Sep 17 00:00:00 2001 From: seymourtang Date: Wed, 28 Oct 2026 22:14:35 +0800 Subject: [PATCH 45/57] chore: align preview SDK versions and release configuration --- .github/workflows/ci.yml | 78 +++++++++----------- .github/workflows/release.yml | 6 +- README.md | 6 +- compat/agora-agent-server-sdk/README.md | 4 +- compat/agora-agent-server-sdk/pyproject.toml | 4 +- docs/getting-started/installation.md | 4 +- docs/guides/agent-workflow.md | 2 +- docs/guides/v3-api-preview.md | 8 +- pyproject.toml | 2 +- scripts/check_release_workflow.py | 15 +++- scripts/generate-v3-preview.py | 7 +- src/agora_agent/core/client_wrapper.py | 5 +- 12 files changed, 76 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f9d18e..620d5ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,17 +6,20 @@ on: jobs: compile: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + container: ubuntu:20.04 steps: + - name: Set up Ubuntu 20.04 and Python 3.8 + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl git python3 python3-pip python3-venv python-is-python3 + python --version - name: Checkout repo uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: 3.8 - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Install dependencies run: poetry install - name: Release metadata check @@ -24,17 +27,20 @@ jobs: - name: Compile run: poetry run mypy . test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + container: ubuntu:20.04 steps: + - name: Set up Ubuntu 20.04 and Python 3.8 + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl git python3 python3-pip python3-venv python-is-python3 + python --version - name: Checkout repo uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: 3.8 - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Install dependencies run: poetry install @@ -42,17 +48,20 @@ jobs: run: poetry run pytest -rP . compat-build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + container: ubuntu:20.04 steps: + - name: Set up Ubuntu 20.04 and Python 3.8 + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl git python3 python3-pip python3-venv python-is-python3 + python --version - name: Checkout repo uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: 3.8 - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Install primary package run: poetry install - name: Build and verify compatibility package @@ -74,46 +83,31 @@ jobs: publish: needs: [compile, test, compat-build] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + container: ubuntu:20.04 steps: + - name: Set up Ubuntu 20.04 and Python 3.8 + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl git python3 python3-pip python3-venv python-is-python3 + python --version - name: Checkout repo uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: 3.8 - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Install dependencies run: poetry install - 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) @@ -134,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 @@ -150,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 f706621..e8e5667 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ and multimodal flows (MLLM) for real-time audio processing. Use one `Agent` builder to configure providers, then call `create_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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 require this `feat/v3-api-preview` checkout; their presence here does not imply a published PyPI release. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. +Saving and project provisioning are part of SDK preview `3.0.0a1`. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. 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.0a1 ``` ## Requirements @@ -173,7 +173,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.0a1`; 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 diff --git a/compat/agora-agent-server-sdk/README.md b/compat/agora-agent-server-sdk/README.md index 7c3042d..a060a84 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.0a1 ``` 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`: @@ -17,4 +17,4 @@ 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) requires the preview checkout and is not implied by installing the compatibility package from PyPI. Existing v2 imports remain available. +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.0a1`. Existing v2 imports remain available. diff --git a/compat/agora-agent-server-sdk/pyproject.toml b/compat/agora-agent-server-sdk/pyproject.toml index 359f876..fed3767 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.12.0" +version = "3.0.0a1" 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.12.0,<3.0.0" +agora-agents = ">=3.0.0a1,<4.0.0" [build-system] requires = ["poetry-core"] diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index cca2853..e797e85 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.0a1 ``` ## Install with Poetry ```sh -poetry add agora-agents +poetry add agora-agents@3.0.0a1 ``` ## Imports diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 505a37d..2b6ef7a 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -2,7 +2,7 @@ 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 require this preview checkout, installed with `python -m pip install -e .` from the repository root. These features are not implied by a published PyPI version. Use token authentication with the public preview origin as described in the [transport setup](./v3-api-preview.md#quick-start-with-staging). +Saving and project secrets/numbers are part of SDK preview `3.0.0a1`, installed with `python -m pip install agora-agents==3.0.0a1`. 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 diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index d682e85..193df2c 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -13,13 +13,13 @@ Saving stores configuration; it does not itself start a session. Call `saved.cre ## Quick start with staging -Install from this repository's `feat/v3-api-preview` checkout: +Install SDK preview `3.0.0a1`: ```sh -python -m pip install -e . +python -m pip install agora-agents==3.0.0a1 ``` -See [installation](../getting-started/installation.md) for prerequisites. v3 is a preview branch feature; these examples do not imply that a released package contains it. The default public preview origin is `https://preview.ai.agora.io`. Configure App ID and App Certificate (or supply `auth_token`), then set: +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' @@ -28,7 +28,7 @@ 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 is token-only: a prebuilt `auth_token` or App ID/App Certificate signing. Internal gateway headers, `X-Studio-Region` and Basic authorization overrides are rejected. Current rollout availability is described in the [release notes](../../changelog.md#unreleased-preview-rollout); the SDK does not enforce region availability. +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 is token-only: a prebuilt `auth_token` or App ID/App Certificate signing. Internal gateway headers, `X-Studio-Region` and Basic authorization overrides are rejected. 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). diff --git a/pyproject.toml b/pyproject.toml index 81bc17e..d5af278 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "agora-agents" -version = "v2.12.0" +version = "3.0.0a1" description = "" readme = "README.md" authors = [] diff --git a/scripts/check_release_workflow.py b/scripts/check_release_workflow.py index a7229a2..61cf077 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.1") +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 index 6b2cede..42d54cb 100644 --- a/scripts/generate-v3-preview.py +++ b/scripts/generate-v3-preview.py @@ -7,6 +7,7 @@ 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" @@ -88,7 +89,7 @@ class RtcSessionTransport(RtcTransport, total=False): lines += ["__all__ = " + repr(names), ""] (OUT / "types.py").write_text("\n".join(lines)) -classes = {key: [] for key in ("agents", "secrets", "numbers", "sessions", "campaigns")} +classes: Dict[str, List[tuple]] = {key: [] for key in ("agents", "secrets", "numbers", "sessions", "campaigns")} for path, operations in spec["paths"].items(): tail = path.split("{appid}/", 1)[1] resource = tail.split("/", 1)[0] @@ -157,13 +158,13 @@ def qualified(value): text += [f"class {raw_name if raw else cname}({'AsyncResource' if asynchronous else 'Resource'}):"] if not raw: text += [' def __init__(self, owner, options):', ' super().__init__(owner, options)', f' self.with_raw_response = {raw_name}(owner, options)'] - for method, args, forwarding, verb, path_expr, headers, query, fields, result in methods: + for method, args, forwarding, verb, path_expr, headers, query, request_fields, result in methods: signature = ", ".join([qualified(a) for a in args] + ['appid: Optional[str] = None', 'request_options: Optional[dict] = None']) ret = f"RawResponse[{qualified(result)}]" if raw else qualified(result) text += [f" {'async ' if asynchronous else ''}def {method}(self, *, {signature}) -> {ret}:"] if raw: expression = lambda value: "{" + ", ".join(repr(k) + ": " + v for k, v in value.items()) + "}" - body = "{" + ", ".join(repr(k) + ": " + k for k in fields) + "}" if fields else "None" + 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']) + ")" diff --git a/src/agora_agent/core/client_wrapper.py b/src/agora_agent/core/client_wrapper.py index 93ad9aa..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 @@ -27,10 +28,10 @@ def __init__( def get_headers(self) -> typing.Dict[str, str]: headers: typing.Dict[str, str] = { - "User-Agent": "agora-agents/v2.12.0", + "User-Agent": f"agora-agents/v{__version__}", "X-Fern-Language": "Python", "X-Fern-SDK-Name": "agora-agents", - "X-Fern-SDK-Version": "v2.12.0", + "X-Fern-SDK-Version": f"v{__version__}", **(self.get_custom_headers() or {}), } supplier = getattr(self, "_authorization_supplier", None) From ebd56498f6ec7e170553a2cc72a3a52ab2df3fb1 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Wed, 28 Oct 2026 12:29:24 -0400 Subject: [PATCH 46/57] fix: align Platform builders and session helpers with documented flows --- README.md | 12 +- changelog.md | 6 + docs/guides/v3-api-preview.md | 6 +- docs/reference/v3.md | 22 ++- docs/reference/vendors.md | 6 + openapi.json | 133 +++++++++++++- openapi.yaml | 77 ++++++++- src/agora_agent/agentkit/agent.py | 26 ++- src/agora_agent/agentkit/agent_session.py | 24 +-- src/agora_agent/agentkit/session_backend.py | 71 +++++--- src/agora_agent/agentkit/v3.py | 62 +++++-- src/agora_agent/agentkit/vendors/llm.py | 62 ++++--- src/agora_agent/core/pydantic_utilities.py | 14 +- src/agora_agent/v3/openapi.json | 133 +++++++++++++- src/agora_agent/v3/types.py | 14 +- tests/custom/test_llm_vendors.py | 14 +- .../custom/test_platform_docs_regressions.py | 162 ++++++++++++++++++ tests/custom/test_v3_preview.py | 12 +- 18 files changed, 730 insertions(+), 126 deletions(-) create mode 100644 tests/custom/test_platform_docs_regressions.py diff --git a/README.md b/README.md index e8e5667..576631f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 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_session(...).start()` for a running session or `save()` for a `SavedAgent`. Async applications use `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. +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.0a1`. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. @@ -19,6 +19,14 @@ Saving and project provisioning are part of SDK preview `3.0.0a1`. Existing sess pip install agora-agents==3.0.0a1 ``` +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+ @@ -26,7 +34,7 @@ pip install agora-agents==3.0.0a1 ## 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 diff --git a/changelog.md b/changelog.md index 96e731d..43cf17a 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## Unreleased + +- 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. + ## [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. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index 193df2c..cc85e39 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -9,7 +9,7 @@ For the everyday builder → session or saved-agent flow, start with the [agent | 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. The low-level campaign API is available; high-level outbound-call helpers and AgentKit analysis helpers are not included. The SDK package version continues to describe package compatibility independently of the REST API version. +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 @@ -169,7 +169,7 @@ Use `await error.latest()` after an async 412. `save()` requires an `Agora` clie 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; the current response contract does not expose an analysis-result field or an AgentKit extraction-results helper. See the [daily workflow](./agent-workflow.md) for session options and unsupported controls. +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. @@ -199,7 +199,7 @@ See [the v3 source](../../src/agora_agent/v3/client.py), [wire schemas](../../sr ## 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. +`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 diff --git a/docs/reference/v3.md b/docs/reference/v3.md index 8d69a05..d9a7dc7 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -113,17 +113,17 @@ Prefix: `/conversational-ai/v1/projects/{appid}/sessions`. | `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. AgentKit session creation remains RTC-oriented; no high-level outbound-call helper is added. Stop-to-hangup/ringing behavior remains open. +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`; the current contract has no `analysis` result field; a nonunique name does not establish ownership by a stored agent. +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. -No extraction-result field is promised by the current session response contract. Unknown rollout fields remain readable in returned dictionaries. AgentKit analysis and outbound-call helpers are not provided. +`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 @@ -152,7 +152,7 @@ All low-level writes disable automatic retries. AgentKit number assignment is th | 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)` | `await saved.assign_number(...)` | +| 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 | @@ -197,3 +197,17 @@ Campaign create accepts 1–50,000 contacts and at most 10 MiB of JSON. `data_po 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 nonzero numeric `agent_uid` 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, 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 5044e44..13884f9 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -1151,3 +1151,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, language is emitted once at module level; explicit interaction language takes precedence over the vendor language. 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 index dc43782..8167110 100644 --- a/openapi.json +++ b/openapi.json @@ -8442,6 +8442,16 @@ "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, @@ -8559,13 +8569,21 @@ }, "keys": { "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[A-Za-z0-9_]+$" + "anyOf": [ + { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$" + }, + { + "$ref": "#/components/schemas/SecretKey" + } + ] }, "type": "array", - "title": "Keys" + "title": "Keys", + "description": "Accepts legacy key names and launch key metadata objects during rollout." }, "create_ts": { "type": "integer", @@ -8987,6 +9005,17 @@ } ], "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, @@ -14453,6 +14482,100 @@ "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": { diff --git a/openapi.yaml b/openapi.yaml index 774381c..20444e6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5195,6 +5195,14 @@ components: 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: @@ -5279,12 +5287,16 @@ components: title: Name keys: items: - type: string - maxLength: 64 - minLength: 1 - pattern: "^[A-Za-z0-9_]+$" + 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 @@ -5556,6 +5568,12 @@ components: - 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: @@ -8803,6 +8821,57 @@ components: 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 diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index 298f8ff..0834d74 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -945,6 +945,28 @@ async def deploy_async(self, *, name: typing.Optional[str] = None, agent_id: typ """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, @@ -1232,8 +1254,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: diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 7ad50da..07e08db 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -400,23 +400,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( @@ -617,7 +601,8 @@ def start(self) -> str: try: resource = self._client.v3.sessions method = resource.start if self._session_backend["stored"] else resource.start_ephemeral - response = method(**self._session_backend["body"]) + 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") @@ -1072,7 +1057,8 @@ async def start(self) -> str: try: resource = self._client.v3.sessions method = resource.start if self._session_backend["stored"] else resource.start_ephemeral - response = await method(**self._session_backend["body"]) + 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") diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 72f1380..2e92c44 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -18,10 +18,10 @@ def contains(value): 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, agent_uid, remote_uids, +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, **unsupported): + 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 @@ -36,25 +36,30 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids invalid = [key for key, value in unsupported.items() if value is not None] if invalid: raise ValueError("Unsupported session options: " + ", ".join(sorted(invalid))) - 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 + 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 - 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) for key, value in (("values", values), ("labels", labels), ("data_policy", data_policy)): if value is not None: body[key] = copy.deepcopy(value) @@ -81,6 +86,8 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids 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: @@ -106,8 +113,8 @@ def create_session(owner, *, asynchronous=False, channel, agent_uid, remote_uids body["idempotency_key"] = idempotency_key session_type = AsyncAgentSession if asynchronous else AgentSession return session_type(client=client, agent=owner, app_id=client.app_id, - name=reference or "", channel=channel, agent_uid=agent_uid, - remote_uids=list(remote_uids), debug=debug, warn=warn, + 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)}) @@ -152,3 +159,27 @@ def legacy_update_body(properties): 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"] + uid = transport.get("uid") + certificate = getattr(session._client, "app_certificate", None) + generate = "token" not in transport or (session._session_backend.get("generated_rtc_token") and body.get("idempotency_key") is None) + if (transport.get("type") == "rtc" and generate and certificate + and isinstance(uid, str) and uid.isascii() and uid.isdigit() and int(uid) != 0): + from .token import generate_convo_ai_token + transport["token"] = generate_convo_ai_token(app_id=session._client.app_id, + app_certificate=certificate, channel_name=transport["channel"], uid=int(uid)) + session._session_backend["generated_rtc_token"] = True + return body diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index a333c0b..7be1206 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -5,6 +5,7 @@ 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 @@ -100,7 +101,24 @@ def module(config, category, errors): 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.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"]) + query = [(key, value) for key, value in parse_qsl(parts.query, keep_blank_values=True) 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",), @@ -247,7 +265,7 @@ def build_body(agent, name=None): 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()) or {} + conversational = dump(agent._resolve_llm_config(platform=True)) or {} messages = conversational.pop("system_messages", None) if messages is not None: source = "llm.system_messages" @@ -307,9 +325,9 @@ def build_body(agent, name=None): if target == "asr": language = agent._field_value(agent._turn_detection, "language") if value is not None or language is not None: - # Preserve v2 interaction-language precedence and its existing - # managed ASR fallback, without adding ASR to partial builders. - value = agent._resolve_asr_config(agent._resolve_turn_detection_config()) + value = dump(value or {"vendor": "fengming" if getattr(agent._client, "area_scope", None) == "cn" else "ares"}) + if language is not None: + value["language"] = language if value is not None: pipeline[target] = module(value, target, errors) if "silence_config" in params: @@ -424,13 +442,19 @@ def _number_id(number): return number -def _policy(max_call_duration_seconds, max_silence_duration_ms): - policy = {} +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 @@ -456,6 +480,11 @@ def create_session(self, *, channel: str, agent_uid: Optional[str] = None, remot 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)) @@ -501,7 +530,7 @@ def _assign(self, number, agent_id, policy): for attempt in range(2): previous = raw.get(number_id=identifier) inbound = {"agent": agent_id} - if policy: + if policy is not None: inbound["call_policy"] = policy try: return raw.update(number_id=identifier, if_match=_require_etag(previous), inbound=inbound).data @@ -509,11 +538,11 @@ def _assign(self, number, agent_id, policy): if attempt: raise - def assign_number(self, number, *, max_call_duration_seconds=None, max_silence_duration_ms=None) -> NumberResponse: - return self._assign(number, self.id, _policy(max_call_duration_seconds, max_silence_duration_ms)) + 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, {}) + return self._assign(number, None, None) class AsyncSavedAgent(_SavedAgentBase): @@ -531,6 +560,11 @@ def create_session(self, *, channel: str, agent_uid: Optional[str] = None, remot 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)) @@ -576,7 +610,7 @@ async def _assign(self, number, agent_id, policy): for attempt in range(2): previous = await raw.get(number_id=identifier) inbound = {"agent": agent_id} - if policy: + 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 @@ -584,11 +618,11 @@ async def _assign(self, number, agent_id, policy): if attempt: raise - async def assign_number(self, number, *, max_call_duration_seconds=None, max_silence_duration_ms=None) -> NumberResponse: - return await self._assign(number, self.id, _policy(max_call_duration_seconds, max_silence_duration_ms)) + 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, {}) + return await self._assign(number, None, None) # Compatibility names are exact aliases; all paths share the same implementation. DeployedAgent = SavedAgent 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/core/pydantic_utilities.py b/src/agora_agent/core/pydantic_utilities.py index 1da6f10..d5a4a8e 100644 --- a/src/agora_agent/core/pydantic_utilities.py +++ b/src/agora_agent/core/pydantic_utilities.py @@ -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/v3/openapi.json b/src/agora_agent/v3/openapi.json index dc43782..8167110 100644 --- a/src/agora_agent/v3/openapi.json +++ b/src/agora_agent/v3/openapi.json @@ -8442,6 +8442,16 @@ "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, @@ -8559,13 +8569,21 @@ }, "keys": { "items": { - "type": "string", - "maxLength": 64, - "minLength": 1, - "pattern": "^[A-Za-z0-9_]+$" + "anyOf": [ + { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9_]+$" + }, + { + "$ref": "#/components/schemas/SecretKey" + } + ] }, "type": "array", - "title": "Keys" + "title": "Keys", + "description": "Accepts legacy key names and launch key metadata objects during rollout." }, "create_ts": { "type": "integer", @@ -8987,6 +9005,17 @@ } ], "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, @@ -14453,6 +14482,100 @@ "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": { diff --git a/src/agora_agent/v3/types.py b/src/agora_agent/v3/types.py index c308a80..866ad1e 100644 --- a/src/agora_agent/v3/types.py +++ b/src/agora_agent/v3/types.py @@ -410,6 +410,8 @@ class SecretDeleted(TypedDict, total=False): class SecretKey(TypedDict, total=False): key: Required[str] + create_ts: int + update_ts: int class SecretKeyWrite(TypedDict, total=False): value: Required[str] @@ -428,7 +430,7 @@ class SecretReplace(TypedDict, total=False): class SecretSet(TypedDict, total=False): name: Required[str] - keys: Required[List[str]] + keys: Required[List[Union[str, SecretKey]]] create_ts: Required[int] update_ts: Required[int] @@ -486,6 +488,7 @@ class SessionResponse(TypedDict, total=False): 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] @@ -911,6 +914,13 @@ class InboundEndCallPolicy(TypedDict, total=False): 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'] @@ -936,4 +946,4 @@ 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', 'AgentCreateModel', 'AgentPatchRequest', 'SessionLifecycle', 'EphemeralSessionAgent', 'StartSessionResponse', 'RtcGeofence', 'RtcSessionTransport', 'SessionTransport'] +__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_llm_vendors.py b/tests/custom/test_llm_vendors.py index c2a5037..6a8a126 100644 --- a/tests/custom/test_llm_vendors.py +++ b/tests/custom/test_llm_vendors.py @@ -90,7 +90,7 @@ def test_gemini_constructs_url_from_api_key_and_model() -> 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..8c8cd12 --- /dev/null +++ b/tests/custom/test_platform_docs_regressions.py @@ -0,0 +1,162 @@ +"""Regression cases reported by the Platform docs team.""" +import copy +import json +import subprocess +import sys + +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, SpeechmaticsSTT + + +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") + assert prepare_start_body(session)["transport"]["token"] == "rtc-generated" + 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 diff --git a/tests/custom/test_v3_preview.py b/tests/custom/test_v3_preview.py index fae1e38..f72ab2c 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -65,7 +65,7 @@ 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", app_certificate="cert", auth_token="raw-token", + 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) @@ -215,7 +215,7 @@ def handler(request): 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", "language": "en-US", "credential": {"mode": "byok", "fields": {"api_key": "$secrets.prod.dg"}}} + 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 @@ -483,12 +483,12 @@ def handler(request): agent.deploy(name="German") pipeline = json.loads(requests[0].content)["pipeline"] assert pipeline["asr"]["language"] == "de-DE" - assert pipeline["asr"]["params"]["language"] == "fr" + 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"] == pipeline["asr"]["params"]["language"] + assert v2["asr"]["params"]["language"] == "fr" def test_explicit_interaction_language_uses_existing_managed_asr_fallback(): @@ -504,8 +504,8 @@ def handler(request): 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"] == "en-US" - assert default["pipeline"]["asr"]["params"]["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]]) From 9306af1d36212e7e77eb3e385fcdd88148631ded Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Wed, 28 Oct 2026 12:30:47 -0400 Subject: [PATCH 47/57] fix: capture Platform RTC signing identity for session handles --- src/agora_agent/agentkit/session_backend.py | 17 ++++++++++------- tests/custom/test_platform_docs_regressions.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 2e92c44..0c614da 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -111,11 +111,15 @@ def create_session(owner, *, asynchronous=False, channel=None, agent_uid=None, r 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 int(agent_uid) != 0: + 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)}) + _session_backend={"stored": stored, "body": copy.deepcopy(body), "signing": signing}) def update_body(properties): @@ -173,13 +177,12 @@ def prepare_start_body(session): """Generate a fixed-UID RTC token once, preserving idempotent retry bodies.""" body = session._session_backend["body"] transport = body["transport"] - uid = transport.get("uid") - certificate = getattr(session._client, "app_certificate", None) + signing = session._session_backend.get("signing") generate = "token" not in transport or (session._session_backend.get("generated_rtc_token") and body.get("idempotency_key") is None) - if (transport.get("type") == "rtc" and generate and certificate - and isinstance(uid, str) and uid.isascii() and uid.isdigit() and int(uid) != 0): + if signing and generate: + if signing["uid"] > 4294967295: + raise ValueError("Numeric agent_uid must fit an unsigned 32-bit RTC UID for token generation") from .token import generate_convo_ai_token - transport["token"] = generate_convo_ai_token(app_id=session._client.app_id, - app_certificate=certificate, channel_name=transport["channel"], uid=int(uid)) + transport["token"] = generate_convo_ai_token(**signing) session._session_backend["generated_rtc_token"] = True return body diff --git a/tests/custom/test_platform_docs_regressions.py b/tests/custom/test_platform_docs_regressions.py index 8c8cd12..88dca5e 100644 --- a/tests/custom/test_platform_docs_regressions.py +++ b/tests/custom/test_platform_docs_regressions.py @@ -130,7 +130,9 @@ def test_rtc_token_generated_once_and_explicit_tokens_preserved(monkeypatch): 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"]) @@ -160,3 +162,17 @@ def test_response_metadata_fields_are_preserved(): 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" + assert prepare_start_body(session)["transport"]["token"] == "2" + large = agent.create_ephemeral_session(channel="r", agent_uid="4294967296", remote_uids=["2"]) + with pytest.raises(ValueError, match="32-bit"): + prepare_start_body(large) From 0d438fcb5f69f9e7a12a17397c54261745ded7a0 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Wed, 28 Oct 2026 12:31:35 -0400 Subject: [PATCH 48/57] fix: preserve raw Gemini credentials when normalizing Platform URLs --- src/agora_agent/agentkit/v3.py | 7 ++++++- tests/custom/test_platform_docs_regressions.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 7be1206..6a022ef 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -117,7 +117,12 @@ def module(config, category, errors): 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"]) - query = [(key, value) for key, value in parse_qsl(parts.query, keep_blank_values=True) if key != "key"] + 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"), diff --git a/tests/custom/test_platform_docs_regressions.py b/tests/custom/test_platform_docs_regressions.py index 88dca5e..dff0e81 100644 --- a/tests/custom/test_platform_docs_regressions.py +++ b/tests/custom/test_platform_docs_regressions.py @@ -176,3 +176,9 @@ def test_rtc_token_identity_range_and_non_idempotent_refresh(monkeypatch): large = agent.create_ephemeral_session(channel="r", agent_uid="4294967296", remote_uids=["2"]) with pytest.raises(ValueError, match="32-bit"): prepare_start_body(large) + + +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" From bfafb6c6e040e12c8f7bc6db7966355a35cc323f Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Wed, 28 Oct 2026 12:32:11 -0400 Subject: [PATCH 49/57] fix: refresh RTC tokens only for new successful-session lifecycles --- src/agora_agent/agentkit/agent_session.py | 2 ++ src/agora_agent/agentkit/session_backend.py | 3 +- .../custom/test_platform_docs_regressions.py | 36 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/agora_agent/agentkit/agent_session.py b/src/agora_agent/agentkit/agent_session.py index 07e08db..464e0e5 100644 --- a/src/agora_agent/agentkit/agent_session.py +++ b/src/agora_agent/agentkit/agent_session.py @@ -607,6 +607,7 @@ def start(self) -> str: 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 @@ -1063,6 +1064,7 @@ async def start(self) -> str: 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 diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 0c614da..55c27c0 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -178,11 +178,12 @@ def prepare_start_body(session): 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 body.get("idempotency_key") is None) + 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: if signing["uid"] > 4294967295: raise ValueError("Numeric agent_uid must fit an unsigned 32-bit RTC UID for token generation") 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/tests/custom/test_platform_docs_regressions.py b/tests/custom/test_platform_docs_regressions.py index dff0e81..4cc2bae 100644 --- a/tests/custom/test_platform_docs_regressions.py +++ b/tests/custom/test_platform_docs_regressions.py @@ -172,6 +172,7 @@ def test_rtc_token_identity_range_and_non_idempotent_refresh(monkeypatch): 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"]) with pytest.raises(ValueError, match="32-bit"): @@ -182,3 +183,38 @@ 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 From 671bed34dcbcd3c471ac47b4bfe15622b20fad38 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Wed, 28 Oct 2026 12:32:34 -0400 Subject: [PATCH 50/57] fix: leave unsupported RTC token identities to server provisioning --- docs/reference/v3.md | 2 +- src/agora_agent/agentkit/session_backend.py | 4 +--- tests/custom/test_platform_docs_regressions.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/reference/v3.md b/docs/reference/v3.md index d9a7dc7..5eee43e 100644 --- a/docs/reference/v3.md +++ b/docs/reference/v3.md @@ -202,7 +202,7 @@ Session start/ephemeral and campaign create/pause/resume/cancel accept optional 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 nonzero numeric `agent_uid` 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, or no certificate, the SDK leaves provisioning to the service; supply your own RTC token when required. This signer supports numeric UIDs only. +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"]) diff --git a/src/agora_agent/agentkit/session_backend.py b/src/agora_agent/agentkit/session_backend.py index 55c27c0..2a2a23c 100644 --- a/src/agora_agent/agentkit/session_backend.py +++ b/src/agora_agent/agentkit/session_backend.py @@ -113,7 +113,7 @@ def create_session(owner, *, asynchronous=False, channel=None, agent_uid=None, r 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 int(agent_uid) != 0: + 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, @@ -180,8 +180,6 @@ def prepare_start_body(session): 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: - if signing["uid"] > 4294967295: - raise ValueError("Numeric agent_uid must fit an unsigned 32-bit RTC UID for token generation") from .token import generate_convo_ai_token transport["token"] = generate_convo_ai_token(**signing) session._session_backend["generated_rtc_token"] = True diff --git a/tests/custom/test_platform_docs_regressions.py b/tests/custom/test_platform_docs_regressions.py index 4cc2bae..96545ef 100644 --- a/tests/custom/test_platform_docs_regressions.py +++ b/tests/custom/test_platform_docs_regressions.py @@ -175,8 +175,7 @@ def test_rtc_token_identity_range_and_non_idempotent_refresh(monkeypatch): 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"]) - with pytest.raises(ValueError, match="32-bit"): - prepare_start_body(large) + assert "token" not in prepare_start_body(large)["transport"] def test_raw_gemini_query_key_migrates_to_credentials(): From ed4466c982b1eb4b707f50ff258d27c7a9daf734 Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Wed, 28 Oct 2026 12:55:07 -0400 Subject: [PATCH 51/57] docs: align Azure and Bedrock required options with constructors --- docs/reference/vendors.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference/vendors.md b/docs/reference/vendors.md index 13884f9..cf8b149 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` | From e280c0788c30a662cf3bd0686ad26c70a1f1fc49 Mon Sep 17 00:00:00 2001 From: seymourtang Date: Thu, 29 Oct 2026 15:21:27 +0800 Subject: [PATCH 52/57] chore: prepare v3.0.0-alpha.2 release --- README.md | 6 +++--- changelog.md | 3 ++- compat/agora-agent-server-sdk/README.md | 4 ++-- compat/agora-agent-server-sdk/pyproject.toml | 4 ++-- docs/getting-started/installation.md | 4 ++-- docs/guides/agent-workflow.md | 2 +- docs/guides/v3-api-preview.md | 4 ++-- pyproject.toml | 2 +- scripts/check_release_workflow.py | 2 +- 9 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 576631f..526543c 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ and multimodal flows (MLLM) for real-time audio processing. 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.0a1`. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. +Saving and project provisioning are part of SDK preview `3.0.0a2`. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. ## Install ```sh -pip install agora-agents==3.0.0a1 +pip install agora-agents==3.0.0a2 ``` For source checkouts on Windows, use a short path and enable Git long paths before checkout: @@ -181,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==3.0.0a1`; 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.0a2`; 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 diff --git a/changelog.md b/changelog.md index 43cf17a..1419016 100644 --- a/changelog.md +++ b/changelog.md @@ -4,11 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). -## Unreleased +## [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 diff --git a/compat/agora-agent-server-sdk/README.md b/compat/agora-agent-server-sdk/README.md index a060a84..b4c6901 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==3.0.0a1 +pip install agora-agents==3.0.0a2 ``` 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`: @@ -17,4 +17,4 @@ 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.0a1`. Existing v2 imports remain available. +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.0a2`. Existing v2 imports remain available. diff --git a/compat/agora-agent-server-sdk/pyproject.toml b/compat/agora-agent-server-sdk/pyproject.toml index fed3767..bb8c643 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 = "3.0.0a1" +version = "3.0.0a2" 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 = ">=3.0.0a1,<4.0.0" +agora-agents = ">=3.0.0a2,<4.0.0" [build-system] requires = ["poetry-core"] diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index e797e85..d38423b 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==3.0.0a1 +pip install agora-agents==3.0.0a2 ``` ## Install with Poetry ```sh -poetry add agora-agents@3.0.0a1 +poetry add agora-agents@3.0.0a2 ``` ## Imports diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 2b6ef7a..07cb117 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -2,7 +2,7 @@ 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.0a1`, installed with `python -m pip install agora-agents==3.0.0a1`. Use token authentication with the public preview origin as described in the [transport setup](./v3-api-preview.md#quick-start-with-staging). +Saving and project secrets/numbers are part of SDK preview `3.0.0a2`, installed with `python -m pip install agora-agents==3.0.0a2`. 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 diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index cc85e39..bee2eb0 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -13,10 +13,10 @@ Saving stores configuration; it does not itself start a session. Call `saved.cre ## Quick start with staging -Install SDK preview `3.0.0a1`: +Install SDK preview `3.0.0a2`: ```sh -python -m pip install agora-agents==3.0.0a1 +python -m pip install agora-agents==3.0.0a2 ``` 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: diff --git a/pyproject.toml b/pyproject.toml index d5af278..2cbf7f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "agora-agents" -version = "3.0.0a1" +version = "3.0.0a2" description = "" readme = "README.md" authors = [] diff --git a/scripts/check_release_workflow.py b/scripts/check_release_workflow.py index 61cf077..797da5e 100644 --- a/scripts/check_release_workflow.py +++ b/scripts/check_release_workflow.py @@ -31,7 +31,7 @@ def read_compat_dependency(path: str) -> str: parser = argparse.ArgumentParser() -parser.add_argument("--tag", help="Git release tag, such as v3.0.0-alpha.1") +parser.add_argument("--tag", help="Git release tag, such as v3.0.0-alpha.2") args = parser.parse_args() root_version = read_version("pyproject.toml") From 78127a6816e927949880fcd5429e12682a79ceeb Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Thu, 29 Oct 2026 10:01:05 -0400 Subject: [PATCH 53/57] fix: validate ConvoAI tokens and preserve Platform provider settings --- changelog.md | 6 + docs/guides/v3-api-preview.md | 2 +- docs/reference/agent.md | 2 +- docs/reference/vendors.md | 2 +- openapi.json | 2 +- openapi.yaml | 9 +- src/agora_agent/agentkit/agent.py | 2 +- src/agora_agent/agentkit/v3.py | 13 +- src/agora_agent/agentkit/vendors/tts.py | 3 +- src/agora_agent/core/pydantic_utilities.py | 2 +- src/agora_agent/v3/openapi.json | 2 +- tests/custom/test_convoai_token_services.py | 116 ++++++++++++++++++ .../custom/test_platform_docs_regressions.py | 52 +++++++- 13 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 tests/custom/test_convoai_token_services.py diff --git a/changelog.md b/changelog.md index 3686993..ed78df2 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## [Unreleased] + +- 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. + ## [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. diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index bee2eb0..a8235f6 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -175,7 +175,7 @@ Existing resources and ordinary inline `Agent.create_session()` behavior remain 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`; provider-specific `params.language` remains separate. A configured STT follows v2 interaction-language precedence (the explicit turn language, otherwise `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. +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 | |---|---| diff --git a/docs/reference/agent.md b/docs/reference/agent.md index eeb6f33..cec79c0 100644 --- a/docs/reference/agent.md +++ b/docs/reference/agent.md @@ -263,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/vendors.md b/docs/reference/vendors.md index f9872ce..073074d 100644 --- a/docs/reference/vendors.md +++ b/docs/reference/vendors.md @@ -1211,4 +1211,4 @@ and LemonSlice selector validation is now mandatory. 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, language is emitted once at module level; explicit interaction language takes precedence over the vendor language. 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. +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 index 8167110..8f86792 100644 --- a/openapi.json +++ b/openapi.json @@ -14583,7 +14583,7 @@ "type": "apiKey", "in": "header", "name": "Authorization", - "description": "An Agora token produced by the existing token system. Send the complete header value: agora token=. SDKs accept a supplied token or generate one from App ID and App Certificate; Customer ID/Customer Secret authentication is not exposed." + "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; Customer ID/Customer Secret authentication is not exposed." } }, "parameters": { diff --git a/openapi.yaml b/openapi.yaml index 20444e6..2f88b7b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -8877,10 +8877,11 @@ components: type: apiKey in: header name: Authorization - description: 'An Agora token produced by the existing token system. Send the - complete header value: agora token=. SDKs accept a supplied token or - generate one from App ID and App Certificate; Customer ID/Customer Secret - authentication is not exposed.' + 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; Customer ID/Customer Secret authentication is not exposed.' parameters: IdempotencyKey: name: Idempotency-Key diff --git a/src/agora_agent/agentkit/agent.py b/src/agora_agent/agentkit/agent.py index 3f9d46b..52ad6a6 100644 --- a/src/agora_agent/agentkit/agent.py +++ b/src/agora_agent/agentkit/agent.py @@ -1148,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: diff --git a/src/agora_agent/agentkit/v3.py b/src/agora_agent/agentkit/v3.py index 6a022ef..0810a7f 100644 --- a/src/agora_agent/agentkit/v3.py +++ b/src/agora_agent/agentkit/v3.py @@ -10,6 +10,8 @@ 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 @@ -107,7 +109,14 @@ def module(config, category, errors): if category == "asr": if "keywords" in config: params["keywords"] = config.pop("keywords") - language = config.get("language", params.pop("language", None)) + 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"}: @@ -332,6 +341,8 @@ def build_body(agent, name=None): 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) 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/core/pydantic_utilities.py b/src/agora_agent/core/pydantic_utilities.py index d5a4a8e..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_) diff --git a/src/agora_agent/v3/openapi.json b/src/agora_agent/v3/openapi.json index 8167110..8f86792 100644 --- a/src/agora_agent/v3/openapi.json +++ b/src/agora_agent/v3/openapi.json @@ -14583,7 +14583,7 @@ "type": "apiKey", "in": "header", "name": "Authorization", - "description": "An Agora token produced by the existing token system. Send the complete header value: agora token=. SDKs accept a supplied token or generate one from App ID and App Certificate; Customer ID/Customer Secret authentication is not exposed." + "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; Customer ID/Customer Secret authentication is not exposed." } }, "parameters": { 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(" Date: Thu, 29 Oct 2026 10:07:11 -0400 Subject: [PATCH 54/57] fix: reject whitespace in token signing credentials --- docs/getting-started/installation.md | 2 +- src/agora_agent/agentkit/token.py | 7 +++---- tests/custom/test_token_credentials.py | 27 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d38423b..f2d1633 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -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/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/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") From 063d3ca412a64d25a505907df89fe5f102658cfc Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Thu, 29 Oct 2026 10:37:21 -0400 Subject: [PATCH 55/57] Prepare alpha.3 preview release metadata and installation docs --- README.md | 6 +++--- changelog.md | 3 +++ compat/agora-agent-server-sdk/README.md | 4 ++-- compat/agora-agent-server-sdk/pyproject.toml | 4 ++-- docs/getting-started/installation.md | 4 ++-- docs/guides/agent-workflow.md | 2 +- docs/guides/v3-api-preview.md | 4 ++-- pyproject.toml | 2 +- 8 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 526543c..f870d20 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ and multimodal flows (MLLM) for real-time audio processing. 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.0a2`. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. +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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. ## Install ```sh -pip install agora-agents==3.0.0a2 +pip install agora-agents==3.0.0a3 ``` For source checkouts on Windows, use a short path and enable Git long paths before checkout: @@ -181,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==3.0.0a2`; 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 diff --git a/changelog.md b/changelog.md index ed78df2..50f0a79 100644 --- a/changelog.md +++ b/changelog.md @@ -6,9 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +## [v3.0.0-alpha.3] — 2026-10-29 + - 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 diff --git a/compat/agora-agent-server-sdk/README.md b/compat/agora-agent-server-sdk/README.md index b4c6901..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==3.0.0a2 +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`: @@ -17,4 +17,4 @@ 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.0a2`. Existing v2 imports remain available. +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 bb8c643..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 = "3.0.0a2" +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 = ">=3.0.0a2,<4.0.0" +agora-agents = ">=3.0.0a3,<4.0.0" [build-system] requires = ["poetry-core"] diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index f2d1633..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==3.0.0a2 +pip install agora-agents==3.0.0a3 ``` ## Install with Poetry ```sh -poetry add agora-agents@3.0.0a2 +poetry add agora-agents@3.0.0a3 ``` ## Imports diff --git a/docs/guides/agent-workflow.md b/docs/guides/agent-workflow.md index 07cb117..ecbd6b6 100644 --- a/docs/guides/agent-workflow.md +++ b/docs/guides/agent-workflow.md @@ -2,7 +2,7 @@ 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.0a2`, installed with `python -m pip install agora-agents==3.0.0a2`. Use token authentication with the public preview origin as described in the [transport setup](./v3-api-preview.md#quick-start-with-staging). +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 diff --git a/docs/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index a8235f6..bb688a8 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -13,10 +13,10 @@ Saving stores configuration; it does not itself start a session. Call `saved.cre ## Quick start with staging -Install SDK preview `3.0.0a2`: +Install SDK preview `3.0.0a3`: ```sh -python -m pip install agora-agents==3.0.0a2 +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: diff --git a/pyproject.toml b/pyproject.toml index 2cbf7f1..f85d0fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "agora-agents" -version = "3.0.0a2" +version = "3.0.0a3" description = "" readme = "README.md" authors = [] From 83b48f5c2c4e8805e8443513646d349c0d17194a Mon Sep 17 00:00:00 2001 From: Hermes Frangoudis Date: Thu, 29 Oct 2026 11:31:36 -0400 Subject: [PATCH 56/57] Support Basic authentication alongside ConvoAI token007 --- README.md | 2 +- changelog.md | 2 + docs/getting-started/authentication.md | 23 ++++- docs/guides/v3-api-preview.md | 2 +- docs/reference/client.md | 8 +- openapi.json | 12 ++- openapi.yaml | 11 ++- src/agora_agent/client.py | 6 +- src/agora_agent/core/http_client.py | 8 +- src/agora_agent/core/public_auth.py | 45 ++++++++- src/agora_agent/pool_client.py | 76 +++++++++------ src/agora_agent/v3/_transport.py | 6 +- src/agora_agent/v3/openapi.json | 12 ++- tests/custom/test_basic_auth.py | 128 +++++++++++++++++++++++++ tests/custom/test_v3_preview.py | 4 +- 15 files changed, 286 insertions(+), 59 deletions(-) create mode 100644 tests/custom/test_basic_auth.py diff --git a/README.md b/README.md index f870d20..3f6ecf3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ and multimodal flows (MLLM) for real-time audio processing. 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 uses `auth_token` or tokens signed from App ID/App Certificate; Basic credentials and internal gateway headers are rejected. The new default origin is `https://preview.ai.agora.io`, with `/conversational-ai/v1/projects/{appid}`. +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 diff --git a/changelog.md b/changelog.md index 50f0a79..25f845b 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## [v3.0.0-alpha.3] — 2026-10-29 +- 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. diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md index 9efae3c..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 @@ -52,7 +52,7 @@ print(client.auth_mode) # "app-credentials" ## Pre-built REST tokens -The public SDK supports token authentication only: pass a raw `auth_token`, or let the SDK sign tokens from `app_id` and `app_certificate`. Customer ID/secret, client ID/secret and Basic authorization overrides are rejected. REST authentication is separate from the optional RTC transport token. +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. ### Pre-built REST token @@ -69,4 +69,21 @@ print(client.auth_mode) # "token" New resources use `https://preview.ai.agora.io/conversational-ai/v1/projects/{appid}`. Do not send internal gateway headers or `X-Studio-Region`. -A client with only a REST `auth_token` 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`. +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", + customer_id="your-customer-id", + customer_secret="your-customer-secret", + # app_certificate="your-app-certificate", # Optional RTC token signing. +) +print(client.auth_mode) # "basic" +``` + +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/guides/v3-api-preview.md b/docs/guides/v3-api-preview.md index bb688a8..d7864bb 100644 --- a/docs/guides/v3-api-preview.md +++ b/docs/guides/v3-api-preview.md @@ -28,7 +28,7 @@ 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 is token-only: a prebuilt `auth_token` or App ID/App Certificate signing. Internal gateway headers, `X-Studio-Region` and Basic authorization overrides are rejected. Current rollout availability is described in the [release notes](../../changelog.md#preview-rollout); the SDK does not enforce region availability. +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). diff --git a/docs/reference/client.md b/docs/reference/client.md index 032cbc1..a06e7fc 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -26,9 +26,11 @@ 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` | Unless `auth_token` supplied | `None` | Agora App Certificate for signing | +| `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 | @@ -36,7 +38,7 @@ client = Agora( | `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) | -`app_id` is required; `app_certificate` is required only when `auth_token` is omitted. Omit `auth_token` to sign REST tokens automatically, or supply a raw prebuilt token. Basic credentials are not exposed. +`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. diff --git a/openapi.json b/openapi.json index 8f86792..188b0e5 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Conversational AI V3 API", "version": "3.0.0", - "description": "Public Conversational AI preview contract used by the Agent SDKs. Authenticate with the existing Agora token system using Authorization: agora token=." + "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": { @@ -14583,7 +14583,12 @@ "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; Customer ID/Customer Secret authentication is not exposed." + "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": { @@ -14621,6 +14626,9 @@ "security": [ { "AgoraTokenAuth": [] + }, + { + "BasicAuth": [] } ] } diff --git a/openapi.yaml b/openapi.yaml index 2f88b7b..9955807 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4,7 +4,7 @@ info: title: Conversational AI V3 API version: 3.0.0 description: 'Public Conversational AI preview contract used by the Agent SDKs. - Authenticate with the existing Agora token system using Authorization: agora token=.' + 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: @@ -8873,6 +8873,12 @@ components: - 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 @@ -8881,7 +8887,7 @@ components: 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; Customer ID/Customer Secret authentication is not exposed.' + App Certificate. HTTP Basic authentication is also supported as an alternative.' parameters: IdempotencyKey: name: Idempotency-Key @@ -8916,3 +8922,4 @@ servers: description: Agora Conversational AI API security: - AgoraTokenAuth: [] +- BasicAuth: [] diff --git a/src/agora_agent/client.py b/src/agora_agent/client.py index f29015f..1ec2270 100644 --- a/src/agora_agent/client.py +++ b/src/agora_agent/client.py @@ -68,8 +68,7 @@ def __init__( ): from .core.public_auth import validate_public_headers validate_public_headers(headers) - if authorization and not authorization.lower().startswith("agora token="): - raise ValueError("Only Agora token authentication is supported") + 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 ) @@ -177,8 +176,7 @@ def __init__( ): from .core.public_auth import validate_public_headers validate_public_headers(headers) - if authorization and not authorization.lower().startswith("agora token="): - raise ValueError("Only Agora token authentication is supported") + 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 ) diff --git a/src/agora_agent/core/http_client.py b/src/agora_agent/core/http_client.py index 272af95..e1fdbd5 100644 --- a/src/agora_agent/core/http_client.py +++ b/src/agora_agent/core/http_client.py @@ -208,7 +208,7 @@ def request( 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("HTTP Basic auth overrides are not supported") + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") request_headers = merge_public_headers( base_headers, headers or {}, @@ -311,7 +311,7 @@ def stream( 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("HTTP Basic auth overrides are not supported") + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") request_headers = merge_public_headers( base_headers, headers or {}, @@ -418,7 +418,7 @@ async def request( 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("HTTP Basic auth overrides are not supported") + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") request_headers = merge_public_headers( base_headers, headers or {}, @@ -521,7 +521,7 @@ async def stream( 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("HTTP Basic auth overrides are not supported") + raise ValueError("Configure Basic credentials on Agora/AsyncAgora; httpx auth would override SDK authentication") request_headers = merge_public_headers( base_headers, headers or {}, diff --git a/src/agora_agent/core/public_auth.py b/src/agora_agent/core/public_auth.py index 88743e0..7c7d5e2 100644 --- a/src/agora_agent/core/public_auth.py +++ b/src/agora_agent/core/public_auth.py @@ -1,7 +1,41 @@ -"""Token-only public SDK authentication policy (including transport overrides).""" +"""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() @@ -11,14 +45,15 @@ def validate_public_headers(headers, *, allow_empty_authorization=False): 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 == "") and not re.fullmatch(r"agora token=\S+", str(value), re.IGNORECASE): - raise ValueError("Only Agora token authentication is supported") + 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("Basic credential overrides are not supported") + 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", {})) @@ -27,7 +62,7 @@ def validate_effective_public_headers(*sources): merged = merge_public_headers(*sources) validate_public_headers(merged) if not any(name.lower() == "authorization" for name in merged): - raise ValueError("Agora token authorization is required") + raise ValueError("Agora token or Basic authorization is required") def merge_public_headers(*sources): diff --git a/src/agora_agent/pool_client.py b/src/agora_agent/pool_client.py index 1ab2479..61735ef 100644 --- a/src/agora_agent/pool_client.py +++ b/src/agora_agent/pool_client.py @@ -20,7 +20,7 @@ from .v3.campaigns import AsyncCampaignsClient, CampaignsClient from .v3.client import AsyncNumbersClient, AsyncSecretsClient, NumbersClient, SecretsClient -_AUTH_MODE = typing.Literal["app-credentials", "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] @@ -135,6 +135,13 @@ class Agora(BaseAgora, typing.Generic[_AreaT]): string — the SDK automatically sets the header as ``Authorization: agora token=``. + 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. @@ -201,6 +208,8 @@ def __new__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -217,6 +226,8 @@ def __new__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -233,6 +244,8 @@ def __new__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -258,6 +271,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -274,6 +289,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -290,6 +307,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -305,6 +324,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -312,25 +333,15 @@ def __init__( debug: bool = False, v3: typing.Optional[V3Options] = None, ): - from .core.public_auth import validate_public_headers + from .core.public_auth import resolve_client_auth, validate_public_headers validate_public_headers(headers) - if auth_token is not None and (not isinstance(auth_token, str) or not auth_token.strip()): - raise ValueError("auth_token must be a nonempty raw token") - if auth_token is None and (not isinstance(app_certificate, str) or not app_certificate.strip()): - raise ValueError("app_certificate is required unless auth_token is supplied") + 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 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: @@ -350,7 +361,7 @@ def __init__( follow_redirects=follow_redirects, httpx_client=httpx_client, ) - if auth_token is None: + 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, ) @@ -491,6 +502,13 @@ class AsyncAgora(BaseAsyncAgora, typing.Generic[_AreaT]): string — the SDK automatically sets the header as ``Authorization: agora token=``. + 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. @@ -541,6 +559,8 @@ def __new__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -557,6 +577,8 @@ def __new__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -573,6 +595,8 @@ def __new__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -598,6 +622,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -614,6 +640,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -630,6 +658,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -645,6 +675,8 @@ def __init__( app_id: 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, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -652,25 +684,15 @@ def __init__( debug: bool = False, v3: typing.Optional[V3Options] = None, ): - from .core.public_auth import validate_public_headers + from .core.public_auth import resolve_client_auth, validate_public_headers validate_public_headers(headers) - if auth_token is not None and (not isinstance(auth_token, str) or not auth_token.strip()): - raise ValueError("auth_token must be a nonempty raw token") - if auth_token is None and (not isinstance(app_certificate, str) or not app_certificate.strip()): - raise ValueError("app_certificate is required unless auth_token is supplied") + 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 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: @@ -690,7 +712,7 @@ def __init__( follow_redirects=follow_redirects, httpx_client=httpx_client, ) - if auth_token is None: + 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, ) diff --git a/src/agora_agent/v3/_transport.py b/src/agora_agent/v3/_transport.py index 9b883ca..84ce72f 100644 --- a/src/agora_agent/v3/_transport.py +++ b/src/agora_agent/v3/_transport.py @@ -90,14 +90,14 @@ def _arguments(self, method, path, body, headers, params, request_options): _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": + 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.setdefault("Authorization", f"agora token={token}") + 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("Basic credential overrides are not supported") + 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) diff --git a/src/agora_agent/v3/openapi.json b/src/agora_agent/v3/openapi.json index 8f86792..188b0e5 100644 --- a/src/agora_agent/v3/openapi.json +++ b/src/agora_agent/v3/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Conversational AI V3 API", "version": "3.0.0", - "description": "Public Conversational AI preview contract used by the Agent SDKs. Authenticate with the existing Agora token system using Authorization: agora token=." + "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": { @@ -14583,7 +14583,12 @@ "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; Customer ID/Customer Secret authentication is not exposed." + "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": { @@ -14621,6 +14626,9 @@ "security": [ { "AgoraTokenAuth": [] + }, + { + "BasicAuth": [] } ] } 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_v3_preview.py b/tests/custom/test_v3_preview.py index f72ab2c..623dea9 100644 --- a/tests/custom/test_v3_preview.py +++ b/tests/custom/test_v3_preview.py @@ -1035,9 +1035,9 @@ async def test_default_public_origin_and_signed_token(async_): @pytest.mark.parametrize("field", ["customer_id", "customer_secret", "client_id", "client_secret", "customerId", "customerSecret", "clientId", "clientSecret", "username", "password"]) -def test_removed_basic_credentials_rejected_every_entrypoint(field): +def test_partial_or_unknown_basic_credentials_rejected(field): for cls in (Agora, AsyncAgora): - with pytest.raises(TypeError): + 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"): From 058627da031714e07fcaee10918a65fa6d336d46 Mon Sep 17 00:00:00 2001 From: seymourtang Date: Fri, 30 Oct 2026 12:20:07 +0800 Subject: [PATCH 57/57] docs: finalize v3.0.0-alpha.3 changelog for 2026-10-30 --- changelog.md | 4 +--- scripts/check_release_workflow.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 25f845b..bfdea28 100644 --- a/changelog.md +++ b/changelog.md @@ -4,9 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). -## [Unreleased] - -## [v3.0.0-alpha.3] — 2026-10-29 +## [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. diff --git a/scripts/check_release_workflow.py b/scripts/check_release_workflow.py index 797da5e..159e410 100644 --- a/scripts/check_release_workflow.py +++ b/scripts/check_release_workflow.py @@ -31,7 +31,7 @@ def read_compat_dependency(path: str) -> str: parser = argparse.ArgumentParser() -parser.add_argument("--tag", help="Git release tag, such as v3.0.0-alpha.2") +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")