From 13f866c387ba5cceb7a55a854df9237941391300 Mon Sep 17 00:00:00 2001 From: Anthony Minessale II Date: Mon, 10 Aug 2026 12:21:07 -0500 Subject: [PATCH 1/2] fix(ci): green the gates the ai_chat/ChatGateway commits reddened MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three ChatGateway commits (8b790ea, cac3118, 20663a6) landed on main without a full run-ci, reddening LINT, FMT, TYPECHECK and NO-CHEAT. This fixes each at its source. LINT (ruff 0.15.21, the pinned version) - ai_chat/__init__.py: RUF022 __all__ sorted. - search/document_processor.py: SIM102 nested if collapsed. FMT - ruff format over ai_chat/gateway.py + core/function_result.py. TYPECHECK (mypy --strict; the config puts tests in scope on purpose: "a new untyped test fails the gate") - tests/unit/ai_chat/test_gateway.py shipped fully unannotated: 44 no-untyped-def + 19 no-untyped-call. Annotated throughout. - FunctionResult.response widened to `str | dict[str, Any]`, so 37 call sites doing `.response.lower()` stopped type-checking. Added `assert isinstance(.response, str)` next to the existing `assert isinstance(, FunctionResult)` — a real assertion that narrows the union, not a cast. - FunctionResult.hold: `bool` subclasses `int`, so excluding bools from the back-compat int-swap left `str | bool`. Handle bool explicitly; the remaining type is `str | None`. hold(120) still means hold(timeout=120). - ChatGateway.visible_messages / last_activity accept None and non-dict items by design (`for msg in messages or []`, `if not isinstance(msg, dict): continue`) and are tested for it, but were typed `list[dict[str, Any]]`. Widened to match the real, documented contract. Free to change: ChatGateway is new surface no port has implemented yet. NO-CHEAT - Three origin tests asserted nothing ("does not raise"), so they passed regardless of the code. Each now pairs the allowed case with the refusal that proves it is an exemption and not open-by-default: localhost vs an unlisted origin, a listed origin vs a lookalike domain, absent vs present-but-unlisted. Verified: LINT clean, FMT clean, NO-CHEAT clean, mypy clean over every file CI reports, 5940 unit tests pass. The 6 remaining mcp_gateway failures are pre-existing (they fail identically on unmodified main) and env-dependent — CI passes them. Not addressed here (deliberately): GEN-FRESH and DRIFT/SEMVER-DIFF are coordinated-pin artifacts. PORTING_SDK_REF is set to wave6/ctor-dunder-fold, so CI builds against that branch; the matching regen is PR #78's half of the wave, not this branch's. --- signalwire/signalwire/ai_chat/__init__.py | 4 +- signalwire/signalwire/ai_chat/gateway.py | 25 +- signalwire/signalwire/core/function_result.py | 12 +- .../signalwire/search/document_processor.py | 11 +- tests/unit/ai_chat/test_gateway.py | 221 ++++++++++++------ tests/unit/core/test_function_result.py | 1 + tests/unit/prefabs/test_concierge.py | 2 + tests/unit/skills/test_datasphere_skill.py | 7 + tests/unit/skills/test_math_skill.py | 11 + .../skills/test_native_vector_search_skill.py | 11 + tests/unit/skills/test_web_search_skill.py | 5 + 11 files changed, 213 insertions(+), 97 deletions(-) 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/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/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: From d0676496c46b2810c1cd7343819d6836e680b6f1 Mon Sep 17 00:00:00 2001 From: Anthony Minessale II Date: Mon, 10 Aug 2026 13:01:57 -0500 Subject: [PATCH 2/2] fix(gen-fresh): regenerate generated types against the pinned wave6 specs CI resolves porting-sdk via PORTING_SDK_REF, currently wave6/ctor-dunder-fold, so GEN-FRESH regenerates from THAT branch's specs and compares. The committed files were generated from main's specs, so six reproduced differently and the gate failed. Regenerated with the pinned ref's specs; `--check` is now clean. Note these are NEWER than the same files on the wave6 branch itself: the swaig specs gained `| str` on several action fields after that branch last regenerated (e.g. `consolidate: bool` -> `bool | str`, `wait: bool` -> `bool | str`). So this is the output current wave6 specs actually produce, which is what CI checks against. Full unit suite still 5940 passed; the 6 mcp_gateway failures are pre-existing and env-dependent (they fail identically on unmodified main). --- .../signalwire/core/post_prompt_generated.py | 51 +- .../core/swaig_actions_generated.py | 155 +- .../core/swaig_request_generated.py | 8 +- .../signalwire/core/swml_verbs_generated.py | 2041 ++++------------- .../relay/protocol_types_generated.py | 8 +- tests/unit/rest/fabric_generated_test.py | 16 +- tests/unit/rest/mfa_generated_test.py | 4 +- 7 files changed, 612 insertions(+), 1671 deletions(-) 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/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