diff --git a/signalwire/signalwire/ai_chat/__init__.py b/signalwire/signalwire/ai_chat/__init__.py index 73542767..ad0671a1 100644 --- a/signalwire/signalwire/ai_chat/__init__.py +++ b/signalwire/signalwire/ai_chat/__init__.py @@ -26,15 +26,15 @@ __all__ = [ "AIChatClient", - "ChatGateway", - "GatewayRejection", "AIChatError", "AuthenticationError", + "ChatGateway", "ChatInProgressError", "ChatLog", "ChatResponse", "ConversationInfo", "ConversationNotFoundError", + "GatewayRejection", "RateLimitError", "SummaryError", ] diff --git a/signalwire/signalwire/ai_chat/gateway.py b/signalwire/signalwire/ai_chat/gateway.py index e2f4f5f4..4d27e259 100644 --- a/signalwire/signalwire/ai_chat/gateway.py +++ b/signalwire/signalwire/ai_chat/gateway.py @@ -73,8 +73,8 @@ SERVICE_DEFAULT_CONVERSATION_TIMEOUT = 3600 # Caps chosen to be invisible to a real conversation and ruinous to a script. -DEFAULT_MAX_NEW_CONVERSATIONS = 60 # per window, per gateway -DEFAULT_MAX_TURNS = 200 # per conversation, ever +DEFAULT_MAX_NEW_CONVERSATIONS = 60 # per window, per gateway +DEFAULT_MAX_TURNS = 200 # per conversation, ever DEFAULT_WINDOW_SECONDS = 60 # Hosts that never need listing, so `pip install` → run → it works. @@ -160,8 +160,10 @@ def __init__( raise ValueError("config_url is required — it is what a key is scoped to.") self.config_url = config_url - self.key = key or os.environ.get("SIGNALWIRE_CHAT_GATEWAY_KEY") or ( - "pk_" + secrets.token_urlsafe(24) + self.key = ( + key + or os.environ.get("SIGNALWIRE_CHAT_GATEWAY_KEY") + or ("pk_" + secrets.token_urlsafe(24)) ) self.allowed_origins = {o.rstrip("/") for o in allowed_origins} self.handle_ttl = handle_ttl @@ -174,14 +176,16 @@ def __init__( self._owns_client = client is None if secret is None: - secret = os.environ.get("SIGNALWIRE_CHAT_GATEWAY_SECRET") or secrets.token_bytes(32) + secret = os.environ.get( + "SIGNALWIRE_CHAT_GATEWAY_SECRET" + ) or secrets.token_bytes(32) self._secret = secret.encode() if isinstance(secret, str) else secret self._mints: list[float] = [] self._turns: dict[str, tuple[int, float]] = {} @staticmethod - def last_activity(messages: list[dict[str, Any]]) -> float | None: + def last_activity(messages: list[dict[str, Any]] | None) -> float | None: """Epoch SECONDS of the newest message, or None if nothing is dated. Bootstraps a browser's idle clock across a reload. Without it a widget @@ -288,7 +292,9 @@ def check_key(self, presented: str | None) -> None: raise GatewayRejection(401, "bad key") @staticmethod - def visible_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + def visible_messages( + messages: list[dict[str, Any]] | None, + ) -> list[dict[str, Any]]: """The transcript a browser may redraw, and nothing else. `chat_log` hands back the conversation as the service holds it: the @@ -341,8 +347,9 @@ def _charge_turn(self, conversation_id: str) -> None: # ── The proxied call ───────────────────────────────────────────── - def prepare(self, body: dict[str, Any], *, origin: str | None, - key: str | None) -> tuple[str, dict[str, Any], str | None]: + def prepare( + self, body: dict[str, Any], *, origin: str | None, key: str | None + ) -> tuple[str, dict[str, Any], str | None]: """Validate a browser request and build the upstream JSON-RPC call. Returns ``(method, params, minted_handle)`` — ``minted_handle`` is set diff --git a/signalwire/signalwire/core/function_result.py b/signalwire/signalwire/core/function_result.py index 23f3d286..b1a50923 100644 --- a/signalwire/signalwire/core/function_result.py +++ b/signalwire/signalwire/core/function_result.py @@ -562,14 +562,16 @@ def hold( Returns: self for method chaining """ - # Back-compat: hold(120) used to mean hold(timeout=120) - if isinstance(prompt, int) and not isinstance(prompt, bool): + # Back-compat: hold(120) used to mean hold(timeout=120). `bool` subclasses + # `int`, so it is excluded explicitly — a bool is neither a prompt nor a + # timeout, and dropping it here keeps the remaining type `str | None`. + if isinstance(prompt, bool): + prompt = None + elif isinstance(prompt, int): timeout, prompt = prompt, None if prompt is not None: - self.set_tool_response( - tool_result="status: on hold", tool_prompt=prompt - ) + self.set_tool_response(tool_result="status: on hold", tool_prompt=prompt) self.post_process = True # Clamp timeout to valid range diff --git a/signalwire/signalwire/core/post_prompt_generated.py b/signalwire/signalwire/core/post_prompt_generated.py index 659fa3e5..b677404b 100644 --- a/signalwire/signalwire/core/post_prompt_generated.py +++ b/signalwire/signalwire/core/post_prompt_generated.py @@ -9,9 +9,10 @@ from typing import Any, Literal, TypeAlias, TypedDict from typing import TYPE_CHECKING -# SwaigRequest is generated in swaig_request_generated; aliased here for the -# swaig_log entry's post_data field. +# Types owned by sibling swaig specs, imported so the cross-file +# $ref fields below resolve to the real type rather than a dict. if TYPE_CHECKING: + from signalwire.core.swaig_actions_generated import SwaigResponse as SwaigResponse from signalwire.core.swaig_request_generated import SwaigRequest as SwaigRequest @@ -69,7 +70,7 @@ class PostPrompt(TypedDict, total=False): class PostPromptData(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - parsed: list[dict[str, Any]] + parsed: list[dict[str, Any] | list[Any]] raw: str substituted: str @@ -160,14 +161,40 @@ class PostPromptSystemLogEntry(TypedDict, total=False): role: str content: str timestamp: int - action: str + action: Literal[ + "attention_timeout", + "attention_wait", + "auto_correct", + "change_step_failed", + "check_for_input", + "context_enter", + "double_turn", + "filler", + "function_call", + "function_error", + "function_loop", + "gather_answer", + "gather_complete", + "gather_question", + "gather_reject", + "gather_start", + "hangup_hook", + "hearing_hint", + "inner_dialog", + "inner_dialog_scorecard", + "manual_say", + "reset", + "session_end", + "session_start", + "startup_hook", + "step_change", + "summarize_start", + "swaig_problem", + ] lang: str tokens: int content_type: str metadata: dict[str, Any] - context: str - step: str - step_index: int class PostPromptSystemEntry(TypedDict, total=False): @@ -184,16 +211,16 @@ class PostPromptSwaigLogEntry(TypedDict, total=False): command_name: str command_arg: str epoch_time: int - native: bool + native: Literal[True] active_count: int | Literal["endless"] url: str post_data: SwaigRequest - post_response: dict[str, Any] - delayed_post_response: dict[str, Any] + post_response: SwaigResponse + delayed_post_response: SwaigResponse mcp_url: str mcp_tool: str - mcp_response: dict[str, Any] - mcp_error: str + mcp_response: str + mcp_error: Literal[True] class PostPromptTimesEntry(TypedDict, total=False): diff --git a/signalwire/signalwire/core/swaig_actions_generated.py b/signalwire/signalwire/core/swaig_actions_generated.py index f967a192..9faf4083 100644 --- a/signalwire/signalwire/core/swaig_actions_generated.py +++ b/signalwire/signalwire/core/swaig_actions_generated.py @@ -17,171 +17,228 @@ class ContextSwitchAction(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - system_prompt: Any - user_prompt: Any - system_pom: Any - user_pom: Any - consolidate: bool - full_reset: bool + consolidate: bool | str + full_reset: bool | str + system_pom: dict[str, Any] + system_prompt: str + user_pom: dict[str, Any] + user_prompt: str class HoldAction(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - timeout: int + timeout: float | str class PlaybackBgAction(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - file: Any - wait: bool + file: str + wait: bool | str class TransferAction(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - dest: Any - summarize: bool + dest: str + summarize: bool | str + + +class SwaigAction(TypedDict, total=False): + """A response-action object. The keys below are the full vocabulary dispatched by actions.c::process_action; an action object sets one or more of them. Each key's source line is the engine dispatch site. + + Open shape: extra server keys permitted; not validated at runtime. + """ + + SWML: str | dict[str, Any] + add_dynamic_hints: list[dict[str, Any] | str] + back_to_back_functions: bool | Literal["forever"] | str + change_context: str + change_step: str + clear_dynamic_hints: bool | str + context_switch: str | ContextSwitchAction + end_of_speech_timeout: int + extensive_data: bool | str + functions_on_speaker_timeout: bool | str + hangup: bool | str + hold: int | str | HoldAction + playback_bg: str | PlaybackBgAction + replace_in_history: str | Literal[True] + say: str + set_global_data: dict[str, Any] + set_meta_data: dict[str, Any] + settings: dict[str, Any] + speech_event_timeout: int + stop: bool | str + stop_playback_bg: bool | str | int | dict[str, Any] | list[Any] | None + toggle_functions: list[dict[str, Any]] + transfer: str | TransferAction + unset_global_data: str | list[str] + unset_meta_data: str | list[str] + user_event: dict[str, Any] + user_input: str + wait_for_user: bool | int | Literal["answer_first"] | str + + +class SwaigResponse(TypedDict, total=False): + """Parsed at actions.c:2228-2276. + + Open shape: extra server keys are permitted and partial payloads are valid; + not validated at runtime (a TypedDict is a plain ``dict``). + """ + + response: str + action: SwaigAction | list[SwaigAction] + post_process: bool class _SwaigActions: """Typed SWAIG response-action builders (one per wire action). The host class provides ``self.action`` (the list serialized to the wire).""" - def add_dynamic_hints(self: _Self, value: list[Any]) -> _Self: - """Add ASR hints. Strings go to `dynamic_hints`; `{hint, ...}` objects go to `dynamic_hearing_hints` (and the `hint` value is also added to `dynamic_hints`). Restarts speech detection""" # actions.c:547 + def SWML(self: _Self, value: str | dict[str, Any]) -> _Self: + """Execute a SWML document inline, or with sibling `transfer:true` transfer the call into it. Gated by `swaig_allow_swml`. **Transfer additionally requires `from_relay`** (`actions.c:142-145`); inline execution captures an optional `ai_response` SWML var back into the conversation""" # actions.c:129 + self.action.append({"SWML": value}) # type: ignore[attr-defined] + return self + + def add_dynamic_hints(self: _Self, value: list[dict[str, Any] | str]) -> _Self: + """Add ASR hints. Strings go to `dynamic_hints`; `{hint, ...}` objects go to `dynamic_hearing_hints` (and the `hint` value is also added to `dynamic_hints`). Restarts speech detection""" # actions.c:550 self.action.append({"add_dynamic_hints": value}) # type: ignore[attr-defined] return self - def back_to_back_functions(self: _Self, value: bool | Literal["forever"]) -> _Self: - """Allow consecutive function calls without a user turn. `true` = `1`, `"forever"` = `2`""" # actions.c:359 + def back_to_back_functions( + self: _Self, value: bool | Literal["forever"] | str + ) -> _Self: + """Allow consecutive function calls without a user turn. `true` = `1`, `"forever"` = `2`""" # actions.c:362 self.action.append({"back_to_back_functions": value}) # type: ignore[attr-defined] return self def change_context(self: _Self, value: str) -> _Self: - """Switch to a named **context** (same machinery as the `change_context` function)""" # actions.c:238 + """Switch to a named **context** (same machinery as the `change_context` function)""" # actions.c:241 self.action.append({"change_context": value}) # type: ignore[attr-defined] return self def change_step(self: _Self, value: str) -> _Self: - """Switch to a named **step** (or `"next"`)""" # actions.c:248 + """Switch to a named **step** (or `"next"`)""" # actions.c:251 self.action.append({"change_step": value}) # type: ignore[attr-defined] return self - def clear_dynamic_hints(self: _Self, value: dict[str, Any]) -> _Self: - """Clear both dynamic hint lists and restart speech detection""" # actions.c:579 + def clear_dynamic_hints(self: _Self, value: bool | str) -> _Self: + """Clear both dynamic hint lists and restart speech detection""" # actions.c:582 self.action.append({"clear_dynamic_hints": value}) # type: ignore[attr-defined] return self def context_switch(self: _Self, value: str | ContextSwitchAction) -> _Self: - """Replace the system prompt / start a new conversation context. Object form: `{system_prompt, user_prompt, system_pom, user_pom, consolidate, full_reset}`. `system_pom`/`user_pom` render to prompt text; prompts are expanded against prompt vars + post_data; `consolidate:true` summarizes first""" # actions.c:594 + """Replace the system prompt / start a new conversation context. Object form: `{system_prompt, user_prompt, system_pom, user_pom, consolidate, full_reset}`. `system_pom`/`user_pom` render to prompt text; prompts are expanded against prompt vars + post_data; `consolidate:true` summarizes first""" # actions.c:597 self.action.append({"context_switch": value}) # type: ignore[attr-defined] return self def end_of_speech_timeout(self: _Self, value: int) -> _Self: - """Set end-of-speech detection timeout (must be >0)""" # actions.c:312 + """Set end-of-speech detection timeout (must be >0)""" # actions.c:315 self.action.append({"end_of_speech_timeout": value}) # type: ignore[attr-defined] return self - def extensive_data(self: _Self, value: bool) -> _Self: - """Enable extensive data in the function/conversation log""" # actions.c:373 + def extensive_data(self: _Self, value: bool | str) -> _Self: + """Enable extensive data in the function/conversation log""" # actions.c:376 self.action.append({"extensive_data": value}) # type: ignore[attr-defined] return self - def functions_on_speaker_timeout(self: _Self, value: bool) -> _Self: - """Set whether functions may fire on speaker timeout""" # actions.c:369 + def functions_on_speaker_timeout(self: _Self, value: bool | str) -> _Self: + """Set whether functions may fire on speaker timeout""" # actions.c:372 self.action.append({"functions_on_speaker_timeout": value}) # type: ignore[attr-defined] return self - def hangup(self: _Self, value: dict[str, Any]) -> _Self: - """Set `offhook = 0` (hang up). Note: a graceful "say goodbye" hangup is the **built-in `hangup` function**, not this action""" # actions.c:294 + def hangup(self: _Self, value: bool | str) -> _Self: + """Set `offhook = 0` (hang up). Note: a graceful "say goodbye" hangup is the **built-in `hangup` function**, not this action""" # actions.c:297 self.action.append({"hangup": value}) # type: ignore[attr-defined] return self def hold(self: _Self, value: int | str | HoldAction) -> _Self: - """Put the call on hold for N seconds. Accepts a number, a time string (`"5m"`, `"1:30"` via `parse_time`), or `{timeout}`. Default 300s; values <0 or >900 clamp to 300""" # actions.c:258 + """Put the call on hold for N seconds. Accepts a number, a time string (`"5m"`, `"1:30"` via `parse_time`), or `{timeout}`. Default 300s; values <0 or >900 clamp to 300""" # actions.c:261 self.action.append({"hold": value}) # type: ignore[attr-defined] return self def playback_bg(self: _Self, value: str | PlaybackBgAction) -> _Self: - """Play an audio file in the background. `{wait:true}` makes the agent wait for it. Replaces any currently-open background file""" # actions.c:695 + """Play an audio file in the background. `{wait:true}` makes the agent wait for it. Replaces any currently-open background file""" # actions.c:698 self.action.append({"playback_bg": value}) # type: ignore[attr-defined] return self def replace_in_history(self: _Self, value: str | Literal[True]) -> _Self: - """Replace the function call's text in conversation history. A string is stored prefixed with `~LN()-; `; `true` stores an empty string""" # actions.c:379 + """Replace the function call's text in conversation history. A string is stored prefixed with `~LN()-; `; `true` stores an empty string""" # actions.c:382 self.action.append({"replace_in_history": value}) # type: ignore[attr-defined] return self def say(self: _Self, value: str) -> _Self: - """Speak text immediately via TTS, then wait for speaking to finish. Also logs `tl_manual_say`""" # actions.c:434 + """Speak text immediately via TTS, then wait for speaking to finish. Also logs `tl_manual_say`""" # actions.c:437 self.action.append({"say": value}) # type: ignore[attr-defined] return self def set_global_data(self: _Self, value: dict[str, Any]) -> _Self: - """Merge keys into global data, then refresh prompt vars. Gated by `swaig_set_global_data`""" # actions.c:498 + """Merge keys into global data, then refresh prompt vars. Gated by `swaig_set_global_data`""" # actions.c:501 self.action.append({"set_global_data": value}) # type: ignore[attr-defined] return self def set_meta_data(self: _Self, value: dict[str, Any]) -> _Self: - """Merge keys into the calling function's metadata store (keyed by its `meta_data_token`)""" # actions.c:459 + """Merge keys into the calling function's metadata store (keyed by its `meta_data_token`)""" # actions.c:462 self.action.append({"set_meta_data": value}) # type: ignore[attr-defined] return self def settings(self: _Self, value: dict[str, Any]) -> _Self: - """Modify LLM settings at runtime (`parse_json_settings`). Gated by `swaig_allow_settings`""" # actions.c:442 + """Modify LLM settings at runtime (`parse_json_settings`). Gated by `swaig_allow_settings`""" # actions.c:445 self.action.append({"settings": value}) # type: ignore[attr-defined] return self def speech_event_timeout(self: _Self, value: int) -> _Self: - """Set speech event timeout (must be >0)""" # actions.c:326 + """Set speech event timeout (must be >0)""" # actions.c:329 self.action.append({"speech_event_timeout": value}) # type: ignore[attr-defined] return self - def stop(self: _Self, value: dict[str, Any]) -> _Self: - """Stop the AI agent immediately (interrupt + `running = 0`)""" # actions.c:452 + def stop(self: _Self, value: bool | str) -> _Self: + """Stop the AI agent immediately (interrupt + `running = 0`)""" # actions.c:455 self.action.append({"stop": value}) # type: ignore[attr-defined] return self - def stop_playback_bg(self: _Self, value: dict[str, Any]) -> _Self: - """Stop/close the background audio file""" # actions.c:685 + def stop_playback_bg( + self: _Self, value: bool | str | int | dict[str, Any] | list[Any] | None + ) -> _Self: + """Stop/close the background audio file""" # actions.c:688 self.action.append({"stop_playback_bg": value}) # type: ignore[attr-defined] return self def toggle_functions(self: _Self, value: list[dict[str, Any]]) -> _Self: - """Enable/disable functions. `active` via `check_active`: `-1` default/toggle, `0` off, `1+` use-count. **Only affects functions sharing the calling function's `meta_data_token`** (`actions.c:419-420`)""" # actions.c:389 + """Enable/disable functions. `active` via `check_active`: `-1` default/toggle, `0` off, `1+` use-count. **Only affects functions sharing the calling function's `meta_data_token`** (`actions.c:419-420`)""" # actions.c:392 self.action.append({"toggle_functions": value}) # type: ignore[attr-defined] return self def transfer(self: _Self, value: str | TransferAction) -> _Self: - """Transfer the call to `dest`. `summarize:true` sets `transfer_summary`. Sets `openai_transfer_check` var, interrupts, stops the loop. Ignored if already interrupted""" # actions.c:136 + """Transfer the call to `dest`. `summarize:true` sets `transfer_summary`. Sets `openai_transfer_check` var, interrupts, stops the loop. Ignored if already interrupted""" # actions.c:343 self.action.append({"transfer": value}) # type: ignore[attr-defined] return self - def unset_global_data(self: _Self, value: str | list[Any]) -> _Self: - """Remove key(s) from global data, then refresh prompt vars. Gated by `swaig_set_global_data`""" # actions.c:515 + def unset_global_data(self: _Self, value: str | list[str]) -> _Self: + """Remove key(s) from global data, then refresh prompt vars. Gated by `swaig_set_global_data`""" # actions.c:518 self.action.append({"unset_global_data": value}) # type: ignore[attr-defined] return self - def unset_meta_data(self: _Self, value: str | list[Any]) -> _Self: - """Remove key(s) from the calling function's metadata store""" # actions.c:477 + def unset_meta_data(self: _Self, value: str | list[str]) -> _Self: + """Remove key(s) from the calling function's metadata store""" # actions.c:480 self.action.append({"unset_meta_data": value}) # type: ignore[attr-defined] return self def user_event(self: _Self, value: dict[str, Any]) -> _Self: - """Fire relay event `calling.user_event` with the object as payload""" # actions.c:231 + """Fire relay event `calling.user_event` with the object as payload""" # actions.c:234 self.action.append({"user_event": value}) # type: ignore[attr-defined] return self def user_input(self: _Self, value: str) -> _Self: - """Push text onto the input queue as if the user spoke it""" # actions.c:541 + """Push text onto the input queue as if the user spoke it""" # actions.c:544 self.action.append({"user_input": value}) # type: ignore[attr-defined] return self def wait_for_user( - self: _Self, value: bool | int | Literal["answer_first"] + self: _Self, value: bool | int | Literal["answer_first"] | str ) -> _Self: - """`true` = `1`, a number sets a count, `"answer_first"` = `2` (require caller answer)""" # actions.c:300 + """`true` = `1`, a number sets a count, `"answer_first"` = `2` (require caller answer)""" # actions.c:303 self.action.append({"wait_for_user": value}) # type: ignore[attr-defined] return self diff --git a/signalwire/signalwire/core/swaig_request_generated.py b/signalwire/signalwire/core/swaig_request_generated.py index a8bb56c0..68563576 100644 --- a/signalwire/signalwire/core/swaig_request_generated.py +++ b/signalwire/signalwire/core/swaig_request_generated.py @@ -13,7 +13,7 @@ class SwaigArgument(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - parsed: list[Any] + parsed: list[dict[str, Any] | list[Any]] raw: str substituted: str @@ -21,13 +21,15 @@ class SwaigArgument(TypedDict, total=False): class SwaigRequest(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" + SWMLCall: dict[str, Any] + SWMLVars: dict[str, Any] ai_session_id: str app_name: str args: str argument: SwaigArgument argument_desc: dict[str, Any] call_id: str - call_log: list[Any] + call_log: list[dict[str, Any]] caller_id_name: str caller_id_num: str channel_active: bool @@ -45,6 +47,6 @@ class SwaigRequest(TypedDict, total=False): meta_data: dict[str, Any] meta_data_token: str project_id: str - raw_call_log: list[Any] + raw_call_log: list[dict[str, Any]] space_id: str version: Literal["2.0"] diff --git a/signalwire/signalwire/core/swml_verbs_generated.py b/signalwire/signalwire/core/swml_verbs_generated.py index 0c981eae..93d32995 100644 --- a/signalwire/signalwire/core/swml_verbs_generated.py +++ b/signalwire/signalwire/core/swml_verbs_generated.py @@ -14,2040 +14,889 @@ _Self = TypeVar("_Self", bound="_SwmlVerbs") -class Section(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - main: list[SWMLMethod] - - -SWMLMethod: TypeAlias = "Answer | AI | AmazonBedrock | Cond | Connect | Denoise | EnterQueue | Execute | Goto | Label | LiveTranscribe | AiSidecar | LiveTranslate | Hangup | JoinRoom | JoinConference | Play | Prompt | ReceiveFax | Record | RecordCall | Request | Return | SendDigits | SendFax | SendSMS | Set | Sleep | SIPRefer | StopDenoise | StopRecordCall | StopTap | Switch | Tap | Transfer | Unset | Pay | DetectMachine | UserEvent" - - -class Answer(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - answer: dict[str, Any] - - -class AI(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - ai: AIObject - - -class AmazonBedrock(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - amazon_bedrock: AmazonBedrockObject - - -class Cond(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - cond: list[CondParams] - - -class Connect(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - connect: ( - ConnectDeviceSingle - | ConnectDeviceSerial - | ConnectDeviceParallel - | ConnectDeviceSerialParallel - ) - - -class Denoise(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - denoise: dict[str, Any] - - -class EnterQueue(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - enter_queue: EnterQueueObject - - -class Execute(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - execute: dict[str, Any] - - -class Goto(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - goto: dict[str, Any] - - -class Label(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - label: str - - -class LiveTranscribe(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - live_transcribe: dict[str, Any] - - -class LiveTranslate(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - live_translate: dict[str, Any] - - -class Hangup(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - hangup: dict[str, Any] - - -class JoinRoom(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - join_room: dict[str, Any] - - -class JoinConference(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - join_conference: JoinConferenceObject - - -class Play(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - play: PlayWithURL | PlayWithURLS - - -class Prompt(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - prompt: dict[str, Any] - - -class ReceiveFax(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - receive_fax: dict[str, Any] - - -class Record(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - record: dict[str, Any] - - -class RecordCall(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - record_call: dict[str, Any] - - -class Request(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - request: dict[str, Any] - - -class Return(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - # non-identifier field 'return': dict[str, Any] - - -class SendDigits(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - send_digits: dict[str, Any] - - -class SendFax(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - send_fax: dict[str, Any] - - -class SendSMS(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - send_sms: SMSWithBody | SMSWithMedia - - -class Set(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - set: dict[str, Any] - - -class Sleep(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - sleep: dict[str, Any] | int | SWMLVar - - -class SIPRefer(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - sip_refer: dict[str, Any] - - -class StopDenoise(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - stop_denoise: dict[str, Any] - - -class StopRecordCall(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - stop_record_call: dict[str, Any] - - -class StopTap(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - stop_tap: dict[str, Any] - - -class Switch(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - switch: dict[str, Any] - - -class Tap(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - tap: dict[str, Any] - - -class Transfer(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - transfer: dict[str, Any] - - -class Unset(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - unset: str | list[str] - - -class Pay(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - pay: dict[str, Any] - - -class DetectMachine(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - detect_machine: dict[str, Any] - - -class UserEvent(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - user_event: dict[str, Any] - - -SWMLVar: TypeAlias = "str" - - -class AIObject(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - global_data: dict[str, Any] - hints: list[str | Hint] - languages: list[Languages] - params: AIParams - post_prompt: AIPostPrompt - post_prompt_url: str - pronounce: list[Pronounce] - prompt: AIPrompt - SWAIG: SWAIG - - -class AmazonBedrockObject(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - global_data: dict[str, Any] - params: BedrockParams - post_prompt: BedrockPostPrompt - post_prompt_url: str - prompt: BedrockPrompt - SWAIG: BedrockSWAIG - - -CondParams: TypeAlias = "CondReg | CondElse" - - -class ConnectDeviceSingle(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - # non-identifier field 'from': str - headers: list[ConnectHeaders] - codecs: str - webrtc_media: bool | SWMLVar - session_timeout: int | SWMLVar - ringback: list[str] | RingbackConfig - result: ConnectSwitch | list[CondParams] - timeout: int | SWMLVar - max_duration: int | SWMLVar - answer_on_bridge: bool | SWMLVar - confirm: str | list[ValidConfirmMethods] - confirm_timeout: int | SWMLVar - username: str - password: str - encryption: Literal["mandatory"] | Literal["optional"] | Literal["forbidden"] - call_state_url: str - transfer_after_bridge: str | SWMLVar - call_state_events: list[CallStatus] - to: str - - -class ConnectDeviceSerial(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - # non-identifier field 'from': str - headers: list[ConnectHeaders] - codecs: str - webrtc_media: bool | SWMLVar - session_timeout: int | SWMLVar - ringback: list[str] | RingbackConfig - result: ConnectSwitch | list[CondParams] - timeout: int | SWMLVar - max_duration: int | SWMLVar - answer_on_bridge: bool | SWMLVar - confirm: str | list[ValidConfirmMethods] - confirm_timeout: int | SWMLVar - username: str - password: str - encryption: Literal["mandatory"] | Literal["optional"] | Literal["forbidden"] - call_state_url: str - transfer_after_bridge: str | SWMLVar - call_state_events: list[CallStatus] - serial: list[ConnectDeviceSingle] - - -class ConnectDeviceParallel(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - # non-identifier field 'from': str - headers: list[ConnectHeaders] - codecs: str - webrtc_media: bool | SWMLVar - session_timeout: int | SWMLVar - ringback: list[str] | RingbackConfig - result: ConnectSwitch | list[CondParams] - timeout: int | SWMLVar - max_duration: int | SWMLVar - answer_on_bridge: bool | SWMLVar - confirm: str | list[ValidConfirmMethods] - confirm_timeout: int | SWMLVar - username: str - password: str - encryption: Literal["mandatory"] | Literal["optional"] | Literal["forbidden"] - call_state_url: str - transfer_after_bridge: str | SWMLVar - call_state_events: list[CallStatus] - parallel: list[ConnectDeviceSingle] - - -class ConnectDeviceSerialParallel(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - # non-identifier field 'from': str - headers: list[ConnectHeaders] - codecs: str - webrtc_media: bool | SWMLVar - session_timeout: int | SWMLVar - ringback: list[str] | RingbackConfig - result: ConnectSwitch | list[CondParams] - timeout: int | SWMLVar - max_duration: int | SWMLVar - answer_on_bridge: bool | SWMLVar - confirm: str | list[ValidConfirmMethods] - confirm_timeout: int | SWMLVar - username: str - password: str - encryption: Literal["mandatory"] | Literal["optional"] | Literal["forbidden"] - call_state_url: str - transfer_after_bridge: str | SWMLVar - call_state_events: list[CallStatus] - serial_parallel: list[list[ConnectDeviceSingle]] - - -class EnterQueueObject(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - queue_name: str - transfer_after_bridge: str | SWMLVar - status_url: str - wait_url: str | SWMLVar - wait_time: int | SWMLVar - - -class ExecuteSwitch(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - variable: str - case: dict[str, Any] - default: list[SWMLMethod] - - -TranscribeAction: TypeAlias = ( - "TranscribeStartAction | Literal['stop'] | TranscribeSummarizeActionUnion" -) - - -TranslateAction: TypeAlias = ( - "StartAction | Literal['stop'] | SummarizeActionUnion | InjectAction" -) - - -class JoinConferenceObject(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - name: str - muted: bool | SWMLVar - beep: Literal["true"] | Literal["false"] | Literal["onEnter"] | Literal["onExit"] - start_on_enter: bool | SWMLVar - end_on_exit: bool | SWMLVar - wait_url: str | SWMLVar - max_participants: int | SWMLVar - record: Literal["do-not-record"] | Literal["record-from-start"] - region: str - trim: Literal["trim-silence"] | Literal["do-not-trim"] - coach: str - status_callback_event: ( - Literal["start"] - | Literal["end"] - | Literal["join"] - | Literal["leave"] - | Literal["mute"] - | Literal["hold"] - | Literal["modify"] - | Literal["speaker"] - | Literal["announcement"] - ) - status_callback: str - status_callback_method: Literal["GET"] | Literal["POST"] - recording_status_callback: str - recording_status_callback_method: Literal["GET"] | Literal["POST"] - recording_status_callback_event: ( - Literal["in-progress"] | Literal["completed"] | Literal["absent"] - ) - result: dict[str, Any] | list[CondParams] - - -class PlayWithURL(TypedDict, total=False): - """Play with a single URL - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - auto_answer: bool | SWMLVar - volume: float | SWMLVar - say_voice: str - say_language: str - say_gender: Literal["male", "female"] - status_url: str - url: play_url | SWMLVar - - -class PlayWithURLS(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - auto_answer: bool | SWMLVar - volume: float | SWMLVar - say_voice: str - say_language: str - say_gender: Literal["male", "female"] - status_url: str - urls: list[play_url] | list[SWMLVar] - - -play_url: TypeAlias = "str" - - -class SMSWithBody(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - to_number: str - from_number: str - region: str - tags: list[str] - body: str - - -class SMSWithMedia(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - to_number: str - from_number: str - region: str - tags: list[str] - media: list[str] - body: str - - -class PayParameters(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - name: str - value: str - - -class PayPrompts(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - actions: list[PayPromptAction] - # non-identifier field 'for': str - attempts: str - card_type: str - error_type: str - - -class Hint(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - hint: str - pattern: str - replace: str - ignore_case: bool | SWMLVar - - -Languages: TypeAlias = "LanguagesWithSoloFillers | LanguagesWithFillers" - - -class AIParams(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - acknowledge_interruptions: bool | SWMLVar - ai_model: ( - Literal["gpt-4o-mini"] | Literal["gpt-4.1-mini"] | Literal["gpt-4.1-nano"] | str - ) - ai_name: str - ai_volume: int | SWMLVar - app_name: str - asr_smart_format: bool | SWMLVar - attention_timeout: AttentionTimeout | Literal[0] | SWMLVar - attention_timeout_prompt: str - asr_diarize: bool | SWMLVar - asr_speaker_affinity: bool | SWMLVar - background_file: str - background_file_loops: int | None | SWMLVar - background_file_volume: int | SWMLVar - enable_barge: str | bool | SWMLVar - enable_inner_dialog: bool | SWMLVar - enable_pause: bool | SWMLVar - enable_turn_detection: bool | SWMLVar - barge_match_string: str - barge_min_words: int | SWMLVar - barge_functions: bool | SWMLVar - conscience: str - convo: list[ConversationMessage] - conversation_id: str - conversation_sliding_window: int | SWMLVar - debug_webhook_level: int | SWMLVar - debug_webhook_url: str - debug: bool | int | SWMLVar - direction: Direction | SWMLVar - digit_terminators: str - digit_timeout: int | SWMLVar - end_of_speech_timeout: int | SWMLVar - enable_thinking: bool | SWMLVar - enable_vision: bool | SWMLVar - energy_level: float | SWMLVar - first_word_timeout: int | SWMLVar - function_wait_for_talking: bool | SWMLVar - functions_on_no_response: bool | SWMLVar - hard_stop_prompt: str - hard_stop_time: str | SWMLVar - hold_music: str - hold_on_process: bool | SWMLVar - inactivity_timeout: int | SWMLVar - inner_dialog_model: ( - Literal["gpt-4o-mini"] | Literal["gpt-4.1-mini"] | Literal["gpt-4.1-nano"] | str - ) - inner_dialog_prompt: str - inner_dialog_synced: bool | SWMLVar - initial_sleep_ms: int | SWMLVar - input_poll_freq: int | SWMLVar - interrupt_on_noise: bool | SWMLVar - interrupt_prompt: str - # deprecated: languages_enabled - languages_enabled: bool | SWMLVar - local_tz: str - llm_diarize_aware: bool | SWMLVar - max_emotion: int | SWMLVar - max_response_tokens: int | SWMLVar - openai_asr_engine: str - outbound_attention_timeout: int | SWMLVar - persist_global_data: bool | SWMLVar - pom_format: Literal["markdown"] | Literal["xml"] - save_conversation: bool | SWMLVar - speech_event_timeout: int | SWMLVar - speech_gen_quick_stops: int | SWMLVar - speech_timeout: int | SWMLVar - speak_when_spoken_to: bool | SWMLVar - start_paused: bool | SWMLVar - static_greeting: str - static_greeting_no_barge: bool | SWMLVar - summary_mode: Literal["string"] | Literal["original"] | SWMLVar - swaig_allow_settings: bool | SWMLVar - swaig_allow_swml: bool | SWMLVar - swaig_post_conversation: bool | SWMLVar - swaig_set_global_data: bool | SWMLVar - swaig_post_swml_vars: bool | list[str] | SWMLVar - thinking_model: ( - Literal["gpt-4o-mini"] | Literal["gpt-4.1-mini"] | Literal["gpt-4.1-nano"] | str - ) - transparent_barge: bool | SWMLVar - transparent_barge_max_time: int | SWMLVar - transfer_summary: bool | SWMLVar - turn_detection_timeout: int | SWMLVar - tts_number_format: Literal["international"] | Literal["national"] - video_listening_file: str - video_idle_file: str - video_talking_file: str - vision_model: ( - Literal["gpt-4o-mini"] | Literal["gpt-4.1-mini"] | Literal["gpt-4.1-nano"] | str - ) - vad_config: str - wait_for_user: bool | SWMLVar - wake_prefix: str - eleven_labs_stability: float | SWMLVar - eleven_labs_similarity: float | SWMLVar - - -AIPostPrompt: TypeAlias = "AIPostPromptText | AIPostPromptPom" - - -class Pronounce(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - replace: str - # non-identifier field 'with': str - ignore_case: bool | SWMLVar - - -AIPrompt: TypeAlias = "AIPromptText | AIPromptPom" - - -class SWAIG(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - defaults: SWAIGDefaults - native_functions: list[SWAIGNativeFunction] - includes: list[SWAIGIncludes] - functions: list[SWAIGFunction] - internal_fillers: SWAIGInternalFiller - - -class BedrockParams(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - attention_timeout: AttentionTimeout | Literal[0] | SWMLVar - hard_stop_time: str | SWMLVar - inactivity_timeout: int | SWMLVar - video_listening_file: str - video_idle_file: str - video_talking_file: str - hard_stop_prompt: str - - -BedrockPostPrompt: TypeAlias = "OmitPropertiesBedrockPostPomptTextOmittedPromptProps | OmitPropertiesBedrockPostPromptPomOmittedPromptProps" - - -BedrockPrompt: TypeAlias = "OmitPropertiesBedrockPromptTextOmittedPromptProps | OmitPropertiesBedrockPromptPomOmittedPromptProps" - - -class BedrockSWAIG(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - functions: list[BedrockSWAIGFunction] - defaults: SWAIGDefaults - native_functions: list[SWAIGNativeFunction] - includes: list[SWAIGIncludes] - - -class CondReg(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - when: str - then: list[SWMLMethod] - # non-identifier field 'else': list[SWMLMethod] - - -class CondElse(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - # non-identifier field 'else': list[SWMLMethod] - - -class ConnectHeaders(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - name: str - value: str - - -class ConnectSwitch(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - variable: str - case: dict[str, Any] - default: list[SWMLMethod] - - -ValidConfirmMethods: TypeAlias = "Cond | Set | Unset | Hangup | Play | Prompt | Record | RecordCall | StopRecordCall | Tap | StopTap | SendDigits | SendSMS | Denoise | StopDenoise" - - -CallStatus: TypeAlias = "Literal['created', 'ringing', 'answered', 'ended']" - - -class TranscribeStartAction(TypedDict, total=False): +class AI(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - start: dict[str, Any] - + ai: dict[str, Any] | list[Any] -TranscribeSummarizeActionUnion: TypeAlias = ( - "TranscribeSummarizeAction | Literal['summarize']" -) - -class StartAction(TypedDict, total=False): +class AiSidecar(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - start: dict[str, Any] - - -SummarizeActionUnion: TypeAlias = "SummarizeAction | Literal['summarize']" + ai_sidecar: dict[str, Any] -class InjectAction(TypedDict, total=False): +class AmazonBedrock(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - inject: dict[str, Any] + amazon_bedrock: dict[str, Any] -PayPromptAction: TypeAlias = "PayPromptSayAction | PayPromptPlayAction" - - -class LanguagesWithSoloFillers(TypedDict, total=False): +class Answer(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - name: str - code: str - voice: str - model: str - emotion: Literal["auto"] - speed: Literal["auto"] - engine: str - params: LanguageParams - fillers: list[str] + answer: dict[str, Any] | list[Any] -class LanguagesWithFillers(TypedDict, total=False): +class CallDeviceStream(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" + authorization_bearer_token: str + codec: str + custom_parameters: Any name: str - code: str - voice: str - model: str - emotion: Literal["auto"] - speed: Literal["auto"] - engine: str - params: LanguageParams - function_fillers: list[str] - speech_fillers: list[str] - - -AttentionTimeout: TypeAlias = "int" - - -class ConversationMessage(TypedDict, total=False): - """A message object representing a single turn in the conversation history. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - role: ConversationRole - content: str - lang: str - - -Direction: TypeAlias = "Literal['inbound', 'outbound']" - - -class AIPostPromptText(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - text: str - - -class AIPostPromptPom(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - pom: list[POM] - - -class AIPromptText(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - text: str - contexts: Contexts - - -class AIPromptPom(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - pom: list[POM] - contexts: Contexts - - -class SWAIGDefaults(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - web_hook_url: str - - -SWAIGNativeFunction: TypeAlias = ( - "Literal['check_time', 'wait_seconds', 'wait_for_user', 'adjust_response_latency']" -) - - -class SWAIGIncludes(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - functions: list[str] + realtime: bool + status_url: str + status_url_method: Literal["GET", "POST"] url: str - meta_data: dict[str, Any] - - -SWAIGFunction: TypeAlias = "UserSWAIGFunction | StartUpHookSWAIGFunction | HangUpHookSWAIGFunction | SummarizeConversationSWAIGFunction" - - -class SWAIGInternalFiller(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - hangup: FunctionFillers - check_time: FunctionFillers - wait_for_user: FunctionFillers - wait_seconds: FunctionFillers - adjust_response_latency: FunctionFillers - next_step: FunctionFillers - change_context: FunctionFillers - get_visual_input: FunctionFillers - get_ideal_strategy: FunctionFillers - - -class OmitPropertiesBedrockPostPomptTextOmittedPromptProps(TypedDict, total=False): - """The template for omitting properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - text: str - - -class OmitPropertiesBedrockPostPromptPomOmittedPromptProps(TypedDict, total=False): - """The template for omitting properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - pom: list[POM] - - -class OmitPropertiesBedrockPromptTextOmittedPromptProps(TypedDict, total=False): - """The template for omitting properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - voice_id: ( - Literal["tiffany"] - | Literal["matthew"] - | Literal["amy"] - | Literal["lupe"] - | Literal["carlos"] - ) - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - text: str - - -class OmitPropertiesBedrockPromptPomOmittedPromptProps(TypedDict, total=False): - """The template for omitting properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - voice_id: ( - Literal["tiffany"] - | Literal["matthew"] - | Literal["amy"] - | Literal["lupe"] - | Literal["carlos"] - ) - max_tokens: int - temperature: float | SWMLVar - top_p: float | SWMLVar - confidence: float | SWMLVar - presence_penalty: float | SWMLVar - frequency_penalty: float | SWMLVar - pom: list[POM] - - -BedrockSWAIGFunction: TypeAlias = "PickPropertiesUserSWAIGFunctionPickedSWAIGFunctionProps | PickPropertiesStartUpHookSWAIGFunctionPickedSWAIGFunctionProps | PickPropertiesHangUpHookSWAIGFunctionPickedSWAIGFunctionProps | PickPropertiesSummarizeConversationSWAIGFunctionPickedSWAIGFunctionProps" - - -TranscribeDirection: TypeAlias = "Literal['remote-caller', 'local-caller']" - - -SpeechEngine: TypeAlias = "Literal['deepgram', 'google']" - - -class TranscribeSummarizeAction(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - summarize: dict[str, Any] -TranslationFilterPreset: TypeAlias = ( - "Literal['polite', 'rude', 'professional', 'shakespeare', 'gen-z']" -) - - -CustomTranslationFilter: TypeAlias = "str" - - -TranslateDirection: TypeAlias = "Literal['remote-caller', 'local-caller']" - - -class SummarizeAction(TypedDict, total=False): +class CallPayParameters(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - summarize: dict[str, Any] + name: str + value: str -class PayPromptSayAction(TypedDict, total=False): +class CallPayPrompts(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - type: Literal["Say"] - phrase: str + actions: list[CallPayPromptsActions] + attempt: str + card_type: str + error_type: str + # non-identifier field 'for': Literal['payment-card-number', 'expiration-date', 'security-code', 'postal-code', 'bank-routing-number', 'bank-account-number', 'payment-processing', 'payment-completed', 'payment-failed', 'payment-canceled'] + play: list[RingbackConfig] + require_matching_inputs: str -class PayPromptPlayAction(TypedDict, total=False): +class CallPayPromptsActions(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - type: Literal["Play"] + type: Literal["Say", "Play"] phrase: str -class LanguageParams(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - stability: float | SWMLVar - similarity: float | SWMLVar - - -ConversationRole: TypeAlias = "Literal['user', 'assistant', 'system']" - - -POM: TypeAlias = "PomSectionBodyContent | PomSectionBulletsContent" - - -class Contexts(TypedDict, total=False): +class Cond(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - default: ContextsObject + cond: list[dict[str, Any]] -class UserSWAIGFunction(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - description: str - purpose: str - parameters: FunctionParameters - fillers: FunctionFillers - argument: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - skip_fillers: bool | SWMLVar - web_hook_url: str - wait_file: str - wait_file_loops: int | str - wait_for_fillers: bool | SWMLVar - function: str - - -class StartUpHookSWAIGFunction(TypedDict, total=False): - """Open shape: extra server keys permitted; not validated at runtime.""" - - description: str - purpose: str - parameters: FunctionParameters - fillers: FunctionFillers - argument: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - skip_fillers: bool | SWMLVar - web_hook_url: str - wait_file: str - wait_file_loops: int | str - wait_for_fillers: bool | SWMLVar - function: Literal["startup_hook"] - - -class HangUpHookSWAIGFunction(TypedDict, total=False): +class Connect(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - description: str - purpose: str - parameters: FunctionParameters - fillers: FunctionFillers - argument: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - skip_fillers: bool | SWMLVar - web_hook_url: str - wait_file: str - wait_file_loops: int | str - wait_for_fillers: bool | SWMLVar - function: Literal["hangup_hook"] - - -class SummarizeConversationSWAIGFunction(TypedDict, total=False): - """An internal reserved function that generates a summary of the conversation and sends any specified properties to the configured webhook after the conversation has ended. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - description: str - purpose: str - parameters: FunctionParameters - fillers: FunctionFillers - argument: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - skip_fillers: bool | SWMLVar - web_hook_url: str - wait_file: str - wait_file_loops: int | str - wait_for_fillers: bool | SWMLVar - function: Literal["summarize_conversation"] - - -FunctionFillers: TypeAlias = "dict[str, Any]" + connect: dict[str, Any] -class PickPropertiesUserSWAIGFunctionPickedSWAIGFunctionProps(TypedDict, total=False): - """The template for picking properties. +class ConnectDevice(TypedDict, total=False): + """Body shape enforced by CHECK_swml_connect_device, swml_schema.c. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - description: str - parameters: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - web_hook_url: str - function: str - - -class PickPropertiesStartUpHookSWAIGFunctionPickedSWAIGFunctionProps( - TypedDict, total=False -): - """The template for picking properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - description: str - parameters: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - web_hook_url: str - function: Literal["startup_hook"] - - -class PickPropertiesHangUpHookSWAIGFunctionPickedSWAIGFunctionProps( - TypedDict, total=False -): - """The template for picking properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - description: str - parameters: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - web_hook_url: str - function: Literal["hangup_hook"] - - -class PickPropertiesSummarizeConversationSWAIGFunctionPickedSWAIGFunctionProps( - TypedDict, total=False -): - """The template for picking properties. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + authorization_bearer_token: str + call_state_events: list[str] + call_state_url: str + codec: str + codecs: str | list[Any] + confirm: str | list[SWMLMethod] | dict[str, Any] + confirm_timeout: int + custom_parameters: dict[str, str] + encryption: Literal["mandatory", "optional", "forbidden"] + # non-identifier field 'from': str + from_name: str + fsvars: dict[str, str] + headers: list[ConnectSipHeader] + name: str + password: str + realtime: bool + session_timeout: float + status_url: str + status_url_method: Literal["GET", "POST"] + timeout: float + to: str + username: str + webrtc_media: bool - description: str - parameters: FunctionParameters - active: bool | SWMLVar - meta_data: dict[str, Any] - meta_data_token: str - data_map: DataMap - web_hook_url: str - function: Literal["summarize_conversation"] +ConnectSerialParallel: TypeAlias = "list[ConnectDevice]" -class PomSectionBodyContent(TypedDict, total=False): - """Content model with body text and optional bullets - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ +class ConnectSipHeader(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - title: str - subsections: list[POM] - numbered: bool | SWMLVar - numberedBullets: bool | SWMLVar - body: str - bullets: list[str] + name: str + value: str -class PomSectionBulletsContent(TypedDict, total=False): - """Content model with bullets and optional body +class Denoise(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + denoise: dict[str, Any] - title: str - subsections: list[POM] - numbered: bool | SWMLVar - numberedBullets: bool | SWMLVar - body: str - bullets: list[str] +class DetectMachine(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" -ContextsObject: TypeAlias = "ContextsPOMObject | ContextsTextObject" + detect_machine: dict[str, Any] -class FunctionParameters(TypedDict, total=False): +class Dial(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - type: Literal["object"] - properties: dict[str, Any] - required: list[str] + dial: dict[str, Any] -class DataMap(TypedDict, total=False): +class Echo(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - output: Output - expressions: list[Expression] - webhooks: list[Webhook] + echo: dict[str, Any] | list[Any] -class ContextsPOMObject(TypedDict, total=False): +class EnterQueue(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - steps: list[ContextSteps] - isolated: bool - enter_fillers: list[FunctionFillers] - exit_fillers: list[FunctionFillers] - pom: list[POM] + enter_queue: dict[str, Any] -class ContextsTextObject(TypedDict, total=False): +class Eval(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - steps: list[ContextSteps] - isolated: bool - enter_fillers: list[FunctionFillers] - exit_fillers: list[FunctionFillers] - text: str + eval: dict[str, Any] + +class Execute(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" -SchemaType: TypeAlias = "StringProperty | IntegerProperty | NumberProperty | BooleanProperty | ArrayProperty | ObjectProperty | NullProperty | OneOfProperty | AllOfProperty | AnyOfProperty | ConstProperty" + execute: dict[str, Any] | list[Any] -class Output(TypedDict, total=False): +class ExecuteRpc(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - response: str - action: list[Action] + execute_rpc: dict[str, Any] -class Expression(TypedDict, total=False): +class Goto(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - string: str - pattern: str - output: Output + goto: dict[str, Any] | list[Any] -class Webhook(TypedDict, total=False): +class Hangup(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - expressions: list[Expression] - error_keys: str | list[str] - url: str - foreach: dict[str, Any] - headers: dict[str, Any] - method: Literal["GET"] | Literal["POST"] | Literal["PUT"] | Literal["DELETE"] - input_args_as_params: bool | SWMLVar - params: dict[str, Any] - require_args: str | list[str] - output: Output + hangup: dict[str, Any] | list[Any] -ContextSteps: TypeAlias = "ContextPOMSteps | ContextTextSteps" +class If(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" + # non-identifier field 'if': dict[str, Any] -class StringProperty(TypedDict, total=False): - """Base interface for all property types - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ +class JoinConference(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - description: str - nullable: bool | SWMLVar - type: Literal["string"] - enum: list[str] - default: str - pattern: str - format: StringFormat + join_conference: dict[str, Any] | list[Any] -class IntegerProperty(TypedDict, total=False): - """Base interface for all property types +class JoinRoom(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + join_room: dict[str, Any] | list[Any] - description: str - nullable: bool | SWMLVar - type: Literal["integer"] - enum: list[int] - default: int | SWMLVar +class Label(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" + + label: dict[str, Any] | list[Any] -class NumberProperty(TypedDict, total=False): - """Base interface for all property types - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ +class LiveTranscribe(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - description: str - nullable: bool | SWMLVar - type: Literal["number"] - enum: list[int | float] | list[SWMLVar] - default: int | float | SWMLVar + live_transcribe: dict[str, Any] -class BooleanProperty(TypedDict, total=False): - """Base interface for all property types +class LiveTranslate(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + live_translate: dict[str, Any] - description: str - nullable: bool | SWMLVar - type: Literal["boolean"] - default: bool | SWMLVar +class Pay(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" -class ArrayProperty(TypedDict, total=False): - """Base interface for all property types + pay: dict[str, Any] | list[Any] - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - description: str - nullable: bool | SWMLVar - type: Literal["array"] - default: list[Any] - items: SchemaType +class Play(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" + play: dict[str, Any] | list[Any] -class ObjectProperty(TypedDict, total=False): - """Base interface for all property types - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ +class Prompt(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" - description: str - nullable: bool | SWMLVar - type: Literal["object"] - default: dict[str, Any] - properties: dict[str, Any] - required: list[str] + prompt: dict[str, Any] | list[Any] -class NullProperty(TypedDict, total=False): +class ReceiveFax(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - type: Literal["null"] - description: str + receive_fax: dict[str, Any] | list[Any] -class OneOfProperty(TypedDict, total=False): +class Record(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - oneOf: list[SchemaType] + record: dict[str, Any] -class AllOfProperty(TypedDict, total=False): +class RecordCall(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - allOf: list[SchemaType] + record_call: dict[str, Any] -class AnyOfProperty(TypedDict, total=False): +class Request(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - anyOf: list[SchemaType] + request: dict[str, Any] -class ConstProperty(TypedDict, total=False): +class Return(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - const: dict[str, Any] + # non-identifier field 'return': dict[str, Any] -Action: TypeAlias = "SWMLAction | ChangeContextAction | ChangeStepAction | ContextSwitchAction | HangupAction | HoldAction | PlaybackBGAction | SayAction | SetGlobalDataAction | SetMetaDataAction | StopAction | StopPlaybackBGAction | ToggleFunctionsAction | UnsetGlobalDataAction | UnsetMetaDataAction | UserInputAction" +RingbackConfig: TypeAlias = "dict[str, Any] | list[Any]" -class ContextPOMSteps(TypedDict, total=False): +class SIPRefer(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - name: str - step_criteria: str - functions: list[str] - valid_contexts: list[str] - skip_user_turn: bool | SWMLVar - end: bool - valid_steps: list[str] - pom: list[POM] + sip_refer: dict[str, Any] | list[Any] + + +SWMLMethod: TypeAlias = "AI | AiSidecar | AmazonBedrock | Answer | Cond | Connect | Denoise | DetectMachine | Dial | Echo | EnterQueue | Eval | Execute | ExecuteRpc | Goto | Hangup | If | JoinConference | JoinRoom | Label | LiveTranscribe | LiveTranslate | Pay | Play | Prompt | ReceiveFax | Record | RecordCall | Request | Return | SIPRefer | SendDigits | SendFax | SendSMS | Set | SetMeta | Sleep | StopDenoise | StopRecordCall | StopStream | StopTap | Stream | Switch | Tap | Transcribe | TranscribeStop | Transfer | Unset | UserEvent" + + +SWMLVar: TypeAlias = "str" -class ContextTextSteps(TypedDict, total=False): +class Section(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - name: str - step_criteria: str - functions: list[str] - valid_contexts: list[str] - skip_user_turn: bool | SWMLVar - end: bool - valid_steps: list[str] - text: str + main: list[SWMLMethod] -StringFormat: TypeAlias = "Literal['date_time', 'time', 'date', 'duration', 'email', 'hostname', 'ipv4', 'ipv6', 'uri', 'uuid']" +class SendDigits(TypedDict, total=False): + """Open shape: extra server keys permitted; not validated at runtime.""" + send_digits: dict[str, Any] | list[Any] -class SWMLAction(TypedDict, total=False): + +class SendFax(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - SWML: dict[str, Any] + send_fax: dict[str, Any] | list[Any] -class ChangeContextAction(TypedDict, total=False): +class SendSMS(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - change_context: str + send_sms: dict[str, Any] -class ChangeStepAction(TypedDict, total=False): +class Set(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - change_step: str + set: dict[str, Any] -class ContextSwitchAction(TypedDict, total=False): +class SetMeta(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - context_switch: dict[str, Any] + set_meta: dict[str, Any] -class HangupAction(TypedDict, total=False): +class Sleep(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - hangup: bool | SWMLVar + sleep: dict[str, Any] | list[Any] -class HoldAction(TypedDict, total=False): +class StopDenoise(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - hold: int | SWMLVar | dict[str, Any] + stop_denoise: dict[str, Any] -class PlaybackBGAction(TypedDict, total=False): +class StopRecordCall(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - playback_bg: dict[str, Any] + stop_record_call: dict[str, Any] | list[Any] -class SayAction(TypedDict, total=False): +class StopStream(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - say: str + stop_stream: dict[str, Any] | list[Any] -class SetGlobalDataAction(TypedDict, total=False): +class StopTap(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - set_global_data: dict[str, Any] + stop_tap: dict[str, Any] | list[Any] -class SetMetaDataAction(TypedDict, total=False): +class Stream(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - set_meta_data: dict[str, Any] + stream: dict[str, Any] | list[Any] -class StopAction(TypedDict, total=False): +class Switch(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - stop: bool | SWMLVar + switch: dict[str, Any] -class StopPlaybackBGAction(TypedDict, total=False): +class Tap(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - stop_playback_bg: bool | SWMLVar + tap: dict[str, Any] | list[Any] -class ToggleFunctionsAction(TypedDict, total=False): +class Transcribe(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - toggle_functions: list[dict[str, Any]] + transcribe: dict[str, Any] -class UnsetGlobalDataAction(TypedDict, total=False): +class TranscribeStop(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - unset_global_data: str | dict[str, Any] + transcribe_stop: dict[str, Any] -class UnsetMetaDataAction(TypedDict, total=False): +class Transfer(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - unset_meta_data: str | dict[str, Any] + transfer: dict[str, Any] | list[Any] -class UserInputAction(TypedDict, total=False): +class Unset(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - user_input: str + unset: list[str] | str -class AiSidecar(TypedDict, total=False): +class UserEvent(TypedDict, total=False): """Open shape: extra server keys permitted; not validated at runtime.""" - ai_sidecar: dict[str, Any] + user_event: dict[str, Any] -class RingbackConfig(TypedDict, total=False): - """Ringback configuration (the modern object form). Declared as a named $defs entry so every generator emits a TYPED shape via $ref rather than collapsing an inline object to an untyped map; the legacy URI array remains the other oneOf branch. +class AiSidecarConfig(TypedDict, total=False): + """Attach an AI sidecar observer to the call. Requires an active live_transcribe. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - url: str - urls: list[str] - volume: float - auto_answer: bool - say_voice: str - say_language: str - say_gender: Literal["male", "female"] - status_url: str - loop: int + SWAIG: dict[str, Any] | SWMLVar + action: dict[str, Any] | SWMLVar + customer_role: Literal["remote-caller", "local-caller"] | SWMLVar + direction: list[Literal["remote-caller", "local-caller"]] | SWMLVar + global_data: dict[str, Any] | SWMLVar + hints: list[str] | SWMLVar + lang: str | SWMLVar + model: str | SWMLVar + params: dict[str, Any] | SWMLVar + permissions: dict[str, Any] | SWMLVar + prompt: dict[str, Any] | str | SWMLVar + url: str | SWMLVar -class ConnectConfig(TypedDict, total=False): - """Dial a SIP URI or phone number. +class AmazonBedrockConfig(TypedDict, total=False): + """Invoke an Amazon Bedrock AI model. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - # non-identifier field 'from': str - headers: list[ConnectHeaders] - codecs: str - webrtc_media: bool | SWMLVar - session_timeout: int | SWMLVar - ringback: list[str] | RingbackConfig - result: ConnectSwitch | list[CondParams] - timeout: int | SWMLVar - max_duration: int | SWMLVar - answer_on_bridge: bool | SWMLVar - confirm: str | list[ValidConfirmMethods] - confirm_timeout: int | SWMLVar - username: str - password: str - encryption: Literal["mandatory"] | Literal["optional"] | Literal["forbidden"] - call_state_url: str - transfer_after_bridge: str | SWMLVar - call_state_events: list[CallStatus] - to: str - serial: list[ConnectDeviceSingle] - parallel: list[ConnectDeviceSingle] - serial_parallel: list[list[ConnectDeviceSingle]] + SWAIG: list[dict[str, Any]] | dict[str, Any] + global_data: dict[str, Any] + params: dict[str, Any] + post_prompt: dict[str, Any] + post_prompt_url: str + prompt: dict[str, Any] -class ExecuteConfig(TypedDict, total=False): - """Execute a specified section or URL as a subroutine, and upon completion, return to the current document. +class ConnectConfig(TypedDict, total=False): + """Connect the call to other endpoints (phone, SIP, etc.). Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - dest: str - params: dict[str, Any] - meta: dict[str, Any] - on_return: list[SWMLMethod] - result: ExecuteSwitch | list[CondParams] + answer_on_bridge: bool | str | SWMLVar + authorization_bearer_token: Any + call_state_events: list[str] | SWMLVar + call_state_url: str | SWMLVar + codec: Any + codecs: str | list[Any] | SWMLVar + confirm: str | list[SWMLMethod] | dict[str, Any] | SWMLVar + confirm_timeout: int | SWMLVar + custom_parameters: Any + encryption: Literal["mandatory", "optional", "forbidden"] | SWMLVar + execute_after_queue: str | SWMLVar + # non-identifier field 'from': str | SWMLVar + from_name: str | SWMLVar + fsvars: dict[str, str] | SWMLVar + headers: list[ConnectSipHeader] + max_duration: float | SWMLVar + name: Any + parallel: list[ConnectDevice] + password: str | SWMLVar + realtime: Any + result: list[Any] | dict[str, Any] + ringback: bool | str | list[RingbackConfig] | dict[str, Any] + serial: list[ConnectDevice] + serial_parallel: list[ConnectSerialParallel] + session_timeout: float | SWMLVar + status_url: str | SWMLVar + status_url_method: Any + timeout: float | SWMLVar + to: str | SWMLVar + username: str | SWMLVar + webrtc_media: bool | SWMLVar -class GotoConfig(TypedDict, total=False): - """Jump to a label within the current section, optionally based on a condition. +class DetectMachineConfig(TypedDict, total=False): + """Start answering machine detection. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - label: str - when: str - max: int | SWMLVar + detect_interruptions: bool | SWMLVar + detect_message_end: bool | SWMLVar + detectors: str | SWMLVar + end_silence_timeout: float | SWMLVar + initial_timeout: float | SWMLVar + machine_ready_timeout: float | SWMLVar + machine_voice_threshold: float | SWMLVar + machine_words_threshold: int | SWMLVar + status_url: str | SWMLVar + timeout: float | SWMLVar + tone: Literal["CNG", "CED", "cng", "ced"] | SWMLVar + wait: bool | SWMLVar -class LiveTranscribeConfig(TypedDict, total=False): - """Start live transcription of the call. The transcription will be sent to the specified webhook URL. +class DialConfig(TypedDict, total=False): + """Dial out to one or more endpoints. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - action: TranscribeAction + answer_on_bridge: bool | str + call_state_events: list[str] | SWMLVar + call_state_url: str | SWMLVar + codecs: str | list[Any] | SWMLVar + confirm: str | list[SWMLMethod] | dict[str, Any] | SWMLVar + confirm_timeout: int | SWMLVar + dest_swml: str | dict[str, Any] | list[Any] + encryption: Literal["mandatory", "optional", "forbidden"] | SWMLVar + execute_after_queue: str | SWMLVar + # non-identifier field 'from': str | SWMLVar + from_name: str | SWMLVar + fsvars: dict[str, str] | SWMLVar + headers: list[ConnectSipHeader] + max_duration: float | SWMLVar + parallel: list[ConnectDevice] + password: str | SWMLVar + result: Any + ringback: RingbackConfig + serial: list[ConnectDevice] + serial_parallel: list[ConnectSerialParallel] + session_timeout: float | SWMLVar + status_url: str | SWMLVar + timeout: float | SWMLVar + to: str | SWMLVar + username: str | SWMLVar + webrtc_media: bool | SWMLVar -class AiSidecarConfig(TypedDict, total=False): - """Start ai_sidecar mode — live_transcribe with an LLM/SWAIG/MCP loop on top. +class EnterQueueConfig(TypedDict, total=False): + """Place the call into a queue. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - prompt: str | dict[str, Any] - lang: str - model: str - direction: list[Literal["remote-caller", "local-caller"]] - customer_role: Literal["remote-caller", "local-caller"] - url: str - SWAIG: SWAIG - permissions: dict[str, Any] - global_data: dict[str, Any] - hints: list[str] - params: dict[str, Any] - action: dict[str, Any] + control_id: str | SWMLVar + execute_after_queue: str | SWMLVar + queue_name: str | SWMLVar + status_url: str | SWMLVar + wait_time: int | SWMLVar + wait_url: str | SWMLVar + whisper_url: str | SWMLVar -class LiveTranslateConfig(TypedDict, total=False): - """Start live translation of the call. The translation will be sent to the specified webhook URL. +class ExecuteRpcConfig(TypedDict, total=False): + """Execute a remote procedure call. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - action: TranslateAction + call_id: str | SWMLVar + method: str | SWMLVar + node_id: str | SWMLVar + params: dict[str, Any] | SWMLVar -class JoinRoomConfig(TypedDict, total=False): - """Join a RELAY room. If the room doesn't exist, it creates a new room. +class IfConfig(TypedDict, total=False): + """Conditional branching (deprecated). Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - name: str + condition: str | SWMLVar + # non-identifier field 'else': list[SWMLMethod] | dict[str, Any] + then: list[SWMLMethod] | dict[str, Any] -class PromptConfig(TypedDict, total=False): - """Play a prompt and wait for input. The input can be received either as digits from the keypad, +class LiveTranscribeConfig(TypedDict, total=False): + """Start live transcription of the call. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - play: play_url | list[play_url] | SWMLVar | list[SWMLVar] - volume: float - say_voice: str - say_language: str - say_gender: Literal["male", "female"] - max_digits: int | SWMLVar - terminators: str - digit_timeout: float | SWMLVar - initial_timeout: float | SWMLVar - speech_timeout: float | SWMLVar - speech_end_timeout: float | SWMLVar - speech_language: str - speech_hints: list[str] | list[SWMLVar] - speech_engine: str - status_url: str + action: Literal["start", "stop", "summarize"] | dict[str, Any] | SWMLVar + hints: list[dict[str, Any] | str] | SWMLVar -class ReceiveFaxConfig(TypedDict, total=False): - """Receive a fax being delivered to this call. +class LiveTranslateConfig(TypedDict, total=False): + """Start live translation of the call. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - status_url: str + action: Literal["inject", "start", "stop", "summarize"] | dict[str, Any] | SWMLVar class RecordConfig(TypedDict, total=False): - """Record the call audio in the foreground, pausing further SWML execution until recording ends. + """Record audio from the call. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - stereo: bool | SWMLVar - format: Literal["wav"] | Literal["mp3"] | Literal["mp4"] - direction: Literal["speak"] | Literal["listen"] - terminators: str + format: Literal["wav", "mp3", "mp4"] | SWMLVar beep: bool | SWMLVar - input_sensitivity: float | SWMLVar - initial_timeout: float | SWMLVar + direction: Literal["listen", "speak", "both"] | SWMLVar end_silence_timeout: float | SWMLVar - max_length: float | SWMLVar - status_url: str + initial_timeout: float | SWMLVar + input_sensitivity: float | SWMLVar + max_length: int | SWMLVar + status_url: str | SWMLVar + stereo: bool | SWMLVar + terminators: str | SWMLVar class RecordCallConfig(TypedDict, total=False): - """Record call in the background. + """Start recording the entire call. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - control_id: str - stereo: bool | SWMLVar - format: Literal["wav"] | Literal["mp3"] | Literal["mp4"] - direction: Literal["speak"] | Literal["listen"] | Literal["both"] - terminators: str + format: Literal["wav", "mp3", "mp4"] | SWMLVar beep: bool | SWMLVar - input_sensitivity: float | SWMLVar - initial_timeout: float | SWMLVar + control_id: str | SWMLVar + direction: Literal["listen", "speak", "both"] | SWMLVar end_silence_timeout: float | SWMLVar - max_length: float | SWMLVar - status_url: str + initial_timeout: float | SWMLVar + input_sensitivity: float | SWMLVar + max_length: int | SWMLVar + status_url: str | SWMLVar + stereo: bool | SWMLVar + terminators: str | SWMLVar class RequestConfig(TypedDict, total=False): - """Send a GET, POST, PUT, or DELETE request to a remote URL. + """Make an HTTP request and store the result. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - url: str - method: Literal["GET"] | Literal["POST"] | Literal["PUT"] | Literal["DELETE"] + body: dict[str, Any] | list[Any] | str | float | bool + connect_timeout: int | SWMLVar headers: dict[str, Any] - body: str | dict[str, Any] - timeout: float | SWMLVar - connect_timeout: float | SWMLVar + method: ( + Literal["get", "GET", "put", "PUT", "POST", "post", "DELETE", "delete"] + | SWMLVar + ) save_variables: bool | SWMLVar + timeout: int | SWMLVar + url: str | SWMLVar -class SendDigitsConfig(TypedDict, total=False): - """Send digit presses as DTMF tones. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - digits: str - - -class SendFaxConfig(TypedDict, total=False): - """Send a fax. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - document: str - header_info: str - identity: str - status_url: str - - -class SipReferConfig(TypedDict, total=False): - """Send SIP REFER to a SIP call. - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ - - to_uri: str - status_url: str - username: str - password: str - - -class StopRecordCallConfig(TypedDict, total=False): - """Stop an active background recording. +class SendSmsConfig(TypedDict, total=False): + """Send an SMS message. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - control_id: str + body: str | SWMLVar + from_number: str | SWMLVar + media: list[str] + region: str | SWMLVar + status_callback: str | SWMLVar + tags: list[str] + to_number: str | SWMLVar -class StopTapConfig(TypedDict, total=False): - """Stop an active tap stream. +class SetMetaConfig(TypedDict, total=False): + """Add customer metadata to call and conference events Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - control_id: str + meta: dict[str, Any] | SWMLVar + private: Any + public: Any class SwitchConfig(TypedDict, total=False): - """Execute different instructions based on a variable's value. + """Conditional branching based on variable value. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - variable: str + default: list[SWMLMethod] | dict[str, Any] case: dict[str, Any] - default: list[SWMLMethod] + variable: str | SWMLVar -class TapConfig(TypedDict, total=False): - """Start background call tap. Media is streamed over Websocket or RTP to customer controlled URI. +class TranscribeConfig(TypedDict, total=False): + """Start transcription on the call. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - uri: str - control_id: str - direction: Literal["speak"] | Literal["listen"] | Literal["both"] - codec: Literal["PCMU"] | Literal["PCMA"] - rtp_ptime: int | SWMLVar - status_url: str + status_url: str | SWMLVar -class TransferConfig(TypedDict, total=False): - """Transfer the execution of the script to a different SWML section, URL, or Relay application. +class UserEventConfig(TypedDict, total=False): + """Fire a custom user event. Open shape: extra server keys are permitted and partial payloads are valid; not validated at runtime (a TypedDict is a plain ``dict``). """ - dest: str - params: dict[str, Any] - meta: dict[str, Any] - - -class PayConfig(TypedDict, total=False): - """Enables secure payment processing during voice calls. When implemented, it manages the entire payment flow - - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + event: dict[str, Any] | SWMLVar - payment_connector_url: str - charge_amount: str - currency: str - description: str - input: Literal["dtmf"] - language: str - max_attempts: int | SWMLVar - min_postal_code_length: int | SWMLVar - parameters: list[PayParameters] - payment_method: Literal["credit-card"] - postal_code: bool | str - prompts: list[PayPrompts] - security_code: bool | SWMLVar - status_url: str - timeout: int | SWMLVar - token_type: Literal["one-time"] | Literal["reusable"] - valid_card_types: str - voice: str +class _SwmlVerbs: + """The SWML verb methods SwmlBuilder installs at runtime (static view).""" -class DetectMachineConfig(TypedDict, total=False): - """A detection method that combines AMD (Answering Machine Detection) and fax detection. + def ai_sidecar(self: _Self, config: AiSidecarConfig | None = None) -> _Self: + """Attach an AI sidecar observer to the call. Requires an active live_transcribe.""" + raise NotImplementedError # installed dynamically at runtime - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + def amazon_bedrock(self: _Self, config: AmazonBedrockConfig | None = None) -> _Self: + """Invoke an Amazon Bedrock AI model.""" + raise NotImplementedError # installed dynamically at runtime - detect_message_end: bool | SWMLVar - detectors: str - end_silence_timeout: float | SWMLVar - initial_timeout: float | SWMLVar - machine_ready_timeout: float | SWMLVar - machine_voice_threshold: float | SWMLVar - machine_words_threshold: int | SWMLVar - status_url: str - timeout: float | SWMLVar - tone: Literal["CED"] | Literal["CNG"] - wait: bool | SWMLVar + def cond(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Body shape enforced by is_valid_cond_method, swml_schema.c:1249.""" + raise NotImplementedError # installed dynamically at runtime + def connect(self: _Self, config: ConnectConfig | None = None) -> _Self: + """Connect the call to other endpoints (phone, SIP, etc.).""" + raise NotImplementedError # installed dynamically at runtime -class UserEventConfig(TypedDict, total=False): - """Allows the user to set and send events to the connected client on the call. + def denoise(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Enable noise reduction on audio.""" + raise NotImplementedError # installed dynamically at runtime - Open shape: extra server keys are permitted and partial payloads are valid; - not validated at runtime (a TypedDict is a plain ``dict``). - """ + def detect_machine(self: _Self, config: DetectMachineConfig | None = None) -> _Self: + """Start answering machine detection.""" + raise NotImplementedError # installed dynamically at runtime - event: dict[str, Any] + def dial(self: _Self, config: DialConfig | None = None) -> _Self: + """Dial out to one or more endpoints.""" + raise NotImplementedError # installed dynamically at runtime + def echo(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the echo verb.""" + raise NotImplementedError # installed dynamically at runtime -class _SwmlVerbs: - """The SWML verb methods SwmlBuilder installs at runtime (static view).""" + def enter_queue(self: _Self, config: EnterQueueConfig | None = None) -> _Self: + """Place the call into a queue.""" + raise NotImplementedError # installed dynamically at runtime - def amazon_bedrock(self: _Self, config: AmazonBedrockObject | None = None) -> _Self: - """Creates a new Bedrock AI Agent""" + def eval(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Evaluate expressions and assign to variables (deprecated).""" raise NotImplementedError # installed dynamically at runtime - def cond(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Execute a sequence of instructions depending on the value of a JavaScript condition.""" + def execute(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the execute verb.""" raise NotImplementedError # installed dynamically at runtime - def connect(self: _Self, config: ConnectConfig | None = None) -> _Self: - """Dial a SIP URI or phone number.""" + def execute_rpc(self: _Self, config: ExecuteRpcConfig | None = None) -> _Self: + """Execute a remote procedure call.""" raise NotImplementedError # installed dynamically at runtime - def denoise(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Start noise reduction. You can stop it at any time using `stop_denoise`.""" + def goto(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the goto verb.""" raise NotImplementedError # installed dynamically at runtime - def enter_queue(self: _Self, config: EnterQueueObject | None = None) -> _Self: - """Place the current call in a named queue where it will wait to be connected to an available agent or resource.""" + def if_(self: _Self, config: IfConfig | None = None) -> _Self: + """Conditional branching (deprecated).""" raise NotImplementedError # installed dynamically at runtime - def execute(self: _Self, config: ExecuteConfig | None = None) -> _Self: - """Execute a specified section or URL as a subroutine, and upon completion, return to the current document.""" + def join_conference(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the join_conference verb.""" raise NotImplementedError # installed dynamically at runtime - def goto(self: _Self, config: GotoConfig | None = None) -> _Self: - """Jump to a label within the current section, optionally based on a condition.""" + def join_room(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the join_room verb.""" raise NotImplementedError # installed dynamically at runtime - def label(self: _Self, value: str) -> _Self: - """Mark any point of the SWML section with a label so that goto can jump to it.""" + def label(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the label verb.""" raise NotImplementedError # installed dynamically at runtime def live_transcribe( self: _Self, config: LiveTranscribeConfig | None = None ) -> _Self: - """Start live transcription of the call. The transcription will be sent to the specified webhook URL.""" - raise NotImplementedError # installed dynamically at runtime - - def ai_sidecar(self: _Self, config: AiSidecarConfig | None = None) -> _Self: - """Start ai_sidecar mode — live_transcribe with an LLM/SWAIG/MCP loop on top.""" + """Start live transcription of the call.""" raise NotImplementedError # installed dynamically at runtime def live_translate(self: _Self, config: LiveTranslateConfig | None = None) -> _Self: - """Start live translation of the call. The translation will be sent to the specified webhook URL.""" - raise NotImplementedError # installed dynamically at runtime - - def join_room(self: _Self, config: JoinRoomConfig | None = None) -> _Self: - """Join a RELAY room. If the room doesn't exist, it creates a new room.""" + """Start live translation of the call.""" raise NotImplementedError # installed dynamically at runtime - def join_conference( - self: _Self, config: JoinConferenceObject | None = None - ) -> _Self: - """Join an ad-hoc audio conference started on either the SignalWire or Compatibility API.""" + def pay(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the pay verb.""" raise NotImplementedError # installed dynamically at runtime - def prompt(self: _Self, config: PromptConfig | None = None) -> _Self: - """Play a prompt and wait for input. The input can be received either as digits from the keypad,""" + def prompt(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the prompt verb.""" raise NotImplementedError # installed dynamically at runtime - def receive_fax(self: _Self, config: ReceiveFaxConfig | None = None) -> _Self: - """Receive a fax being delivered to this call.""" + def receive_fax(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the receive_fax verb.""" raise NotImplementedError # installed dynamically at runtime def record(self: _Self, config: RecordConfig | None = None) -> _Self: - """Record the call audio in the foreground, pausing further SWML execution until recording ends.""" + """Record audio from the call.""" raise NotImplementedError # installed dynamically at runtime def record_call(self: _Self, config: RecordCallConfig | None = None) -> _Self: - """Record call in the background.""" + """Start recording the entire call.""" raise NotImplementedError # installed dynamically at runtime def request(self: _Self, config: RequestConfig | None = None) -> _Self: - """Send a GET, POST, PUT, or DELETE request to a remote URL.""" + """Make an HTTP request and store the result.""" raise NotImplementedError # installed dynamically at runtime def return_(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Return a value from an execute call or exit the script. The value can be any type.""" + """Return from the current section.""" raise NotImplementedError # installed dynamically at runtime - def send_digits(self: _Self, config: SendDigitsConfig | None = None) -> _Self: - """Send digit presses as DTMF tones.""" + def sip_refer(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the sip_refer verb.""" raise NotImplementedError # installed dynamically at runtime - def send_fax(self: _Self, config: SendFaxConfig | None = None) -> _Self: - """Send a fax.""" + def send_digits(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the send_digits verb.""" raise NotImplementedError # installed dynamically at runtime - def send_sms(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Send an outbound SMS or MMS message to a PSTN phone number.""" + def send_fax(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the send_fax verb.""" + raise NotImplementedError # installed dynamically at runtime + + def send_sms(self: _Self, config: SendSmsConfig | None = None) -> _Self: + """Send an SMS message.""" raise NotImplementedError # installed dynamically at runtime def set(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Set script variables to the specified values.""" + """Set one or more variables.""" raise NotImplementedError # installed dynamically at runtime - def sleep(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Pause execution for a specified duration.""" + def set_meta(self: _Self, config: SetMetaConfig | None = None) -> _Self: + """Add customer metadata to call and conference events""" raise NotImplementedError # installed dynamically at runtime - def sip_refer(self: _Self, config: SipReferConfig | None = None) -> _Self: - """Send SIP REFER to a SIP call.""" + def sleep(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the sleep verb.""" raise NotImplementedError # installed dynamically at runtime def stop_denoise(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Stop noise reduction that was started with denoise.""" + """Disable noise reduction on audio.""" raise NotImplementedError # installed dynamically at runtime - def stop_record_call( - self: _Self, config: StopRecordCallConfig | None = None - ) -> _Self: - """Stop an active background recording.""" + def stop_record_call(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the stop_record_call verb.""" + raise NotImplementedError # installed dynamically at runtime + + def stop_stream(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the stop_stream verb.""" + raise NotImplementedError # installed dynamically at runtime + + def stop_tap(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the stop_tap verb.""" raise NotImplementedError # installed dynamically at runtime - def stop_tap(self: _Self, config: StopTapConfig | None = None) -> _Self: - """Stop an active tap stream.""" + def stream(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the stream verb.""" raise NotImplementedError # installed dynamically at runtime def switch(self: _Self, config: SwitchConfig | None = None) -> _Self: - """Execute different instructions based on a variable's value.""" + """Conditional branching based on variable value.""" raise NotImplementedError # installed dynamically at runtime - def tap(self: _Self, config: TapConfig | None = None) -> _Self: - """Start background call tap. Media is streamed over Websocket or RTP to customer controlled URI.""" + def tap(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the tap verb.""" raise NotImplementedError # installed dynamically at runtime - def transfer(self: _Self, config: TransferConfig | None = None) -> _Self: - """Transfer the execution of the script to a different SWML section, URL, or Relay application.""" + def transcribe(self: _Self, config: TranscribeConfig | None = None) -> _Self: + """Start transcription on the call.""" raise NotImplementedError # installed dynamically at runtime - def unset(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: - """Unset specified variables. The variables may have been set using the set method""" + def transcribe_stop(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Stop transcription on the call.""" raise NotImplementedError # installed dynamically at runtime - def pay(self: _Self, config: PayConfig | None = None) -> _Self: - """Enables secure payment processing during voice calls. When implemented, it manages the entire payment flow""" + def transfer(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Add the transfer verb.""" raise NotImplementedError # installed dynamically at runtime - def detect_machine(self: _Self, config: DetectMachineConfig | None = None) -> _Self: - """A detection method that combines AMD (Answering Machine Detection) and fax detection.""" + def unset(self: _Self, config: Mapping[str, Any] | None = None) -> _Self: + """Body shape enforced by CHECK_swml_method_unset, swml_schema.c.""" raise NotImplementedError # installed dynamically at runtime def user_event(self: _Self, config: UserEventConfig | None = None) -> _Self: - """Allows the user to set and send events to the connected client on the call.""" + """Fire a custom user event.""" raise NotImplementedError # installed dynamically at runtime diff --git a/signalwire/signalwire/relay/protocol_types_generated.py b/signalwire/signalwire/relay/protocol_types_generated.py index 8756b49a..cbd20e34 100644 --- a/signalwire/signalwire/relay/protocol_types_generated.py +++ b/signalwire/signalwire/relay/protocol_types_generated.py @@ -176,6 +176,9 @@ class CallingCollectStopParams(TypedDict, total=False): node_id: str +CallingConferenceParams: TypeAlias = "dict[str, Any]" + + class CallingConnectParams(TypedDict, total=False): """Wire schema for the JSON payload of `calling.connect` (params). Extracted from switchblade `PublicCallConnectParams.cs`. @@ -1037,6 +1040,9 @@ class CallingCollectStopResult(TypedDict, total=False): message: str +CallingConferenceResult: TypeAlias = "dict[str, Any]" + + class CallingConnectResult(TypedDict, total=False): """Wire schema for the JSON payload of `calling.connect` (result). Extracted from switchblade `PublicCallConnectResult.cs`. @@ -1719,4 +1725,4 @@ class SignalwireReauthenticateResult(TypedDict, total=False): authentication: str authorization: dict[str, Any] ice_servers: list[Any] - result: Any + result: dict[str, Any] diff --git a/signalwire/signalwire/search/document_processor.py b/signalwire/signalwire/search/document_processor.py index f8f9387b..2e95958f 100644 --- a/signalwire/signalwire/search/document_processor.py +++ b/signalwire/signalwire/search/document_processor.py @@ -1745,11 +1745,14 @@ def _chunk_from_json( # A new topic: remember its heading, and leave it alone - # it already names itself. current_heading = stripped.split("\n", 1)[0].strip() - elif current_heading and stripped.startswith("#"): + elif ( + current_heading + and stripped.startswith("#") + and current_heading.lower() not in chunk_text.lower() + ): # A subsection of that topic: give it the subject back. - if current_heading.lower() not in chunk_text.lower(): - chunk_text = f"{current_heading}\n\n{chunk_text}" - metadata["heading_context"] = current_heading + chunk_text = f"{current_heading}\n\n{chunk_text}" + metadata["heading_context"] = current_heading chunk = self._create_chunk( content=chunk_text, diff --git a/tests/unit/ai_chat/test_gateway.py b/tests/unit/ai_chat/test_gateway.py index 9e78d676..72e52d77 100644 --- a/tests/unit/ai_chat/test_gateway.py +++ b/tests/unit/ai_chat/test_gateway.py @@ -7,7 +7,7 @@ """ import json -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Awaitable, Callable from typing import Any import pytest @@ -19,6 +19,9 @@ CONFIG_URL = "https://agent.example.com/swml" KEY = "pk_test_key" +# What `ChatGateway.prepare` hands back: (service method, params, minted handle). +Prepared = tuple[str, dict[str, Any], str | None] + # ── Stub service ───────────────────────────────────────────────────── @@ -34,13 +37,17 @@ async def handler(request: web.Request) -> web.Response: method = body["method"] result = { "chat": {"response": "hi there"}, - "create_conversation": {"status": "created", - "initial_message": "Hi, I am Sigmond."}, - "chat_log": {"chat_log": [ - {"role": "system", "content": "secret prompt"}, - {"role": "user", "content": "hi"}, - {"role": "assistant", "content": "hi there"}, - ]}, + "create_conversation": { + "status": "created", + "initial_message": "Hi, I am Sigmond.", + }, + "chat_log": { + "chat_log": [ + {"role": "system", "content": "secret prompt"}, + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "hi there"}, + ] + }, }.get(method, {"status": "ended"}) return web.json_response({"jsonrpc": "2.0", "result": result, "id": body["id"]}) @@ -53,7 +60,7 @@ async def handler(request: web.Request) -> web.Response: @pytest.fixture -async def gateway(service) -> AsyncIterator[ChatGateway]: +async def gateway(service: Any) -> AsyncIterator[ChatGateway]: client = AIChatClient(project="p", token="t", url=service.url) gw = ChatGateway( config_url=CONFIG_URL, @@ -66,8 +73,7 @@ async def gateway(service) -> AsyncIterator[ChatGateway]: await client.close() - -def make_gateway(service, **kw) -> ChatGateway: +def make_gateway(service: Any, **kw: Any) -> ChatGateway: """A gateway wired to the stub service. Construction is deliberately fail-fast on credentials, so every gateway gets a client.""" kw.setdefault("secret", b"s") @@ -82,12 +88,12 @@ def make_gateway(service, **kw) -> ChatGateway: # ── Handles ────────────────────────────────────────────────────────── -def test_a_handle_round_trips(gateway): +def test_a_handle_round_trips(gateway: ChatGateway) -> None: handle = gateway.mint_handle() assert gateway.read_handle(handle).startswith("chat-") -def test_the_browser_cannot_forge_a_conversation(gateway): +def test_the_browser_cannot_forge_a_conversation(gateway: ChatGateway) -> None: """The whole reason the gateway mints: with a publishable key, a guessable id would be enough to continue somebody else's chat.""" forged = gateway.mint_handle() @@ -97,20 +103,22 @@ def test_the_browser_cannot_forge_a_conversation(gateway): assert err.value.status == 403 -def test_a_handle_from_another_gateway_is_refused(gateway, service): +def test_a_handle_from_another_gateway_is_refused( + gateway: ChatGateway, service: Any +) -> None: other = make_gateway(service, secret=b"different") with pytest.raises(GatewayRejection): gateway.read_handle(other.mint_handle()) -def test_an_expired_handle_is_refused(service): +def test_an_expired_handle_is_refused(service: Any) -> None: gw = make_gateway(service, handle_ttl=-1) with pytest.raises(GatewayRejection) as err: gw.read_handle(gw.mint_handle()) assert err.value.status == 403 -def test_garbage_is_refused_without_leaking_why(gateway): +def test_garbage_is_refused_without_leaking_why(gateway: ChatGateway) -> None: for bad in ("", "not-a-handle", "a.b.c", "!!!.!!!"): with pytest.raises(GatewayRejection): gateway.read_handle(bad) @@ -123,33 +131,47 @@ def test_garbage_is_refused_without_leaking_why(gateway): "origin", ["http://localhost:3000", "http://127.0.0.1:8080", "http://app.localhost"], ) -def test_localhost_never_needs_listing(gateway, origin): +def test_localhost_never_needs_listing(gateway: ChatGateway, origin: str) -> None: """`pip install` → run → it works, without shipping open by default.""" + # Allowed even though it was never listed... gateway.check_origin(origin) + # ...and the exemption is localhost-specific, not open-by-default. Without + # this contrast the test would still pass against a gateway that allowed + # every origin. + with pytest.raises(GatewayRejection): + gateway.check_origin("https://evil.example.com") -def test_a_listed_origin_is_allowed(gateway): +def test_a_listed_origin_is_allowed(gateway: ChatGateway) -> None: gateway.check_origin("https://shop.example.com") + # A near-miss must not pass: the check is a full-origin match, not a + # substring/prefix one (which would let evil host a lookalike domain). + with pytest.raises(GatewayRejection): + gateway.check_origin("https://shop.example.com.evil.test") -def test_an_unlisted_origin_is_refused(gateway): +def test_an_unlisted_origin_is_refused(gateway: ChatGateway) -> None: """The case this actually defends: a key pasted into someone else's page.""" with pytest.raises(GatewayRejection) as err: gateway.check_origin("https://evil.example.com") assert err.value.status == 403 -def test_a_missing_origin_is_allowed(gateway): +def test_a_missing_origin_is_allowed(gateway: ChatGateway) -> None: """Browsers always send one on these POSTs, so absence means a non-browser caller. Refusing it would break server-side use and stop no attacker, who just omits the header.""" gateway.check_origin(None) + # Absent is not the same as unrecognised — a present-but-unlisted origin is + # still refused, so this is a deliberate exemption rather than a hole. + with pytest.raises(GatewayRejection): + gateway.check_origin("https://evil.example.com") # ── Key ────────────────────────────────────────────────────────────── -def test_the_key_is_required(gateway): +def test_the_key_is_required(gateway: ChatGateway) -> None: for bad in (None, "", "pk_wrong"): with pytest.raises(GatewayRejection) as err: gateway.check_key(bad) @@ -159,38 +181,43 @@ def test_the_key_is_required(gateway): # ── What the browser may ask for ───────────────────────────────────── -def prep(gw, body, origin="https://shop.example.com"): +def prep( + gw: ChatGateway, + body: dict[str, Any], + origin: str | None = "https://shop.example.com", +) -> Prepared: return gw.prepare(body, origin=origin, key=KEY) -def test_config_url_is_ours_not_theirs(gateway): +def test_config_url_is_ours_not_theirs(gateway: ChatGateway) -> None: """If the browser could name it, whoever holds a key picks which agent runs — and which project pays for it.""" _, params, _ = prep(gateway, {"message": "hi", "config_url": "https://evil/swml"}) assert params["config_url"] == CONFIG_URL -def test_the_browser_cannot_name_the_conversation(gateway): +def test_the_browser_cannot_name_the_conversation(gateway: ChatGateway) -> None: _, params, minted = prep(gateway, {"message": "hi", "id": "someone-elses-chat"}) assert params["id"] != "someone-elses-chat" + assert minted is not None assert gateway.read_handle(minted) == params["id"] -def test_only_chat_and_end_pass(gateway): +def test_only_chat_and_end_pass(gateway: ChatGateway) -> None: for method in ("chat_log", "summarize", "delete", "create_conversation"): with pytest.raises(GatewayRejection) as err: prep(gateway, {"method": method, "message": "hi"}) assert err.value.status == 400 -def test_chat_log_is_not_reachable(gateway): +def test_chat_log_is_not_reachable(gateway: ChatGateway) -> None: """Keeping it off the wire is what makes a stolen key a bill, not a breach.""" with pytest.raises(GatewayRejection): prep(gateway, {"method": "chat_log"}) -def test_the_first_chat_mints_and_later_ones_reuse(gateway): +def test_the_first_chat_mints_and_later_ones_reuse(gateway: ChatGateway) -> None: _, first, minted = prep(gateway, {"message": "one"}) assert minted _, second, again = prep(gateway, {"message": "two", "handle": minted}) @@ -198,19 +225,19 @@ def test_the_first_chat_mints_and_later_ones_reuse(gateway): assert second["id"] == first["id"] -def test_end_needs_a_handle(gateway): +def test_end_needs_a_handle(gateway: ChatGateway) -> None: with pytest.raises(GatewayRejection): prep(gateway, {"method": "end"}) -def test_end_maps_to_the_service_method(gateway): +def test_end_maps_to_the_service_method(gateway: ChatGateway) -> None: minted = gateway.mint_handle() method, params, _ = prep(gateway, {"method": "end", "handle": minted}) assert method == "end_conversation" assert params == {"id": gateway.read_handle(minted)} -def test_an_empty_message_is_refused(gateway): +def test_an_empty_message_is_refused(gateway: ChatGateway) -> None: for bad in (None, "", " ", 5): with pytest.raises(GatewayRejection): prep(gateway, {"message": bad}) @@ -219,7 +246,7 @@ def test_an_empty_message_is_refused(gateway): # ── The caps, which are the real control ───────────────────────────── -def test_minting_is_capped(service): +def test_minting_is_capped(service: Any) -> None: """A leaked key does not hammer one conversation — it mints thousands of one-turn ones, and each bills its opening turn.""" gw = make_gateway(service, max_new_conversations=3) @@ -230,7 +257,7 @@ def test_minting_is_capped(service): assert err.value.status == 429 -def test_turns_are_capped_per_conversation(service): +def test_turns_are_capped_per_conversation(service: Any) -> None: gw = make_gateway(service, max_turns=2) handle = gw.mint_handle() for _ in range(2): @@ -240,7 +267,7 @@ def test_turns_are_capped_per_conversation(service): assert err.value.status == 429 -def test_one_conversation_hitting_its_cap_does_not_stop_another(service): +def test_one_conversation_hitting_its_cap_does_not_stop_another(service: Any) -> None: gw = make_gateway(service, max_turns=1) a, b = gw.mint_handle(), gw.mint_handle() gw.prepare({"message": "hi", "handle": a}, origin=None, key=KEY) @@ -256,7 +283,7 @@ def test_one_conversation_hitting_its_cap_does_not_stop_another(service): # belongs to this one, and the cross-loop call simply hangs. -def asgi(gateway): +def asgi(gateway: ChatGateway) -> Any: httpx = pytest.importorskip("httpx") fastapi = pytest.importorskip("fastapi") app = fastapi.FastAPI() @@ -269,7 +296,7 @@ def asgi(gateway): HEADERS = {"Authorization": f"Bearer {KEY}", "Origin": "https://shop.example.com"} -async def test_a_full_exchange_over_http(gateway, service): +async def test_a_full_exchange_over_http(gateway: ChatGateway, service: Any) -> None: async with asgi(gateway) as http: r = await http.post("/chat/", json={"message": "hello"}, headers=HEADERS) assert r.status_code == 200 @@ -290,7 +317,9 @@ async def test_a_full_exchange_over_http(gateway, service): assert service.seen[-1]["method"] == "end_conversation" -async def test_a_second_turn_reuses_the_handle(gateway, service): +async def test_a_second_turn_reuses_the_handle( + gateway: ChatGateway, service: Any +) -> None: async with asgi(gateway) as http: first = await http.post("/chat/", json={"message": "one"}, headers=HEADERS) handle = first.headers["x-chat-handle"] @@ -298,34 +327,34 @@ async def test_a_second_turn_reuses_the_handle(gateway, service): second = await http.post( "/chat/", json={"message": "two", "handle": handle}, headers=HEADERS ) - assert "x-chat-handle" not in second.headers # nothing new minted + assert "x-chat-handle" not in second.headers # nothing new minted assert service.seen[-1]["params"]["id"] == gateway.read_handle(handle) -async def test_http_refuses_a_bad_key(gateway): +async def test_http_refuses_a_bad_key(gateway: ChatGateway) -> None: async with asgi(gateway) as http: r = await http.post( - "/chat/", json={"message": "hi"}, + "/chat/", + json={"message": "hi"}, headers={"Authorization": "Bearer nope"}, ) assert r.status_code == 401 -async def test_http_refuses_an_unlisted_origin(gateway): +async def test_http_refuses_an_unlisted_origin(gateway: ChatGateway) -> None: async with asgi(gateway) as http: r = await http.post( - "/chat/", json={"message": "hi"}, + "/chat/", + json={"message": "hi"}, headers={"Authorization": f"Bearer {KEY}", "Origin": "https://evil.test"}, ) assert r.status_code == 403 assert "access-control-allow-origin" not in r.headers -async def test_preflight_answers_a_listed_origin(gateway): +async def test_preflight_answers_a_listed_origin(gateway: ChatGateway) -> None: async with asgi(gateway) as http: - r = await http.options( - "/chat/", headers={"Origin": "https://shop.example.com"} - ) + r = await http.options("/chat/", headers={"Origin": "https://shop.example.com"}) assert r.status_code == 204 assert r.headers["access-control-allow-origin"] == "https://shop.example.com" assert "X-Chat-Handle" in r.headers["access-control-expose-headers"] @@ -355,7 +384,11 @@ async def handler(request: web.Request) -> web.StreamResponse: await asyncio.sleep(0.01) await resp.write( json.dumps( - {"jsonrpc": "2.0", "result": {"response": "slow reply"}, "id": body["id"]} + { + "jsonrpc": "2.0", + "result": {"response": "slow reply"}, + "id": body["id"], + } ).encode() ) await resp.write_eof() @@ -370,20 +403,23 @@ async def handler(request: web.Request) -> web.StreamResponse: @pytest.fixture -async def slow_gateway(slow_service) -> AsyncIterator[ChatGateway]: +async def slow_gateway(slow_service: Any) -> AsyncIterator[ChatGateway]: client = AIChatClient(project="p", token="t", url=slow_service.url) yield ChatGateway(config_url=CONFIG_URL, key=KEY, client=client, secret=b"s") await client.close() -async def test_the_keepalive_padding_is_relayed_not_swallowed(slow_gateway): +async def test_the_keepalive_padding_is_relayed_not_swallowed( + slow_gateway: ChatGateway, +) -> None: """The regression guard: a gateway that awaits the whole body would strip this padding and recreate, inside the customer's own stack, the very proxy timeout the service pads to survive. """ async with asgi(slow_gateway) as http: r = await http.post( - "/chat/", json={"message": "hi"}, + "/chat/", + json={"message": "hi"}, headers={"Authorization": f"Bearer {KEY}"}, ) assert r.status_code == 200 @@ -391,7 +427,9 @@ async def test_the_keepalive_padding_is_relayed_not_swallowed(slow_gateway): assert json.loads(r.text)["result"]["response"] == "slow reply" -async def test_the_relay_streams_rather_than_collects(slow_gateway, monkeypatch): +async def test_the_relay_streams_rather_than_collects( + slow_gateway: ChatGateway, monkeypatch: pytest.MonkeyPatch +) -> None: """Chunks leave the upstream socket one at a time, and the route hands back a streaming response rather than a completed body. @@ -415,54 +453,64 @@ async def test_the_relay_streams_rather_than_collects(slow_gateway, monkeypatch) r for r in slow_gateway.router().routes if "POST" in getattr(r, "methods", ()) ) scope = { - "type": "http", "method": "POST", "path": "/", "headers": [], + "type": "http", + "method": "POST", + "path": "/", + "headers": [], "query_string": b"", } from starlette.requests import Request - async def receive(): + async def receive() -> dict[str, Any]: return {"type": "http.request", "body": json.dumps({"message": "hi"}).encode()} request = Request(scope, receive) - request._headers = {"authorization": f"Bearer {KEY}"} # type: ignore[attr-defined] - response = await route.endpoint(request) + # Reaching past the public surface on purpose: this asserts the route's + # internal contract (streams rather than materialises), which no public + # client can observe — see the docstring. + request._headers = {"authorization": f"Bearer {KEY}"} # type: ignore[assignment] + response = await route.endpoint(request) # type: ignore[attr-defined] assert isinstance(response, StreamingResponse) # ── start / log ────────────────────────────────────────────────────── -def test_start_mints_and_opens_with_no_message(gateway): +def test_start_mints_and_opens_with_no_message(gateway: ChatGateway) -> None: """A widget wants the agent to speak first, before anyone has typed.""" method, params, minted = prep(gateway, {"method": "start"}) assert method == "create_conversation" + assert minted is not None assert params == {"id": gateway.read_handle(minted), "config_url": CONFIG_URL} -def test_log_is_scoped_to_the_handle_not_the_body(gateway): +def test_log_is_scoped_to_the_handle_not_the_body(gateway: ChatGateway) -> None: """The conversation comes from inside the signed handle, so a caller cannot read somebody else's by naming it.""" handle = gateway.mint_handle() - method, params, _ = prep(gateway, {"method": "log", "handle": handle, - "id": "someone-elses-chat"}) + method, params, _ = prep( + gateway, {"method": "log", "handle": handle, "id": "someone-elses-chat"} + ) assert method == "chat_log" assert params == {"id": gateway.read_handle(handle)} -def test_log_needs_a_handle(gateway): +def test_log_needs_a_handle(gateway: ChatGateway) -> None: with pytest.raises(GatewayRejection): prep(gateway, {"method": "log"}) -def test_the_transcript_hides_everything_but_the_dialogue(gateway): +def test_the_transcript_hides_everything_but_the_dialogue( + gateway: ChatGateway, +) -> None: """chat_log returns the substituted SYSTEM PROMPT and the tool traffic. Relaying that would publish the developer's prompt to anyone with a handle.""" - raw = [ + raw: list[dict[str, Any]] = [ {"role": "system", "content": "You are Sigmond. Secret instructions."}, {"role": "user", "content": "hi", "timestamp": 123}, {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1"}]}, - {"role": "tool", "content": "{\"internal\": \"result\"}"}, + {"role": "tool", "content": '{"internal": "result"}'}, {"role": "assistant", "content": "Hello!", "timestamp": 124}, {"role": "assistant", "content": " "}, ] @@ -481,12 +529,16 @@ def test_the_transcript_hides_everything_but_the_dialogue(gateway): assert "tool_calls" not in blob and "internal" not in blob -def test_the_transcript_reports_seconds_not_microseconds(gateway): +def test_the_transcript_reports_seconds_not_microseconds( + gateway: ChatGateway, +) -> None: """A 1000000x unit slip here is SILENT: a browser bootstrapping its idle clock from a microsecond value reads the conversation as fresh forever and never warns that the next message starts a new one.""" - ts_us = 1_786_258_737_756_596 # microseconds, as the service stores - out = gateway.visible_messages([{"role": "user", "content": "hi", "timestamp": ts_us}]) + ts_us = 1_786_258_737_756_596 # microseconds, as the service stores + out = gateway.visible_messages( + [{"role": "user", "content": "hi", "timestamp": ts_us}] + ) assert out[0]["timestamp"] == pytest.approx(1_786_258_737.756596) assert gateway.last_activity( @@ -494,39 +546,54 @@ def test_the_transcript_reports_seconds_not_microseconds(gateway): ) == pytest.approx(1_786_258_737.756596) -def test_last_activity_takes_the_newest_message_of_any_role(gateway): +def test_last_activity_takes_the_newest_message_of_any_role( + gateway: ChatGateway, +) -> None: """The service's idle clock runs off updated_at, which ANY write moves. Counting only the visible roles would report an older time than the service is measuring and warn early for no reason.""" - assert gateway.last_activity([ - {"role": "user", "content": "first", "timestamp": 1_000_000}, - {"role": "assistant", "content": "second", "timestamp": 3_000_000}, - {"role": "tool", "content": "internal", "timestamp": 5_000_000}, - ]) == 5.0 + assert ( + gateway.last_activity( + [ + {"role": "user", "content": "first", "timestamp": 1_000_000}, + {"role": "assistant", "content": "second", "timestamp": 3_000_000}, + {"role": "tool", "content": "internal", "timestamp": 5_000_000}, + ] + ) + == 5.0 + ) -def test_last_activity_is_none_when_nothing_is_dated(gateway): +def test_last_activity_is_none_when_nothing_is_dated(gateway: ChatGateway) -> None: """None, not 0 — a zero would read as 1970 and expire every conversation the instant it was restored.""" assert gateway.last_activity([{"role": "user", "content": "hi"}]) is None assert gateway.last_activity([]) is None assert gateway.last_activity(None) is None - assert gateway.last_activity([{"role": "user", "timestamp": "not a number"}]) is None + undated: list[Any] = [{"role": "user", "timestamp": "not a number"}] + assert gateway.last_activity(undated) is None -def test_effective_timeout_is_always_a_number(gateway): +def test_effective_timeout_is_always_a_number(gateway: ChatGateway) -> None: """A browser cannot warn about a deadline it was told nothing about, so an unset timeout still reports the service default rather than null.""" assert gateway.effective_timeout == 3600 -def test_the_transcript_survives_junk(gateway): +def test_the_transcript_survives_junk(gateway: ChatGateway) -> None: + # Deliberately malformed input: the service is upstream of us, so a + # transcript that is not a list of dicts must degrade to empty rather than + # raise inside somebody's page. `list[Any]` because that is exactly the + # contract being probed. + junk: list[Any] = ["not a dict", {"role": "user"}] assert gateway.visible_messages([]) == [] assert gateway.visible_messages(None) == [] - assert gateway.visible_messages(["not a dict", {"role": "user"}]) == [] + assert gateway.visible_messages(junk) == [] -async def test_start_then_reload_replays_the_same_conversation(gateway, service): +async def test_start_then_reload_replays_the_same_conversation( + gateway: ChatGateway, service: Any +) -> None: """The reload path end to end: start, keep the handle, read it back.""" async with asgi(gateway) as http: started = await http.post("/chat/", json={"method": "start"}, headers=HEADERS) diff --git a/tests/unit/core/test_function_result.py b/tests/unit/core/test_function_result.py index 0803de39..58d52bdc 100644 --- a/tests/unit/core/test_function_result.py +++ b/tests/unit/core/test_function_result.py @@ -364,6 +364,7 @@ def test_information_response(self) -> None: """Test creating informational response""" result = FunctionResult("Here is the information you requested") + assert isinstance(result.response, str) assert "information" in result.response.lower() diff --git a/tests/unit/prefabs/test_concierge.py b/tests/unit/prefabs/test_concierge.py index c6ea99de..92457160 100644 --- a/tests/unit/prefabs/test_concierge.py +++ b/tests/unit/prefabs/test_concierge.py @@ -367,6 +367,7 @@ def test_known_service_case_insensitive(self) -> None: raw_data={}, ) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) # The lowered input "room service" matches "room service" in SERVICES assert "room service" in result.response.lower() assert "available" in result.response.lower() or "reservation" in result.response.lower() @@ -379,6 +380,7 @@ def test_unknown_service_returns_error(self) -> None: raw_data={}, ) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "sorry" in result.response.lower() or "don't offer" in result.response.lower() assert VENUE_NAME in result.response # Should list available services diff --git a/tests/unit/rest/fabric_generated_test.py b/tests/unit/rest/fabric_generated_test.py index c0dcc9b1..75486ae8 100644 --- a/tests/unit/rest/fabric_generated_test.py +++ b/tests/unit/rest/fabric_generated_test.py @@ -656,7 +656,7 @@ def test_cxml_webhooks_update_error( def test_freeswitch_connectors_create( self, signalwire_client: RestClient, mock: _MockHarness ) -> None: - signalwire_client.fabric.freeswitch_connectors.create(name="x", token="x") # noqa: S106 + signalwire_client.fabric.freeswitch_connectors.create(name="x", token="x") last = mock.last_request() assert last.method == "POST" assert last.matched_route == "fabric.create_freeswitch_connector" @@ -666,7 +666,7 @@ def test_freeswitch_connectors_create_error( ) -> None: mock.push_scenario("fabric.create_freeswitch_connector", 500, {"error": "x"}) with pytest.raises(SignalWireRestError) as exc: - signalwire_client.fabric.freeswitch_connectors.create(name="x", token="x") # noqa: S106 + signalwire_client.fabric.freeswitch_connectors.create(name="x", token="x") assert exc.value.status_code == 500 def test_freeswitch_connectors_delete( @@ -1198,7 +1198,7 @@ def test_subscribers_create_sip_endpoint( ) -> None: signalwire_client.fabric.subscribers.create_sip_endpoint( "test-id", username="x", password="x" - ) # noqa: S106 + ) last = mock.last_request() assert last.method == "POST" assert last.matched_route == "fabric.create_subscriber_sip_endpoint" @@ -1210,7 +1210,7 @@ def test_subscribers_create_sip_endpoint_error( with pytest.raises(SignalWireRestError) as exc: signalwire_client.fabric.subscribers.create_sip_endpoint( "test-id", username="x", password="x" - ) # noqa: S106 + ) assert exc.value.status_code == 500 def test_subscribers_delete( @@ -1556,7 +1556,7 @@ def test_swml_webhooks_update_error( def test_tokens_create_embed_token( self, signalwire_client: RestClient, mock: _MockHarness ) -> None: - signalwire_client.fabric.tokens.create_embed_token(token="x") # noqa: S106 + signalwire_client.fabric.tokens.create_embed_token(token="x") last = mock.last_request() assert last.method == "POST" assert last.matched_route == "fabric.create_embeds_token" @@ -1566,7 +1566,7 @@ def test_tokens_create_embed_token_error( ) -> None: mock.push_scenario("fabric.create_embeds_token", 500, {"error": "x"}) with pytest.raises(SignalWireRestError) as exc: - signalwire_client.fabric.tokens.create_embed_token(token="x") # noqa: S106 + signalwire_client.fabric.tokens.create_embed_token(token="x") assert exc.value.status_code == 500 def test_tokens_create_guest_token( @@ -1620,7 +1620,7 @@ def test_tokens_create_subscriber_token_error( def test_tokens_refresh_subscriber_token( self, signalwire_client: RestClient, mock: _MockHarness ) -> None: - signalwire_client.fabric.tokens.refresh_subscriber_token(refresh_token="x") # noqa: S106 + signalwire_client.fabric.tokens.refresh_subscriber_token(refresh_token="x") last = mock.last_request() assert last.method == "POST" assert last.matched_route == "fabric.refresh_subscriber_token" @@ -1630,5 +1630,5 @@ def test_tokens_refresh_subscriber_token_error( ) -> None: mock.push_scenario("fabric.refresh_subscriber_token", 500, {"error": "x"}) with pytest.raises(SignalWireRestError) as exc: - signalwire_client.fabric.tokens.refresh_subscriber_token(refresh_token="x") # noqa: S106 + signalwire_client.fabric.tokens.refresh_subscriber_token(refresh_token="x") assert exc.value.status_code == 500 diff --git a/tests/unit/rest/mfa_generated_test.py b/tests/unit/rest/mfa_generated_test.py index 9ac38e50..af3927da 100644 --- a/tests/unit/rest/mfa_generated_test.py +++ b/tests/unit/rest/mfa_generated_test.py @@ -54,7 +54,7 @@ def test_mfa_sms_error( def test_mfa_verify( self, signalwire_client: RestClient, mock: _MockHarness ) -> None: - signalwire_client.mfa.verify("test-id", token="x") # noqa: S106 + signalwire_client.mfa.verify("test-id", token="x") last = mock.last_request() assert last.method == "POST" assert last.matched_route == "relay-rest.verify_mfa_token" @@ -64,5 +64,5 @@ def test_mfa_verify_error( ) -> None: mock.push_scenario("relay-rest.verify_mfa_token", 500, {"error": "x"}) with pytest.raises(SignalWireRestError) as exc: - signalwire_client.mfa.verify("test-id", token="x") # noqa: S106 + signalwire_client.mfa.verify("test-id", token="x") assert exc.value.status_code == 500 diff --git a/tests/unit/skills/test_datasphere_skill.py b/tests/unit/skills/test_datasphere_skill.py index 14ff55d6..0ab65662 100644 --- a/tests/unit/skills/test_datasphere_skill.py +++ b/tests/unit/skills/test_datasphere_skill.py @@ -377,18 +377,21 @@ def test_empty_query_returns_error(self) -> None: skill, _ = self._setup_skill() result = skill._search_knowledge_handler({"query": ""}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_whitespace_query_returns_error(self) -> None: skill, _ = self._setup_skill() result = skill._search_knowledge_handler({"query": " "}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_missing_query_key_returns_error(self) -> None: skill, _ = self._setup_skill() result = skill._search_knowledge_handler({}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_successful_search_single_chunk(self) -> None: @@ -484,6 +487,7 @@ def test_timeout_error(self) -> None: result = skill._search_knowledge_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "timed out" in result.response.lower() def test_http_error(self) -> None: @@ -494,6 +498,7 @@ def test_http_error(self) -> None: result = skill._search_knowledge_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "error" in result.response.lower() def test_generic_exception(self) -> None: @@ -502,6 +507,7 @@ def test_generic_exception(self) -> None: result = skill._search_knowledge_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "error" in result.response.lower() def test_request_payload_required_fields(self) -> None: @@ -948,4 +954,5 @@ def test_connection_error(self, mock_session_cls: MagicMock) -> None: result = skill._search_knowledge_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "error" in result.response.lower() diff --git a/tests/unit/skills/test_math_skill.py b/tests/unit/skills/test_math_skill.py index bf6fc40f..10555615 100644 --- a/tests/unit/skills/test_math_skill.py +++ b/tests/unit/skills/test_math_skill.py @@ -176,18 +176,21 @@ def test_empty_string(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": ""}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide" in result.response.lower() def test_whitespace_only(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": " "}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide" in result.response.lower() def test_missing_expression_key(self) -> None: skill = _make_skill() result = skill._calculate_handler({}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide" in result.response.lower() @@ -202,24 +205,28 @@ def test_import_os_rejected(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "import os"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "invalid" in result.response.lower() def test_letters_rejected(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "abc"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "invalid" in result.response.lower() def test_dunder_rejected(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "__import__('os')"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "invalid" in result.response.lower() def test_semicolon_rejected(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "1;2"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "invalid" in result.response.lower() @@ -234,12 +241,14 @@ def test_division_by_zero(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "1/0"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "zero" in result.response.lower() def test_modulo_by_zero(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "10%0"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "zero" in result.response.lower() @@ -254,12 +263,14 @@ def test_incomplete_expression(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "2+"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "error" in result.response.lower() def test_unmatched_parens(self) -> None: skill = _make_skill() result = skill._calculate_handler({"expression": "(2+3"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "error" in result.response.lower() diff --git a/tests/unit/skills/test_native_vector_search_skill.py b/tests/unit/skills/test_native_vector_search_skill.py index 0941eadf..ddc09b87 100644 --- a/tests/unit/skills/test_native_vector_search_skill.py +++ b/tests/unit/skills/test_native_vector_search_skill.py @@ -648,6 +648,7 @@ def test_search_unavailable(self) -> None: result = skill._search_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "not available" in result.response.lower() def test_search_engine_missing(self) -> None: @@ -656,6 +657,7 @@ def test_search_engine_missing(self) -> None: result = skill._search_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "not available" in result.response.lower() def test_empty_query(self) -> None: @@ -664,6 +666,7 @@ def test_empty_query(self) -> None: result = skill._search_handler({"query": ""}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_whitespace_only_query(self) -> None: @@ -672,6 +675,7 @@ def test_whitespace_only_query(self) -> None: result = skill._search_handler({"query": " "}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_missing_query_key(self) -> None: @@ -680,6 +684,7 @@ def test_missing_query_key(self) -> None: result = skill._search_handler({}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_local_search_no_results(self) -> None: @@ -710,7 +715,9 @@ def test_local_search_no_results_with_prefix_postfix(self) -> None: }): result = skill._search_handler({"query": "test"}, {}) + assert isinstance(result.response, str) assert result.response.startswith("[START]") + assert isinstance(result.response, str) assert result.response.endswith("[END]") def test_local_search_with_results(self) -> None: @@ -825,6 +832,7 @@ def test_search_exception_handling_generic(self) -> None: result = skill._search_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "sorry" in result.response.lower() assert "rephrasing" in result.response.lower() @@ -839,6 +847,7 @@ def test_search_exception_handling_nltk(self) -> None: }): result = skill._search_handler({"query": "test"}, {}) + assert isinstance(result.response, str) assert "language processing" in result.response.lower() def test_search_exception_handling_vector(self) -> None: @@ -852,6 +861,7 @@ def test_search_exception_handling_vector(self) -> None: }): result = skill._search_handler({"query": "test"}, {}) + assert isinstance(result.response, str) assert "indexing" in result.response.lower() def test_search_exception_handling_timeout(self) -> None: @@ -865,6 +875,7 @@ def test_search_exception_handling_timeout(self) -> None: }): result = skill._search_handler({"query": "test"}, {}) + assert isinstance(result.response, str) assert "temporarily unavailable" in result.response.lower() def test_response_format_callback_with_results(self) -> None: diff --git a/tests/unit/skills/test_web_search_skill.py b/tests/unit/skills/test_web_search_skill.py index f845cf55..94e97373 100644 --- a/tests/unit/skills/test_web_search_skill.py +++ b/tests/unit/skills/test_web_search_skill.py @@ -408,18 +408,21 @@ def test_empty_query_returns_error(self) -> None: skill = self._setup_skill() result = skill._web_search_handler({"query": ""}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_whitespace_query_returns_error(self) -> None: skill = self._setup_skill() result = skill._web_search_handler({"query": " "}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_missing_query_key_returns_error(self) -> None: skill = self._setup_skill() result = skill._web_search_handler({}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "provide a search query" in result.response.lower() def test_successful_search(self) -> None: @@ -437,6 +440,7 @@ def test_no_search_results(self) -> None: return_value="No search results found for query: test"): result = skill._web_search_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) # Should trigger no_results_message assert "couldn't find" in result.response.lower() or "quality" in result.response.lower() @@ -459,6 +463,7 @@ def test_exception_during_search(self) -> None: side_effect=RuntimeError("connection failed")): result = skill._web_search_handler({"query": "test"}, {}) assert isinstance(result, FunctionResult) + assert isinstance(result.response, str) assert "error" in result.response.lower() def test_no_results_custom_message_with_placeholder(self) -> None: