Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions signalwire/signalwire/ai_chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@

__all__ = [
"AIChatClient",
"ChatGateway",
"GatewayRejection",
"AIChatError",
"AuthenticationError",
"ChatGateway",
"ChatInProgressError",
"ChatLog",
"ChatResponse",
"ConversationInfo",
"ConversationNotFoundError",
"GatewayRejection",
"RateLimitError",
"SummaryError",
]
25 changes: 16 additions & 9 deletions signalwire/signalwire/ai_chat/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions signalwire/signalwire/core/function_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 39 additions & 12 deletions signalwire/signalwire/core/post_prompt_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
Loading
Loading