rasa-pro 3.13.0.dev7__py3-none-any.whl → 3.13.0.dev9__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 (215) hide show
  1. rasa/__main__.py +0 -3
  2. rasa/api.py +1 -1
  3. rasa/cli/dialogue_understanding_test.py +1 -1
  4. rasa/cli/e2e_test.py +1 -1
  5. rasa/cli/evaluate.py +1 -1
  6. rasa/cli/export.py +1 -1
  7. rasa/cli/llm_fine_tuning.py +12 -11
  8. rasa/cli/project_templates/defaults.py +133 -0
  9. rasa/cli/run.py +1 -1
  10. rasa/cli/studio/link.py +53 -0
  11. rasa/cli/studio/pull.py +78 -0
  12. rasa/cli/studio/push.py +78 -0
  13. rasa/cli/studio/studio.py +12 -0
  14. rasa/cli/studio/upload.py +8 -0
  15. rasa/cli/train.py +1 -1
  16. rasa/cli/utils.py +1 -1
  17. rasa/cli/x.py +1 -1
  18. rasa/constants.py +2 -0
  19. rasa/core/__init__.py +0 -16
  20. rasa/core/actions/action.py +5 -1
  21. rasa/core/actions/action_repeat_bot_messages.py +18 -22
  22. rasa/core/actions/action_run_slot_rejections.py +0 -1
  23. rasa/core/agent.py +16 -1
  24. rasa/core/available_endpoints.py +146 -0
  25. rasa/core/brokers/pika.py +1 -2
  26. rasa/core/channels/botframework.py +2 -2
  27. rasa/core/channels/channel.py +2 -2
  28. rasa/core/channels/development_inspector.py +1 -1
  29. rasa/core/channels/facebook.py +1 -4
  30. rasa/core/channels/hangouts.py +8 -5
  31. rasa/core/channels/inspector/README.md +3 -3
  32. rasa/core/channels/inspector/dist/assets/{arc-c4b064fc.js → arc-02053cc1.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-215b5026.js → blockDiagram-38ab4fdb-008b6289.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-2b54a0a3.js → c4Diagram-3d4e48cf-fb2597be.js} +1 -1
  35. rasa/core/channels/inspector/dist/assets/channel-078dada8.js +1 -0
  36. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-daacea5f.js → classDiagram-70f12bd4-7f847e00.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-930d4dc2.js → classDiagram-v2-f2320105-ba1d689b.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/clone-5b4516de.js +1 -0
  39. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-83c206ba.js → createText-2e5e7dd3-dd8e67c4.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-b0eb01d0.js → edges-e0da2a9e-10784939.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-17586500.js → erDiagram-9861fffd-24947ae6.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-be2a1776.js → flowDb-956e92f1-a9ced505.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-c2120ebd.js → flowDiagram-66a62f08-afda9c7c.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-f9613071.js +1 -0
  45. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-a6ab5c48.js → flowchart-elk-definition-4a651766-6ef530b8.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-ef613457.js → ganttDiagram-c361ad54-0c7dd39a.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-d59185b3.js → gitGraphDiagram-72cf32ee-b57239d6.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{graph-0f155405.js → graph-9ed57cec.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{index-3862675e-d5f1d1b7.js → index-3862675e-233090de.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{index-47737d3a.js → index-72184470.js} +3 -3
  51. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-b07d141f.js → infoDiagram-f8f76790-aa116649.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-1936d429.js → journeyDiagram-49397b02-e51877cc.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{layout-dde8d0f3.js → layout-3ca3798c.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{line-0c2c7ee0.js → line-26ee10d3.js} +1 -1
  55. rasa/core/channels/inspector/dist/assets/{linear-35dd89a4.js → linear-aedded32.js} +1 -1
  56. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-56192851.js → mindmap-definition-fc14e90a-d8957261.js} +1 -1
  57. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-fc21ed78.js → pieDiagram-8a3498a8-d771f885.js} +1 -1
  58. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-25e98518.js → quadrantDiagram-120e2f19-09fdf50c.js} +1 -1
  59. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-546ff1f5.js → requirementDiagram-deff3bca-9f0af02e.js} +1 -1
  60. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-02d8b82d.js → sankeyDiagram-04a897e0-84415b37.js} +1 -1
  61. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-3ca5a92e.js → sequenceDiagram-704730f1-8dec4055.js} +1 -1
  62. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-128ea07c.js → stateDiagram-587899a1-c5431d07.js} +1 -1
  63. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-95f290af.js → stateDiagram-v2-d93cdb3a-274e77d9.js} +1 -1
  64. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-4984898a.js → styles-6aaf32cf-e364a1d7.js} +1 -1
  65. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-1bf266ba.js → styles-9a916d00-0dae36f6.js} +1 -1
  66. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-60521c63.js → styles-c10674c1-c4641675.js} +1 -1
  67. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-a25b6e12.js → svgDrawCommon-08f97a94-831fe9a1.js} +1 -1
  68. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-0fc086bf.js → timeline-definition-85554ec2-c3304b3a.js} +1 -1
  69. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-44ee592e.js → xychartDiagram-e933f94c-da799369.js} +1 -1
  70. rasa/core/channels/inspector/dist/index.html +1 -1
  71. rasa/core/channels/inspector/src/components/RecruitmentPanel.tsx +1 -1
  72. rasa/core/channels/mattermost.py +1 -1
  73. rasa/core/channels/rasa_chat.py +2 -4
  74. rasa/core/channels/rest.py +5 -4
  75. rasa/core/channels/socketio.py +56 -41
  76. rasa/core/channels/studio_chat.py +314 -10
  77. rasa/core/channels/vier_cvg.py +1 -2
  78. rasa/core/channels/voice_ready/audiocodes.py +2 -9
  79. rasa/core/channels/voice_stream/audiocodes.py +8 -5
  80. rasa/core/channels/voice_stream/browser_audio.py +1 -1
  81. rasa/core/channels/voice_stream/genesys.py +2 -2
  82. rasa/core/channels/voice_stream/tts/__init__.py +8 -0
  83. rasa/core/channels/voice_stream/twilio_media_streams.py +10 -5
  84. rasa/core/channels/voice_stream/voice_channel.py +39 -23
  85. rasa/core/http_interpreter.py +3 -7
  86. rasa/core/information_retrieval/faiss.py +18 -11
  87. rasa/core/information_retrieval/ingestion/__init__.py +0 -0
  88. rasa/core/information_retrieval/ingestion/faq_parser.py +158 -0
  89. rasa/core/jobs.py +2 -1
  90. rasa/core/nlg/contextual_response_rephraser.py +44 -10
  91. rasa/core/nlg/generator.py +0 -1
  92. rasa/core/nlg/interpolator.py +2 -3
  93. rasa/core/nlg/summarize.py +39 -5
  94. rasa/core/policies/enterprise_search_policy.py +262 -62
  95. rasa/core/policies/enterprise_search_prompt_with_relevancy_check_and_citation_template.jinja2 +63 -0
  96. rasa/core/policies/flow_policy.py +1 -1
  97. rasa/core/policies/flows/flow_executor.py +96 -17
  98. rasa/core/policies/intentless_policy.py +56 -17
  99. rasa/core/processor.py +104 -51
  100. rasa/core/run.py +33 -11
  101. rasa/core/tracker_stores/tracker_store.py +1 -1
  102. rasa/core/training/interactive.py +1 -1
  103. rasa/core/utils.py +24 -97
  104. rasa/dialogue_understanding/coexistence/intent_based_router.py +2 -1
  105. rasa/dialogue_understanding/coexistence/llm_based_router.py +9 -6
  106. rasa/dialogue_understanding/commands/can_not_handle_command.py +2 -0
  107. rasa/dialogue_understanding/commands/cancel_flow_command.py +5 -1
  108. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +2 -0
  109. rasa/dialogue_understanding/commands/clarify_command.py +5 -1
  110. rasa/dialogue_understanding/commands/command_syntax_manager.py +1 -0
  111. rasa/dialogue_understanding/commands/correct_slots_command.py +1 -3
  112. rasa/dialogue_understanding/commands/human_handoff_command.py +2 -0
  113. rasa/dialogue_understanding/commands/knowledge_answer_command.py +4 -2
  114. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +2 -0
  115. rasa/dialogue_understanding/commands/set_slot_command.py +11 -1
  116. rasa/dialogue_understanding/commands/skip_question_command.py +2 -0
  117. rasa/dialogue_understanding/commands/start_flow_command.py +4 -0
  118. rasa/dialogue_understanding/commands/utils.py +26 -2
  119. rasa/dialogue_understanding/generator/__init__.py +7 -1
  120. rasa/dialogue_understanding/generator/command_generator.py +4 -2
  121. rasa/dialogue_understanding/generator/command_parser.py +2 -2
  122. rasa/dialogue_understanding/generator/command_parser_validator.py +63 -0
  123. rasa/dialogue_understanding/generator/nlu_command_adapter.py +2 -2
  124. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +12 -33
  125. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v3_gpt_4o_2024_11_20_template.jinja2 +78 -0
  126. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +26 -461
  127. rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +147 -0
  128. rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +477 -0
  129. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +11 -64
  130. rasa/dialogue_understanding/patterns/cancel.py +1 -2
  131. rasa/dialogue_understanding/patterns/clarify.py +1 -1
  132. rasa/dialogue_understanding/patterns/correction.py +2 -2
  133. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +37 -25
  134. rasa/dialogue_understanding/patterns/domain_for_patterns.py +190 -0
  135. rasa/dialogue_understanding/processor/command_processor.py +6 -7
  136. rasa/dialogue_understanding/processor/command_processor_component.py +3 -3
  137. rasa/dialogue_understanding/stack/frames/flow_stack_frame.py +17 -4
  138. rasa/dialogue_understanding/stack/utils.py +3 -1
  139. rasa/dialogue_understanding/utils.py +68 -12
  140. rasa/dialogue_understanding_test/du_test_case.py +1 -1
  141. rasa/dialogue_understanding_test/du_test_runner.py +4 -22
  142. rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +2 -6
  143. rasa/e2e_test/e2e_test_runner.py +1 -1
  144. rasa/engine/constants.py +1 -1
  145. rasa/engine/graph.py +2 -2
  146. rasa/engine/recipes/default_recipe.py +26 -2
  147. rasa/engine/validation.py +3 -2
  148. rasa/hooks.py +0 -28
  149. rasa/llm_fine_tuning/annotation_module.py +39 -9
  150. rasa/llm_fine_tuning/conversations.py +3 -0
  151. rasa/llm_fine_tuning/llm_data_preparation_module.py +66 -49
  152. rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +1 -5
  153. rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +52 -44
  154. rasa/llm_fine_tuning/paraphrasing_module.py +10 -12
  155. rasa/llm_fine_tuning/storage.py +4 -4
  156. rasa/llm_fine_tuning/utils.py +63 -1
  157. rasa/model_manager/model_api.py +88 -0
  158. rasa/model_manager/trainer_service.py +4 -4
  159. rasa/plugin.py +1 -11
  160. rasa/privacy/__init__.py +0 -0
  161. rasa/privacy/constants.py +83 -0
  162. rasa/privacy/event_broker_utils.py +77 -0
  163. rasa/privacy/privacy_config.py +281 -0
  164. rasa/privacy/privacy_config_schema.json +86 -0
  165. rasa/privacy/privacy_filter.py +340 -0
  166. rasa/privacy/privacy_manager.py +576 -0
  167. rasa/server.py +23 -2
  168. rasa/shared/constants.py +14 -0
  169. rasa/shared/core/command_payload_reader.py +1 -5
  170. rasa/shared/core/constants.py +4 -3
  171. rasa/shared/core/domain.py +7 -0
  172. rasa/shared/core/events.py +38 -10
  173. rasa/shared/core/flows/flow.py +1 -2
  174. rasa/shared/core/flows/flows_yaml_schema.json +3 -0
  175. rasa/shared/core/flows/steps/collect.py +46 -2
  176. rasa/shared/core/flows/validation.py +16 -3
  177. rasa/shared/core/slots.py +28 -0
  178. rasa/shared/core/training_data/story_reader/yaml_story_reader.py +1 -4
  179. rasa/shared/exceptions.py +4 -0
  180. rasa/shared/utils/common.py +1 -1
  181. rasa/shared/utils/llm.py +191 -6
  182. rasa/shared/utils/yaml.py +32 -0
  183. rasa/studio/data_handler.py +3 -3
  184. rasa/studio/download/download.py +37 -60
  185. rasa/studio/download/flows.py +23 -31
  186. rasa/studio/link.py +200 -0
  187. rasa/studio/pull.py +94 -0
  188. rasa/studio/push.py +131 -0
  189. rasa/studio/upload.py +117 -67
  190. rasa/telemetry.py +82 -25
  191. rasa/tracing/config.py +3 -4
  192. rasa/tracing/constants.py +19 -1
  193. rasa/tracing/instrumentation/attribute_extractors.py +10 -2
  194. rasa/tracing/instrumentation/instrumentation.py +53 -2
  195. rasa/tracing/instrumentation/metrics.py +98 -15
  196. rasa/tracing/metric_instrument_provider.py +75 -3
  197. rasa/utils/common.py +1 -27
  198. rasa/utils/log_utils.py +1 -45
  199. rasa/validator.py +2 -8
  200. rasa/version.py +1 -1
  201. {rasa_pro-3.13.0.dev7.dist-info → rasa_pro-3.13.0.dev9.dist-info}/METADATA +7 -8
  202. {rasa_pro-3.13.0.dev7.dist-info → rasa_pro-3.13.0.dev9.dist-info}/RECORD +205 -189
  203. rasa/anonymization/__init__.py +0 -2
  204. rasa/anonymization/anonymisation_rule_yaml_reader.py +0 -91
  205. rasa/anonymization/anonymization_pipeline.py +0 -286
  206. rasa/anonymization/anonymization_rule_executor.py +0 -266
  207. rasa/anonymization/anonymization_rule_orchestrator.py +0 -119
  208. rasa/anonymization/schemas/config.yml +0 -47
  209. rasa/anonymization/utils.py +0 -118
  210. rasa/core/channels/inspector/dist/assets/channel-3730f5fd.js +0 -1
  211. rasa/core/channels/inspector/dist/assets/clone-e847561e.js +0 -1
  212. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-efbbfe00.js +0 -1
  213. {rasa_pro-3.13.0.dev7.dist-info → rasa_pro-3.13.0.dev9.dist-info}/NOTICE +0 -0
  214. {rasa_pro-3.13.0.dev7.dist-info → rasa_pro-3.13.0.dev9.dist-info}/WHEEL +0 -0
  215. {rasa_pro-3.13.0.dev7.dist-info → rasa_pro-3.13.0.dev9.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import copy
3
2
  import inspect
4
3
  import json
5
4
  import logging
@@ -166,11 +165,13 @@ class RestInput(InputChannel):
166
165
  )
