rasa-pro 3.13.7__py3-none-any.whl → 3.14.0.dev2__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 (179) hide show
  1. rasa/agents/__init__.py +0 -0
  2. rasa/agents/agent_factory.py +122 -0
  3. rasa/agents/agent_manager.py +162 -0
  4. rasa/agents/constants.py +31 -0
  5. rasa/agents/core/__init__.py +0 -0
  6. rasa/agents/core/agent_protocol.py +108 -0
  7. rasa/agents/core/types.py +70 -0
  8. rasa/agents/exceptions.py +8 -0
  9. rasa/agents/protocol/__init__.py +5 -0
  10. rasa/agents/protocol/a2a/__init__.py +0 -0
  11. rasa/agents/protocol/a2a/a2a_agent.py +51 -0
  12. rasa/agents/protocol/mcp/__init__.py +0 -0
  13. rasa/agents/protocol/mcp/mcp_base_agent.py +697 -0
  14. rasa/agents/protocol/mcp/mcp_open_agent.py +275 -0
  15. rasa/agents/protocol/mcp/mcp_task_agent.py +447 -0
  16. rasa/agents/schemas/__init__.py +6 -0
  17. rasa/agents/schemas/agent_input.py +24 -0
  18. rasa/agents/schemas/agent_output.py +26 -0
  19. rasa/agents/schemas/agent_tool_result.py +51 -0
  20. rasa/agents/schemas/agent_tool_schema.py +112 -0
  21. rasa/agents/templates/__init__.py +0 -0
  22. rasa/agents/templates/mcp_open_agent_prompt_template.jinja2 +15 -0
  23. rasa/agents/templates/mcp_task_agent_prompt_template.jinja2 +13 -0
  24. rasa/agents/utils.py +72 -0
  25. rasa/api.py +5 -0
  26. rasa/cli/arguments/default_arguments.py +12 -0
  27. rasa/cli/arguments/run.py +2 -0
  28. rasa/cli/dialogue_understanding_test.py +4 -0
  29. rasa/cli/e2e_test.py +4 -0
  30. rasa/cli/inspect.py +3 -0
  31. rasa/cli/llm_fine_tuning.py +5 -0
  32. rasa/cli/run.py +4 -0
  33. rasa/cli/shell.py +3 -0
  34. rasa/cli/train.py +2 -2
  35. rasa/constants.py +6 -0
  36. rasa/core/actions/action.py +69 -39
  37. rasa/core/actions/action_run_slot_rejections.py +1 -1
  38. rasa/core/agent.py +16 -0
  39. rasa/core/available_agents.py +196 -0
  40. rasa/core/available_endpoints.py +30 -0
  41. rasa/core/channels/development_inspector.py +47 -14
  42. rasa/core/channels/inspector/dist/assets/{arc-0b11fe30.js → arc-2e78c586.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-9eef30a7.js → blockDiagram-38ab4fdb-806b712e.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-03e94f28.js → c4Diagram-3d4e48cf-0745efa9.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/channel-c436ca7c.js +1 -0
  46. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-95c09eba.js → classDiagram-70f12bd4-7bd1082b.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-38e8446c.js → classDiagram-v2-f2320105-d937ba49.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/clone-50dd656b.js +1 -0
  49. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-57dc3038.js → createText-2e5e7dd3-a2a564ca.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-4bac0545.js → edges-e0da2a9e-b5256940.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-81795c90.js → erDiagram-9861fffd-e6883ad2.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-89489ae6.js → flowDb-956e92f1-e576fc02.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-cd152627.js → flowDiagram-66a62f08-2e298d01.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-2b2aeaf8.js +1 -0
  55. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-3da369bc.js → flowchart-elk-definition-4a651766-dd7b150a.js} +1 -1
  56. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-85ec16f8.js → ganttDiagram-c361ad54-5b79575c.js} +1 -1
  57. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-495bc140.js → gitGraphDiagram-72cf32ee-3016f40a.js} +1 -1
  58. rasa/core/channels/inspector/dist/assets/{graph-1ec4d266.js → graph-3e19170f.js} +1 -1
  59. rasa/core/channels/inspector/dist/assets/index-1bd9135e.js +1353 -0
  60. rasa/core/channels/inspector/dist/assets/{index-3862675e-0a0e97c9.js → index-3862675e-eb9c86de.js} +1 -1
  61. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-4d54bcde.js → infoDiagram-f8f76790-b4280e4d.js} +1 -1
  62. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-dc097114.js → journeyDiagram-49397b02-556091f8.js} +1 -1
  63. rasa/core/channels/inspector/dist/assets/{layout-1a08981e.js → layout-08436411.js} +1 -1
  64. rasa/core/channels/inspector/dist/assets/{line-95f7f1d3.js → line-683c4f3b.js} +1 -1
  65. rasa/core/channels/inspector/dist/assets/{linear-97e69543.js → linear-cee6d791.js} +1 -1
  66. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-8c71ff03.js → mindmap-definition-fc14e90a-a0bf0b1a.js} +1 -1
  67. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-f14c71c7.js → pieDiagram-8a3498a8-3730d5c4.js} +1 -1
  68. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-f1d3c9ff.js → quadrantDiagram-120e2f19-12a20fed.js} +1 -1
  69. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-bfa2412f.js → requirementDiagram-deff3bca-b9732102.js} +1 -1
  70. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-53f2c97b.js → sankeyDiagram-04a897e0-a2e72776.js} +1 -1
  71. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-319d7c0e.js → sequenceDiagram-704730f1-8b7a76bb.js} +1 -1
  72. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-76a09418.js → stateDiagram-587899a1-e65853ac.js} +1 -1
  73. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-a67f15d4.js → stateDiagram-v2-d93cdb3a-6f58a44b.js} +1 -1
  74. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-0654e7c3.js → styles-6aaf32cf-df25b934.js} +1 -1
  75. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-1394bb9d.js → styles-9a916d00-88357141.js} +1 -1
  76. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-e4c5bdae.js → styles-c10674c1-d600174d.js} +1 -1
  77. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-50957104.js → svgDrawCommon-08f97a94-4adc3e0b.js} +1 -1
  78. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-b0885a6a.js → timeline-definition-85554ec2-42816fa1.js} +1 -1
  79. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-79e6541a.js → xychartDiagram-e933f94c-621eb66a.js} +1 -1
  80. rasa/core/channels/inspector/dist/index.html +2 -2
  81. rasa/core/channels/inspector/index.html +1 -1
  82. rasa/core/channels/inspector/src/App.tsx +53 -7
  83. rasa/core/channels/inspector/src/components/Chat.tsx +3 -2
  84. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +1 -1
  85. rasa/core/channels/inspector/src/components/DialogueStack.tsx +7 -5
  86. rasa/core/channels/inspector/src/components/LatencyDisplay.tsx +268 -0
  87. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -2
  88. rasa/core/channels/inspector/src/helpers/audio/audiostream.ts +8 -3
  89. rasa/core/channels/inspector/src/helpers/formatters.ts +24 -3
  90. rasa/core/channels/inspector/src/theme/base/styles.ts +19 -1
  91. rasa/core/channels/inspector/src/types.ts +12 -0
  92. rasa/core/channels/studio_chat.py +125 -34
  93. rasa/core/channels/voice_ready/twilio_voice.py +1 -1
  94. rasa/core/channels/voice_stream/audiocodes.py +9 -6
  95. rasa/core/channels/voice_stream/browser_audio.py +39 -4
  96. rasa/core/channels/voice_stream/call_state.py +13 -2
  97. rasa/core/channels/voice_stream/genesys.py +16 -13
  98. rasa/core/channels/voice_stream/jambonz.py +13 -11
  99. rasa/core/channels/voice_stream/twilio_media_streams.py +14 -13
  100. rasa/core/channels/voice_stream/util.py +11 -1
  101. rasa/core/channels/voice_stream/voice_channel.py +101 -29
  102. rasa/core/constants.py +4 -0
  103. rasa/core/nlg/contextual_response_rephraser.py +11 -7
  104. rasa/core/nlg/generator.py +21 -5
  105. rasa/core/nlg/response.py +43 -6
  106. rasa/core/nlg/translate.py +8 -0
  107. rasa/core/policies/enterprise_search_policy.py +4 -2
  108. rasa/core/policies/flow_policy.py +2 -2
  109. rasa/core/policies/flows/flow_executor.py +374 -35
  110. rasa/core/policies/flows/mcp_tool_executor.py +240 -0
  111. rasa/core/processor.py +6 -1
  112. rasa/core/run.py +8 -1
  113. rasa/core/utils.py +21 -1
  114. rasa/dialogue_understanding/commands/__init__.py +8 -0
  115. rasa/dialogue_understanding/commands/cancel_flow_command.py +97 -4
  116. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +11 -0
  117. rasa/dialogue_understanding/commands/clarify_command.py +10 -0
  118. rasa/dialogue_understanding/commands/continue_agent_command.py +91 -0
  119. rasa/dialogue_understanding/commands/knowledge_answer_command.py +11 -0
  120. rasa/dialogue_understanding/commands/restart_agent_command.py +162 -0
  121. rasa/dialogue_understanding/commands/start_flow_command.py +129 -8
  122. rasa/dialogue_understanding/commands/utils.py +6 -2
  123. rasa/dialogue_understanding/generator/command_parser.py +4 -0
  124. rasa/dialogue_understanding/generator/llm_based_command_generator.py +50 -12
  125. rasa/dialogue_understanding/generator/prompt_templates/agent_command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +61 -0
  126. rasa/dialogue_understanding/generator/prompt_templates/agent_command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +61 -0
  127. rasa/dialogue_understanding/generator/prompt_templates/agent_command_prompt_v3_claude_3_5_sonnet_20240620_template.jinja2 +81 -0
  128. rasa/dialogue_understanding/generator/prompt_templates/agent_command_prompt_v3_gpt_4o_2024_11_20_template.jinja2 +81 -0
  129. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +7 -6
  130. rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +7 -6
  131. rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +41 -2
  132. rasa/dialogue_understanding/patterns/continue_interrupted.py +163 -1
  133. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +51 -7
  134. rasa/dialogue_understanding/stack/dialogue_stack.py +123 -2
  135. rasa/dialogue_understanding/stack/frames/flow_stack_frame.py +57 -0
  136. rasa/dialogue_understanding/stack/utils.py +3 -2
  137. rasa/dialogue_understanding_test/du_test_runner.py +7 -2
  138. rasa/dialogue_understanding_test/du_test_schema.yml +3 -3
  139. rasa/e2e_test/e2e_test_runner.py +5 -0
  140. rasa/e2e_test/e2e_test_schema.yml +3 -3
  141. rasa/model_manager/model_api.py +1 -1
  142. rasa/model_manager/socket_bridge.py +8 -2
  143. rasa/server.py +10 -0
  144. rasa/shared/agents/__init__.py +0 -0
  145. rasa/shared/agents/utils.py +35 -0
  146. rasa/shared/constants.py +5 -0
  147. rasa/shared/core/constants.py +12 -1
  148. rasa/shared/core/domain.py +5 -5
  149. rasa/shared/core/events.py +319 -0
  150. rasa/shared/core/flows/flows_list.py +2 -2
  151. rasa/shared/core/flows/flows_yaml_schema.json +101 -186
  152. rasa/shared/core/flows/steps/call.py +51 -5
  153. rasa/shared/core/flows/validation.py +45 -7
  154. rasa/shared/core/flows/yaml_flows_io.py +3 -3
  155. rasa/shared/providers/llm/_base_litellm_client.py +39 -7
  156. rasa/shared/providers/llm/litellm_router_llm_client.py +8 -4
  157. rasa/shared/providers/llm/llm_client.py +7 -3
  158. rasa/shared/providers/llm/llm_response.py +49 -0
  159. rasa/shared/providers/llm/self_hosted_llm_client.py +8 -4
  160. rasa/shared/utils/common.py +2 -1
  161. rasa/shared/utils/llm.py +28 -5
  162. rasa/shared/utils/mcp/__init__.py +0 -0
  163. rasa/shared/utils/mcp/server_connection.py +157 -0
  164. rasa/shared/utils/schemas/events.py +42 -0
  165. rasa/studio/upload.py +4 -7
  166. rasa/tracing/instrumentation/instrumentation.py +4 -2
  167. rasa/utils/common.py +53 -0
  168. rasa/utils/licensing.py +21 -10
  169. rasa/utils/plotting.py +1 -1
  170. rasa/version.py +1 -1
  171. {rasa_pro-3.13.7.dist-info → rasa_pro-3.14.0.dev2.dist-info}/METADATA +16 -15
  172. {rasa_pro-3.13.7.dist-info → rasa_pro-3.14.0.dev2.dist-info}/RECORD +175 -138
  173. rasa/core/channels/inspector/dist/assets/channel-51d02e9e.js +0 -1
  174. rasa/core/channels/inspector/dist/assets/clone-cc738fa6.js +0 -1
  175. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-0c716443.js +0 -1
  176. rasa/core/channels/inspector/dist/assets/index-c804b295.js +0 -1335
  177. {rasa_pro-3.13.7.dist-info → rasa_pro-3.14.0.dev2.dist-info}/NOTICE +0 -0
  178. {rasa_pro-3.13.7.dist-info → rasa_pro-3.14.0.dev2.dist-info}/WHEEL +0 -0
  179. {rasa_pro-3.13.7.dist-info → rasa_pro-3.14.0.dev2.dist-info}/entry_points.txt +0 -0
