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.
Files changed (255) hide show
  1. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/PKG-INFO +1 -1
  2. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/pyproject.toml +1 -1
  3. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/__init__.py +1 -1
  4. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/agent_server.py +1 -1
  5. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/__init__.py +3 -0
  6. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/client.py +44 -0
  7. signalwire_sdk-3.3.0.dev107/signalwire/signalwire/ai_chat/handoff.py +402 -0
  8. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent_base.py +169 -2
  9. signalwire_sdk-3.3.0.dev107/signalwire/signalwire/core/capabilities.py +127 -0
  10. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/web_mixin.py +190 -3
  11. signalwire_sdk-3.3.0.dev107/signalwire/signalwire/core/post_prompt.py +262 -0
  12. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_service.py +77 -10
  13. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/PKG-INFO +1 -1
  14. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/SOURCES.txt +3 -0
  15. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/LICENSE +0 -0
  16. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/README.md +0 -0
  17. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/man/sw-agent-init.1 +0 -0
  18. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/man/sw-search.1 +0 -0
  19. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/man/swaig-test.1 +0 -0
  20. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/setup.cfg +0 -0
  21. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/setup.py +0 -0
  22. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/agents/__init__.py +0 -0
  23. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/agents/bedrock.py +0 -0
  24. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/ai_chat/gateway.py +0 -0
  25. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/__init__.py +0 -0
  26. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/build_search.py +0 -0
  27. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/config.py +0 -0
  28. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/__init__.py +0 -0
  29. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/agent_loader.py +0 -0
  30. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/argparse_helpers.py +0 -0
  31. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/dynamic_config.py +0 -0
  32. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/core/service_loader.py +0 -0
  33. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/dokku.py +0 -0
  34. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/execution/__init__.py +0 -0
  35. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/execution/datamap_exec.py +0 -0
  36. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/execution/webhook_exec.py +0 -0
  37. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/init_project.py +0 -0
  38. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/output/__init__.py +0 -0
  39. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/output/output_formatter.py +0 -0
  40. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/output/swml_dump.py +0 -0
  41. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/__init__.py +0 -0
  42. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/data_generation.py +0 -0
  43. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/data_overrides.py +0 -0
  44. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/simulation/mock_env.py +0 -0
  45. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/swaig_test_wrapper.py +0 -0
  46. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/test_swaig.py +0 -0
  47. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/cli/types.py +0 -0
  48. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/__init__.py +0 -0
  49. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/_agent_host.py +0 -0
  50. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/__init__.py +0 -0
  51. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/config/__init__.py +0 -0
  52. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/deployment/__init__.py +0 -0
  53. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/deployment/handlers/__init__.py +0 -0
  54. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/prompt/__init__.py +0 -0
  55. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/prompt/manager.py +0 -0
  56. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/routing/__init__.py +0 -0
  57. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/security/__init__.py +0 -0
  58. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/swml/__init__.py +0 -0
  59. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/__init__.py +0 -0
  60. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/decorator.py +0 -0
  61. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/registry.py +0 -0
  62. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/agent/tools/type_inference.py +0 -0
  63. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/auth_handler.py +0 -0
  64. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/config_loader.py +0 -0
  65. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/contexts.py +0 -0
  66. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/data_map.py +0 -0
  67. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/function_result.py +0 -0
  68. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/logging_config.py +0 -0
  69. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/__init__.py +0 -0
  70. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/_mixin_host.py +0 -0
  71. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/ai_config_mixin.py +0 -0
  72. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/auth_mixin.py +0 -0
  73. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/mcp_server_mixin.py +0 -0
  74. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/prompt_mixin.py +0 -0
  75. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/serverless_mixin.py +0 -0
  76. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/skill_mixin.py +0 -0
  77. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/state_mixin.py +0 -0
  78. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/mixins/tool_mixin.py +0 -0
  79. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/pom_builder.py +0 -0
  80. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/post_prompt_generated.py +0 -0
  81. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/__init__.py +0 -0
  82. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/security_utils.py +0 -0
  83. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/session_manager.py +0 -0
  84. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/webhook_middleware.py +0 -0
  85. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security/webhook_validator.py +0 -0
  86. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/security_config.py +0 -0
  87. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/skill_base.py +0 -0
  88. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/skill_manager.py +0 -0
  89. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swaig_actions_generated.py +0 -0
  90. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swaig_function.py +0 -0
  91. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swaig_request_generated.py +0 -0
  92. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_builder.py +0 -0
  93. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_handler.py +0 -0
  94. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_renderer.py +0 -0
  95. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/swml_verbs_generated.py +0 -0
  96. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/core/web.py +0 -0
  97. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/livewire/__init__.py +0 -0
  98. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/livewire/plugins.py +0 -0
  99. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/__init__.py +0 -0
  100. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/gateway_service.py +0 -0
  101. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/mcp_manager.py +0 -0
  102. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/mcp_gateway/session_manager.py +0 -0
  103. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/pom/__init__.py +0 -0
  104. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/pom/pom.py +0 -0
  105. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/pom/pom_tool.py +0 -0
  106. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/__init__.py +0 -0
  107. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/concierge.py +0 -0
  108. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/faq_bot.py +0 -0
  109. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/info_gatherer.py +0 -0
  110. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/receptionist.py +0 -0
  111. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/prefabs/survey.py +0 -0
  112. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/py.typed +0 -0
  113. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/__init__.py +0 -0
  114. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/call.py +0 -0
  115. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/client.py +0 -0
  116. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/constants.py +0 -0
  117. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/event.py +0 -0
  118. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/message.py +0 -0
  119. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/relay/protocol_types_generated.py +0 -0
  120. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/__init__.py +0 -0
  121. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/_base.py +0 -0
  122. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/_pagination.py +0 -0
  123. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/_request_options.py +0 -0
  124. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/call_handler.py +0 -0
  125. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/client.py +0 -0
  126. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/__init__.py +0 -0
  127. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/_client_tree_generated.py +0 -0
  128. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/addresses.py +0 -0
  129. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/calling.py +0 -0
  130. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/calling_resources_generated.py +0 -0
  131. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/calling_types_generated.py +0 -0
  132. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/chat.py +0 -0
  133. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/chat_resources_generated.py +0 -0
  134. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/chat_types_generated.py +0 -0
  135. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/datasphere.py +0 -0
  136. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/datasphere_resources_generated.py +0 -0
  137. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/datasphere_types_generated.py +0 -0
  138. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fabric.py +0 -0
  139. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fabric_resources_generated.py +0 -0
  140. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fabric_types_generated.py +0 -0
  141. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fax_resources_generated.py +0 -0
  142. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/fax_types_generated.py +0 -0
  143. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/imported_numbers.py +0 -0
  144. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/logs.py +0 -0
  145. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/logs_resources_generated.py +0 -0
  146. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/logs_types_generated.py +0 -0
  147. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/lookup.py +0 -0
  148. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/message_resources_generated.py +0 -0
  149. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/message_types_generated.py +0 -0
  150. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/messages_resources_generated.py +0 -0
  151. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/messages_types_generated.py +0 -0
  152. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/mfa.py +0 -0
  153. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/number_groups.py +0 -0
  154. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/phone_numbers.py +0 -0
  155. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/project.py +0 -0
  156. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/project_resources_generated.py +0 -0
  157. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/project_types_generated.py +0 -0
  158. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/projects_resources_generated.py +0 -0
  159. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/projects_types_generated.py +0 -0
  160. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/pubsub.py +0 -0
  161. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/pubsub_resources_generated.py +0 -0
  162. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/pubsub_types_generated.py +0 -0
  163. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/queues.py +0 -0
  164. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/recordings.py +0 -0
  165. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/registry.py +0 -0
  166. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/relay_rest_resources_generated.py +0 -0
  167. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/relay_rest_types_generated.py +0 -0
  168. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/short_codes.py +0 -0
  169. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/sip_profile.py +0 -0
  170. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/swml_webhooks_types_generated.py +0 -0
  171. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/verified_callers.py +0 -0
  172. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/video.py +0 -0
  173. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/video_resources_generated.py +0 -0
  174. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/video_types_generated.py +0 -0
  175. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/voice_resources_generated.py +0 -0
  176. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/rest/namespaces/voice_types_generated.py +0 -0
  177. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/schema.json +0 -0
  178. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/__init__.py +0 -0
  179. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/document_processor.py +0 -0
  180. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/index_builder.py +0 -0
  181. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/migration.py +0 -0
  182. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/models.py +0 -0
  183. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/pgvector_backend.py +0 -0
  184. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/query_processor.py +0 -0
  185. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/search_engine.py +0 -0
  186. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/search/search_service.py +0 -0
  187. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/README.md +0 -0
  188. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/__init__.py +0 -0
  189. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/api_ninjas_trivia/README.md +0 -0
  190. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/api_ninjas_trivia/__init__.py +0 -0
  191. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/api_ninjas_trivia/skill.py +0 -0
  192. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/claude_skills/README.md +0 -0
  193. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/claude_skills/__init__.py +0 -0
  194. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/claude_skills/skill.py +0 -0
  195. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere/README.md +0 -0
  196. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere/__init__.py +0 -0
  197. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere/skill.py +0 -0
  198. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere_serverless/README.md +0 -0
  199. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere_serverless/__init__.py +0 -0
  200. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datasphere_serverless/skill.py +0 -0
  201. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datetime/README.md +0 -0
  202. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datetime/__init__.py +0 -0
  203. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/datetime/skill.py +0 -0
  204. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/google_maps/__init__.py +0 -0
  205. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/google_maps/skill.py +0 -0
  206. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/info_gatherer/README.md +0 -0
  207. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/info_gatherer/__init__.py +0 -0
  208. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/info_gatherer/skill.py +0 -0
  209. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/joke/README.md +0 -0
  210. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/joke/__init__.py +0 -0
  211. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/joke/skill.py +0 -0
  212. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/math/README.md +0 -0
  213. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/math/__init__.py +0 -0
  214. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/math/skill.py +0 -0
  215. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/mcp_gateway/README.md +0 -0
  216. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/mcp_gateway/__init__.py +0 -0
  217. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/mcp_gateway/skill.py +0 -0
  218. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/native_vector_search/README.md +0 -0
  219. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/native_vector_search/__init__.py +0 -0
  220. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/native_vector_search/skill.py +0 -0
  221. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/play_background_file/README.md +0 -0
  222. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/play_background_file/__init__.py +0 -0
  223. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/play_background_file/skill.py +0 -0
  224. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/registry.py +0 -0
  225. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/spider/README.md +0 -0
  226. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/spider/__init__.py +0 -0
  227. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/spider/skill.py +0 -0
  228. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/swml_transfer/README.md +0 -0
  229. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/swml_transfer/__init__.py +0 -0
  230. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/swml_transfer/skill.py +0 -0
  231. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/weather_api/README.md +0 -0
  232. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/weather_api/__init__.py +0 -0
  233. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/weather_api/skill.py +0 -0
  234. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/README.md +0 -0
  235. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/__init__.py +0 -0
  236. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/skill.py +0 -0
  237. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/skill_improved.py +0 -0
  238. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/web_search/skill_original.py +0 -0
  239. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/wikipedia_search/README.md +0 -0
  240. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/wikipedia_search/__init__.py +0 -0
  241. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/skills/wikipedia_search/skill.py +0 -0
  242. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/__init__.py +0 -0
  243. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/pom_utils.py +0 -0
  244. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/schema_utils.py +0 -0
  245. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/token_generators.py +0 -0
  246. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/url_validator.py +0 -0
  247. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/utils/validators.py +0 -0
  248. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/web/__init__.py +0 -0
  249. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire/web/web_service.py +0 -0
  250. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/dependency_links.txt +0 -0
  251. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/entry_points.txt +0 -0
  252. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/requires.txt +0 -0
  253. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/signalwire/signalwire_sdk.egg-info/top_level.txt +0 -0
  254. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/tests/test_examples.py +0 -0
  255. {signalwire_sdk-3.3.0.dev105 → signalwire_sdk-3.3.0.dev107}/tests/test_mcp_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire-sdk
3
- Version: 3.3.0.dev105
3
+ Version: 3.3.0.dev107
4
4
  Summary: SignalWire SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "signalwire-sdk"
7
- version = "3.3.0.dev105"
7
+ version = "3.3.0.dev107"
8
8
  description = "SignalWire SDK"
9
9
  authors = [
10
10
  {name = "SignalWire Team", email = "info@signalwire.com"}
@@ -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.dev105"
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
@@ -68,7 +68,7 @@ class AgentServer:
68
68
  self.app = FastAPI(
69
69
  title="SignalWire AI Agents",
70
70
  description="Hosted SignalWire AI Agents",
71
- version="3.3.0.dev105",
71
+ version="3.3.0.dev107",
72
72
  redirect_slashes=False,
73
73
  docs_url=None,
74
74
  redoc_url=None,
@@ -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
  ]
@@ -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