167
166
  except CancelledError:
168
167
  structlogger.error(
169
- "rest.message.received.timeout", text=copy.deepcopy(text)
168
+ "rest.message.received.timeout",
169
+ event_info="Message processing was cancelled.",
170
170
  )
171
- except Exception:
171
+ except Exception as e:
172
172
  structlogger.exception(
173
- "rest.message.received.failure", text=copy.deepcopy(text)
173
+ "rest.message.received.failure",
174
+ event_info=f"Message processing failed. Error: {e}",
174
175
  )
175
176
 
176
177
  return response.json(collector.messages)
@@ -191,6 +191,60 @@ class SocketIOInput(InputChannel):
191
191
  return None
192
192
  return SocketIOOutput(self.sio, self.bot_message_evt)
193
193
 
194
+ async def handle_session_request(
195
+ self, sid: Text, data: Optional[Dict] = None
196
+ ) -> None:
197
+ """Handles session requests from the client."""
198
+ if data is None:
199
+ data = {}
200
+ if "session_id" not in data or data["session_id"] is None:
201
+ data["session_id"] = uuid.uuid4().hex
202
+ if self.session_persistence:
203
+ if inspect.iscoroutinefunction(self.sio.enter_room): # type: ignore[union-attr]
204
+ await self.sio.enter_room(sid, data["session_id"]) # type: ignore[union-attr]
205
+ else:
206
+ # for backwards compatibility with python-socketio < 5.10.
207
+ # previously, this function was NOT async.
208
+ self.sio.enter_room(sid, data["session_id"]) # type: ignore[union-attr]
209
+ await self.sio.emit("session_confirm", data["session_id"], room=sid) # type: ignore[union-attr]
210
+ logger.debug(f"User {sid} connected to socketIO endpoint.")
211
+
212
+ async def handle_user_message(
213
+ self,
214
+ sid: Text,
215
+ data: Dict,
216
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
217
+ ) -> None:
218
+ """Handles user messages received from the client."""
219
+ output_channel = SocketIOOutput(self.sio, self.bot_message_evt)
220
+
221
+ if self.session_persistence:
222
+ if not data.get("session_id"):
223
+ rasa.shared.utils.io.raise_warning(
224
+ "A message without a valid session_id "
225
+ "was received. This message will be "
226
+ "ignored. Make sure to set a proper "
227
+ "session id using the "
228
+ "`session_request` socketIO event."
229
+ )
230
+ return
231
+ sender_id = data["session_id"]
232
+ else:
233
+ sender_id = sid
234
+
235
+ metadata = data.get(self.metadata_key, {})
236
+ if isinstance(metadata, Text):
237
+ metadata = json.loads(metadata)
238
+
239
+ message = UserMessage(
240
+ data.get("message", ""),
241
+ output_channel,
242
+ sender_id,
243
+ input_channel=self.name(),
244
+ metadata=metadata,
245
+ )
246
+ await on_new_message(message)
247
+
194
248
  def blueprint(
195
249
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
196
250
  ) -> SocketBlueprint:
