rasa-pro 3.10.16__py3-none-any.whl → 3.11.0a1__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 (185) hide show
  1. README.md +396 -17
  2. rasa/api.py +9 -3
  3. rasa/cli/arguments/default_arguments.py +23 -2
  4. rasa/cli/arguments/run.py +15 -0
  5. rasa/cli/arguments/train.py +3 -9
  6. rasa/cli/e2e_test.py +1 -1
  7. rasa/cli/evaluate.py +1 -1
  8. rasa/cli/inspect.py +8 -4
  9. rasa/cli/llm_fine_tuning.py +12 -15
  10. rasa/cli/run.py +8 -1
  11. rasa/cli/studio/studio.py +8 -18
  12. rasa/cli/train.py +11 -53
  13. rasa/cli/utils.py +8 -10
  14. rasa/cli/x.py +1 -1
  15. rasa/constants.py +1 -1
  16. rasa/core/actions/action.py +2 -0
  17. rasa/core/actions/action_hangup.py +29 -0
  18. rasa/core/agent.py +2 -2
  19. rasa/core/brokers/kafka.py +3 -1
  20. rasa/core/brokers/pika.py +3 -1
  21. rasa/core/channels/__init__.py +8 -6
  22. rasa/core/channels/channel.py +21 -4
  23. rasa/core/channels/development_inspector.py +143 -46
  24. rasa/core/channels/inspector/README.md +1 -1
  25. rasa/core/channels/inspector/dist/assets/{arc-b6e548fe.js → arc-86942a71.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{c4Diagram-d0fbc5ce-fa03ac9e.js → c4Diagram-d0fbc5ce-b0290676.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{classDiagram-936ed81e-ee67392a.js → classDiagram-936ed81e-f6405f6e.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-c3cb15f1-9b283fae.js → classDiagram-v2-c3cb15f1-ef61ac77.js} +1 -1
  29. rasa/core/channels/inspector/dist/assets/{createText-62fc7601-8b6fcc2a.js → createText-62fc7601-f0411e58.js} +1 -1
  30. rasa/core/channels/inspector/dist/assets/{edges-f2ad444c-22e77f4f.js → edges-f2ad444c-7dcc4f3b.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{erDiagram-9d236eb7-60ffc87f.js → erDiagram-9d236eb7-e0c092d7.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{flowDb-1972c806-9dd802e4.js → flowDb-1972c806-fba2e3ce.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{flowDiagram-7ea5b25a-5fa1912f.js → flowDiagram-7ea5b25a-7a70b71a.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-24a5f41a.js +1 -0
  35. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-abe16c3d-622a1fd2.js → flowchart-elk-definition-abe16c3d-00a59b68.js} +1 -1
  36. rasa/core/channels/inspector/dist/assets/{ganttDiagram-9b5ea136-e285a63a.js → ganttDiagram-9b5ea136-293c91fa.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-99d0ae7c-f237bdca.js → gitGraphDiagram-99d0ae7c-07b2d68c.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{index-2c4b9a3b-4b03d70e.js → index-2c4b9a3b-bc959fbd.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/index-3a8a5a28.js +1317 -0
  40. rasa/core/channels/inspector/dist/assets/{infoDiagram-736b4530-72a0fa5f.js → infoDiagram-736b4530-4a350f72.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{journeyDiagram-df861f2b-82218c41.js → journeyDiagram-df861f2b-af464fb7.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{layout-78cff630.js → layout-0071f036.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{line-5038b469.js → line-2f73cc83.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{linear-c4fc4098.js → linear-f014b4cc.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/{mindmap-definition-beec6740-c33c8ea6.js → mindmap-definition-beec6740-d2426fb6.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/{pieDiagram-dbbf0591-a8d03059.js → pieDiagram-dbbf0591-776f01a2.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-4d7f4fd6-6a0e56b2.js → quadrantDiagram-4d7f4fd6-82e00b57.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{requirementDiagram-6fc4c22a-2dc7c7bd.js → requirementDiagram-6fc4c22a-ea13c6bb.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-8f13d901-2360fe39.js → sankeyDiagram-8f13d901-1feca7e9.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-b655622a-41b9f9ad.js → sequenceDiagram-b655622a-070c61d2.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/{stateDiagram-59f0c015-0aad326f.js → stateDiagram-59f0c015-24f46263.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-2b26beab-9847d984.js → stateDiagram-v2-2b26beab-c9056051.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{styles-080da4f6-564d890e.js → styles-080da4f6-08abc34a.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{styles-3dcbcfbf-38957613.js → styles-3dcbcfbf-bc74c25a.js} +1 -1
  55. rasa/core/channels/inspector/dist/assets/{styles-9c745c82-f0fc6921.js → styles-9c745c82-4e5d66de.js} +1 -1
  56. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-4835440b-ef3c5a77.js → svgDrawCommon-4835440b-849c4517.js} +1 -1
  57. rasa/core/channels/inspector/dist/assets/{timeline-definition-5b62e21b-bf3e91c1.js → timeline-definition-5b62e21b-d0fb1598.js} +1 -1
  58. rasa/core/channels/inspector/dist/assets/{xychartDiagram-2b33534f-4d4026c0.js → xychartDiagram-2b33534f-04d115e2.js} +1 -1
  59. rasa/core/channels/inspector/dist/index.html +18 -17
  60. rasa/core/channels/inspector/index.html +17 -16
  61. rasa/core/channels/inspector/package.json +5 -1
  62. rasa/core/channels/inspector/src/App.tsx +117 -67
  63. rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
  64. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +11 -10
  65. rasa/core/channels/inspector/src/components/DialogueStack.tsx +10 -25
  66. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +1 -1
  67. rasa/core/channels/inspector/src/helpers/formatters.test.ts +10 -0
  68. rasa/core/channels/inspector/src/helpers/formatters.ts +107 -41
  69. rasa/core/channels/inspector/src/helpers/utils.ts +92 -7
  70. rasa/core/channels/inspector/src/types.ts +21 -1
  71. rasa/core/channels/inspector/yarn.lock +94 -1
  72. rasa/core/channels/rest.py +51 -46
  73. rasa/core/channels/socketio.py +22 -0
  74. rasa/core/channels/{audiocodes.py → voice_ready/audiocodes.py} +110 -68
  75. rasa/core/channels/{voice_aware → voice_ready}/jambonz.py +11 -4
  76. rasa/core/channels/{voice_aware → voice_ready}/jambonz_protocol.py +57 -5
  77. rasa/core/channels/{twilio_voice.py → voice_ready/twilio_voice.py} +58 -7
  78. rasa/core/channels/{voice_aware → voice_ready}/utils.py +16 -0
  79. rasa/core/channels/voice_stream/asr/__init__.py +0 -0
  80. rasa/core/channels/voice_stream/asr/asr_engine.py +71 -0
  81. rasa/core/channels/voice_stream/asr/asr_event.py +13 -0
  82. rasa/core/channels/voice_stream/asr/deepgram.py +77 -0
  83. rasa/core/channels/voice_stream/audio_bytes.py +7 -0
  84. rasa/core/channels/voice_stream/tts/__init__.py +0 -0
  85. rasa/core/channels/voice_stream/tts/azure.py +100 -0
  86. rasa/core/channels/voice_stream/tts/cartesia.py +114 -0
  87. rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
  88. rasa/core/channels/voice_stream/tts/tts_engine.py +48 -0
  89. rasa/core/channels/voice_stream/twilio_media_streams.py +164 -0
  90. rasa/core/channels/voice_stream/util.py +57 -0
  91. rasa/core/channels/voice_stream/voice_channel.py +247 -0
  92. rasa/core/featurizers/single_state_featurizer.py +1 -22
  93. rasa/core/featurizers/tracker_featurizers.py +18 -115
  94. rasa/core/nlg/contextual_response_rephraser.py +11 -2
  95. rasa/{nlu → core}/persistor.py +16 -38
  96. rasa/core/policies/enterprise_search_policy.py +12 -15
  97. rasa/core/policies/flows/flow_executor.py +8 -18
  98. rasa/core/policies/intentless_policy.py +10 -15
  99. rasa/core/policies/ted_policy.py +33 -58
  100. rasa/core/policies/unexpected_intent_policy.py +7 -15
  101. rasa/core/processor.py +13 -64
  102. rasa/core/run.py +11 -1
  103. rasa/core/secrets_manager/constants.py +4 -0
  104. rasa/core/secrets_manager/factory.py +8 -0
  105. rasa/core/secrets_manager/vault.py +11 -1
  106. rasa/core/training/interactive.py +1 -1
  107. rasa/core/utils.py +1 -11
  108. rasa/dialogue_understanding/coexistence/llm_based_router.py +10 -10
  109. rasa/dialogue_understanding/commands/__init__.py +2 -0
  110. rasa/dialogue_understanding/commands/change_flow_command.py +0 -6
  111. rasa/dialogue_understanding/commands/session_end_command.py +61 -0
  112. rasa/dialogue_understanding/generator/flow_retrieval.py +0 -7
  113. rasa/dialogue_understanding/generator/llm_based_command_generator.py +12 -3
  114. rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
  115. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +3 -28
  116. rasa/dialogue_understanding/generator/nlu_command_adapter.py +1 -19
  117. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +4 -37
  118. rasa/e2e_test/aggregate_test_stats_calculator.py +1 -11
  119. rasa/e2e_test/assertions.py +6 -48
  120. rasa/e2e_test/e2e_test_runner.py +6 -9
  121. rasa/e2e_test/utils/e2e_yaml_utils.py +1 -1
  122. rasa/e2e_test/utils/io.py +1 -3
  123. rasa/engine/graph.py +3 -10
  124. rasa/engine/recipes/config_files/default_config.yml +0 -3
  125. rasa/engine/recipes/default_recipe.py +0 -1
  126. rasa/engine/recipes/graph_recipe.py +0 -1
  127. rasa/engine/runner/dask.py +2 -2
  128. rasa/engine/storage/local_model_storage.py +12 -42
  129. rasa/engine/storage/storage.py +1 -5
  130. rasa/engine/validation.py +1 -78
  131. rasa/keys +1 -0
  132. rasa/model_training.py +13 -16
  133. rasa/nlu/classifiers/diet_classifier.py +25 -38
  134. rasa/nlu/classifiers/logistic_regression_classifier.py +9 -22
  135. rasa/nlu/classifiers/sklearn_intent_classifier.py +16 -37
  136. rasa/nlu/extractors/crf_entity_extractor.py +50 -93
  137. rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py +16 -45
  138. rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py +17 -52
  139. rasa/nlu/featurizers/sparse_featurizer/regex_featurizer.py +3 -5
  140. rasa/server.py +1 -1
  141. rasa/shared/constants.py +3 -12
  142. rasa/shared/core/constants.py +4 -0
  143. rasa/shared/core/domain.py +101 -47
  144. rasa/shared/core/events.py +29 -0
  145. rasa/shared/core/flows/flows_list.py +20 -11
  146. rasa/shared/core/flows/validation.py +25 -0
  147. rasa/shared/core/flows/yaml_flows_io.py +3 -24
  148. rasa/shared/importers/importer.py +40 -39
  149. rasa/shared/importers/multi_project.py +23 -11
  150. rasa/shared/importers/rasa.py +7 -2
  151. rasa/shared/importers/remote_importer.py +196 -0
  152. rasa/shared/importers/utils.py +3 -1
  153. rasa/shared/nlu/training_data/features.py +2 -120
  154. rasa/shared/nlu/training_data/training_data.py +18 -19
  155. rasa/shared/providers/_configs/azure_openai_client_config.py +3 -5
  156. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +1 -6
  157. rasa/shared/providers/llm/_base_litellm_client.py +11 -31
  158. rasa/shared/providers/llm/self_hosted_llm_client.py +3 -15
  159. rasa/shared/utils/common.py +3 -22
  160. rasa/shared/utils/io.py +0 -1
  161. rasa/shared/utils/llm.py +30 -27
  162. rasa/shared/utils/schemas/events.py +2 -0
  163. rasa/shared/utils/schemas/model_config.yml +0 -10
  164. rasa/shared/utils/yaml.py +44 -0
  165. rasa/studio/auth.py +5 -3
  166. rasa/studio/config.py +4 -13
  167. rasa/studio/constants.py +0 -1
  168. rasa/studio/data_handler.py +3 -10
  169. rasa/studio/upload.py +8 -17
  170. rasa/tracing/instrumentation/attribute_extractors.py +1 -1
  171. rasa/utils/io.py +66 -0
  172. rasa/utils/tensorflow/model_data.py +193 -2
  173. rasa/validator.py +0 -12
  174. rasa/version.py +1 -1
  175. rasa_pro-3.11.0a1.dist-info/METADATA +576 -0
  176. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/RECORD +181 -164
  177. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-1844e5a5.js +0 -1
  178. rasa/core/channels/inspector/dist/assets/index-a5d3e69d.js +0 -1040
  179. rasa/utils/tensorflow/feature_array.py +0 -366
  180. rasa_pro-3.10.16.dist-info/METADATA +0 -196
  181. /rasa/core/channels/{voice_aware → voice_ready}/__init__.py +0 -0
  182. /rasa/core/channels/{voice_native → voice_stream}/__init__.py +0 -0
  183. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/NOTICE +0 -0
  184. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/WHEEL +0 -0
  185. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0a1.dist-info}/entry_points.txt +0 -0
@@ -2,11 +2,12 @@ from typing import Any, Awaitable, Callable, Dict, 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
@@ -19,8 +20,10 @@ structlogger = structlog.get_logger()
19
20
 
20
21
  CHANNEL_NAME = "jambonz"
21
22
 
23
+ DEFAULT_HANGUP_DELAY_SECONDS = 1
22
24
 
23
- class JambonzVoiceAwareInput(InputChannel):
25
+
26
+ class JambonzVoiceReadyInput(InputChannel):
24
27
  """Connector for the Jambonz platform."""
25
28
 
26
29
  @classmethod
@@ -32,7 +35,7 @@ class JambonzVoiceAwareInput(InputChannel):
32
35
  return cls()
33
36
 
34
37
  def __init__(self) -> None:
35
- """Initializes the JambonzVoiceAwareInput channel."""
38
+ """Initializes the JambonzVoiceReadyInput channel."""
36
39
  mark_as_experimental_feature("Jambonz Channel")
37
40
  validate_voice_license_scope()
38
41
 
@@ -101,3 +104,7 @@ class JambonzWebsocketOutput(OutputChannel):
101
104
  ) -> None:
102
105
  """Send an activity."""
103
106
  await self.add_message(json_message)
107
+
108
+ async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
109
+ """Indicate that the conversation should be ended."""
110
+ await send_ws_hangup_message(DEFAULT_HANGUP_DELAY_SECONDS, self.ws)
@@ -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
+ )
@@ -1,9 +1,11 @@
1
1
  from sanic import Blueprint, response
2
- from sanic.request import Request
2
+ from sanic.request import Request, RequestParameters
3
3
  from sanic.response import HTTPResponse
4
4
  from twilio.twiml.voice_response import VoiceResponse, Gather
5
5
  from typing import Text, Callable, Awaitable, List, Any, Dict, Optional
6
+ from dataclasses import asdict
6
7
 
8
+ import structlog
7
9
  import rasa.utils.io
8
10
  import rasa.shared.utils.io
9
11
  from rasa.shared.core.events import BotUttered
@@ -13,6 +15,19 @@ from rasa.core.channels.channel import (
13
15
  CollectingOutputChannel,
14
16
  UserMessage,
15
17
  )
18
+ from rasa.core.channels.voice_ready.utils import CallParameters
19
+
20
+ logger = structlog.get_logger(__name__)
21
+
22
+
23
+ def map_call_params(form: RequestParameters) -> CallParameters:
24
+ """Map the Audiocodes parameters to the CallParameters dataclass."""
25
+ return CallParameters(
26
+ call_id=form.get("CallSid"),
27
+ user_phone=form.get("Caller"),
28
+ bot_phone=form.get("Called"),
29
+ direction=form.get("Direction"),
30
+ )
16
31
 
17
32
 
18
33
  class TwilioVoiceInput(InputChannel):
@@ -105,7 +120,6 @@ class TwilioVoiceInput(InputChannel):
105
120
  credentials = credentials or {}
106
121
 
107
122
  return cls(
108
- credentials.get("initial_prompt", "hello"),
109
123
  credentials.get(
110
124
  "reprompt_fallback_phrase",
111
125
  "I'm sorry I didn't get that could you rephrase.",
@@ -118,7 +132,6 @@ class TwilioVoiceInput(InputChannel):
118
132
 
119
133
  def __init__(
120
134
  self,
121
- initial_prompt: Optional[Text],
122
135
  reprompt_fallback_phrase: Optional[Text],
123
136
  assistant_voice: Optional[Text],
124
137
  speech_timeout: Text = "5",
@@ -128,14 +141,12 @@ class TwilioVoiceInput(InputChannel):
128
141
  """Creates a connection to Twilio voice.
129
142
 
130
143
  Args:
131
- initial_prompt: text to use to prompt a conversation when call is answered.
132
144
  reprompt_fallback_phrase: phrase to use if no user response.
133
145
  assistant_voice: name of the assistant voice to use.
134
146
  speech_timeout: how long to pause when user finished speaking.
135
147
  speech_model: type of transcription model to use from Twilio.
136
148
  enhanced: toggle to use Twilio's premium speech transcription model.
137
149
  """
138
- self.initial_prompt = initial_prompt
139
150
  self.reprompt_fallback_phrase = reprompt_fallback_phrase
140
151
  self.assistant_voice = assistant_voice
141
152
  self.speech_timeout = speech_timeout
@@ -239,22 +250,43 @@ class TwilioVoiceInput(InputChannel):
239
250
  text = request.form.get("SpeechResult")
240
251
  input_channel = self.name()
241
252
  call_status = request.form.get("CallStatus")
253
+ metadata = {}
242
254
 
243
255
  collector = TwilioVoiceCollectingOutputChannel()
244
256
 
257
+ logger.debug(
258
+ "twilio_voice.webhook",
259
+ sender_id=sender_id,
260
+ text=text,
261
+ call_status=call_status,
262
+ )
245
263
  # Provide an initial greeting to answer the user's call.
246
264
  if (text is None) and (call_status == "ringing"):
247
- text = self.initial_prompt
265
+ text = "/session_start"
266
+ metadata = asdict(map_call_params(request.form))
267
+
268
+ # when call is disconnected
269
+ if call_status == "completed":
270
+ text = "/session_end"
271
+ metadata = {"reason": "user disconnected"}
248
272
 
249
273
  # determine the response.
250
274
  if text is not None:
275
+ logger.info("twilio_voice.webhook.text_not_none", sender_id=sender_id)
251
276
  await on_new_message(
252
- UserMessage(text, collector, sender_id, input_channel=input_channel)
277
+ UserMessage(
278
+ text,
279
+ collector,
280
+ sender_id,
281
+ input_channel=input_channel,
282
+ metadata=metadata,
283
+ )
253
284
  )
254
285
 
255
286
  twilio_response = self._build_twilio_voice_response(collector.messages)
256
287
  # If the user doesn't respond resend the last message.
257
288
  else:
289
+ logger.info("twilio_voice.webhook.text_none", sender_id=sender_id)
258
290
  # Get last user utterance from tracker.
259
291
  tracker = await request.app.ctx.agent.tracker_store.retrieve(sender_id)
260
292
  last_response = None
@@ -285,6 +317,7 @@ class TwilioVoiceInput(InputChannel):
285
317
  self, messages: List[Dict[Text, Any]]
286
318
  ) -> VoiceResponse:
287
319
  """Builds the Twilio Voice Response object."""
320
+ logger.debug("twilio_voice.build_twilio_voice_response", messages=messages)
288
321
  voice_response = VoiceResponse()
289
322
  gather = Gather(
290
323
  input="speech",
@@ -299,6 +332,11 @@ class TwilioVoiceInput(InputChannel):
299
332
  # Add a listener to the last message to listen for user response.
300
333
  for i, message in enumerate(messages):
301
334
  msg_text = message["text"]
335
+ # Check if the message is a hangup message.
336
+ if message.get("custom", {}).get("hangup"):
337
+ voice_response.hangup()
338
+ break
339
+
302
340
  if i + 1 == len(messages):
303
341
  gather.say(msg_text, voice=self.assistant_voice)
304
342
  voice_response.append(gather)
@@ -365,3 +403,16 @@ class TwilioVoiceCollectingOutputChannel(CollectingOutputChannel):
365
403
  "with a visual elements such as images and emojis "
366
404
  "that are used in your voice channel."
367
405
  )
406
+
407
+ async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
408
+ """
409
+ Indicate that the conversation should be ended.
410
+
411
+ Parent class is a collecting output channel, so we don't actually hang up
412
+ but we add a custom message to the list of messages to be sent.
413
+ This message will be picked up by _build_twilio_voice_response
414
+ which will hang up the call.
415
+ """
416
+ await self._persist_message(
417
+ self._message(recipient_id, custom={"hangup": True})
418
+ )
@@ -1,4 +1,6 @@
1
1
  import structlog
2
+ from dataclasses import dataclass
3
+ from typing import Optional
2
4
 
3
5
  from rasa.utils.licensing import (
4
6
  PRODUCT_AREA,
@@ -18,3 +20,17 @@ def validate_voice_license_scope() -> None:
18
20
 
19
21
  voice_product_scope = PRODUCT_AREA + " " + VOICE_SCOPE
20
22
  validate_license_from_env(product_area=voice_product_scope)
23
+
24
+
25
+ @dataclass
26
+ class CallParameters:
27
+ """Standardized call parameters for voice channels."""
28
+
29
+ call_id: str
30
+ user_phone: str
31
+ bot_phone: str
32
+ user_name: Optional[str] = None
33
+ user_host: Optional[str] = None
34
+ bot_host: Optional[str] = None
35
+ direction: Optional[str] = None
36
+ stream_id: Optional[str] = None
File without changes
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, AsyncIterator, Any, Generic, Optional, Type, TypeVar
3
+
4
+ from websockets.legacy.client import WebSocketClientProtocol
5
+
6
+ from rasa.core.channels.voice_stream.asr.asr_event import ASREvent
7
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
8
+ from rasa.core.channels.voice_stream.util import MergeableConfig
9
+ from rasa.shared.exceptions import ConnectionException
10
+
11
+ T = TypeVar("T", bound="ASREngineConfig")
12
+ E = TypeVar("E", bound="ASREngine")
13
+
14
+
15
+ @dataclass
16
+ class ASREngineConfig(MergeableConfig):
17
+ pass
18
+
19
+
20
+ class ASREngine(Generic[T]):
21
+ def __init__(self, config: Optional[T] = None):
22
+ self.config = self.get_default_config().merge(config)
23
+ self.asr_socket: Optional[WebSocketClientProtocol] = None
24
+
25
+ async def connect(self) -> None:
26
+ self.asr_socket = await self.open_websocket_connection()
27
+
28
+ async def open_websocket_connection(self) -> WebSocketClientProtocol:
29
+ """Connect to the ASR system."""
30
+ raise NotImplementedError
31
+
32
+ @classmethod
33
+ def from_config_dict(cls: Type[E], config: Dict) -> E:
34
+ raise NotImplementedError
35
+
36
+ async def close_connection(self) -> None:
37
+ if self.asr_socket:
38
+ await self.asr_socket.close()
39
+
40
+ async def signal_audio_done(self) -> None:
41
+ """Signal to the ASR Api that you are done sending data."""
42
+ raise NotImplementedError
43
+
44
+ async def send_audio_chunks(self, chunk: RasaAudioBytes) -> None:
45
+ """Send audio chunks to the ASR system via the websocket."""
46
+ if self.asr_socket is None:
47
+ raise ConnectionException("Websocket not connected.")
48
+ engine_bytes = self.rasa_audio_bytes_to_engine_bytes(chunk)
49
+ await self.asr_socket.send(engine_bytes)
50
+
51
+ def rasa_audio_bytes_to_engine_bytes(self, chunk: RasaAudioBytes) -> bytes:
52
+ """Convert RasaAudioBytes to bytes usable by this engine."""
53
+ raise NotImplementedError
54
+
55
+ async def stream_asr_events(self) -> AsyncIterator[ASREvent]:
56
+ """Stream the events returned by the ASR system as it is fed audio bytes."""
57
+ if self.asr_socket is None:
58
+ raise ConnectionException("Websocket not connected.")
59
+ async for message in self.asr_socket:
60
+ asr_event = self.engine_event_to_asr_event(message)
61
+ if asr_event:
62
+ yield asr_event
63
+
64
+ def engine_event_to_asr_event(self, e: Any) -> Optional[ASREvent]:
65
+ """Translate an engine event to a common ASREvent."""
66
+ raise NotImplementedError
67
+
68
+ @staticmethod
69
+ def get_default_config() -> T:
70
+ """Get the default config for this component."""
71
+ raise NotImplementedError
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class ASREvent:
6
+ @classmethod
7
+ def name(cls) -> str:
8
+ return cls.__name__
9
+
10
+
11
+ @dataclass
12
+ class NewTranscript(ASREvent):
13
+ text: str
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Dict, Optional
3
+ import json
4
+ import os
5
+
6
+ import websockets
7
+ from websockets.legacy.client import WebSocketClientProtocol
8
+
9
+ from rasa.core.channels.voice_stream.asr.asr_engine import ASREngine, ASREngineConfig
10
+ from rasa.core.channels.voice_stream.asr.asr_event import ASREvent, NewTranscript
11
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
12
+
13
+ DEEPGRAM_API_KEY = "DEEPGRAM_API_KEY"
14
+
15
+
16
+ @dataclass
17
+ class DeepgramASRConfig(ASREngineConfig):
18
+ endpoint: Optional[str] = None
19
+ # number of miliseconds of silence to determine end of speech
20
+ endpointing: Optional[int] = None
21
+
22
+
23
+ class DeepgramASR(ASREngine[DeepgramASRConfig]):
24
+ def __init__(self, config: Optional[DeepgramASRConfig] = None):
25
+ super().__init__(config)
26
+ self.accumulated_transcript = ""
27
+
28
+ async def open_websocket_connection(self) -> WebSocketClientProtocol:
29
+ """Connect to the ASR system."""
30
+ deepgram_api_key = os.environ.get(DEEPGRAM_API_KEY)
31
+ extra_headers = {"Authorization": f"Token {deepgram_api_key}"}
32
+ api_url = self._get_api_url()
33
+ query_params = self._get_query_params()
34
+ return await websockets.connect( # type: ignore
35
+ api_url + query_params,
36
+ extra_headers=extra_headers,
37
+ )
38
+
39
+ def _get_api_url(self) -> str:
40
+ return f"wss://{self.config.endpoint}/v1/listen?"
41
+
42
+ def _get_query_params(self) -> str:
43
+ return (
44
+ f"encoding=mulaw&sample_rate=8000&endpointing={self.config.endpointing}"
45
+ f"&vad_events=true"
46
+ )
47
+
48
+ async def signal_audio_done(self) -> None:
49
+ """Signal to the ASR Api that you are done sending data."""
50
+ if self.asr_socket is None:
51
+ raise AttributeError("Websocket not connected.")
52
+ await self.asr_socket.send(json.dumps({"type": "CloseStream"}))
53
+
54
+ def rasa_audio_bytes_to_engine_bytes(self, chunk: RasaAudioBytes) -> bytes:
55
+ """Convert RasaAudioBytes to bytes usable by this engine."""
56
+ return chunk
57
+
58
+ def engine_event_to_asr_event(self, e: Any) -> Optional[ASREvent]:
59
+ """Translate an engine event to a common ASREvent."""
60
+ data = json.loads(e)
61
+ if data.get("is_final"):
62
+ transcript = data["channel"]["alternatives"][0]["transcript"]
63
+ if data.get("speech_final"):
64
+ full_transcript = self.accumulated_transcript + transcript
65
+ self.accumulated_transcript = ""
66
+ return NewTranscript(full_transcript)
67
+ else:
68
+ self.accumulated_transcript += transcript
69
+ return None
70
+
71
+ @staticmethod
72
+ def get_default_config() -> DeepgramASRConfig:
73
+ return DeepgramASRConfig("api.deepgram.com", 400)
74
+
75
+ @classmethod
76
+ def from_config_dict(cls, config: Dict) -> "DeepgramASR":
77
+ return DeepgramASR(DeepgramASRConfig.from_dict(config))
@@ -0,0 +1,7 @@
1
+ from typing import NewType
2
+
3
+ # a common intermediate audio byte format that acts as a common data format,
4
+ # to prevent quadratic complexity between formats of channels, asr engines,
5
+ # and tts engines
6
+ # currently corresponds to raw wave, 8khz, 8bit, mono channel, mulaw encoding
7
+ RasaAudioBytes = NewType("RasaAudioBytes", bytes)
File without changes
@@ -0,0 +1,100 @@
1
+ import os
2
+ from typing import AsyncIterator, Dict, Optional
3
+ from dataclasses import dataclass
4
+
5
+ import aiohttp
6
+ import structlog
7
+ from aiohttp import ClientConnectorError
8
+
9
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
10
+ from rasa.core.channels.voice_stream.tts.tts_engine import (
11
+ TTSEngine,
12
+ TTSEngineConfig,
13
+ TTSError,
14
+ )
15
+ from rasa.shared.exceptions import ConnectionException
16
+
17
+
18
+ structlogger = structlog.get_logger()
19
+
20
+
21
+ @dataclass
22
+ class AzureTTSConfig(TTSEngineConfig):
23
+ speech_region: Optional[str] = None
24
+
25
+
26
+ class AzureTTS(TTSEngine[AzureTTSConfig]):
27
+ session: Optional[aiohttp.ClientSession] = None
28
+
29
+ def __init__(self, config: Optional[AzureTTSConfig] = None):
30
+ super().__init__(config)
31
+ # Have to create this class-shared session lazily at run time otherwise
32
+ # the async event loop doesn't work
33
+ if self.__class__.session is None or self.__class__.session.closed:
34
+ self.__class__.session = aiohttp.ClientSession()
35
+
36
+ async def synthesize(
37
+ self, text: str, config: Optional[AzureTTSConfig] = None
38
+ ) -> AsyncIterator[RasaAudioBytes]:
39
+ """Generate speech from text using a remote TTS system."""
40
+ config = self.config.merge(config)
41
+ azure_speech_url = self.get_tts_endpoint(config)
42
+ headers = self.get_request_headers()
43
+ body = self.create_request_body(text, config)
44
+ if self.session is None:
45
+ raise ConnectionException("Client session is not initialized")
46
+ try:
47
+ async with self.session.post(
48
+ azure_speech_url, headers=headers, data=body, chunked=True
49
+ ) as response:
50
+ if 200 <= response.status < 300:
51
+ async for data in response.content.iter_chunked(1024):
52
+ yield self.engine_bytes_to_rasa_audio_bytes(data)
53
+ return
54
+ else:
55
+ structlogger.error(
56
+ "azure.synthesize.rest.failed",
57
+ status_code=response.status,
58
+ msg=response.text(),
59
+ )
60
+ raise TTSError(f"TTS failed: {response.text()}")
61
+ except ClientConnectorError as e:
62
+ raise TTSError(e)
63
+
64
+ @staticmethod
65
+ def get_request_headers() -> dict[str, str]:
66
+ azure_speech_api_key = os.environ["AZURE_SPEECH_API_KEY"]
67
+ return {
68
+ "Ocp-Apim-Subscription-Key": azure_speech_api_key,
69
+ "Content-Type": "application/ssml+xml",
70
+ "X-Microsoft-OutputFormat": "raw-8khz-8bit-mono-mulaw",
71
+ }
72
+
73
+ @staticmethod
74
+ def get_tts_endpoint(config: AzureTTSConfig) -> str:
75
+ return f"https://{config.speech_region}.tts.speech.microsoft.com/cognitiveservices/v1"
76
+
77
+ @staticmethod
78
+ def create_request_body(text: str, conf: AzureTTSConfig) -> str:
79
+ return f"""
80
+ <speak version='1.0' xml:lang='{conf.language}'>
81
+ <voice xml:lang='{conf.language}' name='{conf.voice}'>
82
+ {text}
83
+ </voice>
84
+ </speak>"""
85
+
86
+ def engine_bytes_to_rasa_audio_bytes(self, chunk: bytes) -> RasaAudioBytes:
87
+ """Convert the generated tts audio bytes into rasa audio bytes."""
88
+ return RasaAudioBytes(chunk)
89
+
90
+ @staticmethod
91
+ def get_default_config() -> AzureTTSConfig:
92
+ return AzureTTSConfig(
93
+ language="en-US",
94
+ voice="en-US-JennyNeural",
95
+ speech_region="germanywestcentral",
96
+ )
97
+
98
+ @classmethod
99
+ def from_config_dict(cls, config: Dict) -> "AzureTTS":
100
+ return cls(AzureTTSConfig.from_dict(config))