rasa-pro 3.10.15__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 (238) 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/project_templates/calm/config.yml +5 -7
  11. rasa/cli/project_templates/calm/endpoints.yml +15 -2
  12. rasa/cli/project_templates/tutorial/config.yml +8 -5
  13. rasa/cli/project_templates/tutorial/data/flows.yml +1 -1
  14. rasa/cli/project_templates/tutorial/data/patterns.yml +5 -0
  15. rasa/cli/project_templates/tutorial/domain.yml +14 -0
  16. rasa/cli/project_templates/tutorial/endpoints.yml +5 -0
  17. rasa/cli/run.py +7 -0
  18. rasa/cli/scaffold.py +4 -2
  19. rasa/cli/studio/upload.py +0 -15
  20. rasa/cli/train.py +14 -53
  21. rasa/cli/utils.py +14 -11
  22. rasa/cli/x.py +7 -7
  23. rasa/constants.py +3 -1
  24. rasa/core/actions/action.py +77 -33
  25. rasa/core/actions/action_hangup.py +29 -0
  26. rasa/core/actions/action_repeat_bot_messages.py +89 -0
  27. rasa/core/actions/e2e_stub_custom_action_executor.py +5 -1
  28. rasa/core/actions/http_custom_action_executor.py +4 -0
  29. rasa/core/agent.py +2 -2
  30. rasa/core/brokers/kafka.py +3 -1
  31. rasa/core/brokers/pika.py +3 -1
  32. rasa/core/channels/__init__.py +10 -6
  33. rasa/core/channels/channel.py +41 -4
  34. rasa/core/channels/development_inspector.py +150 -46
  35. rasa/core/channels/inspector/README.md +1 -1
  36. rasa/core/channels/inspector/dist/assets/{arc-b6e548fe.js → arc-bc141fb2.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{c4Diagram-d0fbc5ce-fa03ac9e.js → c4Diagram-d0fbc5ce-be2db283.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{classDiagram-936ed81e-ee67392a.js → classDiagram-936ed81e-55366915.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-c3cb15f1-9b283fae.js → classDiagram-v2-c3cb15f1-bb529518.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{createText-62fc7601-8b6fcc2a.js → createText-62fc7601-b0ec81d6.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{edges-f2ad444c-22e77f4f.js → edges-f2ad444c-6166330c.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{erDiagram-9d236eb7-60ffc87f.js → erDiagram-9d236eb7-5ccc6a8e.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{flowDb-1972c806-9dd802e4.js → flowDb-1972c806-fca3bfe4.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{flowDiagram-7ea5b25a-5fa1912f.js → flowDiagram-7ea5b25a-4739080f.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-736177bf.js +1 -0
  46. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-abe16c3d-622a1fd2.js → flowchart-elk-definition-abe16c3d-7c1b0e0f.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{ganttDiagram-9b5ea136-e285a63a.js → ganttDiagram-9b5ea136-772fd050.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-99d0ae7c-f237bdca.js → gitGraphDiagram-99d0ae7c-8eae1dc9.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{index-2c4b9a3b-4b03d70e.js → index-2c4b9a3b-f55afcdf.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/index-e7cef9de.js +1317 -0
  51. rasa/core/channels/inspector/dist/assets/{infoDiagram-736b4530-72a0fa5f.js → infoDiagram-736b4530-124d4a14.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{journeyDiagram-df861f2b-82218c41.js → journeyDiagram-df861f2b-7c4fae44.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{layout-78cff630.js → layout-b9885fb6.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{line-5038b469.js → line-7c59abb6.js} +1 -1
  55. rasa/core/channels/inspector/dist/assets/{linear-c4fc4098.js → linear-4776f780.js} +1 -1
  56. rasa/core/channels/inspector/dist/assets/{mindmap-definition-beec6740-c33c8ea6.js → mindmap-definition-beec6740-2332c46c.js} +1 -1
  57. rasa/core/channels/inspector/dist/assets/{pieDiagram-dbbf0591-a8d03059.js → pieDiagram-dbbf0591-8fb39303.js} +1 -1
  58. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-4d7f4fd6-6a0e56b2.js → quadrantDiagram-4d7f4fd6-3c7180a2.js} +1 -1
  59. rasa/core/channels/inspector/dist/assets/{requirementDiagram-6fc4c22a-2dc7c7bd.js → requirementDiagram-6fc4c22a-e910bcb8.js} +1 -1
  60. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-8f13d901-2360fe39.js → sankeyDiagram-8f13d901-ead16c89.js} +1 -1
  61. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-b655622a-41b9f9ad.js → sequenceDiagram-b655622a-29a02a19.js} +1 -1
  62. rasa/core/channels/inspector/dist/assets/{stateDiagram-59f0c015-0aad326f.js → stateDiagram-59f0c015-042b3137.js} +1 -1
  63. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-2b26beab-9847d984.js → stateDiagram-v2-2b26beab-2178c0f3.js} +1 -1
  64. rasa/core/channels/inspector/dist/assets/{styles-080da4f6-564d890e.js → styles-080da4f6-23ffa4fc.js} +1 -1
  65. rasa/core/channels/inspector/dist/assets/{styles-3dcbcfbf-38957613.js → styles-3dcbcfbf-94f59763.js} +1 -1
  66. rasa/core/channels/inspector/dist/assets/{styles-9c745c82-f0fc6921.js → styles-9c745c82-78a6bebc.js} +1 -1
  67. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-4835440b-ef3c5a77.js → svgDrawCommon-4835440b-eae2a6f6.js} +1 -1
  68. rasa/core/channels/inspector/dist/assets/{timeline-definition-5b62e21b-bf3e91c1.js → timeline-definition-5b62e21b-5c968d92.js} +1 -1
  69. rasa/core/channels/inspector/dist/assets/{xychartDiagram-2b33534f-4d4026c0.js → xychartDiagram-2b33534f-fd3db0d5.js} +1 -1
  70. rasa/core/channels/inspector/dist/index.html +18 -15
  71. rasa/core/channels/inspector/index.html +17 -14
  72. rasa/core/channels/inspector/package.json +5 -1
  73. rasa/core/channels/inspector/src/App.tsx +118 -68
  74. rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
  75. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +11 -10
  76. rasa/core/channels/inspector/src/components/DialogueStack.tsx +10 -25
  77. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -3
  78. rasa/core/channels/inspector/src/helpers/audiostream.ts +165 -0
  79. rasa/core/channels/inspector/src/helpers/formatters.test.ts +10 -0
  80. rasa/core/channels/inspector/src/helpers/formatters.ts +107 -41
  81. rasa/core/channels/inspector/src/helpers/utils.ts +92 -7
  82. rasa/core/channels/inspector/src/types.ts +21 -1
  83. rasa/core/channels/inspector/yarn.lock +94 -1
  84. rasa/core/channels/rest.py +51 -46
  85. rasa/core/channels/socketio.py +28 -1
  86. rasa/core/channels/telegram.py +1 -1
  87. rasa/core/channels/twilio.py +1 -1
  88. rasa/core/channels/{audiocodes.py → voice_ready/audiocodes.py} +122 -69
  89. rasa/core/channels/{voice_aware → voice_ready}/jambonz.py +26 -8
  90. rasa/core/channels/{voice_aware → voice_ready}/jambonz_protocol.py +57 -5
  91. rasa/core/channels/{twilio_voice.py → voice_ready/twilio_voice.py} +64 -28
  92. rasa/core/channels/voice_ready/utils.py +37 -0
  93. rasa/core/channels/voice_stream/asr/__init__.py +0 -0
  94. rasa/core/channels/voice_stream/asr/asr_engine.py +89 -0
  95. rasa/core/channels/voice_stream/asr/asr_event.py +18 -0
  96. rasa/core/channels/voice_stream/asr/azure.py +129 -0
  97. rasa/core/channels/voice_stream/asr/deepgram.py +90 -0
  98. rasa/core/channels/voice_stream/audio_bytes.py +8 -0
  99. rasa/core/channels/voice_stream/browser_audio.py +107 -0
  100. rasa/core/channels/voice_stream/call_state.py +23 -0
  101. rasa/core/channels/voice_stream/tts/__init__.py +0 -0
  102. rasa/core/channels/voice_stream/tts/azure.py +106 -0
  103. rasa/core/channels/voice_stream/tts/cartesia.py +118 -0
  104. rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
  105. rasa/core/channels/voice_stream/tts/tts_engine.py +58 -0
  106. rasa/core/channels/voice_stream/twilio_media_streams.py +173 -0
  107. rasa/core/channels/voice_stream/util.py +57 -0
  108. rasa/core/channels/voice_stream/voice_channel.py +427 -0
  109. rasa/core/information_retrieval/qdrant.py +1 -0
  110. rasa/core/nlg/contextual_response_rephraser.py +45 -17
  111. rasa/{nlu → core}/persistor.py +203 -68
  112. rasa/core/policies/enterprise_search_policy.py +119 -63
  113. rasa/core/policies/flows/flow_executor.py +15 -22
  114. rasa/core/policies/intentless_policy.py +83 -28
  115. rasa/core/processor.py +25 -0
  116. rasa/core/run.py +12 -2
  117. rasa/core/secrets_manager/constants.py +4 -0
  118. rasa/core/secrets_manager/factory.py +8 -0
  119. rasa/core/secrets_manager/vault.py +11 -1
  120. rasa/core/training/interactive.py +33 -34
  121. rasa/core/utils.py +47 -21
  122. rasa/dialogue_understanding/coexistence/llm_based_router.py +41 -14
  123. rasa/dialogue_understanding/commands/__init__.py +6 -0
  124. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +60 -0
  125. rasa/dialogue_understanding/commands/session_end_command.py +61 -0
  126. rasa/dialogue_understanding/commands/user_silence_command.py +59 -0
  127. rasa/dialogue_understanding/commands/utils.py +5 -0
  128. rasa/dialogue_understanding/generator/constants.py +2 -0
  129. rasa/dialogue_understanding/generator/flow_retrieval.py +47 -9
  130. rasa/dialogue_understanding/generator/llm_based_command_generator.py +38 -15
  131. rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
  132. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +35 -13
  133. rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +3 -0
  134. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +60 -13
  135. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +53 -0
  136. rasa/dialogue_understanding/patterns/repeat.py +37 -0
  137. rasa/dialogue_understanding/patterns/user_silence.py +37 -0
  138. rasa/dialogue_understanding/processor/command_processor.py +21 -1
  139. rasa/e2e_test/aggregate_test_stats_calculator.py +1 -11
  140. rasa/e2e_test/assertions.py +136 -61
  141. rasa/e2e_test/assertions_schema.yml +23 -0
  142. rasa/e2e_test/e2e_test_case.py +85 -6
  143. rasa/e2e_test/e2e_test_runner.py +2 -3
  144. rasa/engine/graph.py +0 -1
  145. rasa/engine/loader.py +12 -0
  146. rasa/engine/recipes/config_files/default_config.yml +0 -3
  147. rasa/engine/recipes/default_recipe.py +0 -1
  148. rasa/engine/recipes/graph_recipe.py +0 -1
  149. rasa/engine/runner/dask.py +2 -2
  150. rasa/engine/storage/local_model_storage.py +12 -42
  151. rasa/engine/storage/storage.py +1 -5
  152. rasa/engine/validation.py +527 -74
  153. rasa/model_manager/__init__.py +0 -0
  154. rasa/model_manager/config.py +40 -0
  155. rasa/model_manager/model_api.py +559 -0
  156. rasa/model_manager/runner_service.py +286 -0
  157. rasa/model_manager/socket_bridge.py +146 -0
  158. rasa/model_manager/studio_jwt_auth.py +86 -0
  159. rasa/model_manager/trainer_service.py +325 -0
  160. rasa/model_manager/utils.py +87 -0
  161. rasa/model_manager/warm_rasa_process.py +187 -0
  162. rasa/model_service.py +112 -0
  163. rasa/model_training.py +42 -23
  164. rasa/nlu/tokenizers/whitespace_tokenizer.py +3 -14
  165. rasa/server.py +4 -2
  166. rasa/shared/constants.py +60 -8
  167. rasa/shared/core/constants.py +13 -0
  168. rasa/shared/core/domain.py +107 -50
  169. rasa/shared/core/events.py +29 -0
  170. rasa/shared/core/flows/flow.py +5 -0
  171. rasa/shared/core/flows/flows_list.py +19 -6
  172. rasa/shared/core/flows/flows_yaml_schema.json +10 -0
  173. rasa/shared/core/flows/utils.py +39 -0
  174. rasa/shared/core/flows/validation.py +121 -0
  175. rasa/shared/core/flows/yaml_flows_io.py +15 -27
  176. rasa/shared/core/slots.py +5 -0
  177. rasa/shared/importers/importer.py +59 -41
  178. rasa/shared/importers/multi_project.py +23 -11
  179. rasa/shared/importers/rasa.py +12 -3
  180. rasa/shared/importers/remote_importer.py +196 -0
  181. rasa/shared/importers/utils.py +3 -1
  182. rasa/shared/nlu/training_data/formats/rasa_yaml.py +18 -3
  183. rasa/shared/nlu/training_data/training_data.py +18 -19
  184. rasa/shared/providers/_configs/litellm_router_client_config.py +220 -0
  185. rasa/shared/providers/_configs/model_group_config.py +167 -0
  186. rasa/shared/providers/_configs/openai_client_config.py +1 -1
  187. rasa/shared/providers/_configs/rasa_llm_client_config.py +73 -0
  188. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +1 -0
  189. rasa/shared/providers/_configs/utils.py +16 -0
  190. rasa/shared/providers/_utils.py +79 -0
  191. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +13 -29
  192. rasa/shared/providers/embedding/azure_openai_embedding_client.py +54 -21
  193. rasa/shared/providers/embedding/default_litellm_embedding_client.py +24 -0
  194. rasa/shared/providers/embedding/litellm_router_embedding_client.py +135 -0
  195. rasa/shared/providers/llm/_base_litellm_client.py +34 -22
  196. rasa/shared/providers/llm/azure_openai_llm_client.py +50 -29
  197. rasa/shared/providers/llm/default_litellm_llm_client.py +24 -0
  198. rasa/shared/providers/llm/litellm_router_llm_client.py +182 -0
  199. rasa/shared/providers/llm/rasa_llm_client.py +112 -0
  200. rasa/shared/providers/llm/self_hosted_llm_client.py +5 -29
  201. rasa/shared/providers/mappings.py +19 -0
  202. rasa/shared/providers/router/__init__.py +0 -0
  203. rasa/shared/providers/router/_base_litellm_router_client.py +183 -0
  204. rasa/shared/providers/router/router_client.py +73 -0
  205. rasa/shared/utils/common.py +40 -24
  206. rasa/shared/utils/health_check/__init__.py +0 -0
  207. rasa/shared/utils/health_check/embeddings_health_check_mixin.py +31 -0
  208. rasa/shared/utils/health_check/health_check.py +258 -0
  209. rasa/shared/utils/health_check/llm_health_check_mixin.py +31 -0
  210. rasa/shared/utils/io.py +27 -6
  211. rasa/shared/utils/llm.py +353 -43
  212. rasa/shared/utils/schemas/events.py +2 -0
  213. rasa/shared/utils/schemas/model_config.yml +0 -10
  214. rasa/shared/utils/yaml.py +181 -38
  215. rasa/studio/data_handler.py +3 -1
  216. rasa/studio/upload.py +160 -74
  217. rasa/telemetry.py +94 -17
  218. rasa/tracing/config.py +3 -1
  219. rasa/tracing/instrumentation/attribute_extractors.py +95 -18
  220. rasa/tracing/instrumentation/instrumentation.py +121 -0
  221. rasa/utils/common.py +5 -0
  222. rasa/utils/endpoints.py +27 -1
  223. rasa/utils/io.py +8 -16
  224. rasa/utils/log_utils.py +9 -2
  225. rasa/utils/sanic_error_handler.py +32 -0
  226. rasa/validator.py +110 -4
  227. rasa/version.py +1 -1
  228. {rasa_pro-3.10.15.dist-info → rasa_pro-3.11.0.dist-info}/METADATA +14 -12
  229. {rasa_pro-3.10.15.dist-info → rasa_pro-3.11.0.dist-info}/RECORD +234 -183
  230. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-1844e5a5.js +0 -1
  231. rasa/core/channels/inspector/dist/assets/index-a5d3e69d.js +0 -1040
  232. rasa/core/channels/voice_aware/utils.py +0 -20
  233. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +0 -407
  234. /rasa/core/channels/{voice_aware → voice_ready}/__init__.py +0 -0
  235. /rasa/core/channels/{voice_native → voice_stream}/__init__.py +0 -0
  236. {rasa_pro-3.10.15.dist-info → rasa_pro-3.11.0.dist-info}/NOTICE +0 -0
  237. {rasa_pro-3.10.15.dist-info → rasa_pro-3.11.0.dist-info}/WHEEL +0 -0
  238. {rasa_pro-3.10.15.dist-info → rasa_pro-3.11.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,23 @@
1
+ import asyncio
2
+ from contextvars import ContextVar
3
+ from werkzeug.local import LocalProxy
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+
8
+ # Per voice session data
9
+ # This is similar to how flask makes the "request" object available as a global variable
10
+ # It's a "global" variable that is local to an async task (i.e. websocket session)
11
+ @dataclass
12
+ class CallState:
13
+ is_user_speaking: bool = False
14
+ is_bot_speaking: bool = False
15
+ silence_timeout_watcher: Optional[asyncio.Task] = None
16
+ silence_timeout: Optional[float] = None
17
+ latest_bot_audio_id: Optional[str] = None
18
+ should_hangup: bool = False
19
+ connection_failed: bool = False
20
+
21
+
22
+ _call_state: ContextVar[CallState] = ContextVar("call_state")
23
+ call_state = LocalProxy(_call_state)
File without changes
@@ -0,0 +1,106 @@
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, ClientTimeout
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.constants import AZURE_SPEECH_API_KEY_ENV_VAR
16
+ from rasa.shared.exceptions import ConnectionException
17
+
18
+
19
+ structlogger = structlog.get_logger()
20
+
21
+
22
+ @dataclass
23
+ class AzureTTSConfig(TTSEngineConfig):
24
+ speech_region: Optional[str] = None
25
+
26
+
27
+ class AzureTTS(TTSEngine[AzureTTSConfig]):
28
+ session: Optional[aiohttp.ClientSession] = None
29
+ required_env_vars = (AZURE_SPEECH_API_KEY_ENV_VAR,)
30
+
31
+ def __init__(self, config: Optional[AzureTTSConfig] = None):
32
+ super().__init__(config)
33
+ timeout = ClientTimeout(total=self.config.timeout)
34
+ # Have to create this class-shared session lazily at run time otherwise
35
+ # the async event loop doesn't work
36
+ if self.__class__.session is None or self.__class__.session.closed:
37
+ self.__class__.session = aiohttp.ClientSession(timeout=timeout)
38
+
39
+ async def synthesize(
40
+ self, text: str, config: Optional[AzureTTSConfig] = None
41
+ ) -> AsyncIterator[RasaAudioBytes]:
42
+ """Generate speech from text using a remote TTS system."""
43
+ config = self.config.merge(config)
44
+ azure_speech_url = self.get_tts_endpoint(config)
45
+ headers = self.get_request_headers()
46
+ body = self.create_request_body(text, config)
47
+ if self.session is None:
48
+ raise ConnectionException("Client session is not initialized")
49
+ try:
50
+ async with self.session.post(
51
+ azure_speech_url, headers=headers, data=body, chunked=True
52
+ ) as response:
53
+ if 200 <= response.status < 300:
54
+ async for data in response.content.iter_chunked(1024):
55
+ yield self.engine_bytes_to_rasa_audio_bytes(data)
56
+ return
57
+ else:
58
+ structlogger.error(
59
+ "azure.synthesize.rest.failed",
60
+ status_code=response.status,
61
+ msg=response.text(),
62
+ )
63
+ raise TTSError(f"TTS failed: {response.text()}")
64
+ except ClientConnectorError as e:
65
+ raise TTSError(e)
66
+ except TimeoutError as e:
67
+ raise TTSError(e)
68
+
69
+ @staticmethod
70
+ def get_request_headers() -> dict[str, str]:
71
+ azure_speech_api_key = os.environ[AZURE_SPEECH_API_KEY_ENV_VAR]
72
+ return {
73
+ "Ocp-Apim-Subscription-Key": azure_speech_api_key,
74
+ "Content-Type": "application/ssml+xml",
75
+ "X-Microsoft-OutputFormat": "raw-8khz-8bit-mono-mulaw",
76
+ }
77
+
78
+ @staticmethod
79
+ def get_tts_endpoint(config: AzureTTSConfig) -> str:
80
+ return f"https://{config.speech_region}.tts.speech.microsoft.com/cognitiveservices/v1"
81
+
82
+ @staticmethod
83
+ def create_request_body(text: str, conf: AzureTTSConfig) -> str:
84
+ return f"""
85
+ <speak version='1.0' xml:lang='{conf.language}'>
86
+ <voice xml:lang='{conf.language}' name='{conf.voice}'>
87
+ {text}
88
+ </voice>
89
+ </speak>"""
90
+
91
+ def engine_bytes_to_rasa_audio_bytes(self, chunk: bytes) -> RasaAudioBytes:
92
+ """Convert the generated tts audio bytes into rasa audio bytes."""
93
+ return RasaAudioBytes(chunk)
94
+
95
+ @staticmethod
96
+ def get_default_config() -> AzureTTSConfig:
97
+ return AzureTTSConfig(
98
+ language="en-US",
99
+ voice="en-US-JennyNeural",
100
+ timeout=10,
101
+ speech_region="germanywestcentral",
102
+ )
103
+
104
+ @classmethod
105
+ def from_config_dict(cls, config: Dict) -> "AzureTTS":
106
+ return cls(AzureTTSConfig.from_dict(config))
@@ -0,0 +1,118 @@
1
+ from dataclasses import dataclass
2
+ from typing import AsyncIterator, Dict, Optional
3
+ import os
4
+ import aiohttp
5
+ import structlog
6
+ from aiohttp import ClientConnectorError, ClientTimeout
7
+
8
+ from rasa.core.channels.voice_stream.tts.tts_engine import (
9
+ TTSEngineConfig,
10
+ )
11
+
12
+ from rasa.core.channels.voice_stream.audio_bytes import HERTZ, RasaAudioBytes
13
+ from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine, TTSError
14
+ from rasa.shared.constants import CARTESIA_API_KEY_ENV_VAR
15
+ from rasa.shared.exceptions import ConnectionException
16
+
17
+ structlogger = structlog.get_logger()
18
+
19
+
20
+ @dataclass
21
+ class CartesiaTTSConfig(TTSEngineConfig):
22
+ model_id: Optional[str] = None
23
+ version: Optional[str] = None
24
+
25
+
26
+ class CartesiaTTS(TTSEngine[CartesiaTTSConfig]):
27
+ session: Optional[aiohttp.ClientSession] = None
28
+ required_env_vars = (CARTESIA_API_KEY_ENV_VAR,)
29
+
30
+ def __init__(self, config: Optional[CartesiaTTSConfig] = None):
31
+ super().__init__(config)
32
+ timeout = ClientTimeout(total=self.config.timeout)
33
+ # Have to create this class-shared session lazily at run time otherwise
34
+ # the async event loop doesn't work
35
+ if self.__class__.session is None or self.__class__.session.closed:
36
+ self.__class__.session = aiohttp.ClientSession(timeout=timeout)
37
+
38
+ @staticmethod
39
+ def get_tts_endpoint() -> str:
40
+ """Create the endpoint string for cartesia."""
41
+ return "https://api.cartesia.ai/tts/bytes"
42
+
43
+ @staticmethod
44
+ def get_request_body(text: str, config: CartesiaTTSConfig) -> Dict:
45
+ """Create the request body for cartesia."""
46
+ # more info on payload:
47
+ # https://docs.cartesia.ai/reference/api-reference/rest/stream-speech-bytes
48
+ return {
49
+ "model_id": config.model_id,
50
+ "transcript": text,
51
+ "language": config.language,
52
+ "voice": {
53
+ "mode": "id",
54
+ "id": config.voice,
55
+ },
56
+ "output_format": {
57
+ "container": "raw",
58
+ "encoding": "pcm_mulaw",
59
+ "sample_rate": HERTZ,
60
+ },
61
+ }
62
+
63
+ @staticmethod
64
+ def get_request_headers(config: CartesiaTTSConfig) -> dict[str, str]:
65
+ cartesia_api_key = os.environ[CARTESIA_API_KEY_ENV_VAR]
66
+ return {
67
+ "Cartesia-Version": str(config.version),
68
+ "Content-Type": "application/json",
69
+ "X-API-Key": str(cartesia_api_key),
70
+ }
71
+
72
+ async def synthesize(
73
+ self, text: str, config: Optional[CartesiaTTSConfig] = None
74
+ ) -> AsyncIterator[RasaAudioBytes]:
75
+ """Generate speech from text using a remote TTS system."""
76
+ config = self.config.merge(config)
77
+ payload = self.get_request_body(text, config)
78
+ headers = self.get_request_headers(config)
79
+ url = self.get_tts_endpoint()
80
+ if self.session is None:
81
+ raise ConnectionException("Client session is not initialized")
82
+ try:
83
+ async with self.session.post(
84
+ url, headers=headers, json=payload, chunked=True
85
+ ) as response:
86
+ if 200 <= response.status < 300:
87
+ async for data in response.content.iter_chunked(1024):
88
+ yield self.engine_bytes_to_rasa_audio_bytes(data)
89
+ return
90
+ else:
91
+ structlogger.error(
92
+ "cartesia.synthesize.rest.failed",
93
+ status_code=response.status,
94
+ msg=response.text(),
95
+ )
96
+ raise TTSError(f"TTS failed: {response.text()}")
97
+ except ClientConnectorError as e:
98
+ raise TTSError(e)
99
+ except TimeoutError as e:
100
+ raise TTSError(e)
101
+
102
+ def engine_bytes_to_rasa_audio_bytes(self, chunk: bytes) -> RasaAudioBytes:
103
+ """Convert the generated tts audio bytes into rasa audio bytes."""
104
+ return RasaAudioBytes(chunk)
105
+
106
+ @staticmethod
107
+ def get_default_config() -> CartesiaTTSConfig:
108
+ return CartesiaTTSConfig(
109
+ language="en",
110
+ voice="248be419-c632-4f23-adf1-5324ed7dbf1d",
111
+ timeout=10,
112
+ model_id="sonic-english",
113
+ version="2024-06-10",
114
+ )
115
+
116
+ @classmethod
117
+ def from_config_dict(cls, config: Dict) -> "CartesiaTTS":
118
+ return cls(CartesiaTTSConfig.from_dict(config))
@@ -0,0 +1,27 @@
1
+ from typing import Optional
2
+ from collections import OrderedDict
3
+ import logging
4
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class TTSCache:
10
+ """An LRU Cache for TTS based on pythons OrderedDict."""
11
+
12
+ def __init__(self, max_size: int):
13
+ self.cache: OrderedDict[str, RasaAudioBytes] = OrderedDict()
14
+ self.max_size = max_size
15
+
16
+ def get(self, text: str) -> Optional[RasaAudioBytes]:
17
+ if text not in self.cache:
18
+ return None
19
+ else:
20
+ self.cache.move_to_end(text)
21
+ return self.cache[text]
22
+
23
+ def put(self, text: str, audio_bytes: RasaAudioBytes) -> None:
24
+ self.cache[text] = audio_bytes
25
+ self.cache.move_to_end(text)
26
+ if len(self.cache) > self.max_size:
27
+ self.cache.popitem(last=False)
@@ -0,0 +1,58 @@
1
+ from typing import AsyncIterator, Dict, Generic, Optional, Tuple, Type, TypeVar
2
+ from dataclasses import dataclass
3
+
4
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
5
+ from rasa.core.channels.voice_stream.util import MergeableConfig
6
+ from rasa.shared.exceptions import RasaException
7
+ from rasa.shared.utils.common import validate_environment
8
+
9
+
10
+ class TTSError(RasaException):
11
+ pass
12
+
13
+
14
+ T = TypeVar("T", bound="TTSEngineConfig")
15
+ E = TypeVar("E", bound="TTSEngine")
16
+
17
+
18
+ @dataclass
19
+ class TTSEngineConfig(MergeableConfig):
20
+ language: Optional[str] = None
21
+ voice: Optional[str] = None
22
+ timeout: Optional[int] = None
23
+
24
+
25
+ class TTSEngine(Generic[T]):
26
+ required_env_vars: Tuple[str, ...] = ()
27
+ required_packages: Tuple[str, ...] = ()
28
+
29
+ def __init__(self, config: Optional[T] = None):
30
+ self.config = self.get_default_config().merge(config)
31
+ validate_environment(
32
+ self.required_env_vars,
33
+ self.required_packages,
34
+ f"TTS Engine {self.__class__.__name__}",
35
+ )
36
+
37
+ async def close_connection(self) -> None:
38
+ """Cleanup the connection if necessary."""
39
+ return
40
+
41
+ async def synthesize(
42
+ self, text: str, config: Optional[T] = None
43
+ ) -> AsyncIterator[RasaAudioBytes]:
44
+ """Generate speech from text using a remote TTS system."""
45
+ yield RasaAudioBytes(b"")
46
+
47
+ def engine_bytes_to_rasa_audio_bytes(self, chunk: bytes) -> RasaAudioBytes:
48
+ """Convert the generated tts audio bytes into rasa audio bytes."""
49
+ raise NotImplementedError
50
+
51
+ @staticmethod
52
+ def get_default_config() -> T:
53
+ """Get the default config for this component."""
54
+ raise NotImplementedError
55
+
56
+ @classmethod
57
+ def from_config_dict(cls: Type[E], config: Dict) -> E:
58
+ raise NotImplementedError
@@ -0,0 +1,173 @@
1
+ import base64
2
+ import json
3
+ import uuid
4
+
5
+ import structlog
6
+ from typing import Any, Awaitable, Callable, Dict, Optional, Text, Tuple
7
+
8
+ from sanic import Blueprint, HTTPResponse, Request, response
9
+ from sanic import Websocket # type: ignore
10
+
11
+
12
+ from rasa.core.channels import UserMessage
13
+ from rasa.core.channels.voice_ready.utils import CallParameters
14
+ from rasa.core.channels.voice_stream.call_state import call_state
15
+ from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine
16
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
17
+ from rasa.core.channels.voice_stream.voice_channel import (
18
+ EndConversationAction,
19
+ NewAudioAction,
20
+ VoiceChannelAction,
21
+ ContinueConversationAction,
22
+ VoiceInputChannel,
23
+ VoiceOutputChannel,
24
+ )
25
+
26
+ logger = structlog.get_logger(__name__)
27
+
28
+
29
+ def map_call_params(data: Dict[Text, Any]) -> CallParameters:
30
+ """Map the twilio stream parameters to the CallParameters dataclass."""
31
+ stream_sid = data["streamSid"]
32
+ parameters = data["start"]["customParameters"]
33
+ return CallParameters(
34
+ call_id=parameters.get("call_id", ""),
35
+ user_phone=parameters.get("user_phone", ""),
36
+ bot_phone=parameters.get("bot_phone", ""),
37
+ direction=parameters.get("direction"),
38
+ stream_id=stream_sid,
39
+ )
40
+
41
+
42
+ class TwilioMediaStreamsOutputChannel(VoiceOutputChannel):
43
+ @classmethod
44
+ def name(cls) -> str:
45
+ return "twilio_media_streams"
46
+
47
+ def rasa_audio_bytes_to_channel_bytes(
48
+ self, rasa_audio_bytes: RasaAudioBytes
49
+ ) -> bytes:
50
+ return base64.b64encode(rasa_audio_bytes)
51
+
52
+ def create_marker_message(self, recipient_id: str) -> Tuple[str, str]:
53
+ message_id = uuid.uuid4().hex
54
+ mark_message = json.dumps(
55
+ {
56
+ "event": "mark",
57
+ "streamSid": recipient_id,
58
+ "mark": {"name": message_id},
59
+ }
60
+ )
61
+ return mark_message, message_id
62
+
63
+ def channel_bytes_to_message(self, recipient_id: str, channel_bytes: bytes) -> str:
64
+ media_message = json.dumps(
65
+ {
66
+ "event": "media",
67
+ "streamSid": recipient_id,
68
+ "media": {
69
+ "payload": channel_bytes.decode("utf-8"),
70
+ },
71
+ }
72
+ )
73
+ return media_message
74
+
75
+
76
+ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
77
+ @classmethod
78
+ def name(cls) -> str:
79
+ return "twilio_media_streams"
80
+
81
+ def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
82
+ return RasaAudioBytes(base64.b64decode(input_bytes))
83
+
84
+ async def collect_call_parameters(
85
+ self, channel_websocket: Websocket
86
+ ) -> Optional[CallParameters]:
87
+ async for message in channel_websocket:
88
+ data = json.loads(message)
89
+ if data["event"] == "start":
90
+ # retrieve parameters set in the webhook - contains info about the
91
+ # caller
92
+ return map_call_params(data)
93
+ return None
94
+
95
+ def map_input_message(
96
+ self,
97
+ message: Any,
98
+ ) -> VoiceChannelAction:
99
+ data = json.loads(message)
100
+ if data["event"] == "media":
101
+ audio_bytes = self.channel_bytes_to_rasa_audio_bytes(
102
+ data["media"]["payload"]
103
+ )
104
+ return NewAudioAction(audio_bytes)
105
+ elif data["event"] == "stop":
106
+ return EndConversationAction()
107
+ elif data["event"] == "mark":
108
+ if data["mark"]["name"] == call_state.latest_bot_audio_id:
109
+ # Just finished streaming last audio bytes
110
+ call_state.is_bot_speaking = False # type: ignore[attr-defined]
111
+ if call_state.should_hangup:
112
+ logger.debug(
113
+ "twilio_streams.hangup", marker=call_state.latest_bot_audio_id
114
+ )
115
+ return EndConversationAction()
116
+ else:
117
+ call_state.is_bot_speaking = True # type: ignore[attr-defined]
118
+ return ContinueConversationAction()
119
+
120
+ def create_output_channel(
121
+ self, voice_websocket: Websocket, tts_engine: TTSEngine
122
+ ) -> VoiceOutputChannel:
123
+ return TwilioMediaStreamsOutputChannel(
124
+ voice_websocket,
125
+ tts_engine,
126
+ self.tts_cache,
127
+ )
128
+
129
+ def websocket_stream_url(self) -> str:
130
+ """Returns the websocket stream URL."""
131
+ # depending on the config value, the url might contain http as a
132
+ # protocol or not - we'll make sure both work
133
+ if self.server_url.startswith("http"):
134
+ base_url = self.server_url.replace("http", "ws")
135
+ else:
136
+ base_url = f"wss://{self.server_url}"
137
+ return f"{base_url}/webhooks/twilio_media_streams/websocket"
138
+
139
+ def blueprint(
140
+ self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
141
+ ) -> Blueprint:
142
+ """Defines a Sanic bluelogger.debug."""
143
+ blueprint = Blueprint("twilio_media_streams", __name__)
144
+
145
+ @blueprint.route("/", methods=["GET"])
146
+ async def health(_: Request) -> HTTPResponse:
147
+ return response.json({"status": "ok"})
148
+
149
+ @blueprint.route("/webhook", methods=["POST"])
150
+ async def receive(request: Request) -> HTTPResponse:
151
+ from twilio.twiml.voice_response import Connect, VoiceResponse
152
+
153
+ voice_response = VoiceResponse()
154
+ start = Connect()
155
+ stream = start.stream(url=self.websocket_stream_url())
156
+ # pass information about the call to the webhook - so we can
157
+ # store it in the input channel
158
+ stream.parameter(name="call_id", value=request.form.get("CallSid", None))
159
+ stream.parameter(name="user_phone", value=request.form.get("From", None))
160
+ stream.parameter(name="bot_phone", value=request.form.get("To", None))
161
+ stream.parameter(
162
+ name="direction", value=request.form.get("Direction", None)
163
+ )
164
+
165
+ voice_response.append(start)
166
+
167
+ return response.text(str(voice_response), content_type="text/xml")
168
+
169
+ @blueprint.websocket("/websocket") # type: ignore
170
+ async def handle_message(request: Request, ws: Websocket) -> None:
171
+ await self.run_audio_streaming(on_new_message, ws)
172
+
173
+ return blueprint
@@ -0,0 +1,57 @@
1
+ import wave
2
+ import audioop
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Optional, Type, TypeVar
5
+
6
+ import structlog
7
+
8
+ from rasa.core.channels.voice_stream.audio_bytes import HERTZ, RasaAudioBytes
9
+ from rasa.shared.exceptions import RasaException
10
+
11
+ structlogger = structlog.get_logger()
12
+
13
+
14
+ def read_wav_to_rasa_audio_bytes(file_name: str) -> Optional[RasaAudioBytes]:
15
+ """Reads rasa audio bytes from a file."""
16
+ if not file_name.endswith(".wav"):
17
+ raise RasaException("Should only read .wav files with this method.")
18
+ wave_object = wave.open(file_name, "rb")
19
+ wave_data = wave_object.readframes(wave_object.getnframes())
20
+ if wave_object.getnchannels() != 1:
21
+ wave_data = audioop.tomono(wave_data, wave_object.getsampwidth(), 1, 1)
22
+ if wave_object.getsampwidth() != 1:
23
+ wave_data = audioop.lin2lin(wave_data, wave_object.getsampwidth(), 1)
24
+ # 8 bit is unsigned
25
+ # wave_data = audioop.bias(wave_data, 1, 128)
26
+ if wave_object.getframerate() != HERTZ:
27
+ wave_data, _ = audioop.ratecv(
28
+ wave_data, 1, 1, wave_object.getframerate(), HERTZ, None
29
+ )
30
+ wave_data = audioop.lin2ulaw(wave_data, 1)
31
+ return RasaAudioBytes(wave_data)
32
+
33
+
34
+ def generate_silence(length_in_seconds: float = 1.0) -> RasaAudioBytes:
35
+ return RasaAudioBytes(b"\00" * int(length_in_seconds * HERTZ))
36
+
37
+
38
+ T = TypeVar("T", bound="MergeableConfig")
39
+
40
+
41
+ @dataclass
42
+ class MergeableConfig:
43
+ def __init__(self) -> None:
44
+ pass
45
+
46
+ def merge(self: T, other: Optional[T]) -> T:
47
+ """Merges two configs while dropping None values of the second config."""
48
+ if other is None:
49
+ return self
50
+ other_dict = asdict(other)
51
+ other_dict_clean = {k: v for k, v in other_dict.items() if v is not None}
52
+ merged = {**asdict(self), **other_dict_clean}
53
+ return self.from_dict(merged)
54
+
55
+ @classmethod
56
+ def from_dict(cls: Type[T], data: dict[str, Optional[str]]) -> T:
57
+ return cls(**data)