@@ -26,6 +26,7 @@ from rasa.core.channels.voice_ready.utils import (
26
26
  from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
27
27
  from rasa.core.channels.voice_stream.call_state import call_state
28
28
  from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine
29
+ from rasa.core.channels.voice_stream.util import repack_voice_credentials
29
30
  from rasa.core.channels.voice_stream.voice_channel import (
30
31
  ContinueConversationAction,
31
32
  EndConversationAction,
@@ -120,20 +121,20 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
120
121
  cls,
121
122
  credentials: Optional[Dict[str, Any]],
122
123
  ) -> VoiceInputChannel:
123
- credentials = credentials or {}
124
+ cls.validate_credentials(credentials)
125
+ new_creds = repack_voice_credentials(credentials)
126
+ return cls(**new_creds)
124
127
 
125
- username = credentials.get("username")
126
- password = credentials.get("password")
128
+ @classmethod
129
+ def validate_credentials(
130
+ cls,
131
+ credentials: Optional[Dict[str, Any]],
132
+ ) -> None:
133
+ cls.validate_basic_credentials(credentials)
134
+ username = credentials.get("username") if credentials else None
135
+ password = credentials.get("password") if credentials else None
127
136
  validate_username_password_credentials(username, password, "TwilioMediaStreams")
128
137
 
129
- return cls(
130
- credentials["server_url"],
131
- credentials["asr"],
132
- credentials["tts"],
133
- username=username,
134
- password=password,
135
- )
136
-
137
138
  @classmethod
138
139
  def name(cls) -> str:
139
140
  return "twilio_media_streams"
@@ -175,14 +176,14 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
175
176
  elif data["event"] == "mark":
176
177
  if data["mark"]["name"] == call_state.latest_bot_audio_id:
177
178
  # Just finished streaming last audio bytes
178
- call_state.is_bot_speaking = False # type: ignore[attr-defined]
179
+ call_state.is_bot_speaking = False
179
180
  if call_state.should_hangup:
180
181
  logger.debug(
181
182
  "twilio_streams.hangup", marker=call_state.latest_bot_audio_id
182
183
  )
183
184
  return EndConversationAction()
184
185
  else:
185
- call_state.is_bot_speaking = True # type: ignore[attr-defined]
186
+ call_state.is_bot_speaking = True
186
187
  return ContinueConversationAction()
187
188
 
188
189
  def create_output_channel(
@@ -1,7 +1,7 @@
1
1
  import audioop
2
2
  import wave
3
3
  from dataclasses import asdict, dataclass
4
- from typing import Optional, Type, TypeVar
4
+ from typing import Dict, Optional, Type, TypeVar
5
5
 
6
6
  import structlog
7
7
 
@@ -55,3 +55,13 @@ class MergeableConfig:
55
55
  @classmethod
56
56
  def from_dict(cls: Type[T], data: dict[str, Optional[str]]) -> T:
57
57
  return cls(**data)
58
+
59
+
60
+ def repack_voice_credentials(
61
+ credentials: Dict[str, str],
62
+ ) -> Dict[str, str]:
63
+ """Repack voice credentials to ensure they are in the correct format."""
64
+ new_creds = {**credentials}
65
+ new_creds["asr_config"] = new_creds.pop("asr", None)
66
+ new_creds["tts_config"] = new_creds.pop("tts", None)
67
+ return new_creds
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import copy
5
+ import time
3
6
  from dataclasses import asdict, dataclass
4
7
  from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, Tuple
5
8
 
@@ -189,7 +192,7 @@ class VoiceOutputChannel(OutputChannel):
189
192
  def update_silence_timeout(self) -> None:
190
193
  """Updates the silence timeout for the session."""
191
194
  if self.tracker_state:
192
- call_state.silence_timeout = self.tracker_state["slots"][ # type: ignore[attr-defined]
195
+ call_state.silence_timeout = self.tracker_state["slots"][
193
196
  SILENCE_TIMEOUT_SLOT
194
197
  ]
195
198
  logger.debug(
@@ -207,22 +210,63 @@ class VoiceOutputChannel(OutputChannel):
207
210
  """Uses the concise button output format for voice channels."""
208
211
  await self.send_text_with_buttons_concise(recipient_id, text, buttons, **kwargs)
209
212
 
213
+ def _track_rasa_processing_latency(self) -> None:
214
+ """Track and log Rasa processing completion latency."""
215
+ if call_state.rasa_processing_start_time:
216
+ call_state.rasa_processing_latency_ms = (
217
+ time.time() - call_state.rasa_processing_start_time
218
+ ) * 1000
219
+ logger.debug(
220
+ "voice_channel.rasa_processing_latency",
221
+ latency_ms=call_state.rasa_processing_latency_ms,
222
+ )
223
+
224
+ def _track_tts_first_byte_latency(self) -> None:
225
+ """Track and log TTS first byte latency."""
226
+ if call_state.tts_start_time:
227
+ call_state.tts_first_byte_latency_ms = (
228
+ time.time() - call_state.tts_start_time
229
+ ) * 1000
230
+ logger.debug(
231
+ "voice_channel.tts_first_byte_latency",
232
+ latency_ms=call_state.tts_first_byte_latency_ms,
233
+ )
234
+
235
+ def _track_tts_complete_latency(self) -> None:
236
+ """Track and log TTS completion latency."""
237
+ if call_state.tts_start_time:
238
+ call_state.tts_complete_latency_ms = (
239
+ time.time() - call_state.tts_start_time
240
+ ) * 1000
241
+ logger.debug(
242
+ "voice_channel.tts_complete_latency",
243
+ latency_ms=call_state.tts_complete_latency_ms,
244
+ )
245
+
210
246
  async def send_text_message(
211
247
  self, recipient_id: str, text: str, **kwargs: Any
212
248
  ) -> None:
213
249
  text = remove_emojis(text)
214
250
  self.update_silence_timeout()
251
+
252
+ # Track Rasa processing completion
253
+ self._track_rasa_processing_latency()
254
+
255
+ # Track TTS start time
256
+ call_state.tts_start_time = time.time()
257
+
215
258
  cached_audio_bytes = self.tts_cache.get(text)
216
259
  collected_audio_bytes = RasaAudioBytes(b"")
217
260
  seconds_marker = -1
218
261
  last_sent_offset = 0
262
+ first_audio_sent = False
219
263
  logger.debug("voice_channel.sending_audio", text=text)
220
264
 
221
265
  # Send start marker before first chunk
222
266
  try:
223
267
  await self.send_start_marker(recipient_id)
224
268
  except (WebsocketClosed, ServerError):
225
- call_state.connection_failed = True # type: ignore[attr-defined]
269
+ call_state.connection_failed = True
226
270
 
227
271
  if cached_audio_bytes:
228
272
  audio_stream = self.chunk_audio(cached_audio_bytes)
@@ -244,6 +288,11 @@ class VoiceOutputChannel(OutputChannel):
244
288
 
245
289
  if should_send:
246
290
  try:
291
+ # Track TTS first byte time
292
+ if not first_audio_sent:
293
+ self._track_tts_first_byte_latency()
294
+ first_audio_sent = True
295
+
247
296
  # Send only the new bytes since last send
248
297
  new_bytes = RasaAudioBytes(collected_audio_bytes[last_sent_offset:])
249
298
  await self.send_audio_bytes(recipient_id, new_bytes)
@@ -256,24 +305,31 @@ class VoiceOutputChannel(OutputChannel):
256
305
 
257
306
  except (WebsocketClosed, ServerError):
258
307
  # ignore sending error, and keep collecting and caching audio bytes
259
- call_state.connection_failed = True # type: ignore[attr-defined]
308
+ call_state.connection_failed = True
260
309
 
261
310
  # Send any remaining audio not yet sent
262
311
  remaining_bytes = len(collected_audio_bytes) - last_sent_offset
263
312
  if remaining_bytes > 0:
264
313
  try:
314
+ # Track TTS first byte time if not already tracked
315
+ if not first_audio_sent:
316
+ self._track_tts_first_byte_latency()
317
+
265
318
  new_bytes = RasaAudioBytes(collected_audio_bytes[last_sent_offset:])
266
319
  await self.send_audio_bytes(recipient_id, new_bytes)
267
320
  except (WebsocketClosed, ServerError):
268
321
  # ignore sending error
269
- call_state.connection_failed = True # type: ignore[attr-defined]
322
+ call_state.connection_failed = True
323
+
324
+ # Track TTS completion time
325
+ self._track_tts_complete_latency()
270
326
 
271
327
  try:
272
328
  await self.send_end_marker(recipient_id)
273
329
  except (WebsocketClosed, ServerError):
274
330
  # ignore sending error
275
331
  pass
276
- call_state.latest_bot_audio_id = self.latest_message_id # type: ignore[attr-defined]
332
+ call_state.latest_bot_audio_id = self.latest_message_id
277
333
 
278
334
  if not cached_audio_bytes:
279
335
  self.tts_cache.put(text, collected_audio_bytes)
@@ -298,7 +354,7 @@ class VoiceOutputChannel(OutputChannel):
298
354
  return
299
355
 
300
356
  async def hangup(self, recipient_id: str, **kwargs: Any) -> None:
301
- call_state.should_hangup = True # type: ignore[attr-defined]
357
+ call_state.should_hangup = True
302
358
 
303
359
 
304
360
  class VoiceInputChannel(InputChannel):
@@ -345,32 +401,32 @@ class VoiceInputChannel(InputChannel):
345
401
  if call_state.silence_timeout_watcher:
346
402
  logger.debug("voice_channel.cancelling_current_timeout_watcher_task")
347
403
  call_state.silence_timeout_watcher.cancel()
348
- call_state.silence_timeout_watcher = None # type: ignore[attr-defined]
404
+ call_state.silence_timeout_watcher = None
349
405
 
350
406
  @classmethod
351
- def from_credentials(
352
- cls,
353
- credentials: Optional[Dict[str, Any]],
354
- ) -> InputChannel:
407
+ def validate_basic_credentials(cls, credentials: Optional[Dict[str, Any]]) -> None:
408
+ """Validate the basic credentials for the voice channel."""
355
409
  if not credentials:
356
410
  cls.raise_missing_credentials_exception()
357
-
358
- if not credentials.get("server_url"):
359
- raise InvalidConfigException("No server_url provided in credentials.")
360
- if not credentials.get("asr"):
411
+ if not isinstance(credentials, dict):
361
412
  raise InvalidConfigException(
362
- "No ASR configuration provided in credentials."
413
+ "Credentials must be a dictionary for voice channel."
363
414
  )
364
- if not credentials.get("tts"):
415
+
416
+ required_keys = {"server_url", "asr", "tts"}
417
+ credentials_keys = set(credentials.keys())
418
+ if not required_keys.issubset(credentials_keys):
419
+ missing_fields = required_keys - credentials_keys
365
420
  raise InvalidConfigException(
366
- "No TTS configuration provided in credentials."
421
+ f"Missing required fields in credentials: {', '.join(missing_fields)} "
422
+ f"for channel {cls.name()}"
367
423
  )
368
424
 
369
- return cls(
370
- server_url=credentials["server_url"],
371
- asr_config=credentials["asr"],
372
- tts_config=credentials["tts"],
373
- )
425
+ @classmethod
426
+ def from_credentials(
427
+ cls, credentials: Optional[Dict[str, Any]]
428
+ ) -> VoiceInputChannel:
429
+ raise NotImplementedError
374
430
 
375
431
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
376
432
  raise NotImplementedError
@@ -439,10 +495,8 @@ class VoiceInputChannel(InputChannel):
439
495
  if was_bot_speaking_before and not is_bot_speaking_after:
440
496
  logger.debug("voice_channel.bot_stopped_speaking")
441
497
  self._cancel_silence_timeout_watcher()
442
- call_state.silence_timeout_watcher = ( # type: ignore[attr-defined]
443
- asyncio.create_task(
444
- self.monitor_silence_timeout(asr_event_queue)
445
- )
498
+ call_state.silence_timeout_watcher = asyncio.create_task(
499
+ self.monitor_silence_timeout(asr_event_queue)
446
500
  )
447
501
  if isinstance(channel_action, NewAudioAction):
448
502
  await asr_engine.send_audio_chunks(channel_action.audio_bytes)
@@ -498,6 +552,16 @@ class VoiceInputChannel(InputChannel):
498
552
  """Create a matching voice output channel for this voice input channel."""
499
553
  raise NotImplementedError
500
554
 
555
+ def _track_asr_latency(self) -> None:
556
+ """Track and log ASR processing latency."""
557
+ if call_state.user_speech_start_time:
558
+ call_state.asr_latency_ms = (
559
+ time.time() - call_state.user_speech_start_time
560
+ ) * 1000
561
+ logger.debug(
562
+ "voice_channel.asr_latency", latency_ms=call_state.asr_latency_ms
563
+ )
564
+
501
565
  async def handle_asr_event(
502
566
  self,
503
567
  e: ASREvent,
@@ -511,7 +575,12 @@ class VoiceInputChannel(InputChannel):
511
575
  logger.debug(
512
576
  "VoiceInputChannel.handle_asr_event.new_transcript", transcript=e.text
513
577
  )
514
- call_state.is_user_speaking = False # type: ignore[attr-defined]
578
+ call_state.is_user_speaking = False
579
+
580
+ # Track ASR and Rasa latencies
581
+ self._track_asr_latency()
582
+ call_state.rasa_processing_start_time = time.time()
583
+
515
584
  output_channel = self.create_output_channel(voice_websocket, tts_engine)
516
585
  message = UserMessage(
517
586
  text=e.text,
@@ -522,8 +591,11 @@ class VoiceInputChannel(InputChannel):
522
591
  )
523
592
  await on_new_message(message)
524
593
  elif isinstance(e, UserIsSpeaking):
594
+ # Track when user starts speaking for ASR latency calculation
595
+ if not call_state.is_user_speaking:
596
+ call_state.user_speech_start_time = time.time()
525
597
  self._cancel_silence_timeout_watcher()
526
- call_state.is_user_speaking = True # type: ignore[attr-defined]
598
+ call_state.is_user_speaking = True
527
599
  elif isinstance(e, UserSilence):
528
600
  output_channel = self.create_output_channel(voice_websocket, tts_engine)
529
601
  message = UserMessage(
rasa/core/constants.py CHANGED
@@ -31,6 +31,10 @@ BEARER_TOKEN_PREFIX = "Bearer "
31
31
  # The lowest priority is intended to be used by machine learning policies.
32
32
  DEFAULT_POLICY_PRIORITY = 1
33
33
 
34
+ DEFAULT_SUB_AGENTS = "sub_agents"
35
+
36
+ MCP_SERVERS_KEY = "mcp_servers"
37
+
34
38
  # The priority of intent-prediction policies.
35
39
  # This should be below all rule based policies but higher than ML
36
40
  # based policies. This enables a loop inside ensemble where if none
@@ -225,8 +225,10 @@ class ContextualResponseRephraser(
225
225
 
226
226
  @measure_llm_latency
227
227
  async def _generate_llm_response(self, prompt: str) -> Optional[LLMResponse]:
228
- """Use LLM to generate a response, returning an LLMResponse object
229
- containing both the generated text (choices) and metadata.
228
+ """Use LLM to generate a response.
229
+
230
+ Returns an LLMResponse object containing both the generated text
231
+ (choices) and metadata.
230
232
 
231
233
  Args:
232
234
  prompt: The prompt to send to the LLM.
@@ -315,14 +317,18 @@ class ContextualResponseRephraser(
315
317
  return response
316
318
 
317
319
  prompt_template_text = self._template_for_response_rephrasing(response)
320
+
321
+ # Last user message (=current input) should always be in prompt if available
318
322
  last_message_by_user = getattr(tracker.latest_message, "text", "")
319
323
  current_input = (
320
324
  f"{USER}: {last_message_by_user}" if last_message_by_user else ""
321
325
  )
322
326
 
327
+ # Only summarise conversation history if flagged
323
328
  if self.summarize_history:
324
329
  history = await self._create_history(tracker)
325
330
  else:
331
+ # Count multiple utterances by bot/user as single turn
326
332
  turns_wrapper = (
327
333
  _count_multiple_utterances_as_single_turn
328
334
  if self.count_multiple_utterances_as_single_turn
@@ -365,6 +371,7 @@ class ContextualResponseRephraser(
365
371
  )
366
372
 
367
373
  if not (llm_response and llm_response.choices and llm_response.choices[0]):
374
+ # If the LLM fails to generate a response, return the original response.
368
375
  return response
369
376
 
370
377
  updated_text = llm_response.choices[0]
@@ -412,12 +419,9 @@ class ContextualResponseRephraser(
412
419
  Returns:
413
420
  The generated response.
414
421
  """
415
- filled_slots = tracker.current_slot_values()
416
- stack_context = tracker.stack.current_context()
417
- templated_response = self.generate_from_slots(
422
+ templated_response = await super().generate(
418
423
  utter_action=utter_action,
419
- filled_slots=filled_slots,
420
- stack_context=stack_context,
424
+ tracker=tracker,
421
425
  output_channel=output_channel,
422
426
  **kwargs,
423
427
  )
@@ -6,6 +6,8 @@ from pypred import Predicate
6
6
 
7
7
  import rasa.shared.utils.common
8
8
  import rasa.shared.utils.io
9
+ from rasa.core.nlg.translate import has_translation
10
+ from rasa.engine.language import Language
9
11
  from rasa.shared.constants import CHANNEL, RESPONSE_CONDITION
10
12
  from rasa.shared.core.domain import Domain
11
13
  from rasa.shared.core.trackers import DialogueStateTracker
@@ -131,11 +133,23 @@ class ResponseVariationFilter:
131
133
 
132
134
  return True
133
135
 
136
+ def _filter_by_language(
137
+ self, responses: List[Dict[Text, Any]], language: Optional[Language] = None
138
+ ) -> List[Dict[Text, Any]]:
139
+ if not language:
140
+ return responses
141
+
142
+ if filtered := [r for r in responses if has_translation(r, language)]:
143
+ return filtered
144
+ # if no translation is found, return the original response variations
145
+ return responses
146
+
134
147
  def responses_for_utter_action(
135
148
  self,
136
149
  utter_action: Text,
137
150
  output_channel: Text,
138
151
  filled_slots: Dict[Text, Any],
152
+ language: Optional[Language] = None,
139
153
  ) -> List[Dict[Text, Any]]:
140
154
  """Returns array of responses that fit the channel, action and condition."""
141
155
  # filter responses without a condition
@@ -176,16 +190,16 @@ class ResponseVariationFilter:
176
190
  )
177
191
 
178
192
  if conditional_channel:
179
- return conditional_channel
193
+ return self._filter_by_language(conditional_channel, language)
180
194
 
181
195
  if default_channel:
182
- return default_channel
196
+ return self._filter_by_language(default_channel, language)
183
197
 
184
198
  if conditional_no_channel:
185
- return conditional_no_channel
199
+ return self._filter_by_language(conditional_no_channel, language)
186
200
 
187
201
  if default_no_channel:
188
- return default_no_channel
202
+ return self._filter_by_language(default_no_channel, language)
189
203
 
190
204
  # if there is no response variation selected,
191
205
  # return the internal error response to prevent
@@ -198,7 +212,9 @@ class ResponseVariationFilter:
198
212
  f"a default variation and that all the conditions are valid. "
199
213
  f"Returning the internal error response.",
200
214
  )
201
- return self.responses.get("utter_internal_error_rasa", [])
215
+ return self._filter_by_language(
216
+ self.responses.get("utter_internal_error_rasa", []), language
217
+ )
202
218
 
203
219
  def get_response_variation_id(
204
220
  self,
rasa/core/nlg/response.py CHANGED
@@ -5,8 +5,11 @@ from typing import Any, Dict, List, Optional, Text
5
5
  from rasa.core.constants import DEFAULT_TEMPLATE_ENGINE, TEMPLATE_ENGINE_CONFIG_KEY
6
6
  from rasa.core.nlg import interpolator
7
7
  from rasa.core.nlg.generator import NaturalLanguageGenerator, ResponseVariationFilter
8
- from rasa.shared.constants import RESPONSE_CONDITION
8
+ from rasa.core.nlg.translate import get_translated_buttons, get_translated_text
9
+ from rasa.engine.language import Language
10
+ from rasa.shared.constants import BUTTONS, RESPONSE_CONDITION, TEXT
9
11
  from rasa.shared.core.domain import RESPONSE_KEYS_TO_INTERPOLATE
12
+ from rasa.shared.core.flows.constants import KEY_TRANSLATION
10
13
  from rasa.shared.core.trackers import DialogueStateTracker
11
14
  from rasa.shared.nlu.constants import METADATA
12
15
 
@@ -30,7 +33,11 @@ class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
30
33
 
31
34
  # noinspection PyUnusedLocal
32
35
  def _random_response_for(
33
- self, utter_action: Text, output_channel: Text, filled_slots: Dict[Text, Any]
36
+ self,
37
+ utter_action: Text,
38
+ output_channel: Text,
39
+ filled_slots: Dict[Text, Any],
40
+ language: Optional[Language] = None,
34
41
  ) -> Optional[Dict[Text, Any]]:
35
42
  """Select random response for the utter action from available ones.
36
43
 
@@ -42,7 +49,7 @@ class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
42
49
  if utter_action in self.responses:
43
50
  response_filter = ResponseVariationFilter(self.responses)
44
51
  suitable_responses = response_filter.responses_for_utter_action(
45
- utter_action, output_channel, filled_slots
52
+ utter_action, output_channel, filled_slots, language
46
53
  )
47
54
 
48
55
  if suitable_responses:
@@ -75,9 +82,36 @@ class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
75
82
  """Generate a response for the requested utter action."""
76
83
  filled_slots = tracker.current_slot_values()
77
84
  stack_context = tracker.stack.current_context()
78
- return self.generate_from_slots(
79
- utter_action, filled_slots, stack_context, output_channel, **kwargs
85
+ response = self.generate_from_slots(
86
+ utter_action,
87
+ filled_slots,
88
+ stack_context,
89
+ output_channel,
90
+ tracker.current_language,
91
+ **kwargs,
80
92
  )
93
+ if response is not None:
94
+ return self.translate_response(response, tracker.current_language)
95
+ return None
96
+
97
+ def translate_response(
98
+ self, response: Dict[Text, Any], language: Optional[Language] = None
99
+ ) -> Dict[Text, Any]:
100
+ message_copy = copy.deepcopy(response)
101
+
102
+ text = get_translated_text(
103
+ text=message_copy.pop(TEXT, None),
104
+ translation=message_copy.pop(KEY_TRANSLATION, {}),
105
+ language=language,
106
+ )
107
+
108
+ buttons = get_translated_buttons(
109
+ buttons=message_copy.pop(BUTTONS, None), language=language
110
+ )
111
+ message_copy[TEXT] = text
112
+ if buttons:
113
+ message_copy[BUTTONS] = buttons
114
+ return message_copy
81
115
 
82
116
  def generate_from_slots(
83
117
  self,
@@ -85,12 +119,15 @@ class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
85
119
  filled_slots: Dict[Text, Any],
86
120
  stack_context: Dict[Text, Any],
87
121
  output_channel: Text,
122
+ language: Optional[Language] = None,
88
123
  **kwargs: Any,
89
124
  ) -> Optional[Dict[Text, Any]]:
90
125
  """Generate a response for the requested utter action."""
91
126
  # Fetching a random response for the passed utter action
92
127
  r = copy.deepcopy(
93
- self._random_response_for(utter_action, output_channel, filled_slots)
128
+ self._random_response_for(
129
+ utter_action, output_channel, filled_slots, language
130
+ )
94
131
  )
95
132
  # Filling the slots in the response with placeholders and returning the response
96
133
  if r is not None:
@@ -23,6 +23,14 @@ def get_translated_text(
23
23
  return translation.get(language_code, text)
24
24
 
25
25
 
26
+ def has_translation(
27
+ message: Dict[Text, Any], language: Optional[Language] = None
28
+ ) -> bool:
29
+ """Check if the message has a translation for the given language."""
30
+ language_code = language.code if language else None
31
+ return language_code in message.get(KEY_TRANSLATION, {})
32
+
33
+
26
34
  def get_translated_buttons(
27
35
  buttons: Optional[List[Dict[Text, Any]]], language: Optional[Language] = None
28
36
  ) -> Optional[List[Dict[Text, Any]]]:
@@ -63,6 +63,8 @@ from rasa.shared.constants import (
63
63
  )
64
64
  from rasa.shared.core.constants import (
65
65
  ACTION_CANCEL_FLOW,
66
+ ACTION_METADATA_MESSAGE_KEY,
67
+ ACTION_METADATA_TEXT_KEY,
66
68
  ACTION_SEND_TEXT_NAME,
67
69
  DEFAULT_SLOT_NAMES,
68
70
  )
@@ -585,8 +587,8 @@ class EnterpriseSearchPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Po
585
587
  return self._create_prediction_internal_error(domain, tracker)
586
588
 
587
589
  action_metadata = {
588
- "message": {
589
- "text": response,
590
+ ACTION_METADATA_MESSAGE_KEY: {
591
+ ACTION_METADATA_TEXT_KEY: response,
590
592
  SEARCH_RESULTS_METADATA_KEY: [
591
593
  result.text for result in documents.results
592
594
  ],
@@ -137,7 +137,7 @@ class FlowPolicy(Policy):
137
137
 
138
138
  # create executor and predict next action
139
139
  try:
140
- prediction = flow_executor.advance_flows(
140
+ prediction = await flow_executor.advance_flows(
141
141
  tracker, domain.action_names_or_texts, flows
142
142
  )
143
143
  return self._create_prediction_result(
@@ -164,7 +164,7 @@ class FlowPolicy(Policy):
164
164
  # we retry, with the internal error frame on the stack
165
165
  events = tracker.create_stack_updated_events(updated_stack)
166
166
  tracker.update_with_events(events)
167
- prediction = flow_executor.advance_flows(
167
+ prediction = await flow_executor.advance_flows(
168
168
  tracker, domain.action_names_or_texts, flows
169
169
  )
170
170
  collected_events = events + (prediction.events or [])