@@ -233,49 +287,10 @@ class SocketIOInput(InputChannel):
233
287
 
234
288
  @sio.on("session_request", namespace=self.namespace)
235
289
  async def session_request(sid: Text, data: Optional[Dict]) -> None:
236
- if data is None:
237
- data = {}
238
- if "session_id" not in data or data["session_id"] is None:
239
- data["session_id"] = uuid.uuid4().hex
240
- if self.session_persistence:
241
- if inspect.iscoroutinefunction(sio.enter_room):
242
- await sio.enter_room(sid, data["session_id"])
243
- else:
244
- # for backwards compatibility with python-socketio < 5.10.
245
- # previously, this function was NOT async.
246
- sio.enter_room(sid, data["session_id"])
247
- await sio.emit("session_confirm", data["session_id"], room=sid)
248
- logger.debug(f"User {sid} connected to socketIO endpoint.")
290
+ await self.handle_session_request(sid, data)
249
291
 
250
292
  @sio.on(self.user_message_evt, namespace=self.namespace)
251
293
  async def handle_message(sid: Text, data: Dict) -> None:
252
- output_channel = SocketIOOutput(sio, self.bot_message_evt)
253
-
254
- if self.session_persistence:
255
- if not data.get("session_id"):
256
- rasa.shared.utils.io.raise_warning(
257
- "A message without a valid session_id "
258
- "was received. This message will be "
259
- "ignored. Make sure to set a proper "
260
- "session id using the "
261
- "`session_request` socketIO event."
262
- )
263
- return
264
- sender_id = data["session_id"]
265
- else:
266
- sender_id = sid
267
-
268
- metadata = data.get(self.metadata_key, {})
269
- if isinstance(metadata, Text):
270
- metadata = json.loads(metadata)
271
-
272
- message = UserMessage(
273
- data.get("message", ""),
274
- output_channel,
275
- sender_id,
276
- input_channel=self.name(),
277
- metadata=metadata,
278
- )
279
- await on_new_message(message)
294
+ await self.handle_user_message(sid, data, on_new_message)
280
295
 
