signalwire-sdk 3.3.0.dev105__tar.gz → 3.3.0.dev107__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/PKG-INFO +1 -1
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/pyproject.toml +1 -1
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/__init__.py +1 -1
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/agent_server.py +1 -1
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/__init__.py +3 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/client.py +44 -0
- signalwire_sdk-3.3.0.dev107/signalwire/signalwire/ai_chat/handoff.py +402 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent_base.py +169 -2
- signalwire_sdk-3.3.0.dev107/signalwire/signalwire/core/capabilities.py +127 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/web_mixin.py +190 -3
- signalwire_sdk-3.3.0.dev107/signalwire/signalwire/core/post_prompt.py +262 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_service.py +77 -10
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/PKG-INFO +1 -1
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/SOURCES.txt +3 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/LICENSE +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/man/sw-agent-init.1 +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/man/sw-search.1 +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/man/swaig-test.1 +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/setup.cfg +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/setup.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/agents/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/agents/bedrock.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/gateway.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/build_search.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/config.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/agent_loader.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/argparse_helpers.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/dynamic_config.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/service_loader.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/dokku.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/execution/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/execution/datamap_exec.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/execution/webhook_exec.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/init_project.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/output/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/output/output_formatter.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/output/swml_dump.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/data_generation.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/data_overrides.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/mock_env.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/swaig_test_wrapper.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/test_swaig.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/types.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/_agent_host.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/config/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/deployment/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/deployment/handlers/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/prompt/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/prompt/manager.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/routing/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/security/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/swml/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/decorator.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/registry.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/type_inference.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/auth_handler.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/config_loader.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/contexts.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/data_map.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/function_result.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/logging_config.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/_mixin_host.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/ai_config_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/auth_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/mcp_server_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/prompt_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/serverless_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/skill_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/state_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/tool_mixin.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/pom_builder.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/post_prompt_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/security_utils.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/session_manager.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/webhook_middleware.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/webhook_validator.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security_config.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/skill_base.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/skill_manager.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swaig_actions_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swaig_function.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swaig_request_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_builder.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_handler.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_renderer.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_verbs_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/web.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/livewire/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/livewire/plugins.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/gateway_service.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/mcp_manager.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/session_manager.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/pom/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/pom/pom.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/pom/pom_tool.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/concierge.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/faq_bot.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/info_gatherer.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/receptionist.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/survey.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/py.typed +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/call.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/client.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/constants.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/event.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/message.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/protocol_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/_base.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/_pagination.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/_request_options.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/call_handler.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/client.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/_client_tree_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/addresses.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/calling.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/calling_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/calling_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/chat.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/chat_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/chat_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/datasphere.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/datasphere_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/datasphere_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fabric.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fabric_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fabric_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fax_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fax_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/imported_numbers.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/logs.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/logs_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/logs_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/lookup.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/message_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/message_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/messages_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/messages_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/mfa.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/number_groups.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/phone_numbers.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/project.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/project_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/project_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/projects_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/projects_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/pubsub.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/pubsub_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/pubsub_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/queues.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/recordings.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/registry.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/relay_rest_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/relay_rest_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/short_codes.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/sip_profile.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/swml_webhooks_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/verified_callers.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/video.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/video_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/video_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/voice_resources_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/voice_types_generated.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/schema.json +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/document_processor.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/index_builder.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/migration.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/models.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/pgvector_backend.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/query_processor.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/search_engine.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/search_service.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/api_ninjas_trivia/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/api_ninjas_trivia/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/api_ninjas_trivia/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/claude_skills/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/claude_skills/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/claude_skills/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere_serverless/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere_serverless/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere_serverless/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datetime/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datetime/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datetime/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/google_maps/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/google_maps/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/info_gatherer/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/info_gatherer/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/info_gatherer/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/joke/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/joke/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/joke/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/math/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/math/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/math/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/mcp_gateway/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/mcp_gateway/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/mcp_gateway/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/native_vector_search/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/native_vector_search/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/native_vector_search/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/play_background_file/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/play_background_file/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/play_background_file/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/registry.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/spider/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/spider/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/spider/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/swml_transfer/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/swml_transfer/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/swml_transfer/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/weather_api/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/weather_api/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/weather_api/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/skill_improved.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/skill_original.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/wikipedia_search/README.md +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/wikipedia_search/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/wikipedia_search/skill.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/pom_utils.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/schema_utils.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/token_generators.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/url_validator.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/validators.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/web/__init__.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/web/web_service.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/dependency_links.txt +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/entry_points.txt +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/requires.txt +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/top_level.txt +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/tests/test_examples.py +0 -0
- {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/tests/test_mcp_integration.py +0 -0
{signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/__init__.py
RENAMED
|
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
|
|
|
52
52
|
from signalwire.utils.schema_utils import SchemaValidationError
|
|
53
53
|
from signalwire.web import WebService
|
|
54
54
|
|
|
55
|
-
__version__ = "3.3.0.
|
|
55
|
+
__version__ = "3.3.0.dev107"
|
|
56
56
|
|
|
57
57
|
# §6.2-python (owner decision: NO packaging split — one package, all deps required;
|
|
58
58
|
# IMPORT-TIME behavior only): the agent/web symbols are lazy-imported via module
|
|
@@ -11,6 +11,7 @@ Async client for the SignalWire AI Chat service — see
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from .gateway import ChatGateway, GatewayRejection
|
|
14
|
+
from .handoff import HandoffRouter, NonceEntry
|
|
14
15
|
from .client import (
|
|
15
16
|
AIChatClient,
|
|
16
17
|
AIChatError,
|
|
@@ -35,6 +36,8 @@ __all__ = [
|
|
|
35
36
|
"ConversationInfo",
|
|
36
37
|
"ConversationNotFoundError",
|
|
37
38
|
"GatewayRejection",
|
|
39
|
+
"HandoffRouter",
|
|
40
|
+
"NonceEntry",
|
|
38
41
|
"RateLimitError",
|
|
39
42
|
"SummaryError",
|
|
40
43
|
]
|
{signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/client.py
RENAMED
|
@@ -38,6 +38,7 @@ Credentials come from the constructor or the standard environment variables
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
import os
|
|
41
|
+
import re
|
|
41
42
|
from collections.abc import AsyncIterator
|
|
42
43
|
from contextlib import asynccontextmanager
|
|
43
44
|
from dataclasses import dataclass, field
|
|
@@ -45,8 +46,11 @@ from typing import Any
|
|
|
45
46
|
|
|
46
47
|
import aiohttp
|
|
47
48
|
|
|
49
|
+
from signalwire.core.logging_config import get_logger
|
|
48
50
|
from signalwire.rest._base import _user_agent
|
|
49
51
|
|
|
52
|
+
logger = get_logger("ai_chat.client")
|
|
53
|
+
|
|
50
54
|
DEFAULT_PATH = "/api/ai/chat"
|
|
51
55
|
|
|
52
56
|
# The service streams keepalive whitespace ahead of slow responses (every
|
|
@@ -169,6 +173,45 @@ class ChatLog:
|
|
|
169
173
|
# ── Client ───────────────────────────────────────────────────────────
|
|
170
174
|
|
|
171
175
|
|
|
176
|
+
# Characters the chat service keeps in a conversation id. Anything else is
|
|
177
|
+
# stripped on arrival, silently and without error.
|
|
178
|
+
_ID_SAFE = re.compile(r"[^a-zA-Z0-9_\-.:]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _warn_if_id_will_be_altered(conversation_id: str) -> None:
|
|
182
|
+
"""Warn when the service will not store the id it is being given.
|
|
183
|
+
|
|
184
|
+
The service sanitizes conversation ids and drops disallowed characters
|
|
185
|
+
without reporting it, so a caller that composes ids -- ``root~2`` for a
|
|
186
|
+
second leg of ``root``, say -- gets back ``root2``, which is a DIFFERENT,
|
|
187
|
+
valid-looking id. Everything filed under the original is then unreachable,
|
|
188
|
+
with no error at any layer to indicate the id changed.
|
|
189
|
+
|
|
190
|
+
Two of the three characters worth avoiding come from this SDK itself:
|
|
191
|
+
``_`` and ``-`` occur inside ``secrets.token_urlsafe`` output, which is
|
|
192
|
+
what ``ChatGateway.mint_handle`` uses to generate ids, so a suffix built
|
|
193
|
+
from either cannot be told apart from the id it was appended to. ``:`` is
|
|
194
|
+
the gateway's own handle delimiter. That leaves ``.`` as the safe
|
|
195
|
+
separator for composing ids.
|
|
196
|
+
"""
|
|
197
|
+
if not isinstance(conversation_id, str) or not conversation_id:
|
|
198
|
+
return
|
|
199
|
+
cleaned = _ID_SAFE.sub("", conversation_id)
|
|
200
|
+
if cleaned != conversation_id:
|
|
201
|
+
removed = "".join(sorted({c for c in conversation_id if _ID_SAFE.match(c)}))
|
|
202
|
+
logger.warning(
|
|
203
|
+
"conversation_id_will_be_sanitized",
|
|
204
|
+
requested=conversation_id,
|
|
205
|
+
stored_as=cleaned,
|
|
206
|
+
removed_characters=removed,
|
|
207
|
+
message=(
|
|
208
|
+
"[signalwire] the chat service will store this conversation "
|
|
209
|
+
"under a different id; anything filed under the requested id "
|
|
210
|
+
"will not be found. Use '.' to compose ids."
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
172
215
|
class AIChatClient:
|
|
173
216
|
"""Async client for the SignalWire AI Chat service."""
|
|
174
217
|
|
|
@@ -340,6 +383,7 @@ class AIChatClient:
|
|
|
340
383
|
reinit: bool = False,
|
|
341
384
|
) -> ConversationInfo:
|
|
342
385
|
"""Create a conversation (or reinitialize an existing one)."""
|
|
386
|
+
_warn_if_id_will_be_altered(conversation_id)
|
|
343
387
|
params: dict[str, Any] = {"id": conversation_id, "config_url": config_url}
|
|
344
388
|
if user_message:
|
|
345
389
|
params["user_message"] = user_message
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Copyright (c) 2026 SignalWire
|
|
4
|
+
|
|
5
|
+
This file is part of the SignalWire SDK.
|
|
6
|
+
|
|
7
|
+
Licensed under the MIT License.
|
|
8
|
+
See LICENSE file in the project root for full license information.
|
|
9
|
+
|
|
10
|
+
Moving one conversation between voice and text.
|
|
11
|
+
|
|
12
|
+
:class:`~signalwire.ai_chat.gateway.ChatGateway` lets a browser hold a text
|
|
13
|
+
conversation. This module is the other half of what a browser client needs: the
|
|
14
|
+
three routes it calls to move that conversation to a phone call and back, and
|
|
15
|
+
to type into a live call.
|
|
16
|
+
|
|
17
|
+
Why this is in the SDK rather than in each application: the browser side is
|
|
18
|
+
already shipped. The SignalWire address widget hardcodes ``{gateway-url}/handoff``,
|
|
19
|
+
``{gateway-url}/escalate`` and ``{gateway-url}/say`` against the same URL that
|
|
20
|
+
points at a ``ChatGateway``, and sends ``handoff_nonce`` and ``chat_handle`` as
|
|
21
|
+
user variables. Without these routes a gateway answers the widget's JSON-RPC
|
|
22
|
+
and 404s everything else -- so the SDK would be shipping a gateway its own
|
|
23
|
+
widget considers incomplete, with the missing half specified nowhere.
|
|
24
|
+
|
|
25
|
+
MECHANISM VS POLICY
|
|
26
|
+
-------------------
|
|
27
|
+
This class owns the wire contract only: the routes, the nonce, the ordering
|
|
28
|
+
guarantee, and the spend guards. It owns nothing about what a conversation
|
|
29
|
+
*is*. Where a leg's transcript gets written, what a resumed greeting says, how
|
|
30
|
+
much history to carry -- all of that is the application's, injected as
|
|
31
|
+
callbacks.
|
|
32
|
+
|
|
33
|
+
THE NONCE
|
|
34
|
+
---------
|
|
35
|
+
A browser cannot be trusted to name a call. ``ai_message`` takes a ``call_id``,
|
|
36
|
+
and a page-supplied one would let anyone who learned or guessed an id inject
|
|
37
|
+
speech into a stranger's live call. So the browser proves which call it is on
|
|
38
|
+
instead: the application puts a random ``handoff_nonce`` in the user variables
|
|
39
|
+
of one dial, registers it here against that call's ids, and the browser
|
|
40
|
+
presents it later. The nonce appears nowhere else, so knowing it is proof of
|
|
41
|
+
having placed the call.
|
|
42
|
+
|
|
43
|
+
Redemption for a handle is single use. Typing is not -- it is repeatable for
|
|
44
|
+
the life of the call, bounded by ``max_messages_per_call``.
|
|
45
|
+
|
|
46
|
+
An unknown nonce is answered exactly like an expired one, so this cannot be
|
|
47
|
+
used to probe whether a given call is live.
|
|
48
|
+
|
|
49
|
+
THE ORDERING GUARANTEE
|
|
50
|
+
----------------------
|
|
51
|
+
A medium never starts until the one it replaces has finished and its record is
|
|
52
|
+
durable. ``/handoff`` ends the call and waits for the application to confirm
|
|
53
|
+
capture before minting a handle; ``/escalate`` ends the chat leg and waits
|
|
54
|
+
before returning. Skipping the wait means the new medium's config fetch races a
|
|
55
|
+
record that is still seconds away, and it opens knowing nothing -- which is
|
|
56
|
+
what polling and retries elsewhere end up papering over.
|
|
57
|
+
|
|
58
|
+
The wait is event-driven. An earlier polling implementation deadlocked: a
|
|
59
|
+
synchronous sleep inside an async route blocked the event loop, and therefore
|
|
60
|
+
blocked the very webhook it was waiting for, which then arrived milliseconds
|
|
61
|
+
after the wait timed out -- every time.
|
|
62
|
+
|
|
63
|
+
DEPLOYMENT
|
|
64
|
+
----------
|
|
65
|
+
The nonce registry lives in this process, like ``ChatGateway``'s rate-limit
|
|
66
|
+
counters. A redemption must reach the replica that served the dial. Run one
|
|
67
|
+
replica, use sticky routing, or supply a shared ``registry``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# NOTE: deliberately no `from __future__ import annotations` -- FastAPI resolves
|
|
71
|
+
# route annotations against module globals, and `Request` is imported inside
|
|
72
|
+
# router() below. Stringified annotations would make it unresolvable, and
|
|
73
|
+
# FastAPI would silently treat `request` as a query parameter (422 on every
|
|
74
|
+
# call). gateway.py avoids the future import for the same reason.
|
|
75
|
+
import asyncio
|
|
76
|
+
import time
|
|
77
|
+
from collections.abc import Awaitable, Callable
|
|
78
|
+
from dataclasses import dataclass, field
|
|
79
|
+
from typing import TYPE_CHECKING, Any
|
|
80
|
+
|
|
81
|
+
from signalwire.core.logging_config import get_logger
|
|
82
|
+
|
|
83
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
84
|
+
from fastapi import APIRouter
|
|
85
|
+
|
|
86
|
+
from signalwire.ai_chat.gateway import ChatGateway
|
|
87
|
+
|
|
88
|
+
logger = get_logger("ai_chat.handoff")
|
|
89
|
+
|
|
90
|
+
__all__ = ["HandoffRouter", "NonceEntry"]
|
|
91
|
+
|
|
92
|
+
DEFAULT_NONCE_TTL = 3600
|
|
93
|
+
DEFAULT_MAX_MESSAGES_PER_CALL = 200
|
|
94
|
+
DEFAULT_CAPTURE_TIMEOUT = 8.0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class NonceEntry:
|
|
99
|
+
"""What a nonce is a capability for."""
|
|
100
|
+
|
|
101
|
+
conversation_id: str
|
|
102
|
+
call_id: str | None = None
|
|
103
|
+
issued_at: float = field(default_factory=time.monotonic)
|
|
104
|
+
messages: int = 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Application-supplied policy.
|
|
108
|
+
#
|
|
109
|
+
# capture_leg(conversation_id, medium) -> awaitable/bool
|
|
110
|
+
# End the leg and write its record. Return True once the record is durable.
|
|
111
|
+
# Called before the replacement medium is allowed to exist.
|
|
112
|
+
# end_call(call_id) -> awaitable/None
|
|
113
|
+
# Hang the call up server-side so its teardown hooks fire immediately.
|
|
114
|
+
# send_message(call_id, text) -> awaitable/bool
|
|
115
|
+
# Inject typed text into the live call as if the caller had spoken it.
|
|
116
|
+
CaptureLeg = Callable[[str, str], "Awaitable[bool] | bool"]
|
|
117
|
+
EndCall = Callable[[str], "Awaitable[None] | None"]
|
|
118
|
+
SendMessage = Callable[[str, str], "Awaitable[bool] | bool"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _maybe_await(value: Any) -> Any:
|
|
122
|
+
"""Allow every injected callback to be sync or async."""
|
|
123
|
+
if asyncio.iscoroutine(value) or isinstance(value, asyncio.Future):
|
|
124
|
+
return await value
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class HandoffRouter:
|
|
129
|
+
"""The three routes a browser client needs beside a :class:`ChatGateway`.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
gateway: The gateway that owns the conversations. Used to mint handles
|
|
133
|
+
and to check origins, so both halves of the URL enforce the same
|
|
134
|
+
origin policy.
|
|
135
|
+
capture_leg: Called as ``capture_leg(conversation_id, medium)`` to end
|
|
136
|
+
a leg and write its record. Must return truthy only once that
|
|
137
|
+
record is durable. May be sync or async. When omitted, no wait
|
|
138
|
+
happens and the ordering guarantee is not provided.
|
|
139
|
+
end_call: Called as ``end_call(call_id)`` to hang up server-side.
|
|
140
|
+
send_message: Called as ``send_message(call_id, text)`` for ``/say``.
|
|
141
|
+
Omit to leave typing disabled (the route then answers 404).
|
|
142
|
+
next_conversation_id: Called as ``next_conversation_id(conversation_id)``
|
|
143
|
+
to produce the id for the NEW leg. Defaults to appending ``.N``.
|
|
144
|
+
A fresh id is required because an ended conversation cannot be
|
|
145
|
+
reopened; the separator must be ``.`` -- see
|
|
146
|
+
``_warn_if_id_will_be_altered`` in ``ai_chat.client``.
|
|
147
|
+
nonce_ttl: Seconds a nonce stays redeemable.
|
|
148
|
+
max_messages_per_call: Ceiling on typed messages for one call. Each is
|
|
149
|
+
a billable turn, so this is a spend guard as much as an abuse one.
|
|
150
|
+
capture_timeout: Seconds to wait for ``capture_leg``. A ceiling, not a
|
|
151
|
+
budget -- capture is normally sub-second.
|
|
152
|
+
registry: Optional shared mapping for the nonce table. Supply one
|
|
153
|
+
backed by shared storage to run more than one replica.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
*,
|
|
159
|
+
gateway: "ChatGateway",
|
|
160
|
+
capture_leg: CaptureLeg | None = None,
|
|
161
|
+
end_call: EndCall | None = None,
|
|
162
|
+
send_message: SendMessage | None = None,
|
|
163
|
+
next_conversation_id: Callable[[str], str] | None = None,
|
|
164
|
+
nonce_ttl: int = DEFAULT_NONCE_TTL,
|
|
165
|
+
max_messages_per_call: int = DEFAULT_MAX_MESSAGES_PER_CALL,
|
|
166
|
+
capture_timeout: float = DEFAULT_CAPTURE_TIMEOUT,
|
|
167
|
+
registry: dict[str, NonceEntry] | None = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self.gateway = gateway
|
|
170
|
+
self.capture_leg = capture_leg
|
|
171
|
+
self.end_call = end_call
|
|
172
|
+
self.send_message = send_message
|
|
173
|
+
self.next_conversation_id = next_conversation_id or self._default_next_id
|
|
174
|
+
self.nonce_ttl = nonce_ttl
|
|
175
|
+
self.max_messages_per_call = max_messages_per_call
|
|
176
|
+
self.capture_timeout = capture_timeout
|
|
177
|
+
self._nonces: dict[str, NonceEntry] = registry if registry is not None else {}
|
|
178
|
+
|
|
179
|
+
# -- nonce lifecycle ---------------------------------------------------
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _default_next_id(conversation_id: str) -> str:
|
|
183
|
+
"""``root`` -> ``root.1``; ``root.2`` -> ``root.3``.
|
|
184
|
+
|
|
185
|
+
``.`` specifically: the chat service strips ``~`` silently, ``_`` and
|
|
186
|
+
``-`` already occur inside generated ids so a suffix built from either
|
|
187
|
+
cannot be distinguished from the id it was appended to, and ``:`` is
|
|
188
|
+
the gateway's handle delimiter.
|
|
189
|
+
"""
|
|
190
|
+
root, _, tail = conversation_id.rpartition(".")
|
|
191
|
+
if root and tail.isdigit():
|
|
192
|
+
return f"{root}.{int(tail) + 1}"
|
|
193
|
+
return f"{conversation_id}.1"
|
|
194
|
+
|
|
195
|
+
def register(
|
|
196
|
+
self, nonce: str, *, conversation_id: str, call_id: str | None = None
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Record what a nonce is a capability for.
|
|
199
|
+
|
|
200
|
+
Call this from the dynamic-config callback of the dial that carried the
|
|
201
|
+
nonce, reading ``call_id`` from the request the platform sent -- never
|
|
202
|
+
from anything the browser supplied.
|
|
203
|
+
"""
|
|
204
|
+
if not nonce or not isinstance(nonce, str):
|
|
205
|
+
return
|
|
206
|
+
self._prune()
|
|
207
|
+
self._nonces[nonce] = NonceEntry(
|
|
208
|
+
conversation_id=conversation_id, call_id=call_id
|
|
209
|
+
)
|
|
210
|
+
logger.info(
|
|
211
|
+
"handoff_nonce_registered",
|
|
212
|
+
conversation_id=conversation_id,
|
|
213
|
+
call_id=call_id,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _prune(self) -> None:
|
|
217
|
+
cutoff = time.monotonic() - self.nonce_ttl
|
|
218
|
+
for nonce in [n for n, e in self._nonces.items() if e.issued_at < cutoff]:
|
|
219
|
+
self._nonces.pop(nonce, None)
|
|
220
|
+
|
|
221
|
+
def _lookup(self, nonce: Any) -> NonceEntry | None:
|
|
222
|
+
if not nonce or not isinstance(nonce, str):
|
|
223
|
+
return None
|
|
224
|
+
self._prune()
|
|
225
|
+
return self._nonces.get(nonce)
|
|
226
|
+
|
|
227
|
+
# -- operations --------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
async def _capture(self, conversation_id: str, medium: str) -> bool:
|
|
230
|
+
"""Await the application's capture, bounded. Never raises."""
|
|
231
|
+
if self.capture_leg is None:
|
|
232
|
+
return False
|
|
233
|
+
try:
|
|
234
|
+
return bool(
|
|
235
|
+
await asyncio.wait_for(
|
|
236
|
+
_maybe_await(self.capture_leg(conversation_id, medium)),
|
|
237
|
+
timeout=self.capture_timeout,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
except TimeoutError:
|
|
241
|
+
logger.warning(
|
|
242
|
+
"handoff_capture_timeout",
|
|
243
|
+
conversation_id=conversation_id,
|
|
244
|
+
medium=medium,
|
|
245
|
+
note="starting the next medium without this leg's record",
|
|
246
|
+
)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
logger.error(
|
|
249
|
+
"handoff_capture_failed",
|
|
250
|
+
conversation_id=conversation_id,
|
|
251
|
+
error=str(exc),
|
|
252
|
+
)
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
async def redeem(self, nonce: str) -> str | None:
|
|
256
|
+
"""Exchange a nonce for a chat handle. Single use.
|
|
257
|
+
|
|
258
|
+
Ends the call, waits for its record, and only then mints a handle for a
|
|
259
|
+
new leg of the same conversation.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
The signed handle, or None for an unknown, expired or already
|
|
263
|
+
redeemed nonce -- deliberately indistinguishable from each other.
|
|
264
|
+
"""
|
|
265
|
+
entry = self._lookup(nonce)
|
|
266
|
+
if entry is None:
|
|
267
|
+
return None
|
|
268
|
+
# Consumed even if what follows fails: a nonce is one attempt.
|
|
269
|
+
self._nonces.pop(nonce, None)
|
|
270
|
+
|
|
271
|
+
if entry.call_id and self.end_call is not None:
|
|
272
|
+
try:
|
|
273
|
+
await _maybe_await(self.end_call(entry.call_id))
|
|
274
|
+
except Exception as exc:
|
|
275
|
+
logger.warning("handoff_end_call_failed", error=str(exc))
|
|
276
|
+
|
|
277
|
+
await self._capture(entry.conversation_id, "voice")
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
handle: str = self.gateway.mint_handle(
|
|
281
|
+
self.next_conversation_id(entry.conversation_id)
|
|
282
|
+
)
|
|
283
|
+
except Exception as exc:
|
|
284
|
+
logger.error("handoff_mint_failed", error=str(exc))
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
logger.info("handoff_redeemed", conversation_id=entry.conversation_id)
|
|
288
|
+
return handle
|
|
289
|
+
|
|
290
|
+
async def escalate(self, handle: str) -> bool:
|
|
291
|
+
"""End a chat leg and wait for its record, before a call is placed.
|
|
292
|
+
|
|
293
|
+
The browser calls this and waits, so a voice leg started immediately
|
|
294
|
+
afterwards is guaranteed to find the text leg already recorded.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
conversation_id = self.gateway.read_handle(handle)
|
|
298
|
+
except Exception:
|
|
299
|
+
return False
|
|
300
|
+
await self._capture(conversation_id, "chat")
|
|
301
|
+
logger.info("handoff_escalated", conversation_id=conversation_id)
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
async def say(self, nonce: str, text: str) -> bool:
|
|
305
|
+
"""Deliver typed text into the live call the nonce names.
|
|
306
|
+
|
|
307
|
+
Does NOT consume the nonce -- typing is repeatable for the life of the
|
|
308
|
+
call. Addressed by nonce rather than by any browser-supplied call id,
|
|
309
|
+
and no other request field is forwarded: ``global_data`` in particular
|
|
310
|
+
is trusted agent state that step logic branches on, and letting a page
|
|
311
|
+
write it would be a far larger hole than injecting text.
|
|
312
|
+
"""
|
|
313
|
+
if self.send_message is None:
|
|
314
|
+
return False
|
|
315
|
+
cleaned = (text or "").strip()
|
|
316
|
+
if not cleaned:
|
|
317
|
+
return False
|
|
318
|
+
entry = self._lookup(nonce)
|
|
319
|
+
if entry is None or not entry.call_id:
|
|
320
|
+
return False
|
|
321
|
+
if entry.messages >= self.max_messages_per_call:
|
|
322
|
+
logger.warning("handoff_say_cap_reached", call_id=entry.call_id)
|
|
323
|
+
return False
|
|
324
|
+
try:
|
|
325
|
+
await _maybe_await(self.send_message(entry.call_id, cleaned))
|
|
326
|
+
except Exception as exc:
|
|
327
|
+
logger.error("handoff_say_failed", error=str(exc))
|
|
328
|
+
return False
|
|
329
|
+
entry.messages += 1
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
# -- transport ---------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
def router(self) -> "APIRouter":
|
|
335
|
+
"""Build the router. Mount at the SAME prefix as the gateway's.
|
|
336
|
+
|
|
337
|
+
The browser derives all three paths from one configured URL, so they
|
|
338
|
+
must be siblings of the gateway's JSON-RPC endpoint::
|
|
339
|
+
|
|
340
|
+
agent.mount(gateway.router(), prefix="/chat")
|
|
341
|
+
agent.mount(handoff.router(), prefix="/chat")
|
|
342
|
+
"""
|
|
343
|
+
from fastapi import APIRouter, Request
|
|
344
|
+
from fastapi.responses import JSONResponse
|
|
345
|
+
|
|
346
|
+
router = APIRouter()
|
|
347
|
+
|
|
348
|
+
def _forbidden_origin(request: Request) -> JSONResponse | None:
|
|
349
|
+
try:
|
|
350
|
+
self.gateway.check_origin(request.headers.get("origin"))
|
|
351
|
+
except Exception:
|
|
352
|
+
return JSONResponse({"error": "origin not allowed"}, status_code=403)
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
async def _body(request: Request) -> dict[str, Any]:
|
|
356
|
+
try:
|
|
357
|
+
data = await request.json()
|
|
358
|
+
except Exception:
|
|
359
|
+
return {}
|
|
360
|
+
return data if isinstance(data, dict) else {}
|
|
361
|
+
|
|
362
|
+
@router.post("/handoff")
|
|
363
|
+
async def _handoff(request: Request) -> JSONResponse:
|
|
364
|
+
denied = _forbidden_origin(request)
|
|
365
|
+
if denied:
|
|
366
|
+
return denied
|
|
367
|
+
nonce = (await _body(request)).get("nonce")
|
|
368
|
+
if not isinstance(nonce, str):
|
|
369
|
+
return JSONResponse({"error": "not found"}, status_code=404)
|
|
370
|
+
handle = await self.redeem(nonce)
|
|
371
|
+
if not handle:
|
|
372
|
+
# Same answer for unknown, expired and already-redeemed.
|
|
373
|
+
return JSONResponse({"error": "not found"}, status_code=404)
|
|
374
|
+
return JSONResponse({"handle": handle})
|
|
375
|
+
|
|
376
|
+
@router.post("/escalate")
|
|
377
|
+
async def _escalate(request: Request) -> JSONResponse:
|
|
378
|
+
denied = _forbidden_origin(request)
|
|
379
|
+
if denied:
|
|
380
|
+
return denied
|
|
381
|
+
handle = (await _body(request)).get("handle")
|
|
382
|
+
if not handle or not isinstance(handle, str):
|
|
383
|
+
return JSONResponse({"error": "bad request"}, status_code=400)
|
|
384
|
+
if not await self.escalate(handle):
|
|
385
|
+
return JSONResponse({"error": "not found"}, status_code=404)
|
|
386
|
+
return JSONResponse({"ok": True})
|
|
387
|
+
|
|
388
|
+
@router.post("/say")
|
|
389
|
+
async def _say(request: Request) -> JSONResponse:
|
|
390
|
+
denied = _forbidden_origin(request)
|
|
391
|
+
if denied:
|
|
392
|
+
return denied
|
|
393
|
+
data = await _body(request)
|
|
394
|
+
nonce = data.get("nonce")
|
|
395
|
+
text = data.get("text", "")
|
|
396
|
+
if not isinstance(nonce, str) or not isinstance(text, str):
|
|
397
|
+
return JSONResponse({"error": "not found"}, status_code=404)
|
|
398
|
+
if not await self.say(nonce, text):
|
|
399
|
+
return JSONResponse({"error": "not found"}, status_code=404)
|
|
400
|
+
return JSONResponse({"ok": True})
|
|
401
|
+
|
|
402
|
+
return router
|