rasa-pro 3.10.16__py3-none-any.whl → 3.11.0__py3-none-any.whl

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.

Potentially problematic release.


This version of rasa-pro might be problematic. Click here for more details.

Files changed (240) hide show
  1. rasa/__main__.py +31 -15
  2. rasa/api.py +12 -2
  3. rasa/cli/arguments/default_arguments.py +24 -4
  4. rasa/cli/arguments/run.py +15 -0
  5. rasa/cli/arguments/shell.py +5 -1
  6. rasa/cli/arguments/train.py +17 -9
  7. rasa/cli/evaluate.py +7 -7
  8. rasa/cli/inspect.py +19 -7
  9. rasa/cli/interactive.py +1 -0
  10. rasa/cli/llm_fine_tuning.py +11 -14
  11. rasa/cli/project_templates/calm/config.yml +5 -7
  12. rasa/cli/project_templates/calm/endpoints.yml +15 -2
  13. rasa/cli/project_templates/tutorial/config.yml +8 -5
  14. rasa/cli/project_templates/tutorial/data/flows.yml +1 -1
  15. rasa/cli/project_templates/tutorial/data/patterns.yml +5 -0
  16. rasa/cli/project_templates/tutorial/domain.yml +14 -0
  17. rasa/cli/project_templates/tutorial/endpoints.yml +5 -0
  18. rasa/cli/run.py +7 -0
  19. rasa/cli/scaffold.py +4 -2
  20. rasa/cli/studio/upload.py +0 -15
  21. rasa/cli/train.py +14 -53
  22. rasa/cli/utils.py +14 -11
  23. rasa/cli/x.py +7 -7
  24. rasa/constants.py +3 -1
  25. rasa/core/actions/action.py +77 -33
  26. rasa/core/actions/action_hangup.py +29 -0
  27. rasa/core/actions/action_repeat_bot_messages.py +89 -0
  28. rasa/core/actions/e2e_stub_custom_action_executor.py +5 -1
  29. rasa/core/actions/http_custom_action_executor.py +4 -0
  30. rasa/core/agent.py +2 -2
  31. rasa/core/brokers/kafka.py +3 -1
  32. rasa/core/brokers/pika.py +3 -1
  33. rasa/core/channels/__init__.py +10 -6
  34. rasa/core/channels/channel.py +41 -4
  35. rasa/core/channels/development_inspector.py +150 -46
  36. rasa/core/channels/inspector/README.md +1 -1
  37. rasa/core/channels/inspector/dist/assets/{arc-b6e548fe.js → arc-bc141fb2.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{c4Diagram-d0fbc5ce-fa03ac9e.js → c4Diagram-d0fbc5ce-be2db283.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{classDiagram-936ed81e-ee67392a.js → classDiagram-936ed81e-55366915.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-c3cb15f1-9b283fae.js → classDiagram-v2-c3cb15f1-bb529518.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{createText-62fc7601-8b6fcc2a.js → createText-62fc7601-b0ec81d6.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{edges-f2ad444c-22e77f4f.js → edges-f2ad444c-6166330c.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{erDiagram-9d236eb7-60ffc87f.js → erDiagram-9d236eb7-5ccc6a8e.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{flowDb-1972c806-9dd802e4.js → flowDb-1972c806-fca3bfe4.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/{flowDiagram-7ea5b25a-5fa1912f.js → flowDiagram-7ea5b25a-4739080f.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-736177bf.js +1 -0
  47. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-abe16c3d-622a1fd2.js → flowchart-elk-definition-abe16c3d-7c1b0e0f.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{ganttDiagram-9b5ea136-e285a63a.js → ganttDiagram-9b5ea136-772fd050.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-99d0ae7c-f237bdca.js → gitGraphDiagram-99d0ae7c-8eae1dc9.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{index-2c4b9a3b-4b03d70e.js → index-2c4b9a3b-f55afcdf.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/index-e7cef9de.js +1317 -0
  52. rasa/core/channels/inspector/dist/assets/{infoDiagram-736b4530-72a0fa5f.js → infoDiagram-736b4530-124d4a14.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{journeyDiagram-df861f2b-82218c41.js → journeyDiagram-df861f2b-7c4fae44.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{layout-78cff630.js → layout-b9885fb6.js} +1 -1
  55. rasa/core/channels/inspector/dist/assets/{line-5038b469.js → line-7c59abb6.js} +1 -1
  56. rasa/core/channels/inspector/dist/assets/{linear-c4fc4098.js → linear-4776f780.js} +1 -1
  57. rasa/core/channels/inspector/dist/assets/{mindmap-definition-beec6740-c33c8ea6.js → mindmap-definition-beec6740-2332c46c.js} +1 -1
  58. rasa/core/channels/inspector/dist/assets/{pieDiagram-dbbf0591-a8d03059.js → pieDiagram-dbbf0591-8fb39303.js} +1 -1
  59. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-4d7f4fd6-6a0e56b2.js → quadrantDiagram-4d7f4fd6-3c7180a2.js} +1 -1
  60. rasa/core/channels/inspector/dist/assets/{requirementDiagram-6fc4c22a-2dc7c7bd.js → requirementDiagram-6fc4c22a-e910bcb8.js} +1 -1
  61. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-8f13d901-2360fe39.js → sankeyDiagram-8f13d901-ead16c89.js} +1 -1
  62. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-b655622a-41b9f9ad.js → sequenceDiagram-b655622a-29a02a19.js} +1 -1
  63. rasa/core/channels/inspector/dist/assets/{stateDiagram-59f0c015-0aad326f.js → stateDiagram-59f0c015-042b3137.js} +1 -1
  64. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-2b26beab-9847d984.js → stateDiagram-v2-2b26beab-2178c0f3.js} +1 -1
  65. rasa/core/channels/inspector/dist/assets/{styles-080da4f6-564d890e.js → styles-080da4f6-23ffa4fc.js} +1 -1
  66. rasa/core/channels/inspector/dist/assets/{styles-3dcbcfbf-38957613.js → styles-3dcbcfbf-94f59763.js} +1 -1
  67. rasa/core/channels/inspector/dist/assets/{styles-9c745c82-f0fc6921.js → styles-9c745c82-78a6bebc.js} +1 -1
  68. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-4835440b-ef3c5a77.js → svgDrawCommon-4835440b-eae2a6f6.js} +1 -1
  69. rasa/core/channels/inspector/dist/assets/{timeline-definition-5b62e21b-bf3e91c1.js → timeline-definition-5b62e21b-5c968d92.js} +1 -1
  70. rasa/core/channels/inspector/dist/assets/{xychartDiagram-2b33534f-4d4026c0.js → xychartDiagram-2b33534f-fd3db0d5.js} +1 -1
  71. rasa/core/channels/inspector/dist/index.html +18 -17
  72. rasa/core/channels/inspector/index.html +17 -16
  73. rasa/core/channels/inspector/package.json +5 -1
  74. rasa/core/channels/inspector/src/App.tsx +118 -68
  75. rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
  76. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +11 -10
  77. rasa/core/channels/inspector/src/components/DialogueStack.tsx +10 -25
  78. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -3
  79. rasa/core/channels/inspector/src/helpers/audiostream.ts +165 -0
  80. rasa/core/channels/inspector/src/helpers/formatters.test.ts +10 -0
  81. rasa/core/channels/inspector/src/helpers/formatters.ts +107 -41
  82. rasa/core/channels/inspector/src/helpers/utils.ts +92 -7
  83. rasa/core/channels/inspector/src/types.ts +21 -1
  84. rasa/core/channels/inspector/yarn.lock +94 -1
  85. rasa/core/channels/rest.py +51 -46
  86. rasa/core/channels/socketio.py +28 -1
  87. rasa/core/channels/telegram.py +1 -1
  88. rasa/core/channels/twilio.py +1 -1
  89. rasa/core/channels/{audiocodes.py → voice_ready/audiocodes.py} +122 -69
  90. rasa/core/channels/{voice_aware → voice_ready}/jambonz.py +26 -8
  91. rasa/core/channels/{voice_aware → voice_ready}/jambonz_protocol.py +57 -5
  92. rasa/core/channels/{twilio_voice.py → voice_ready/twilio_voice.py} +64 -28
  93. rasa/core/channels/voice_ready/utils.py +37 -0
  94. rasa/core/channels/voice_stream/asr/__init__.py +0 -0
  95. rasa/core/channels/voice_stream/asr/asr_engine.py +89 -0
  96. rasa/core/channels/voice_stream/asr/asr_event.py +18 -0
  97. rasa/core/channels/voice_stream/asr/azure.py +129 -0
  98. rasa/core/channels/voice_stream/asr/deepgram.py +90 -0
  99. rasa/core/channels/voice_stream/audio_bytes.py +8 -0
  100. rasa/core/channels/voice_stream/browser_audio.py +107 -0
  101. rasa/core/channels/voice_stream/call_state.py +23 -0
  102. rasa/core/channels/voice_stream/tts/__init__.py +0 -0
  103. rasa/core/channels/voice_stream/tts/azure.py +106 -0
  104. rasa/core/channels/voice_stream/tts/cartesia.py +118 -0
  105. rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
  106. rasa/core/channels/voice_stream/tts/tts_engine.py +58 -0
  107. rasa/core/channels/voice_stream/twilio_media_streams.py +173 -0
  108. rasa/core/channels/voice_stream/util.py +57 -0
  109. rasa/core/channels/voice_stream/voice_channel.py +427 -0
  110. rasa/core/information_retrieval/qdrant.py +1 -0
  111. rasa/core/nlg/contextual_response_rephraser.py +45 -17
  112. rasa/{nlu → core}/persistor.py +203 -68
  113. rasa/core/policies/enterprise_search_policy.py +119 -63
  114. rasa/core/policies/flows/flow_executor.py +15 -22
  115. rasa/core/policies/intentless_policy.py +83 -28
  116. rasa/core/processor.py +25 -0
  117. rasa/core/run.py +12 -2
  118. rasa/core/secrets_manager/constants.py +4 -0
  119. rasa/core/secrets_manager/factory.py +8 -0
  120. rasa/core/secrets_manager/vault.py +11 -1
  121. rasa/core/training/interactive.py +33 -34
  122. rasa/core/utils.py +47 -21
  123. rasa/dialogue_understanding/coexistence/llm_based_router.py +41 -14
  124. rasa/dialogue_understanding/commands/__init__.py +6 -0
  125. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +60 -0
  126. rasa/dialogue_understanding/commands/session_end_command.py +61 -0
  127. rasa/dialogue_understanding/commands/user_silence_command.py +59 -0
  128. rasa/dialogue_understanding/commands/utils.py +5 -0
  129. rasa/dialogue_understanding/generator/constants.py +2 -0
  130. rasa/dialogue_understanding/generator/flow_retrieval.py +47 -9
  131. rasa/dialogue_understanding/generator/llm_based_command_generator.py +38 -15
  132. rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
  133. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +35 -13
  134. rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +3 -0
  135. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +60 -13
  136. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +53 -0
  137. rasa/dialogue_understanding/patterns/repeat.py +37 -0
  138. rasa/dialogue_understanding/patterns/user_silence.py +37 -0
  139. rasa/dialogue_understanding/processor/command_processor.py +21 -1
  140. rasa/e2e_test/aggregate_test_stats_calculator.py +1 -11
  141. rasa/e2e_test/assertions.py +136 -61
  142. rasa/e2e_test/assertions_schema.yml +23 -0
  143. rasa/e2e_test/e2e_test_case.py +85 -6
  144. rasa/e2e_test/e2e_test_runner.py +2 -3
  145. rasa/e2e_test/utils/e2e_yaml_utils.py +1 -1
  146. rasa/engine/graph.py +3 -10
  147. rasa/engine/loader.py +12 -0
  148. rasa/engine/recipes/config_files/default_config.yml +0 -3
  149. rasa/engine/recipes/default_recipe.py +0 -1
  150. rasa/engine/recipes/graph_recipe.py +0 -1
  151. rasa/engine/runner/dask.py +2 -2
  152. rasa/engine/storage/local_model_storage.py +12 -42
  153. rasa/engine/storage/storage.py +1 -5
  154. rasa/engine/validation.py +527 -74
  155. rasa/model_manager/__init__.py +0 -0
  156. rasa/model_manager/config.py +40 -0
  157. rasa/model_manager/model_api.py +559 -0
  158. rasa/model_manager/runner_service.py +286 -0
  159. rasa/model_manager/socket_bridge.py +146 -0
  160. rasa/model_manager/studio_jwt_auth.py +86 -0
  161. rasa/model_manager/trainer_service.py +325 -0
  162. rasa/model_manager/utils.py +87 -0
  163. rasa/model_manager/warm_rasa_process.py +187 -0
  164. rasa/model_service.py +112 -0
  165. rasa/model_training.py +42 -23
  166. rasa/nlu/tokenizers/whitespace_tokenizer.py +3 -14
  167. rasa/server.py +4 -2
  168. rasa/shared/constants.py +60 -8
  169. rasa/shared/core/constants.py +13 -0
  170. rasa/shared/core/domain.py +107 -50
  171. rasa/shared/core/events.py +29 -0
  172. rasa/shared/core/flows/flow.py +5 -0
  173. rasa/shared/core/flows/flows_list.py +19 -6
  174. rasa/shared/core/flows/flows_yaml_schema.json +10 -0
  175. rasa/shared/core/flows/utils.py +39 -0
  176. rasa/shared/core/flows/validation.py +121 -0
  177. rasa/shared/core/flows/yaml_flows_io.py +15 -27
  178. rasa/shared/core/slots.py +5 -0
  179. rasa/shared/importers/importer.py +59 -41
  180. rasa/shared/importers/multi_project.py +23 -11
  181. rasa/shared/importers/rasa.py +12 -3
  182. rasa/shared/importers/remote_importer.py +196 -0
  183. rasa/shared/importers/utils.py +3 -1
  184. rasa/shared/nlu/training_data/formats/rasa_yaml.py +18 -3
  185. rasa/shared/nlu/training_data/training_data.py +18 -19
  186. rasa/shared/providers/_configs/litellm_router_client_config.py +220 -0
  187. rasa/shared/providers/_configs/model_group_config.py +167 -0
  188. rasa/shared/providers/_configs/openai_client_config.py +1 -1
  189. rasa/shared/providers/_configs/rasa_llm_client_config.py +73 -0
  190. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +1 -0
  191. rasa/shared/providers/_configs/utils.py +16 -0
  192. rasa/shared/providers/_utils.py +79 -0
  193. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +13 -29
  194. rasa/shared/providers/embedding/azure_openai_embedding_client.py +54 -21
  195. rasa/shared/providers/embedding/default_litellm_embedding_client.py +24 -0
  196. rasa/shared/providers/embedding/litellm_router_embedding_client.py +135 -0
  197. rasa/shared/providers/llm/_base_litellm_client.py +34 -22
  198. rasa/shared/providers/llm/azure_openai_llm_client.py +50 -29
  199. rasa/shared/providers/llm/default_litellm_llm_client.py +24 -0
  200. rasa/shared/providers/llm/litellm_router_llm_client.py +182 -0
  201. rasa/shared/providers/llm/rasa_llm_client.py +112 -0
  202. rasa/shared/providers/llm/self_hosted_llm_client.py +5 -29
  203. rasa/shared/providers/mappings.py +19 -0
  204. rasa/shared/providers/router/__init__.py +0 -0
  205. rasa/shared/providers/router/_base_litellm_router_client.py +183 -0
  206. rasa/shared/providers/router/router_client.py +73 -0
  207. rasa/shared/utils/common.py +40 -24
  208. rasa/shared/utils/health_check/__init__.py +0 -0
  209. rasa/shared/utils/health_check/embeddings_health_check_mixin.py +31 -0
  210. rasa/shared/utils/health_check/health_check.py +258 -0
  211. rasa/shared/utils/health_check/llm_health_check_mixin.py +31 -0
  212. rasa/shared/utils/io.py +27 -6
  213. rasa/shared/utils/llm.py +354 -44
  214. rasa/shared/utils/schemas/events.py +2 -0
  215. rasa/shared/utils/schemas/model_config.yml +0 -10
  216. rasa/shared/utils/yaml.py +181 -38
  217. rasa/studio/data_handler.py +3 -1
  218. rasa/studio/upload.py +160 -74
  219. rasa/telemetry.py +94 -17
  220. rasa/tracing/config.py +3 -1
  221. rasa/tracing/instrumentation/attribute_extractors.py +95 -18
  222. rasa/tracing/instrumentation/instrumentation.py +121 -0
  223. rasa/utils/common.py +5 -0
  224. rasa/utils/endpoints.py +27 -1
  225. rasa/utils/io.py +8 -16
  226. rasa/utils/log_utils.py +9 -2
  227. rasa/utils/sanic_error_handler.py +32 -0
  228. rasa/validator.py +110 -16
  229. rasa/version.py +1 -1
  230. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/METADATA +16 -14
  231. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/RECORD +236 -185
  232. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-1844e5a5.js +0 -1
  233. rasa/core/channels/inspector/dist/assets/index-a5d3e69d.js +0 -1040
  234. rasa/core/channels/voice_aware/utils.py +0 -20
  235. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +0 -407
  236. /rasa/core/channels/{voice_aware → voice_ready}/__init__.py +0 -0
  237. /rasa/core/channels/{voice_native → voice_stream}/__init__.py +0 -0
  238. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/NOTICE +0 -0
  239. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/WHEEL +0 -0
  240. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/entry_points.txt +0 -0
@@ -1,32 +1,59 @@
1
1
  import copy
2
- import datetime
2
+ from datetime import datetime, timezone, timedelta
3
3
  import json
4
- import logging
5
4
  import uuid
6
5
  from typing import Any, Awaitable, Callable, Dict, List, Optional, Text, Union
6
+ from dataclasses import asdict
7
7
 
8
8
  import structlog
9
9
  from jsonschema import ValidationError, validate
10
10
  from rasa.core import jobs
11
11
  from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
12
- from rasa.core.channels.voice_aware.utils import validate_voice_license_scope
12
+ from rasa.core.channels.voice_ready.utils import (
13
+ validate_voice_license_scope,
14
+ CallParameters,
15
+ )
13
16
  from rasa.shared.constants import INTENT_MESSAGE_PREFIX
17
+ from rasa.shared.core.constants import USER_INTENT_SESSION_START
14
18
  from rasa.shared.exceptions import RasaException
15
19
  from sanic import Blueprint, response
16
20
  from sanic.exceptions import NotFound, SanicException, ServerError
17
21
  from sanic.request import Request
18
22
  from sanic.response import HTTPResponse
19
23
 
24
+ from rasa.utils.io import remove_emojis
20
25
 
21
- logger = logging.getLogger(__name__)
22
26
  structlogger = structlog.get_logger()
23
27
 
24
28
  CHANNEL_NAME = "audiocodes"
25
29
  KEEP_ALIVE_SECONDS = 120
26
30
  KEEP_ALIVE_EXPIRATION_FACTOR = 1.5
27
-
28
-
29
- class Unauthorized(SanicException):
31
+ EVENT_START = "start"
32
+ EVENT_DTMF = "DTMF"
33
+ ACTIVITY_MESSAGE = "message"
34
+ ACTIVITY_EVENT = "event"
35
+ INFO_UNKNOWN = "unknown"
36
+ ACTIVITY_ID_KEY = "id"
37
+ CREDENTIALS_TOKEN_KEY = "token"
38
+ CREDENTIALS_USE_WEBSOCKET_KEY = "use_websocket"
39
+ CREDENTIALS_KEEP_ALIVE_KEY = "keep_alive"
40
+ CREDENTIALS_KEEP_ALIVE_EXPIRATION_FACTOR_KEY = "keep_alive_expiration_factor"
41
+ CLEANUP_INTERVAL_MINUTES = 10
42
+
43
+
44
+ def map_call_params(parameters: Dict[Text, Any]) -> CallParameters:
45
+ """Map the Audiocodes parameters to the CallParameters dataclass."""
46
+ return CallParameters(
47
+ call_id=parameters.get("vaigConversationId"),
48
+ user_phone=parameters.get("callee"),
49
+ bot_phone=parameters.get("caller"),
50
+ user_name=parameters.get("callerDisplayName"),
51
+ user_host=parameters.get("callerHost"),
52
+ bot_host=parameters.get("calleeHost"),
53
+ )
54
+
55
+
56
+ class HttpUnauthorized(SanicException):
30
57
  """**Status**: 401 Not Authorized."""
31
58
 
32
59
  status_code = 401
@@ -41,30 +68,43 @@ class Conversation:
41
68
  self.update()
42
69
 
43
70
  def update(self) -> None:
44
- self.last_activity: datetime.datetime = datetime.datetime.utcnow()
71
+ """Update the last activity time."""
72
+ self.last_activity: datetime = datetime.now(timezone.utc)
45
73
 
46
74
  @staticmethod
47
75
  def get_metadata(activity: Dict[Text, Any]) -> Optional[Dict[Text, Any]]:
48
- return activity.get("parameters")
76
+ """Get metadata from the activity."""
77
+ return asdict(map_call_params(activity["parameters"]))
49
78
 
50
79
  @staticmethod
51
80
  def _handle_event(event: Dict[Text, Any]) -> Text:
52
- text = f'{INTENT_MESSAGE_PREFIX}vaig_event_{event["name"]}'
53
- event_params = {}
54
- if "parameters" in event:
55
- event_params.update(event["parameters"])
56
- if "value" in event:
57
- event_params.update({"value": event["value"]})
58
- if len(event_params) > 0:
81
+ """Handle start and DTMF event and return the corresponding text."""
82
+ structlogger.debug("audiocodes.handle.event", event_payload=event)
83
+ if "name" not in event:
84
+ structlogger.warning(
85
+ "audiocodes.handle.event.no_name_key", event_payload=event
86
+ )
87
+ return ""
88
+
89
+ if event["name"] == EVENT_START:
90
+ text = f"{INTENT_MESSAGE_PREFIX}{USER_INTENT_SESSION_START}"
91
+ elif event["name"] == EVENT_DTMF:
92
+ text = f"{INTENT_MESSAGE_PREFIX}vaig_event_DTMF"
93
+ event_params = {"value": event["value"]}
59
94
  text += json.dumps(event_params)
95
+ else:
96
+ structlogger.warning(
97
+ "audiocodes.handle.event.unknown_event", event_payload=event
98
+ )
99
+ return ""
100
+
60
101
  return text
61
102
 
62
- def is_active_conversation(
63
- self, now: datetime.datetime, delta: datetime.timedelta
64
- ) -> bool:
103
+ def is_active_conversation(self, now: datetime, delta: timedelta) -> bool:
104
+ """Check if the conversation is active."""
65
105
  if now - self.last_activity > delta:
66
- logger.warning(
67
- f"Conversation {self.conversation_id} is invalid due to inactivity"
106
+ structlogger.warning(
107
+ "audiocodes.conversation.inactive", conversation=self.conversation_id
68
108
  )
69
109
  return False
70
110
  return True
@@ -75,24 +115,25 @@ class Conversation:
75
115
  output_channel: OutputChannel,
76
116
  on_new_message: Callable[[UserMessage], Awaitable[Any]],
77
117
  ) -> None:
78
- logger.debug("(handle_activities) --- Activities:")
118
+ """Handle activities sent by Audiocodes."""
119
+ structlogger.debug("audiocodes.handle.activities")
79
120
  for activity in message["activities"]:
80
121
  text = None
81
- if activity["id"] in self.activity_ids:
82
- logger.warning(
83
- "Got activity that already handled. Activity ID:"
84
- f' {activity["id"]}'
122
+ if activity[ACTIVITY_ID_KEY] in self.activity_ids:
123
+ structlogger.warning(
124
+ "audiocodes.handle.activities.duplicate_activity",
125
+ activity_id=activity[ACTIVITY_ID_KEY],
85
126
  )
86
127
  continue
87
- self.activity_ids.append(activity["id"])
88
- if activity["type"] == "message":
128
+ self.activity_ids.append(activity[ACTIVITY_ID_KEY])
129
+ if activity["type"] == ACTIVITY_MESSAGE:
89
130
  text = activity["text"]
90
- elif activity["type"] == "event":
131
+ elif activity["type"] == ACTIVITY_EVENT:
91
132
  text = self._handle_event(activity)
92
133
  else:
93
- logger.warning(
94
- "Received an activity from audiocodes that we can not "
95
- f"handle. Activity: {activity}"
134
+ structlogger.warning(
135
+ "audiocodes.handle.activities.unknown_activity_type",
136
+ activity=activity,
96
137
  )
97
138
  if not text:
98
139
  continue
@@ -111,13 +152,14 @@ class Conversation:
111
152
  elif isinstance(user_msg.text, str):
112
153
  anonymized_info = user_msg.text
113
154
  else:
114
- anonymized_info = "unknown"
155
+ anonymized_info = INFO_UNKNOWN
115
156
 
116
157
  structlogger.exception(
117
158
  "audiocodes.handle.activities.failure",
118
159
  user_message=copy.deepcopy(anonymized_info),
160
+ error=e,
161
+ exc_info=True,
119
162
  )
120
- logger.debug(e, exc_info=True)
121
163
 
122
164
  await output_channel.send_custom_json(
123
165
  self.conversation_id,
@@ -158,11 +200,12 @@ class AudiocodesInput(InputChannel):
158
200
  raise RasaException(f"Invalid credentials: {e.message}")
159
201
 
160
202
  return cls(
161
- credentials.get("token", ""),
162
- credentials.get("use_websocket", True),
163
- credentials.get("keep_alive", KEEP_ALIVE_SECONDS),
203
+ credentials.get(CREDENTIALS_TOKEN_KEY, ""),
204
+ credentials.get(CREDENTIALS_USE_WEBSOCKET_KEY, True),
205
+ credentials.get(CREDENTIALS_KEEP_ALIVE_KEY, KEEP_ALIVE_SECONDS),
164
206
  credentials.get(
165
- "keep_alive_expiration_factor", KEEP_ALIVE_EXPIRATION_FACTOR
207
+ CREDENTIALS_KEEP_ALIVE_EXPIRATION_FACTOR_KEY,
208
+ KEEP_ALIVE_EXPIRATION_FACTOR,
166
209
  ),
167
210
  )
168
211
 
@@ -185,12 +228,12 @@ class AudiocodesInput(InputChannel):
185
228
  if self.scheduler_job:
186
229
  self.scheduler_job.remove()
187
230
  self.scheduler_job = (await jobs.scheduler()).add_job(
188
- self.clean_old_conversations, "interval", minutes=10
231
+ self.clean_old_conversations, "interval", minutes=CLEANUP_INTERVAL_MINUTES
189
232
  )
190
233
 
191
234
  def _check_token(self, token: Optional[Text]) -> None:
192
235
  if not token:
193
- raise Unauthorized("Authentication token required.")
236
+ raise HttpUnauthorized("Authentication token required.")
194
237
 
195
238
  def _get_conversation(
196
239
  self, token: Optional[Text], conversation_id: Text
@@ -203,14 +246,11 @@ class AudiocodesInput(InputChannel):
203
246
  return conversation
204
247
 
205
248
  def clean_old_conversations(self) -> None:
206
- logger.debug(
207
- "Performing clean old conversations, current number:"
208
- f" {len(self.conversations)}"
209
- )
210
- now = datetime.datetime.utcnow()
211
- delta = datetime.timedelta(
212
- seconds=self.keep_alive * self.keep_alive_expiration_factor
249
+ structlogger.debug(
250
+ "audiocodes.clean_old_conversations", current_number=len(self.conversations)
213
251
  )
252
+ now = datetime.now(timezone.utc)
253
+ delta = timedelta(seconds=self.keep_alive * self.keep_alive_expiration_factor)
214
254
  self.conversations = {
215
255
  k: v
216
256
  for k, v in self.conversations.items()
@@ -221,9 +261,8 @@ class AudiocodesInput(InputChannel):
221
261
  conversation_id = body["conversation"]
222
262
  if conversation_id in self.conversations:
223
263
  raise ServerError("Conversation already exists")
224
- logger.debug(
225
- "(handle_start_conversation) --- New Conversation has arrived."
226
- f" Conversation: {conversation_id}"
264
+ structlogger.debug(
265
+ "audiocodes.handle_start_conversation", conversation=conversation_id
227
266
  )
228
267
  self.conversations[conversation_id] = Conversation(conversation_id)
229
268
  urls = {
@@ -248,16 +287,15 @@ class AudiocodesInput(InputChannel):
248
287
  """Triggered on new websocket connection."""
249
288
  if self.use_websocket is False:
250
289
  raise ConnectionRefusedError("websocket is unavailable")
251
- logger.debug(
252
- "(new_client_connection) --- New client is trying to connect."
253
- f" Conversation: {conversation_id}"
290
+ structlogger.debug(
291
+ "audiocodes.new_client_connection", conversation=conversation_id
254
292
  )
255
293
  conversation = self._get_conversation(request.token, conversation_id)
256
294
  if conversation:
257
295
  if conversation.ws:
258
- logger.debug(
259
- "(new_client_connection) --- The client was already connected."
260
- f" Conversation: {conversation_id}"
296
+ structlogger.debug(
297
+ "audiocodes.new_client_connection.already_connected",
298
+ conversation=conversation_id,
261
299
  )
262
300
  else:
263
301
  conversation.ws = ws
@@ -265,11 +303,9 @@ class AudiocodesInput(InputChannel):
265
303
  try:
266
304
  await ws.recv()
267
305
  except Exception:
268
- logger.debug(
269
- (
270
- "(new_client_connection) --- Websocket was closed by client: "
271
- f"{conversation_id}"
272
- )
306
+ structlogger.warning(
307
+ "audiocodes.new_client_connection.closed",
308
+ conversation=conversation_id,
273
309
  )
274
310
  if conversation:
275
311
  conversation.ws = None
@@ -308,10 +344,7 @@ class AudiocodesInput(InputChannel):
308
344
  Example of payload:
309
345
  {"conversation": <conversation_id>, "activities": List[Activity]}.
310
346
  """
311
- logger.debug(
312
- "(on_activities) --- New activities from the user. Conversation: "
313
- f"{conversation_id}"
314
- )
347
+ structlogger.debug("audiocodes.on_activities", conversation=conversation_id)
315
348
  conversation = self._get_conversation(request.token, conversation_id)
316
349
  if conversation is None:
317
350
  return response.json({})
@@ -350,16 +383,21 @@ class AudiocodesInput(InputChannel):
350
383
  {"conversation": <conversation_id>, "reason": Optional[Text]}.
351
384
  """
352
385
  self._get_conversation(request.token, conversation_id)
353
- reason = json.dumps({"reason": request.json.get("reason")})
386
+ reason = {"reason": request.json.get("reason")}
354
387
  await on_new_message(
355
388
  UserMessage(
356
- text=f"{INTENT_MESSAGE_PREFIX}vaig_event_end{reason}",
389
+ text=f"{INTENT_MESSAGE_PREFIX}session_end",
357
390
  output_channel=None,
358
391
  sender_id=conversation_id,
392
+ metadata=reason,
359
393
  )
360
394
  )
361
395
  del self.conversations[conversation_id]
362
- logger.debug("(disconnect) --- Conversation was deleted")
396
+ structlogger.debug(
397
+ "audiocodes.disconnect",
398
+ conversation=conversation_id,
399
+ request=request.json,
400
+ )
363
401
  return response.json({})
364
402
 
365
403
  @ac_webhook.route("/conversation/<conversation_id>/keepalive", methods=["POST"])
@@ -397,7 +435,7 @@ class AudiocodesOutput(OutputChannel):
397
435
  )
398
436
  message.update(
399
437
  {
400
- "timestamp": datetime.datetime.utcnow().isoformat("T")[:-3] + "Z",
438
+ "timestamp": datetime.now(timezone.utc).isoformat("T")[:-3] + "Z",
401
439
  "id": str(uuid.uuid4()),
402
440
  }
403
441
  )
@@ -411,6 +449,7 @@ class AudiocodesOutput(OutputChannel):
411
449
  self, recipient_id: Text, text: Text, **kwargs: Any
412
450
  ) -> None:
413
451
  """Send a text message."""
452
+ text = remove_emojis(text)
414
453
  await self.add_message({"type": "message", "text": text})
415
454
 
416
455
  async def send_image_url(
@@ -429,6 +468,20 @@ class AudiocodesOutput(OutputChannel):
429
468
  """Send an activity."""
430
469
  await self.add_message(json_message)
431
470
 
471
+ async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
472
+ """Indicate that the conversation should be ended."""
473
+ await self.add_message({"type": "event", "name": "hangup"})
474
+
475
+ async def send_text_with_buttons(
476
+ self,
477
+ recipient_id: str,
478
+ text: str,
479
+ buttons: List[Dict[str, Any]],
480
+ **kwargs: Any,
481
+ ) -> None:
482
+ """Uses the concise button output format for voice channels."""
483
+ await self.send_text_with_buttons_concise(recipient_id, text, buttons, **kwargs)
484
+
432
485
 
433
486
  class WebsocketOutput(AudiocodesOutput):
434
487
  def __init__(self, ws: Any, conversation_id: Text) -> None:
@@ -1,26 +1,29 @@
1
- from typing import Any, Awaitable, Callable, Dict, Optional, Text
1
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Text
2
2
 
3
3
  import structlog
4
4
  from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
5
- from rasa.core.channels.voice_aware.jambonz_protocol import (
5
+ from rasa.core.channels.voice_ready.jambonz_protocol import (
6
6
  send_ws_text_message,
7
7
  websocket_message_handler,
8
+ send_ws_hangup_message,
8
9
  )
9
- from rasa.core.channels.voice_aware.utils import validate_voice_license_scope
10
+ from rasa.core.channels.voice_ready.utils import validate_voice_license_scope
10
11
  from rasa.shared.exceptions import RasaException
11
12
  from sanic import Blueprint, response, Websocket # type: ignore[attr-defined]
12
13
  from sanic.request import Request
13
14
  from sanic.response import HTTPResponse
14
15
 
15
- from rasa.shared.utils.common import mark_as_experimental_feature
16
-
16
+ from rasa.shared.utils.common import mark_as_beta_feature
17
+ from rasa.utils.io import remove_emojis
17
18
 
18
19
  structlogger = structlog.get_logger()
19
20
 
20
21
  CHANNEL_NAME = "jambonz"
21
22
 
23
+ DEFAULT_HANGUP_DELAY_SECONDS = 1
24
+
22
25
 
23
- class JambonzVoiceAwareInput(InputChannel):
26
+ class JambonzVoiceReadyInput(InputChannel):
24
27
  """Connector for the Jambonz platform."""
25
28
 
26
29
  @classmethod
@@ -32,8 +35,8 @@ class JambonzVoiceAwareInput(InputChannel):
32
35
  return cls()
33
36
 
34
37
  def __init__(self) -> None:
35
- """Initializes the JambonzVoiceAwareInput channel."""
36
- mark_as_experimental_feature("Jambonz Channel")
38
+ """Initializes the JambonzVoiceReadyInput channel."""
39
+ mark_as_beta_feature("Jambonz Channel")
37
40
  validate_voice_license_scope()
38
41
 
39
42
  def blueprint(
@@ -84,6 +87,7 @@ class JambonzWebsocketOutput(OutputChannel):
84
87
  self, recipient_id: Text, text: Text, **kwargs: Any
85
88
  ) -> None:
86
89
  """Send a text message."""
90
+ text = remove_emojis(text)
87
91
  await self.add_message({"type": "message", "text": text})
88
92
 
89
93
  async def send_image_url(
@@ -101,3 +105,17 @@ class JambonzWebsocketOutput(OutputChannel):
101
105
  ) -> None:
102
106
  """Send an activity."""
103
107
  await self.add_message(json_message)
108
+
109
+ async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
110
+ """Indicate that the conversation should be ended."""
111
+ await send_ws_hangup_message(DEFAULT_HANGUP_DELAY_SECONDS, self.ws)
112
+
113
+ async def send_text_with_buttons(
114
+ self,
115
+ recipient_id: str,
116
+ text: str,
117
+ buttons: List[Dict[str, Any]],
118
+ **kwargs: Any,
119
+ ) -> None:
120
+ """Uses the concise button output format for voice channels."""
121
+ await self.send_text_with_buttons_concise(recipient_id, text, buttons, **kwargs)
@@ -5,6 +5,8 @@ from typing import Any, Awaitable, Callable, Dict, List, Text
5
5
 
6
6
  import structlog
7
7
  from rasa.core.channels.channel import UserMessage
8
+ from rasa.core.channels.voice_ready.utils import CallParameters
9
+ from dataclasses import asdict
8
10
  from sanic import Websocket # type: ignore[attr-defined]
9
11
 
10
12
 
@@ -17,12 +19,20 @@ class NewSessionMessage:
17
19
 
18
20
  call_sid: str
19
21
  message_id: str
22
+ call_params: CallParameters
20
23
 
21
24
  @staticmethod
22
25
  def from_message(message: Dict[str, Any]) -> "NewSessionMessage":
26
+ structlogger.debug("jambonz.websocket.message.new_session", message=message)
27
+ call_params = CallParameters(
28
+ call_id=message.get("call_sid"),
29
+ user_phone=message.get("data", {}).get("from"),
30
+ bot_phone=message.get("data", {}).get("to"),
31
+ )
23
32
  return NewSessionMessage(
24
33
  message.get("call_sid"),
25
34
  message.get("msgid"),
35
+ call_params,
26
36
  )
27
37
 
28
38
 
@@ -82,6 +92,10 @@ class CallStatusChanged:
82
92
 
83
93
  @staticmethod
84
94
  def from_message(message: Dict[str, Any]) -> "CallStatusChanged":
95
+ structlogger.debug(
96
+ "jambonz.websocket.message.call_status_changed",
97
+ message=message,
98
+ )
85
99
  return CallStatusChanged(
86
100
  message.get("call_sid"), message.get("data", {}).get("call_status")
87
101
  )
@@ -145,7 +159,7 @@ async def websocket_message_handler(
145
159
  await handle_session_reconnect(session_reconnect)
146
160
  elif message.get("type") == "call:status":
147
161
  call_status = CallStatusChanged.from_message(message)
148
- await handle_call_status(call_status)
162
+ await handle_call_status(call_status, on_new_message, ws)
149
163
  elif message.get("type") == "verb:hook" and message.get("hook") == "/gather":
150
164
  hook_trigger_reason = message.get("data", {}).get("reason")
151
165
 
@@ -184,7 +198,7 @@ async def handle_new_session(
184
198
  ws: Websocket,
185
199
  ) -> None:
186
200
  """Handle new session message."""
187
- from rasa.core.channels.voice_aware.jambonz import JambonzWebsocketOutput
201
+ from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
188
202
 
189
203
  structlogger.debug("jambonz.websocket.message.new_call", call_sid=message.call_sid)
190
204
  output_channel = JambonzWebsocketOutput(ws, message.call_sid)
@@ -192,7 +206,7 @@ async def handle_new_session(
192
206
  text="/session_start",
193
207
  output_channel=output_channel,
194
208
  sender_id=message.call_sid,
195
- metadata={},
209
+ metadata=asdict(message.call_params),
196
210
  )
197
211
  await send_config_ack(message.message_id, ws)
198
212
  await on_new_message(user_msg)
@@ -208,7 +222,7 @@ async def handle_gather_completed(
208
222
 
209
223
  This includes results of gather calles with their transcription.
210
224
  """
211
- from rasa.core.channels.voice_aware.jambonz import JambonzWebsocketOutput
225
+ from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
212
226
 
213
227
  if not transcript_result.is_final:
214
228
  # in case of a non final transcript, we are going to wait for the final
@@ -256,7 +270,11 @@ async def handle_gather_timeout(gather_timeout: GatherTimeout, ws: Websocket) ->
256
270
  await send_gather_input(ws)
257
271
 
258
272
 
259
- async def handle_call_status(call_status: CallStatusChanged) -> None:
273
+ async def handle_call_status(
274
+ call_status: CallStatusChanged,
275
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
276
+ ws: Websocket,
277
+ ) -> None:
260
278
  """Handle changes in the call status."""
261
279
  structlogger.debug(
262
280
  "jambonz.websocket.message.call_status_changed",
@@ -264,6 +282,19 @@ async def handle_call_status(call_status: CallStatusChanged) -> None:
264
282
  message=call_status.status,
265
283
  )
266
284
 
285
+ if call_status.status == "completed":
286
+ structlogger.debug("jambonz.websocket.message.call_completed")
287
+ from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
288
+
289
+ output_channel = JambonzWebsocketOutput(ws, call_status.call_sid)
290
+ user_msg = UserMessage(
291
+ text="/session_end",
292
+ output_channel=output_channel,
293
+ sender_id=call_status.call_sid,
294
+ metadata={},
295
+ )
296
+ await on_new_message(user_msg)
297
+
267
298
 
268
299
  async def handle_session_reconnect(session_reconnect: SessionReconnect) -> None:
269
300
  """Handle session reconnect message."""
@@ -301,6 +332,7 @@ async def send_config_ack(message_id: str, ws: Websocket) -> None:
301
332
 
302
333
  async def send_gather_input(ws: Websocket) -> None:
303
334
  """Send a gather input command to jambonz."""
335
+ structlogger.debug("jambonz.websocket.send.gather")
304
336
  await ws.send(
305
337
  json.dumps(
306
338
  {
@@ -342,3 +374,23 @@ async def send_ws_text_message(ws: Websocket, text: Text) -> None:
342
374
  }
343
375
  )
344
376
  )
377
+
378
+
379
+ async def send_ws_hangup_message(hangup_delay_seconds: int, ws: Websocket) -> None:
380
+ """Send a hangup message to the websocket using the jambonz interface."""
381
+ structlogger.debug("jambonz.websocket.send.hangup")
382
+ await ws.send(
383
+ json.dumps(
384
+ {
385
+ "type": "command",
386
+ "command": "redirect",
387
+ "queueCommand": True,
388
+ "data": [
389
+ {"pause": {"length": hangup_delay_seconds}},
390
+ {
391
+ "hangup": {},
392
+ },
393
+ ],
394
+ }
395
+ )
396
+ )