281
296
  return socketio_webhook
@@ -1,5 +1,8 @@
1
1
  import asyncio
2
+ import audioop
3
+ import base64
2
4
  import json
5
+ import uuid
3
6
  from functools import partial
4
7
  from typing import (
5
8
  TYPE_CHECKING,
@@ -10,12 +13,25 @@ from typing import (
10
13
  List,
11
14
  Optional,
12
15
  Text,
16
+ Tuple,
13
17
  )
14
18
 
15
19
  import structlog
16
- from sanic import Sanic
17
20
 
21
+ from rasa.core.channels import UserMessage
18
22
  from rasa.core.channels.socketio import SocketBlueprint, SocketIOInput
23
+ from rasa.core.channels.voice_ready.utils import CallParameters
24
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
25
+ from rasa.core.channels.voice_stream.call_state import call_state
26
+ from rasa.core.channels.voice_stream.tts import TTSEngine
27
+ from rasa.core.channels.voice_stream.voice_channel import (
28
+ ContinueConversationAction,
29
+ EndConversationAction,
30
+ NewAudioAction,
31
+ VoiceChannelAction,
32
+ VoiceInputChannel,
33
+ VoiceOutputChannel,
34
+ )
19
35
  from rasa.hooks import hookimpl
20
36
  from rasa.plugin import plugin_manager
21
37
  from rasa.shared.core.constants import ACTION_LISTEN_NAME
@@ -23,7 +39,10 @@ from rasa.shared.core.events import ActionExecuted
23
39
  from rasa.shared.core.trackers import EventVerbosity
24
40
 
25
41
  if TYPE_CHECKING:
26
- from rasa.core.channels.channel import UserMessage
42
+ from sanic import Sanic, Websocket # type: ignore[attr-defined]
43
+ from socketio import AsyncServer
44
+
45
+ from rasa.core.channels.channel import InputChannel, UserMessage
27
46
  from rasa.shared.core.trackers import DialogueStateTracker
28
47
 
29
48
 
@@ -83,7 +102,9 @@ class StudioTrackerUpdatePlugin:
83
102
 
84
103
  def handle_tracker_update(self, tracker: "DialogueStateTracker") -> None:
85
104
  """Handles a tracker update when triggered by a hook."""
86
- structlogger.info("studio_chat.after_tracker_update", tracker=tracker)
105
+ structlogger.info(
106
+ "studio_chat.after_tracker_update", sender_id=tracker.sender_id
107
+ )
87
108
  # directly create a dump to avoid the tracker getting modified by another
88
109
  # function before it gets published (since the publishing is scheduled
89
110
  # as an async task)
@@ -100,26 +121,84 @@ class StudioTrackerUpdatePlugin:
100
121
  self._cancel_tasks()
101
122
 
102
123
 
103
- class StudioChatInput(SocketIOInput):
124
+ class StudioChatInput(SocketIOInput, VoiceInputChannel):
104
125
  """Input channel for the communication between Rasa Studio and Rasa Pro."""
105
126
 
127
+ requires_voice_license = False
128
+
106
129
  @classmethod
107
130
  def name(cls) -> Text:
108
131
  return "studio_chat"
109
132
 
110
133
  def __init__(
111
134
  self,
112
- *args: Any,
113
- **kwargs: Any,
135
+ server_url: str,
136
+ asr_config: Dict,
137
+ tts_config: Dict,
138
+ user_message_evt: Text = "user_uttered",
139
+ bot_message_evt: Text = "bot_uttered",
140
+ namespace: Optional[Text] = None,
141
+ session_persistence: bool = False,
142
+ socketio_path: Optional[Text] = "/socket.io",
143
+ jwt_key: Optional[Text] = None,
144
+ jwt_method: Optional[Text] = "HS256",
145
+ metadata_key: Optional[Text] = "metadata",
114
146
  ) -> None:
115
- """Creates a ``SocketIOInput`` object."""
147
+ """Creates a `StudioChatInput` object."""
116
148
  from rasa.core.agent import Agent
117
149
 
118
- super().__init__(*args, **kwargs)
119
150
  self.agent: Optional[Agent] = None
120
151
 
152
+ # Initialize the SocketIO input channel
153
+ SocketIOInput.__init__(
154
+ self,
155
+ user_message_evt=user_message_evt,
156
+ bot_message_evt=bot_message_evt,
157
+ namespace=namespace,
158
+ session_persistence=session_persistence,
159
+ socketio_path=socketio_path,
160
+ jwt_key=jwt_key,
161
+ jwt_method=jwt_method,
162
+ metadata_key=metadata_key,
163
+ )
164
+
165
+ # Initialize the Voice Input Channel
166
+ VoiceInputChannel.__init__(
167
+ self,
168
+ server_url=server_url,
169
+ asr_config=asr_config,
170
+ tts_config=tts_config,
171
+ )
172
+
173
+ # Dictionaries to manage active connections and background tasks
174
+ # `active_connections` holds the active voice sessions
175
+ # `background_tasks` holds the asyncio tasks for voice streaming
176
+ self.active_connections: Dict[str, SocketIOVoiceWebsocketAdapter] = {}
177
+ self.background_tasks: Dict[str, asyncio.Task] = {}
178
+
121
179
  self._register_tracker_update_hook()
122
180
 
181
+ @classmethod
182
+ def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> "InputChannel":
183
+ """Creates a StudioChatInput channel from credentials."""
184
+ credentials = credentials or {}
185
+
186
+ return cls(
187
+ # Voice specific parameters
188
+ server_url=credentials.get("server_url", ""),
189
+ asr_config=credentials.get("asr", {}),
190
+ tts_config=credentials.get("tts", {}),
191
+ # SocketIO parameters
192
+ user_message_evt=credentials.get("user_message_evt", "user_uttered"),
193
+ bot_message_evt=credentials.get("bot_message_evt", "bot_uttered"),
194
+ namespace=credentials.get("namespace"),
195
+ session_persistence=credentials.get("session_persistence", False),
196
+ socketio_path=credentials.get("socketio_path", "/socket.io"),
197
+ jwt_key=credentials.get("jwt_key"),
198
+ jwt_method=credentials.get("jwt_method", "HS256"),
199
+ metadata_key=credentials.get("metadata_key", "metadata"),
200
+ )
201
+
123
202
  def _register_tracker_update_hook(self) -> None:
124
203
  plugin_manager().register(StudioTrackerUpdatePlugin(self))
125
204
 
@@ -185,11 +264,121 @@ class StudioChatInput(SocketIOInput):
185
264
  output_channel = self.get_output_channel()
186
265
 
187
266
  await processor._run_prediction_loop(output_channel, tracker)
188
- await processor.run_anonymization_pipeline(tracker)
189
267
  await self.agent.tracker_store.save(tracker)
190
268
 
191
269
  await self.on_tracker_updated(tracker)
192
270
 
271
+ def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
272
+ """Voice method to convert channel bytes to RasaAudioBytes."""
273
+ return RasaAudioBytes(audioop.lin2ulaw(input_bytes, 4))
274
+
275
+ async def collect_call_parameters(
276
+ self, channel_websocket: "Websocket"
277
+ ) -> Optional[CallParameters]:
278
+ """Voice method to collect call parameters"""
279
+ session_id = channel_websocket.session_id
280
+ return CallParameters(session_id, "local", "local", stream_id=session_id)
281
+
282
+ def map_input_message(
283
+ self,
284
+ message: Any,
285
+ ws: "Websocket",
286
+ ) -> VoiceChannelAction:
287
+ """Voice method to map websocket messages to actions."""
288
+ if "audio" in message:
289
+ channel_bytes = base64.b64decode(message["audio"])
290
+ audio_bytes = self.channel_bytes_to_rasa_audio_bytes(channel_bytes)
291
+ return NewAudioAction(audio_bytes)
292
+ elif "marker" in message:
293
+ if message["marker"] == call_state.latest_bot_audio_id:
294
+ # Just finished streaming last audio bytes
295
+ call_state.is_bot_speaking = False # type: ignore[attr-defined]
296
+ if call_state.should_hangup:
297
+ structlogger.debug(
298
+ "studio_chat.hangup", marker=call_state.latest_bot_audio_id
299
+ )
300
+ return EndConversationAction()
301
+ else:
302
+ call_state.is_bot_speaking = True # type: ignore[attr-defined]
303
+ return ContinueConversationAction()
304
+
305
+ def create_output_channel(
306
+ self, voice_websocket: "Websocket", tts_engine: TTSEngine
307
+ ) -> VoiceOutputChannel:
308
+ """Create a voice output channel"""
309
+ return StudioVoiceOutputChannel(
310
+ voice_websocket,
311
+ tts_engine,
312
+ self.tts_cache,
313
+ )
314
+
315
+ def _start_voice_session(
316
+ self,
317
+ session_id: str,
318
+ sid: str,
319
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
320
+ ) -> None:
321
+ """Create SocketIO WebSocket Adaptor & start async task for voice streaming."""
322
+ if sid in self.active_connections:
323
+ structlogger.warning(
324
+ "studio_chat._start_voice_session.session_already_active",
325
+ session_id=sid,
326
+ )
327
+ return
328
+
329
+ structlogger.info(
330
+ "studio_chat._start_voice_session.starting_session", session_id=sid
331
+ )
332
+
333
+ # Create a websocket adapter for this connection
334
+ ws_adapter = SocketIOVoiceWebsocketAdapter(
335
+ sio=self.sio,
336
+ session_id=session_id,
337
+ sid=sid,
338
+ bot_message_evt=self.bot_message_evt,
339
+ )
340
+ self.active_connections[sid] = ws_adapter
341
+
342
+ # Start voice streaming in an async task
343
+ task = asyncio.create_task(
344
+ self._handle_voice_streaming(on_new_message, ws_adapter, sid)
345
+ )
346
+ self.background_tasks[sid] = task
347
+ task.add_done_callback(lambda _: self._cleanup_tasks_for_sid(sid))
348
+
349
+ async def _handle_voice_streaming(
350
+ self,
351
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
352
+ ws_adapter: "Websocket",
353
+ sid: str,
354
+ ) -> None:
355
+ """Handle voice streaming for a Socket.IO connection."""
356
+ try:
357
+ await self.run_audio_streaming(on_new_message, ws_adapter)
358
+ except Exception as e:
359
+ structlogger.exception(
360
+ "studio_voice.voice_streaming.error",
361
+ error=str(e),
362
+ sid=sid,
363
+ )
364
+ if sid in self.active_connections:
365
+ del self.active_connections[sid]
366
+
367
+ def _cleanup_tasks_for_sid(self, sid: str) -> None:
368
+ if sid in self.background_tasks:
369
+ task = self.background_tasks.pop(sid)
370
+ task.cancel()
371
+ if sid in self.active_connections:
372
+ del self.active_connections[sid]
373
+
374
+ @hookimpl # type: ignore[misc]
375
+ def after_server_stop(self) -> None:
376
+ """Cleanup background tasks and active connections when the server stops."""
377
+ structlogger.info("studio_chat.after_server_stop.cleanup")
378
+ self.active_connections.clear()
379
+ for task in self.background_tasks.values():
380
+ task.cancel()
381
+
193
382
  def blueprint(
194
383
  self, on_new_message: Callable[["UserMessage"], Awaitable[Any]]
195
384
  ) -> SocketBlueprint:
@@ -202,11 +391,126 @@ class StudioChatInput(SocketIOInput):
202
391
  return socket_blueprint
203
392
 
204
393
  @socket_blueprint.listener("after_server_start") # type: ignore[misc]
205
- async def after_server_start(app: Sanic, _: asyncio.AbstractEventLoop) -> None:
394
+ async def after_server_start(
395
+ app: "Sanic", _: asyncio.AbstractEventLoop
396
+ ) -> None:
206
397
  self.agent = app.ctx.agent
207
398
 
399
+ @self.sio.on("disconnect", namespace=self.namespace)
400
+ async def disconnect(sid: Text) -> None:
401
+ structlogger.debug("studio_chat.sio.disconnect", sid=sid)
402
+ self._cleanup_tasks_for_sid(sid)
403
+
404
+ @self.sio.on("session_request", namespace=self.namespace)
405
+ async def session_request(sid: Text, data: Optional[Dict]) -> None:
406
+ """Overrides the base SocketIOInput session_request handler.
407
+
408
+ Args:
409
+ sid: ID of the session (from SocketIO).
410
+ data:
411
+ - session_id: Studio Chat channel is used with a Bridge Architecture
412
+ (Model Service's Socket Bridge), so we use session_id to remain
413
+ consistent across the bridge. Session ID becomes the sender_id
414
+ for the UserMessage.
415
+ - is_voice: Boolean indicating if its a voice session.
416
+ """
417
+ # Call parent session_request handler first
418
+ await self.handle_session_request(sid, data)
419
+
420
+ # start a voice session if requested
421
+ if data and data.get("is_voice", False):
422
+ self._start_voice_session(data["session_id"], sid, on_new_message)
423
+
424
+ @self.sio.on(self.user_message_evt, namespace=self.namespace)
425
+ async def handle_message(sid: Text, data: Dict) -> None:
426
+ """Overrides the base SocketIOInput handle_message handler."""
427
+ # Handle voice messages
428
+ if "audio" in data or "marker" in data:
429
+ if sid in self.active_connections:
430
+ # Route audio messages to the voice adapter queue
431
+ ws = self.active_connections[sid]
432
+ ws.put_message(data)
433
+ return
434
+
435
+ # Handle text messages
436
+ await self.handle_user_message(sid, data, on_new_message)
437
+
208
438
  @self.sio.on("update_tracker", namespace=self.namespace)
209
439
  async def on_update_tracker(sid: Text, data: Dict) -> None:
210
440
  await self.handle_tracker_update(sid, data)
211
441
 
212
442
  return socket_blueprint
443
+
444
+
445
+ class StudioVoiceOutputChannel(VoiceOutputChannel):
446
+ @classmethod
447
+ def name(cls) -> str:
448
+ return "studio_chat"
449
+
450
+ def rasa_audio_bytes_to_channel_bytes(
451
+ self, rasa_audio_bytes: RasaAudioBytes
452
+ ) -> bytes:
453
+ return audioop.ulaw2lin(rasa_audio_bytes, 4)
454
+
455
+ def channel_bytes_to_message(self, recipient_id: str, channel_bytes: bytes) -> str:
456
+ return json.dumps({"audio": base64.b64encode(channel_bytes).decode("utf-8")})
457
+
458
+ def create_marker_message(self, recipient_id: str) -> Tuple[str, str]:
459
+ message_id = uuid.uuid4().hex
460
+ return json.dumps({"marker": message_id}), message_id
461
+
462
+
463
+ class SocketIOVoiceWebsocketAdapter:
464
+ """Adapter to make Socket.IO work like a Sanic WebSocket for voice channels."""
465
+
466
+ def __init__(
467
+ self, sio: "AsyncServer", session_id: str, sid: str, bot_message_evt: str
468
+ ) -> None:
469
+ self.sio = sio
470
+ self.bot_message_evt = bot_message_evt
471
+ self._closed = False
472
+ self._receive_queue: asyncio.Queue[Any] = asyncio.Queue()
473
+
474
+ # the messages need to be emitted on room=sid
475
+ self.sid = sid
476
+
477
+ # used by collect_call_parameters
478
+ # ultimately, this becomes the sender_id
479
+ self.session_id = session_id
480
+
481
+ @property
482
+ def closed(self) -> bool:
483
+ return self._closed
484
+
485
+ async def send(self, data: Any) -> None:
486
+ """Send data to the client."""
487
+ if not self.closed:
488
+ await self.sio.emit(self.bot_message_evt, data, room=self.sid)
489
+
490
+ async def recv(self) -> Any:
491
+ """Receive data from the client."""
492
+ if self.closed:
493
+ raise ConnectionError("WebSocket is closed")
494
+ return await self._receive_queue.get()
495
+
496
+ def put_message(self, message: Any) -> None:
497
+ """Put message in the internal receive queue."""
498
+ self._receive_queue.put_nowait(message)
499
+
500
+ async def close(self, code: int = 1000, reason: str = "") -> None:
501
+ """Close the connection."""
502
+ self._closed = True
503
+ # at this point, the client should have disconnected
504
+
505
+ def __aiter__(self) -> "SocketIOVoiceWebsocketAdapter":
506
+ """Allow the adapter to be used in an async for loop."""
507
+ return self
508
+
509
+ async def __anext__(self) -> Any:
510
+ if self.closed:
511
+ raise StopAsyncIteration
512
+ try:
513
+ message = await self.recv()
514
+ return message
515
+ except Exception:
516
+ raise StopAsyncIteration
@@ -129,9 +129,8 @@ class CVGOutput(OutputChannel):
129
129
  )
