rasa-pro 3.12.0.dev13__py3-none-any.whl → 3.12.0rc2__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 (139) hide show
  1. README.md +10 -13
  2. rasa/anonymization/anonymization_rule_executor.py +16 -10
  3. rasa/cli/data.py +16 -0
  4. rasa/cli/project_templates/calm/config.yml +2 -2
  5. rasa/cli/project_templates/calm/domain/list_contacts.yml +1 -2
  6. rasa/cli/project_templates/calm/domain/remove_contact.yml +1 -2
  7. rasa/cli/project_templates/calm/domain/shared.yml +1 -4
  8. rasa/cli/project_templates/calm/endpoints.yml +2 -2
  9. rasa/cli/utils.py +12 -0
  10. rasa/core/actions/action.py +84 -191
  11. rasa/core/actions/action_handle_digressions.py +35 -13
  12. rasa/core/actions/action_run_slot_rejections.py +16 -4
  13. rasa/core/channels/__init__.py +2 -0
  14. rasa/core/channels/studio_chat.py +19 -0
  15. rasa/core/channels/telegram.py +42 -24
  16. rasa/core/channels/voice_ready/utils.py +1 -1
  17. rasa/core/channels/voice_stream/asr/asr_engine.py +10 -4
  18. rasa/core/channels/voice_stream/asr/azure.py +14 -1
  19. rasa/core/channels/voice_stream/asr/deepgram.py +20 -4
  20. rasa/core/channels/voice_stream/audiocodes.py +264 -0
  21. rasa/core/channels/voice_stream/browser_audio.py +4 -1
  22. rasa/core/channels/voice_stream/call_state.py +3 -0
  23. rasa/core/channels/voice_stream/genesys.py +6 -2
  24. rasa/core/channels/voice_stream/tts/azure.py +9 -1
  25. rasa/core/channels/voice_stream/tts/cartesia.py +14 -8
  26. rasa/core/channels/voice_stream/voice_channel.py +23 -2
  27. rasa/core/constants.py +2 -0
  28. rasa/core/nlg/contextual_response_rephraser.py +18 -1
  29. rasa/core/nlg/generator.py +83 -15
  30. rasa/core/nlg/response.py +6 -3
  31. rasa/core/nlg/translate.py +55 -0
  32. rasa/core/policies/enterprise_search_prompt_with_citation_template.jinja2 +1 -1
  33. rasa/core/policies/flows/flow_executor.py +19 -7
  34. rasa/core/processor.py +71 -9
  35. rasa/dialogue_understanding/commands/can_not_handle_command.py +20 -2
  36. rasa/dialogue_understanding/commands/cancel_flow_command.py +24 -6
  37. rasa/dialogue_understanding/commands/change_flow_command.py +20 -2
  38. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +20 -2
  39. rasa/dialogue_understanding/commands/clarify_command.py +29 -3
  40. rasa/dialogue_understanding/commands/command.py +1 -16
  41. rasa/dialogue_understanding/commands/command_syntax_manager.py +55 -0
  42. rasa/dialogue_understanding/commands/handle_digressions_command.py +1 -7
  43. rasa/dialogue_understanding/commands/human_handoff_command.py +20 -2
  44. rasa/dialogue_understanding/commands/knowledge_answer_command.py +20 -2
  45. rasa/dialogue_understanding/commands/prompt_command.py +94 -0
  46. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +20 -2
  47. rasa/dialogue_understanding/commands/set_slot_command.py +24 -2
  48. rasa/dialogue_understanding/commands/skip_question_command.py +20 -2
  49. rasa/dialogue_understanding/commands/start_flow_command.py +22 -2
  50. rasa/dialogue_understanding/commands/utils.py +71 -4
  51. rasa/dialogue_understanding/generator/__init__.py +2 -0
  52. rasa/dialogue_understanding/generator/command_parser.py +15 -12
  53. rasa/dialogue_understanding/generator/constants.py +3 -0
  54. rasa/dialogue_understanding/generator/llm_based_command_generator.py +12 -5
  55. rasa/dialogue_understanding/generator/llm_command_generator.py +5 -3
  56. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +17 -3
  57. rasa/dialogue_understanding/generator/prompt_templates/__init__.py +0 -0
  58. rasa/dialogue_understanding/generator/{single_step → prompt_templates}/command_prompt_template.jinja2 +2 -0
  59. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +77 -0
  60. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_default.jinja2 +68 -0
  61. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +84 -0
  62. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +522 -0
  63. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +12 -310
  64. rasa/dialogue_understanding/patterns/collect_information.py +1 -1
  65. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +16 -0
  66. rasa/dialogue_understanding/patterns/validate_slot.py +65 -0
  67. rasa/dialogue_understanding/processor/command_processor.py +39 -0
  68. rasa/dialogue_understanding/stack/utils.py +38 -0
  69. rasa/dialogue_understanding_test/du_test_case.py +58 -18
  70. rasa/dialogue_understanding_test/du_test_result.py +14 -10
  71. rasa/dialogue_understanding_test/io.py +14 -0
  72. rasa/e2e_test/assertions.py +6 -8
  73. rasa/e2e_test/llm_judge_prompts/answer_relevance_prompt_template.jinja2 +5 -1
  74. rasa/e2e_test/llm_judge_prompts/groundedness_prompt_template.jinja2 +4 -0
  75. rasa/e2e_test/utils/io.py +0 -37
  76. rasa/engine/graph.py +1 -0
  77. rasa/engine/language.py +140 -0
  78. rasa/engine/recipes/config_files/default_config.yml +4 -0
  79. rasa/engine/recipes/default_recipe.py +2 -0
  80. rasa/engine/recipes/graph_recipe.py +2 -0
  81. rasa/engine/storage/local_model_storage.py +1 -0
  82. rasa/engine/storage/storage.py +4 -1
  83. rasa/llm_fine_tuning/conversations.py +1 -1
  84. rasa/model_manager/runner_service.py +7 -4
  85. rasa/model_manager/socket_bridge.py +7 -6
  86. rasa/shared/constants.py +15 -13
  87. rasa/shared/core/constants.py +2 -0
  88. rasa/shared/core/flows/constants.py +11 -0
  89. rasa/shared/core/flows/flow.py +83 -19
  90. rasa/shared/core/flows/flows_yaml_schema.json +31 -3
  91. rasa/shared/core/flows/steps/collect.py +1 -36
  92. rasa/shared/core/flows/utils.py +28 -4
  93. rasa/shared/core/flows/validation.py +1 -1
  94. rasa/shared/core/slot_mappings.py +208 -5
  95. rasa/shared/core/slots.py +137 -1
  96. rasa/shared/core/trackers.py +74 -1
  97. rasa/shared/importers/importer.py +50 -2
  98. rasa/shared/nlu/training_data/schemas/responses.yml +19 -12
  99. rasa/shared/providers/_configs/azure_entra_id_config.py +541 -0
  100. rasa/shared/providers/_configs/azure_openai_client_config.py +138 -3
  101. rasa/shared/providers/_configs/client_config.py +3 -1
  102. rasa/shared/providers/_configs/default_litellm_client_config.py +3 -1
  103. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +3 -1
  104. rasa/shared/providers/_configs/litellm_router_client_config.py +3 -1
  105. rasa/shared/providers/_configs/model_group_config.py +4 -2
  106. rasa/shared/providers/_configs/oauth_config.py +33 -0
  107. rasa/shared/providers/_configs/openai_client_config.py +3 -1
  108. rasa/shared/providers/_configs/rasa_llm_client_config.py +3 -1
  109. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +3 -1
  110. rasa/shared/providers/constants.py +6 -0
  111. rasa/shared/providers/embedding/azure_openai_embedding_client.py +28 -3
  112. rasa/shared/providers/embedding/litellm_router_embedding_client.py +3 -1
  113. rasa/shared/providers/llm/_base_litellm_client.py +42 -17
  114. rasa/shared/providers/llm/azure_openai_llm_client.py +81 -25
  115. rasa/shared/providers/llm/default_litellm_llm_client.py +3 -1
  116. rasa/shared/providers/llm/litellm_router_llm_client.py +29 -8
  117. rasa/shared/providers/llm/llm_client.py +23 -7
  118. rasa/shared/providers/llm/openai_llm_client.py +9 -3
  119. rasa/shared/providers/llm/rasa_llm_client.py +11 -2
  120. rasa/shared/providers/llm/self_hosted_llm_client.py +30 -11
  121. rasa/shared/providers/router/_base_litellm_router_client.py +3 -1
  122. rasa/shared/providers/router/router_client.py +3 -1
  123. rasa/shared/utils/constants.py +3 -0
  124. rasa/shared/utils/llm.py +33 -7
  125. rasa/shared/utils/pykwalify_extensions.py +24 -0
  126. rasa/shared/utils/schemas/domain.yml +26 -0
  127. rasa/telemetry.py +2 -1
  128. rasa/tracing/config.py +2 -0
  129. rasa/tracing/constants.py +12 -0
  130. rasa/tracing/instrumentation/instrumentation.py +36 -0
  131. rasa/tracing/instrumentation/metrics.py +41 -0
  132. rasa/tracing/metric_instrument_provider.py +40 -0
  133. rasa/validator.py +372 -7
  134. rasa/version.py +1 -1
  135. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/METADATA +13 -14
  136. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/RECORD +139 -124
  137. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/NOTICE +0 -0
  138. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/WHEEL +0 -0
  139. {rasa_pro-3.12.0.dev13.dist-info → rasa_pro-3.12.0rc2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,264 @@
1
+ import asyncio
2
+ import base64
3
+ import json
4
+ from typing import Any, Awaitable, Callable, Dict, Optional, Text
5
+
6
+ import structlog
7
+ from sanic import ( # type: ignore[attr-defined]
8
+ Blueprint,
9
+ HTTPResponse,
10
+ Request,
11
+ Websocket,
12
+ response,
13
+ )
14
+
15
+ from rasa.core.channels import UserMessage
16
+ from rasa.core.channels.voice_ready.utils import CallParameters
17
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
18
+ from rasa.core.channels.voice_stream.call_state import (
19
+ call_state,
20
+ )
21
+ from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine
22
+ from rasa.core.channels.voice_stream.voice_channel import (
23
+ ContinueConversationAction,
24
+ EndConversationAction,
25
+ NewAudioAction,
26
+ VoiceChannelAction,
27
+ VoiceInputChannel,
28
+ VoiceOutputChannel,
29
+ )
30
+
31
+ logger = structlog.get_logger(__name__)
32
+
33
+
34
+ def map_call_params(data: Dict[Text, Any]) -> CallParameters:
35
+ """Map the audiocodes stream parameters to the CallParameters dataclass."""
36
+ return CallParameters(
37
+ call_id=data["conversationId"],
38
+ user_phone=data["caller"],
39
+ # Bot phone is not available in the Audiocodes API
40
+ direction="inbound", # AudioCodes calls are always inbound
41
+ )
42
+
43
+
44
+ class AudiocodesVoiceOutputChannel(VoiceOutputChannel):
45
+ @classmethod
46
+ def name(cls) -> str:
47
+ return "ac_voice"
48
+
49
+ def rasa_audio_bytes_to_channel_bytes(
50
+ self, rasa_audio_bytes: RasaAudioBytes
51
+ ) -> bytes:
52
+ return base64.b64encode(rasa_audio_bytes)
53
+
54
+ def channel_bytes_to_message(self, recipient_id: str, channel_bytes: bytes) -> str:
55
+ media_message = json.dumps(
56
+ {
57
+ "type": "playStream.chunk",
58
+ "streamId": str(call_state.stream_id),
59
+ "audioChunk": channel_bytes.decode("utf-8"),
60
+ }
61
+ )
62
+ return media_message
63
+
64
+ async def send_start_marker(self, recipient_id: str) -> None:
65
+ """Send playStream.start before first audio chunk."""
66
+ call_state.stream_id += 1 # type: ignore[attr-defined]
67
+ media_message = json.dumps(
68
+ {
69
+ "type": "playStream.start",
70
+ "streamId": str(call_state.stream_id),
71
+ }
72
+ )
73
+ logger.debug("Sending start marker", stream_id=call_state.stream_id)
74
+ await self.voice_websocket.send(media_message)
75
+
76
+ async def send_intermediate_marker(self, recipient_id: str) -> None:
77
+ """Audiocodes doesn't need intermediate markers, so do nothing."""
78
+ pass
79
+
80
+ async def send_end_marker(self, recipient_id: str) -> None:
81
+ """Send playStream.stop after last audio chunk."""
82
+ media_message = json.dumps(
83
+ {
84
+ "type": "playStream.stop",
85
+ "streamId": str(call_state.stream_id),
86
+ }
87
+ )
88
+ logger.debug("Sending end marker", stream_id=call_state.stream_id)
89
+ await self.voice_websocket.send(media_message)
90
+
91
+
92
+ class AudiocodesVoiceInputChannel(VoiceInputChannel):
93
+ @classmethod
94
+ def name(cls) -> str:
95
+ return "ac_voice"
96
+
97
+ def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
98
+ return RasaAudioBytes(base64.b64decode(input_bytes))
99
+
100
+ async def collect_call_parameters(
101
+ self, channel_websocket: Websocket
102
+ ) -> Optional[CallParameters]:
103
+ async for message in channel_websocket:
104
+ data = json.loads(message)
105
+ if data["type"] == "session.initiate":
106
+ # retrieve parameters set in the webhook - contains info about the
107
+ # caller
108
+ logger.info("received initiate message", data=data)
109
+ self._send_accepted(channel_websocket, data)
110
+ return map_call_params(data)
111
+ else:
112
+ logger.warning("ac_voice.unknown_message", data=data)
113
+ return None
114
+
115
+ def map_input_message(
116
+ self,
117
+ message: Any,
118
+ ws: Websocket,
119
+ ) -> VoiceChannelAction:
120
+ data = json.loads(message)
121
+ if data["type"] == "activities":
122
+ activities = data["activities"]
123
+ for activity in activities:
124
+ logger.debug("ac_voice.activity", data=activity)
125
+ if activity["name"] == "start":
126
+ pass
127
+ elif activity["name"] == "dtmf":
128
+ # TODO: handle DTMF input
129
+ pass
130
+ elif activity["name"] == "playFinished":
131
+ logger.debug("ac_voice.playFinished", data=activity)
132
+ if call_state.should_hangup:
133
+ logger.info("audiocodes.hangup")
134
+ self._send_hangup(ws, data)
135
+ # the conversation should continue until
136
+ # we receive a end message from audiocodes
137
+ pass
138
+ else:
139
+ logger.warning("ac_voice.unknown_activity", data=activity)
140
+ elif data["type"] == "userStream.start":
141
+ logger.debug("ac_voice.userStream.start", data=data)
142
+ self._send_recognition_started(ws, data)
143
+ elif data["type"] == "userStream.chunk":
144
+ audio_bytes = self.channel_bytes_to_rasa_audio_bytes(data["audioChunk"])
145
+ return NewAudioAction(audio_bytes)
146
+ elif data["type"] == "userStream.stop":
147
+ logger.debug("ac_voice.stop_recognition", data=data)
148
+ self._send_recognition_ended(ws, data)
149
+ elif data["type"] == "session.resume":
150
+ logger.debug("ac_voice.resume", data=data)
151
+ self._send_accepted(ws, data)
152
+ elif data["type"] == "session.end":
153
+ logger.debug("ac_voice.end", data=data)
154
+ return EndConversationAction()
155
+ elif data["type"] == "connection.validate":
156
+ # not part of call flow; only sent when integration is created
157
+ self._send_validated(ws, data)
158
+ else:
159
+ logger.warning("ac_voice.unknown_message", data=data)
160
+
161
+ return ContinueConversationAction()
162
+
163
+ def _send_accepted(self, ws: Websocket, data: Dict[Text, Any]) -> None:
164
+ supported_formats = data.get("supportedMediaFormats", [])
165
+ preferred_format = "raw/mulaw"
166
+
167
+ if preferred_format not in supported_formats:
168
+ logger.warning(
169
+ "ac_voice.format_not_supported",
170
+ supported_formats=supported_formats,
171
+ preferred_format=preferred_format,
172
+ )
173
+ raise
174
+
175
+ payload = {
176
+ "type": "session.accepted",
177
+ "mediaFormat": "raw/mulaw",
178
+ }
179
+ _schedule_async_task(ws.send(json.dumps(payload)))
180
+
181
+ def _send_recognition_started(self, ws: Websocket, data: Dict[Text, Any]) -> None:
182
+ payload = {"type": "userStream.started"}
183
+ _schedule_async_task(ws.send(json.dumps(payload)))
184
+
185
+ def _send_recognition_ended(self, ws: Websocket, data: Dict[Text, Any]) -> None:
186
+ payload = {"type": "userStream.stopped"}
187
+ _schedule_async_task(ws.send(json.dumps(payload)))
188
+
189
+ def _send_hypothesis(self, ws: Websocket, data: Dict[Text, Any]) -> None:
190
+ """
191
+ TODO: The hypothesis message is sent by the bot to provide partial
192
+ recognition results. Using this message is recommended,
193
+ as VAIC relies on it for performing barge-in.
194
+ """
195
+ pass
196
+
197
+ def _send_recognition(self, ws: Websocket, data: Dict[Text, Any]) -> None:
198
+ """
199
+ TODO: The recognition message is sent by the bot to provide
200
+ the final recognition result. Using this message is recommended
201
+ mainly for logging purposes.
202
+ """
203
+ pass
204
+
205
+ def _send_hangup(self, ws: Websocket, data: Dict[Text, Any]) -> None:
206
+ payload = {
207
+ "conversationId": data["conversationId"],
208
+ "type": "activities",
209
+ "activities": [{"type": "event", "name": "hangup"}],
210
+ }
211
+ _schedule_async_task(ws.send(json.dumps(payload)))
212
+
213
+ def _send_validated(self, ws: Websocket, data: Dict[Text, Any]) -> None:
214
+ payload = {
215
+ "type": "connection.validated",
216
+ "success": True,
217
+ }
218
+ _schedule_async_task(ws.send(json.dumps(payload)))
219
+
220
+ def create_output_channel(
221
+ self, voice_websocket: Websocket, tts_engine: TTSEngine
222
+ ) -> VoiceOutputChannel:
223
+ return AudiocodesVoiceOutputChannel(
224
+ voice_websocket,
225
+ tts_engine,
226
+ self.tts_cache,
227
+ )
228
+
229
+ def blueprint(
230
+ self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
231
+ ) -> Blueprint:
232
+ """Defines a Sanic bluelogger.debug."""
233
+ blueprint = Blueprint("ac_voice", __name__)
234
+
235
+ @blueprint.route("/", methods=["GET"])
236
+ async def health(_: Request) -> HTTPResponse:
237
+ return response.json({"status": "ok"})
238
+
239
+ @blueprint.websocket("/websocket") # type: ignore
240
+ async def receive(request: Request, ws: Websocket) -> None:
241
+ # TODO: validate API key header
242
+ logger.info("audiocodes.receive", message="Starting audio streaming")
243
+ try:
244
+ await self.run_audio_streaming(on_new_message, ws)
245
+ except Exception as e:
246
+ logger.exception(
247
+ "audiocodes.receive",
248
+ message="Error during audio streaming",
249
+ error=e,
250
+ )
251
+ # return 500 error
252
+ raise
253
+
254
+ return blueprint
255
+
256
+
257
+ def _schedule_async_task(coro: Awaitable[Any]) -> None:
258
+ """Helper function to schedule a coroutine in the event loop.
259
+
260
+ Args:
261
+ coro: The coroutine to schedule
262
+ """
263
+ loop = asyncio.get_running_loop()
264
+ loop.call_soon_threadsafe(lambda: loop.create_task(coro))
@@ -106,6 +106,9 @@ class BrowserAudioInputChannel(VoiceInputChannel):
106
106
 
107
107
  @blueprint.websocket("/websocket") # type: ignore
108
108
  async def handle_message(request: Request, ws: Websocket) -> None:
109
- await self.run_audio_streaming(on_new_message, ws)
109
+ try:
110
+ await self.run_audio_streaming(on_new_message, ws)
111
+ except Exception as e:
112
+ logger.error("browser_audio.handle_message.error", error=e)
110
113
 
111
114
  return blueprint
@@ -25,6 +25,9 @@ class CallState:
25
25
  server_sequence_number: int = 0
26
26
  audio_buffer: bytearray = field(default_factory=bytearray)
27
27
 
28
+ # Audiocodes requires a stream ID at start and end of stream
29
+ stream_id: int = 0
30
+
28
31
 
29
32
  _call_state: ContextVar[CallState] = ContextVar("call_state")
30
33
  call_state = LocalProxy(_call_state)
@@ -104,7 +104,10 @@ class GenesysOutputChannel(VoiceOutputChannel):
104
104
  current_position = end_position
105
105
 
106
106
  async def send_marker_message(self, recipient_id: str) -> None:
107
- """Send a message that marks positions in the audio stream."""
107
+ """
108
+ Send a message that marks positions in the audio stream.
109
+ Genesys does not support this feature, so we do nothing here.
110
+ """
108
111
  pass
109
112
 
110
113
 
@@ -190,6 +193,8 @@ class GenesysInputChannel(VoiceInputChannel):
190
193
  if call_state.should_hangup:
191
194
  logger.info("genesys.hangup")
192
195
  self.disconnect(ws, data)
196
+ # the conversation should continue until
197
+ # we receive a close message from Genesys
193
198
  elif msg_type == "dtmf":
194
199
  logger.info("genesys.handle_dtmf", message=data)
195
200
  elif msg_type == "error":
@@ -259,7 +264,6 @@ class GenesysInputChannel(VoiceInputChannel):
259
264
  logger.debug("genesys.handle_close.closed", response=response)
260
265
 
261
266
  _schedule_ws_task(ws.send(json.dumps(response)))
262
- _schedule_ws_task(ws.close())
263
267
 
264
268
  def disconnect(self, ws: Websocket, data: dict) -> None:
265
269
  """
@@ -21,6 +21,7 @@ structlogger = structlog.get_logger()
21
21
  @dataclass
22
22
  class AzureTTSConfig(TTSEngineConfig):
23
23
  speech_region: Optional[str] = None
24
+ endpoint: Optional[str] = None
24
25
 
25
26
 
26
27
  class AzureTTS(TTSEngine[AzureTTSConfig]):
@@ -76,7 +77,13 @@ class AzureTTS(TTSEngine[AzureTTSConfig]):
76
77
 
77
78
  @staticmethod
78
79
  def get_tts_endpoint(config: AzureTTSConfig) -> str:
79
- return f"https://{config.speech_region}.tts.speech.microsoft.com/cognitiveservices/v1"
80
+ if config.endpoint is not None:
81
+ return config.endpoint
82
+ else:
83
+ return (
84
+ f"https://{config.speech_region}.tts.speech.microsoft.com/"
85
+ f"cognitiveservices/v1"
86
+ )
80
87
 
81
88
  @staticmethod
82
89
  def create_request_body(text: str, conf: AzureTTSConfig) -> str:
@@ -99,6 +106,7 @@ class AzureTTS(TTSEngine[AzureTTSConfig]):
99
106
  voice="en-US-JennyNeural",
100
107
  timeout=10,
101
108
  speech_region="eastus",
109
+ endpoint=None,
102
110
  )
103
111
 
104
112
  @classmethod
@@ -24,6 +24,7 @@ structlogger = structlog.get_logger()
24
24
  class CartesiaTTSConfig(TTSEngineConfig):
25
25
  model_id: Optional[str] = None
26
26
  version: Optional[str] = None
27
+ endpoint: Optional[str] = None
27
28
 
28
29
 
29
30
  class CartesiaTTS(TTSEngine[CartesiaTTSConfig]):
@@ -38,11 +39,6 @@ class CartesiaTTS(TTSEngine[CartesiaTTSConfig]):
38
39
  if self.__class__.session is None or self.__class__.session.closed:
39
40
  self.__class__.session = aiohttp.ClientSession(timeout=timeout)
40
41
 
41
- @staticmethod
42
- def get_tts_endpoint() -> str:
43
- """Create the endpoint string for cartesia."""
44
- return "https://api.cartesia.ai/tts/sse"
45
-
46
42
  @staticmethod
47
43
  def get_request_body(text: str, config: CartesiaTTSConfig) -> Dict:
48
44
  """Create the request body for cartesia."""
@@ -79,7 +75,7 @@ class CartesiaTTS(TTSEngine[CartesiaTTSConfig]):
79
75
  config = self.config.merge(config)
80
76
  payload = self.get_request_body(text, config)
81
77
  headers = self.get_request_headers(config)
82
- url = self.get_tts_endpoint()
78
+ url = self.config.endpoint
83
79
  if self.session is None:
84
80
  raise ConnectionException("Client session is not initialized")
85
81
  try:
@@ -101,13 +97,22 @@ class CartesiaTTS(TTSEngine[CartesiaTTSConfig]):
101
97
  channel_bytes
102
98
  )
103
99
  return
100
+ elif response.status == 401:
101
+ structlogger.error(
102
+ "cartesia.synthesize.rest.unauthorized",
103
+ status_code=response.status,
104
+ )
105
+ raise TTSError(
106
+ "Unauthorized. Please make sure you have the correct API key."
107
+ )
104
108
  else:
109
+ response_text = await response.text()
105
110
  structlogger.error(
106
111
  "cartesia.synthesize.rest.failed",
107
112
  status_code=response.status,
108
- msg=response.text(),
113
+ msg=response_text,
109
114
  )
110
- raise TTSError(f"TTS failed: {response.text()}")
115
+ raise TTSError(f"TTS failed: {response_text}")
111
116
  except ClientConnectorError as e:
112
117
  raise TTSError(e)
113
118
  except TimeoutError as e:
@@ -125,6 +130,7 @@ class CartesiaTTS(TTSEngine[CartesiaTTSConfig]):
125
130
  timeout=10,
126
131
  model_id="sonic-english",
127
132
  version="2024-06-10",
133
+ endpoint="https://api.cartesia.ai/tts/sse",
128
134
  )
129
135
 
130
136
  @classmethod
@@ -148,6 +148,19 @@ class VoiceOutputChannel(OutputChannel):
148
148
  await self.voice_websocket.send(marker_message)
149
149
  self.latest_message_id = mark_id
150
150
 
151
+ async def send_start_marker(self, recipient_id: str) -> None:
152
+ """Send a marker message before the first audio chunk."""
153
+ # Default implementation uses the generic marker message
154
+ await self.send_marker_message(recipient_id)
155
+
156
+ async def send_intermediate_marker(self, recipient_id: str) -> None:
157
+ """Send a marker message during audio streaming."""
158
+ await self.send_marker_message(recipient_id)
159
+
160
+ async def send_end_marker(self, recipient_id: str) -> None:
161
+ """Send a marker message after the last audio chunk."""
162
+ await self.send_marker_message(recipient_id)
163
+
151
164
  def update_silence_timeout(self) -> None:
152
165
  """Updates the silence timeout for the session."""
153
166
  if self.tracker_state:
@@ -173,6 +186,13 @@ class VoiceOutputChannel(OutputChannel):
173
186
  cached_audio_bytes = self.tts_cache.get(text)
174
187
  collected_audio_bytes = RasaAudioBytes(b"")
175
188
  seconds_marker = -1
189
+
190
+ # Send start marker before first chunk
191
+ try:
192
+ await self.send_start_marker(recipient_id)
193
+ except (WebsocketClosed, ServerError):
194
+ call_state.connection_failed = True # type: ignore[attr-defined]
195
+
176
196
  if cached_audio_bytes:
177
197
  audio_stream = self.chunk_audio(cached_audio_bytes)
178
198
  else:
@@ -189,15 +209,16 @@ class VoiceOutputChannel(OutputChannel):
189
209
  await self.send_audio_bytes(recipient_id, audio_bytes)
190
210
  full_seconds_of_audio = len(collected_audio_bytes) // HERTZ
191
211
  if full_seconds_of_audio > seconds_marker:
192
- await self.send_marker_message(recipient_id)
212
+ await self.send_intermediate_marker(recipient_id)
193
213
  seconds_marker = full_seconds_of_audio
194
214
 
195
215
  except (WebsocketClosed, ServerError):
196
216
  # ignore sending error, and keep collecting and caching audio bytes
197
217
  call_state.connection_failed = True # type: ignore[attr-defined]
198
218
  collected_audio_bytes = RasaAudioBytes(collected_audio_bytes + audio_bytes)
219
+
199
220
  try:
200
- await self.send_marker_message(recipient_id)
221
+ await self.send_end_marker(recipient_id)
201
222
  except (WebsocketClosed, ServerError):
202
223
  # ignore sending error
203
224
  pass
rasa/core/constants.py CHANGED
@@ -110,3 +110,5 @@ UTTER_SOURCE_METADATA_KEY = "utter_source"
110
110
  DOMAIN_GROUND_TRUTH_METADATA_KEY = "domain_ground_truth"
111
111
  ACTIVE_FLOW_METADATA_KEY = "active_flow"
112
112
  STEP_ID_METADATA_KEY = "step_id"
113
+ KEY_IS_CALM_SYSTEM = "is_calm_system"
114
+ KEY_IS_COEXISTENCE_ASSISTANT = "is_coexistence_assistant"
@@ -64,7 +64,7 @@ DEFAULT_LLM_CONFIG = {
64
64
  DEFAULT_RESPONSE_VARIATION_PROMPT_TEMPLATE = """The following is a conversation with
65
65
  an AI assistant. The assistant is helpful, creative, clever, and very friendly.
66
66
  Rephrase the suggested AI response staying close to the original message and retaining
67
- its meaning. Use simple english.
67
+ its meaning. Use simple {{language}}.
68
68
 
69
69
  Context / previous conversation with the user:
70
70
  {{history}}
@@ -164,6 +164,22 @@ class ContextualResponseRephraser(
164
164
  response[PROMPTS] = prompts
165
165
  return response
166
166
 
167
+ @staticmethod
168
+ def get_language_label(tracker: DialogueStateTracker) -> str:
169
+ """Fetches the label of the language to be used for the rephraser.
170
+
171
+ Args:
172
+ tracker: The tracker to get the language from.
173
+
174
+ Returns:
175
+ The label of the current language, or "English" if no language is set.
176
+ """
177
+ return (
178
+ tracker.current_language.label
179
+ if tracker.current_language
180
+ else tracker.default_language.label
181
+ )
182
+
167
183
  def _last_message_if_human(self, tracker: DialogueStateTracker) -> Optional[str]:
168
184
  """Returns the latest message from the tracker.
169
185
 
@@ -281,6 +297,7 @@ class ContextualResponseRephraser(
281
297
  suggested_response=response_text,
282
298
  current_input=current_input,
283
299
  slots=tracker.current_slot_values(),
300
+ language=self.get_language_label(tracker),
284
301
  )
285
302
  log_llm(
286
303
  logger=structlogger,
@@ -1,6 +1,9 @@
1
- import logging
2
1
  from typing import Any, Dict, List, Optional, Text, Union
3
2
 
3
+ import structlog
4
+ from jinja2 import Template
5
+ from pypred import Predicate
6
+
4
7
  import rasa.shared.utils.common
5
8
  import rasa.shared.utils.io
6
9
  from rasa.shared.constants import CHANNEL, RESPONSE_CONDITION
@@ -8,7 +11,7 @@ from rasa.shared.core.domain import Domain
8
11
  from rasa.shared.core.trackers import DialogueStateTracker
9
12
  from rasa.utils.endpoints import EndpointConfig
10
13
 
11
- logger = logging.getLogger(__name__)
14
+ structlogger = structlog.get_logger()
12
15
 
13
16
 
14
17
  class NaturalLanguageGenerator:
@@ -74,7 +77,11 @@ def _create_from_endpoint_config(
74
77
  else:
75
78
  nlg = _load_from_module_name_in_endpoint_config(endpoint_config, domain)
76
79
 
77
- logger.debug(f"Instantiated NLG to '{nlg.__class__.__name__}'.")
80
+ structlogger.debug(
81
+ "rasa.core.nlg.generator.create",
82
+ nlg_class_name=nlg.__class__.__name__,
83
+ event_info=f"Instantiated NLG to '{nlg.__class__.__name__}'.",
84
+ )
78
85
  return nlg
79
86
 
80
87
 
@@ -112,18 +119,15 @@ class ResponseVariationFilter:
112
119
  ) -> bool:
113
120
  """Checks if the conditional response variation matches the filled slots."""
114
121
  constraints = response.get(RESPONSE_CONDITION, [])
115
- for constraint in constraints:
116
- name = constraint["name"]
117
- value = constraint["value"]
118
- filled_slots_value = filled_slots.get(name)
119
- if isinstance(filled_slots_value, str) and isinstance(value, str):
120
- if filled_slots_value.casefold() != value.casefold():
122
+ if isinstance(constraints, str) and not _evaluate_predicate(
123
+ constraints, filled_slots
124
+ ):
125
+ return False
126
+
127
+ elif isinstance(constraints, list):
128
+ for constraint in constraints:
129
+ if not _evaluate_and_deprecate_condition(constraint, filled_slots):
121
130
  return False
122
- # slot values can be of different data types
123
- # such as int, float, bool, etc. hence, this check
124
- # executes when slot values are not strings
125
- elif filled_slots_value != value:
126
- return False
127
131
 
128
132
  return True
129
133
 
@@ -180,7 +184,21 @@ class ResponseVariationFilter:
180
184
  if conditional_no_channel:
181
185
  return conditional_no_channel
182
186
 
183
- return default_no_channel
187
+ if default_no_channel:
188
+ return default_no_channel
189
+
190
+ # if there is no response variation selected,
191
+ # return the internal error response to prevent
192
+ # the bot from staying silent
193
+ structlogger.error(
194
+ "rasa.core.nlg.generator.responses_for_utter_action.no_response",
195
+ utter_action=utter_action,
196
+ event_info=f"No response variation selected for the predicted "
197
+ f"utterance {utter_action}. Please check you have provided "
198
+ f"a default variation and that all the conditions are valid. "
199
+ f"Returning the internal error response.",
200
+ )
201
+ return self.responses.get("utter_internal_error_rasa", [])
184
202
 
185
203
  def get_response_variation_id(
186
204
  self,
@@ -228,3 +246,53 @@ class ResponseVariationFilter:
228
246
  response_ids.add(response_variation_id)
229
247
 
230
248
  return True
249
+
250
+
251
+ def _evaluate_and_deprecate_condition(
252
+ constraint: Dict[Text, Any], filled_slots: Dict[Text, Any]
253
+ ) -> bool:
254
+ """Evaluates the condition of a response variation."""
255
+ rasa.shared.utils.io.raise_deprecation_warning(
256
+ "Using a dictionary as a condition in a response variation is deprecated. "
257
+ "Please use a pypred string predicate instead. "
258
+ "Dictionary conditions will be removed in Rasa Open Source 4.0.0 .",
259
+ warn_until_version="4.0.0",
260
+ )
261
+
262
+ name = constraint["name"]
263
+ value = constraint["value"]
264
+ filled_slots_value = filled_slots.get(name)
265
+ if isinstance(filled_slots_value, str) and isinstance(value, str):
266
+ if filled_slots_value.casefold() != value.casefold():
267
+ return False
268
+ # slot values can be of different data types
269
+ # such as int, float, bool, etc. hence, this check
270
+ # executes when slot values are not strings
271
+ elif filled_slots_value != value:
272
+ return False
273
+
274
+ return True
275
+
276
+
277
+ def _evaluate_predicate(constraint: str, filled_slots: Dict[Text, Any]) -> bool:
278
+ """Evaluates the condition of a response variation."""
279
+ context = {"slots": filled_slots}
280
+ document = context.copy()
281
+ try:
282
+ rendered_template = Template(constraint).render(context)
283
+ predicate = Predicate(rendered_template)
284
+ result = predicate.evaluate(document)
285
+ structlogger.debug(
286
+ "rasa.core.nlg.generator.evaluate_conditional_response_predicate",
287
+ predicate=predicate.description(),
288
+ result=result,
289
+ )
290
+ return result
291
+ except (TypeError, Exception) as e:
292
+ structlogger.error(
293
+ "rasa.core.nlg.generator.evaluate_conditional_response_predicate.error",
294
+ predicate=constraint,
295
+ document=document,
296
+ error=str(e),
297
+ )
298
+ return False
rasa/core/nlg/response.py CHANGED
@@ -49,9 +49,12 @@ class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
49
49
  selected_response = np.random.choice(suitable_responses)
50
50
  condition = selected_response.get(RESPONSE_CONDITION)
51
51
  if condition:
52
- formatted_response_conditions = self._format_response_conditions(
53
- condition
54
- )
52
+ if isinstance(condition, list):
53
+ formatted_response_conditions = (
54
+ self._format_response_conditions(condition)
55
+ )
56
+ else:
57
+ formatted_response_conditions = condition
55
58
  logger.debug(
56
59
  "Selecting response variation with conditions:"
57
60
  f"{formatted_response_conditions}"