130
130
 
131
131
  logger.info(
132
- "Creating incoming UserMessage: {text=%s, output_channel=%s, sender_id=%s, metadata=%s}" # noqa: E501
132
+ "Creating incoming UserMessage: {output_channel=%s, sender_id=%s, metadata=%s}" # noqa: E501
133
133
  % (
134
- user_message.text,
135
134
  user_message.output_channel,
136
135
  user_message.sender_id,
137
136
  user_message.metadata,
@@ -177,7 +177,7 @@ class Conversation:
177
177
  else:
178
178
  structlogger.warning(
179
179
  "audiocodes.handle.activities.unknown_activity_type",
180
- activity=activity,
180
+ activity_type=activity["type"],
181
181
  )
182
182
  continue
183
183
 
@@ -193,16 +193,9 @@ class Conversation:
193
193
  try:
194
194
  await on_new_message(user_msg)
195
195
  except Exception as e: # skipcq: PYL-W0703
196
- if isinstance(user_msg.text, dict):
197
- anonymized_info = json.dumps(user_msg.text)
198
- elif isinstance(user_msg.text, str):
199
- anonymized_info = user_msg.text
200
- else:
201
- anonymized_info = INFO_UNKNOWN
202
-
203
196
  structlogger.exception(
204
197
  "audiocodes.handle.activities.failure",
205
- user_message=copy.deepcopy(anonymized_info),
198
+ sender_id=self.conversation_id,
206
199
  error=e,
207
200
  exc_info=True,
208
201
  )
@@ -108,15 +108,19 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
108
108
  server_url: str,
109
109
  asr_config: Dict,
110
110
  tts_config: Dict,
111
- monitor_silence: bool = False,
112
111
  ):
113
112
  mark_as_beta_feature("Audiocodes (audiocodes_stream) Channel")
114
- super().__init__(server_url, asr_config, tts_config, monitor_silence)
113
+ super().__init__(
114
+ server_url=server_url,
115
+ asr_config=asr_config,
116
+ tts_config=tts_config,
117
+ )
115
118
  self.token = token
116
119
 
117
120
  @classmethod
118
121
  def from_credentials(
119
- cls, credentials: Optional[Dict[str, Any]]
122
+ cls,
123
+ credentials: Optional[Dict[str, Any]],
120
124
  ) -> VoiceInputChannel:
121
125
  if not credentials:
122
126
  raise ValueError("No credentials given for Audiocodes voice channel.")
@@ -126,7 +130,6 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
126
130
  server_url=credentials["server_url"],
127
131
  asr_config=credentials["asr"],
128
132
  tts_config=credentials["tts"],
129
- monitor_silence=credentials.get("monitor_silence", False),
130
133
  )
131
134
 
132
135
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
@@ -292,7 +295,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
292
295
  def blueprint(
293
296
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
294
297
  ) -> Blueprint:
295
- """Defines a Sanic bluelogger.debug."""
298
+ """Defines a Sanic blueprint"""
296
299
  blueprint = Blueprint("audiocodes_stream", __name__)
297
300
 
298
301
  @blueprint.route("/", methods=["GET"])
@@ -97,7 +97,7 @@ class BrowserAudioInputChannel(VoiceInputChannel):
97
97
  def blueprint(
98
98
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
99
99
  ) -> Blueprint:
100
- """Defines a Sanic bluelogger.debug."""
100
+ """Defines a Sanic blueprint"""
101
101
  blueprint = Blueprint("browser_audio", __name__)
102
102
 
103
103
  @blueprint.route("/", methods=["GET"])
@@ -99,7 +99,8 @@ class GenesysInputChannel(VoiceInputChannel):
99
99
 
100
100
  @classmethod
101
101
  def from_credentials(
102
- cls, credentials: Optional[Dict[str, Any]]
102
+ cls,
103
+ credentials: Optional[Dict[str, Any]],
103
104
  ) -> VoiceInputChannel:
104
105
  if not credentials:
105
106
  raise ValueError("No credentials given for Genesys voice channel.")
@@ -113,7 +114,6 @@ class GenesysInputChannel(VoiceInputChannel):
113
114
  server_url=credentials["server_url"],
114
115
  asr_config=credentials["asr"],
115
116
  tts_config=credentials["tts"],
116
- monitor_silence=credentials.get("monitor_silence", False),
117
117
  )
118
118
 
119
119
  def _ensure_channel_data_initialized(self) -> None:
@@ -0,0 +1,8 @@
1
+ from rasa.core.channels.voice_stream.tts.tts_cache import TTSCache
2
+ from rasa.core.channels.voice_stream.tts.tts_engine import (
3
+ TTSEngine,
4
+ TTSEngineConfig,
5
+ TTSError,
6
+ )
7
+
8
+ __all__ = ["TTSEngine", "TTSEngineConfig", "TTSError", "TTSCache"]