rasa-pro 3.13.0.dev20250613__py3-none-any.whl → 3.13.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 (160) hide show
  1. rasa/cli/e2e_test.py +0 -7
  2. rasa/cli/export.py +2 -0
  3. rasa/cli/project_templates/tutorial/config.yml +1 -1
  4. rasa/cli/project_templates/tutorial/endpoints.yml +1 -1
  5. rasa/cli/studio/download.py +1 -23
  6. rasa/cli/studio/link.py +0 -17
  7. rasa/cli/studio/pull.py +3 -2
  8. rasa/cli/studio/push.py +1 -1
  9. rasa/cli/studio/train.py +1 -5
  10. rasa/cli/studio/upload.py +1 -1
  11. rasa/core/agent.py +6 -0
  12. rasa/core/channels/__init__.py +3 -0
  13. rasa/core/channels/development_inspector.py +1 -1
  14. rasa/core/channels/facebook.py +1 -4
  15. rasa/core/channels/inspector/README.md +3 -3
  16. rasa/core/channels/inspector/dist/assets/{arc-c4b064fc.js → arc-371401b1.js} +1 -1
  17. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-215b5026.js → blockDiagram-38ab4fdb-3f126156.js} +1 -1
  18. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-2b54a0a3.js → c4Diagram-3d4e48cf-12f22eb7.js} +1 -1
  19. rasa/core/channels/inspector/dist/assets/channel-f1efda17.js +1 -0
  20. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-daacea5f.js → classDiagram-70f12bd4-03b1d386.js} +1 -1
  21. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-930d4dc2.js → classDiagram-v2-f2320105-84f69d63.js} +1 -1
  22. rasa/core/channels/inspector/dist/assets/clone-fdf164e2.js +1 -0
  23. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-83c206ba.js → createText-2e5e7dd3-ca47fd38.js} +1 -1
  24. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-b0eb01d0.js → edges-e0da2a9e-f837ca8a.js} +1 -1
  25. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-17586500.js → erDiagram-9861fffd-8717ac54.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-be2a1776.js → flowDb-956e92f1-94f38b83.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-c2120ebd.js → flowDiagram-66a62f08-b616f9fb.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-7d7a1629.js +1 -0
  29. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-a6ab5c48.js → flowchart-elk-definition-4a651766-f5d24bb8.js} +1 -1
  30. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-ef613457.js → ganttDiagram-c361ad54-b43ba8d9.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-d59185b3.js → gitGraphDiagram-72cf32ee-c3aafaa5.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{graph-0f155405.js → graph-0d0a2c10.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{index-3862675e-d5f1d1b7.js → index-3862675e-58ea0305.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{index-47737d3a.js → index-cce6f8a1.js} +3 -3
  35. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-b07d141f.js → infoDiagram-f8f76790-b8f60461.js} +1 -1
  36. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-1936d429.js → journeyDiagram-49397b02-95be5545.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{layout-dde8d0f3.js → layout-da885b9b.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{line-0c2c7ee0.js → line-f1c817d3.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{linear-35dd89a4.js → linear-d42801e6.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-56192851.js → mindmap-definition-fc14e90a-a38923a6.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-fc21ed78.js → pieDiagram-8a3498a8-ca6e71e9.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-25e98518.js → quadrantDiagram-120e2f19-b290dae9.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-546ff1f5.js → requirementDiagram-deff3bca-03f02ceb.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-02d8b82d.js → sankeyDiagram-04a897e0-c49eee40.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-3ca5a92e.js → sequenceDiagram-704730f1-b2cd6a3d.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-128ea07c.js → stateDiagram-587899a1-e53a2028.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-95f290af.js → stateDiagram-v2-d93cdb3a-e1982a03.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-4984898a.js → styles-6aaf32cf-d0226ca5.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-1bf266ba.js → styles-9a916d00-0e21dc00.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-60521c63.js → styles-c10674c1-9588494e.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-a25b6e12.js → svgDrawCommon-08f97a94-be478d4f.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-0fc086bf.js → timeline-definition-85554ec2-74631749.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-44ee592e.js → xychartDiagram-e933f94c-a043552f.js} +1 -1
  54. rasa/core/channels/inspector/dist/index.html +1 -1
  55. rasa/core/channels/inspector/src/components/RecruitmentPanel.tsx +1 -1
  56. rasa/core/channels/socketio.py +56 -41
  57. rasa/core/channels/studio_chat.py +311 -8
  58. rasa/core/channels/voice_ready/audiocodes.py +1 -1
  59. rasa/core/channels/voice_ready/jambonz.py +5 -6
  60. rasa/core/channels/voice_ready/twilio_voice.py +13 -12
  61. rasa/core/channels/voice_ready/utils.py +22 -0
  62. rasa/core/channels/voice_stream/asr/azure.py +9 -0
  63. rasa/core/channels/voice_stream/audiocodes.py +5 -11
  64. rasa/core/channels/voice_stream/browser_audio.py +1 -1
  65. rasa/core/channels/voice_stream/genesys.py +35 -16
  66. rasa/core/channels/voice_stream/jambonz.py +232 -0
  67. rasa/core/channels/voice_stream/tts/__init__.py +8 -0
  68. rasa/core/channels/voice_stream/twilio_media_streams.py +12 -7
  69. rasa/core/channels/voice_stream/voice_channel.py +53 -15
  70. rasa/core/exporter.py +36 -0
  71. rasa/core/information_retrieval/faiss.py +18 -11
  72. rasa/core/information_retrieval/ingestion/faq_parser.py +158 -0
  73. rasa/core/nlg/contextual_response_rephraser.py +10 -1
  74. rasa/core/policies/enterprise_search_policy.py +189 -263
  75. rasa/core/policies/enterprise_search_policy_config.py +241 -0
  76. rasa/core/policies/enterprise_search_prompt_with_relevancy_check_and_citation_template.jinja2 +6 -5
  77. rasa/core/policies/intentless_policy.py +47 -10
  78. rasa/core/processor.py +6 -0
  79. rasa/core/utils.py +11 -2
  80. rasa/dialogue_understanding/coexistence/llm_based_router.py +13 -11
  81. rasa/dialogue_understanding/commands/__init__.py +4 -0
  82. rasa/dialogue_understanding/commands/cancel_flow_command.py +4 -2
  83. rasa/dialogue_understanding/commands/clarify_command.py +2 -2
  84. rasa/dialogue_understanding/commands/correct_slots_command.py +5 -6
  85. rasa/dialogue_understanding/commands/error_command.py +1 -1
  86. rasa/dialogue_understanding/commands/human_handoff_command.py +1 -3
  87. rasa/dialogue_understanding/commands/set_slot_command.py +4 -4
  88. rasa/dialogue_understanding/commands/skip_question_command.py +1 -3
  89. rasa/dialogue_understanding/commands/start_flow_command.py +3 -3
  90. rasa/dialogue_understanding/generator/command_generator.py +11 -1
  91. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +3 -2
  92. rasa/dialogue_understanding/generator/nlu_command_adapter.py +2 -2
  93. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_template.jinja2 +0 -2
  94. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +1 -0
  95. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +1 -0
  96. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v3_claude_3_5_sonnet_20240620_template.jinja2 +79 -0
  97. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v3_gpt_4o_2024_11_20_template.jinja2 +1 -0
  98. rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +2 -2
  99. rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +2 -18
  100. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +17 -11
  101. rasa/dialogue_understanding/patterns/cancel.py +1 -2
  102. rasa/dialogue_understanding/patterns/clarify.py +1 -1
  103. rasa/dialogue_understanding/patterns/correction.py +2 -2
  104. rasa/dialogue_understanding/processor/command_processor.py +11 -12
  105. rasa/dialogue_understanding/stack/utils.py +3 -1
  106. rasa/e2e_test/constants.py +1 -1
  107. rasa/e2e_test/e2e_test_coverage_report.py +1 -1
  108. rasa/engine/graph.py +2 -2
  109. rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +2 -6
  110. rasa/model_manager/runner_service.py +20 -4
  111. rasa/model_manager/trainer_service.py +6 -0
  112. rasa/privacy/privacy_manager.py +26 -11
  113. rasa/shared/constants.py +14 -0
  114. rasa/shared/core/command_payload_reader.py +1 -5
  115. rasa/shared/core/events.py +1 -3
  116. rasa/shared/core/flows/constants.py +2 -0
  117. rasa/shared/core/flows/flow.py +126 -12
  118. rasa/shared/core/flows/flows_list.py +18 -1
  119. rasa/shared/core/flows/steps/link.py +7 -2
  120. rasa/shared/core/flows/validation.py +25 -5
  121. rasa/shared/core/training_data/story_reader/yaml_story_reader.py +1 -4
  122. rasa/shared/providers/_configs/azure_openai_client_config.py +2 -2
  123. rasa/shared/providers/_configs/default_litellm_client_config.py +1 -1
  124. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +1 -1
  125. rasa/shared/providers/_configs/openai_client_config.py +1 -1
  126. rasa/shared/providers/_configs/rasa_llm_client_config.py +1 -1
  127. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +1 -1
  128. rasa/shared/providers/_configs/utils.py +0 -99
  129. rasa/shared/utils/common.py +1 -1
  130. rasa/shared/utils/configs.py +110 -0
  131. rasa/shared/utils/constants.py +0 -3
  132. rasa/shared/utils/llm.py +123 -8
  133. rasa/shared/utils/pykwalify_extensions.py +0 -9
  134. rasa/studio/constants.py +1 -0
  135. rasa/studio/data_handler.py +30 -9
  136. rasa/studio/download.py +171 -0
  137. rasa/studio/link.py +13 -2
  138. rasa/studio/prompts.py +221 -0
  139. rasa/studio/pull/__init__.py +0 -0
  140. rasa/studio/{download/flows.py → pull/data.py} +2 -131
  141. rasa/studio/{download → pull}/domains.py +1 -1
  142. rasa/studio/pull/pull.py +239 -0
  143. rasa/studio/push.py +7 -0
  144. rasa/studio/train.py +1 -1
  145. rasa/studio/upload.py +61 -5
  146. rasa/studio/utils.py +33 -0
  147. rasa/tracing/instrumentation/attribute_extractors.py +21 -7
  148. rasa/utils/common.py +11 -0
  149. rasa/version.py +1 -1
  150. {rasa_pro-3.13.0.dev20250613.dist-info → rasa_pro-3.13.0rc2.dist-info}/METADATA +4 -4
  151. {rasa_pro-3.13.0.dev20250613.dist-info → rasa_pro-3.13.0rc2.dist-info}/RECORD +155 -147
  152. rasa/core/channels/inspector/dist/assets/channel-3730f5fd.js +0 -1
  153. rasa/core/channels/inspector/dist/assets/clone-e847561e.js +0 -1
  154. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-efbbfe00.js +0 -1
  155. rasa/studio/download/download.py +0 -416
  156. rasa/studio/pull.py +0 -94
  157. /rasa/{studio/download → core/information_retrieval/ingestion}/__init__.py +0 -0
  158. {rasa_pro-3.13.0.dev20250613.dist-info → rasa_pro-3.13.0rc2.dist-info}/NOTICE +0 -0
  159. {rasa_pro-3.13.0.dev20250613.dist-info → rasa_pro-3.13.0rc2.dist-info}/WHEEL +0 -0
  160. {rasa_pro-3.13.0.dev20250613.dist-info → rasa_pro-3.13.0rc2.dist-info}/entry_points.txt +0 -0
@@ -29,6 +29,7 @@ from rasa.core.channels.voice_stream.voice_channel import (
29
29
  VoiceInputChannel,
30
30
  VoiceOutputChannel,
31
31
  )
32
+ from rasa.shared.exceptions import InvalidConfigException
32
33
 
33
34
  """
34
35
  Genesys throws a rate limit error with too many audio messages.
@@ -91,9 +92,14 @@ class GenesysInputChannel(VoiceInputChannel):
91
92
  return "genesys"
92
93
 
93
94
  def __init__(
94
- self, api_key: Text, client_secret: Optional[Text], *args: Any, **kwargs: Any
95
+ self,
96
+ server_url: str,
97
+ asr_config: Dict,
98
+ tts_config: Dict,
99
+ api_key: Optional[Text] = None,
100
+ client_secret: Optional[Text] = None,
95
101
  ) -> None:
96
- super().__init__(*args, **kwargs)
102
+ super().__init__(server_url, asr_config, tts_config)
97
103
  self.api_key = api_key
98
104
  self.client_secret = client_secret
99
105
 
@@ -101,20 +107,33 @@ class GenesysInputChannel(VoiceInputChannel):
101
107
  def from_credentials(
102
108
  cls,
103
109
  credentials: Optional[Dict[str, Any]],
104
- ) -> VoiceInputChannel:
105
- if not credentials:
106
- raise ValueError("No credentials given for Genesys voice channel.")
107
-
108
- if not credentials.get("api_key"):
109
- raise ValueError("No API key given for Genesys voice channel (api_key).")
110
-
111
- return cls(
112
- api_key=credentials["api_key"],
113
- client_secret=credentials.get("client_secret"),
114
- server_url=credentials["server_url"],
115
- asr_config=credentials["asr"],
116
- tts_config=credentials["tts"],
117
- )
110
+ ) -> "GenesysInputChannel":
111
+ """Create a channel from credentials dictionary.
112
+
113
+ Args:
114
+ credentials: Dictionary containing the required credentials:
115
+ - server_url: URL where the server is hosted
116
+ - asr: ASR engine configuration
117
+ - tts: TTS engine configuration
118
+ - api_key: Required API key for Genesys authentication
119
+ - client_secret: Optional client secret for signature verification
120
+
121
+ Returns:
122
+ GenesysInputChannel instance
123
+ """
124
+ channel = super().from_credentials(credentials)
125
+
126
+ # Check required Genesys-specific credentials
127
+ if not credentials.get("api_key"): # type: ignore[union-attr]
128
+ raise InvalidConfigException(
129
+ "No API key given for Genesys voice channel (api_key)."
130
+ )
131
+
132
+ # Update channel with Genesys-specific credentials
133
+ channel.api_key = credentials["api_key"] # type: ignore[index,attr-defined]
134
+ channel.client_secret = credentials.get("client_secret") # type: ignore[union-attr,attr-defined]
135
+
136
+ return channel # type: ignore[return-value]
118
137
 
119
138
  def _ensure_channel_data_initialized(self) -> None:
120
139
  """Initialize Genesys-specific channel data if not already present.
@@ -0,0 +1,232 @@
1
+ import audioop
2
+ import json
3
+ import uuid
4
+ from typing import Any, Awaitable, Callable, Dict, Optional, Text, Tuple
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, requires_basic_auth
16
+ from rasa.core.channels.voice_ready.utils import (
17
+ CallParameters,
18
+ validate_username_password_credentials,
19
+ )
20
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
21
+ from rasa.core.channels.voice_stream.call_state import call_state
22
+ from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine
23
+ from rasa.core.channels.voice_stream.voice_channel import (
24
+ ContinueConversationAction,
25
+ EndConversationAction,
26
+ NewAudioAction,
27
+ VoiceChannelAction,
28
+ VoiceInputChannel,
29
+ VoiceOutputChannel,
30
+ )
31
+
32
+ logger = structlog.get_logger()
33
+
34
+ JAMBONZ_STREAMS_WEBSOCKET_PATH = "webhooks/jambonz_streams/websocket"
35
+
36
+
37
+ def map_call_params(data: Dict[Text, str]) -> CallParameters:
38
+ """Map the twilio stream parameters to the CallParameters dataclass."""
39
+ call_sid = data.get("callSid", "None")
40
+ from_number = data.get("from", "Unknown")
41
+ to_number = data.get("to")
42
+ return CallParameters(
43
+ call_id=call_sid,
44
+ user_phone=from_number,
45
+ bot_phone=to_number,
46
+ stream_id=call_sid,
47
+ )
48
+
49
+
50
+ class JambonzStreamOutputChannel(VoiceOutputChannel):
51
+ @classmethod
52
+ def name(cls) -> str:
53
+ return "jambonz_stream"
54
+
55
+ async def send_audio_bytes(
56
+ self, recipient_id: str, audio_bytes: RasaAudioBytes
57
+ ) -> None:
58
+ """Overridden to send binary websocket messages for Jambonz.
59
+
60
+ Converts 8kHz μ-law to 8kHz L16 PCM for Jambonz streaming.
61
+ """
62
+ pcm = audioop.ulaw2lin(audio_bytes, 2)
63
+ await self.voice_websocket.send(pcm)
64
+
65
+ def create_marker_message(self, recipient_id: str) -> Tuple[str, str]:
66
+ """Create a marker message to track audio stream position."""
67
+ marker_id = uuid.uuid4().hex
68
+ return json.dumps({"type": "mark", "data": {"name": marker_id}}), marker_id
69
+
70
+
71
+ class JambonzStreamInputChannel(VoiceInputChannel):
72
+ @classmethod
73
+ def name(cls) -> str:
74
+ return "jambonz_stream"
75
+
76
+ def __init__(
77
+ self,
78
+ server_url: str,
79
+ asr_config: Dict,
80
+ tts_config: Dict,
81
+ username: Optional[Text] = None,
82
+ password: Optional[Text] = None,
83
+ ) -> None:
84
+ """Initialize the channel.
85
+
86
+ Args:
87
+ username: Optional username for basic auth
88
+ password: Optional password for basic auth
89
+ """
90
+ super().__init__(server_url, asr_config, tts_config)
91
+ self.username = username
92
+ self.password = password
93
+
94
+ @classmethod
95
+ def from_credentials(
96
+ cls, credentials: Optional[Dict[Text, Any]]
97
+ ) -> "JambonzStreamInputChannel":
98
+ """Create a channel from credentials dictionary.
99
+
100
+ Args:
101
+ credentials: Dictionary containing the required credentials:
102
+ - server_url: URL where the server is hosted
103
+ - asr: ASR engine configuration
104
+ - tts: TTS engine configuration
105
+ - username: Optional username for basic auth
106
+ - password: Optional password for basic auth
107
+
108
+ Returns:
109
+ JambonzStreamInputChannel instance
110
+ """
111
+ # Get common credentials from parent
112
+ channel = super().from_credentials(credentials)
113
+
114
+ # Check optional basic auth credentials
115
+ username = credentials.get("username") # type: ignore[union-attr]
116
+ password = credentials.get("password") # type: ignore[union-attr]
117
+ validate_username_password_credentials(username, password, "Jambonz Stream")
118
+
119
+ # Update channel with auth credentials
120
+ channel.username = username # type: ignore[attr-defined]
121
+ channel.password = password # type: ignore[attr-defined]
122
+
123
+ return channel # type: ignore[return-value]
124
+
125
+ def _websocket_stream_url(self) -> str:
126
+ """Returns the websocket stream URL."""
127
+ # depending on the config value, the url might contain http as a
128
+ # protocol or not - we'll make sure both work
129
+ if self.server_url.startswith("http"):
130
+ base_url = self.server_url.replace("http", "ws")
131
+ else:
132
+ base_url = f"wss://{self.server_url}"
133
+ return f"{base_url}/{JAMBONZ_STREAMS_WEBSOCKET_PATH}"
134
+
135
+ def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
136
+ """Convert Jambonz audio bytes (L16 PCM) to Rasa audio bytes (μ-law)."""
137
+ ulaw = audioop.lin2ulaw(input_bytes, 2)
138
+ return RasaAudioBytes(ulaw)
139
+
140
+ async def collect_call_parameters(
141
+ self, channel_websocket: Websocket
142
+ ) -> Optional[CallParameters]:
143
+ # Wait for initial metadata message
144
+ message = await channel_websocket.recv()
145
+ logger.debug("jambonz.collect_call_parameters", message=message)
146
+ metadata = json.loads(message)
147
+ return map_call_params(metadata)
148
+
149
+ def map_input_message(self, message: Any, ws: Websocket) -> VoiceChannelAction:
150
+ # Handle binary audio frames
151
+ if isinstance(message, bytes):
152
+ channel_bytes = message
153
+ audio_bytes = self.channel_bytes_to_rasa_audio_bytes(channel_bytes)
154
+ return NewAudioAction(audio_bytes)
155
+
156
+ # Handle JSON messages
157
+ data = json.loads(message)
158
+ if data["type"] == "mark":
159
+ if data["data"]["name"] == call_state.latest_bot_audio_id:
160
+ # Just finished streaming last audio bytes
161
+ call_state.is_bot_speaking = False # type: ignore[attr-defined]
162
+ if call_state.should_hangup:
163
+ logger.debug(
164
+ "jambonz.hangup", marker=call_state.latest_bot_audio_id
165
+ )
166
+ return EndConversationAction()
167
+ else:
168
+ call_state.is_bot_speaking = True # type: ignore[attr-defined]
169
+ elif data["event"] == "dtmf":
170
+ # TODO: handle DTMF input
171
+ logger.debug("jambonz.dtmf.received", dtmf=data["dtmf"])
172
+ else:
173
+ logger.warning("jambonz.unexpected_message", message=data)
174
+
175
+ return ContinueConversationAction()
176
+
177
+ def create_output_channel(
178
+ self, voice_websocket: Websocket, tts_engine: TTSEngine
179
+ ) -> VoiceOutputChannel:
180
+ return JambonzStreamOutputChannel(
181
+ voice_websocket,
182
+ tts_engine,
183
+ self.tts_cache,
184
+ )
185
+
186
+ def blueprint(
187
+ self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
188
+ ) -> Blueprint:
189
+ blueprint = Blueprint("jambonz_stream", __name__)
190
+
191
+ @blueprint.route("/", methods=["GET"])
192
+ async def health(_: Request) -> HTTPResponse:
193
+ return response.json({"status": "ok"})
194
+
195
+ @blueprint.route("/call_status", methods=["POST"])
196
+ @requires_basic_auth(self.username, self.password)
197
+ async def call_status(request: Request) -> HTTPResponse:
198
+ """Handle call status updates from Jambonz."""
199
+ data = request.json
200
+ logger.debug("jambonz.call_status.received", data=data)
201
+ return response.json({"status": "ok"})
202
+
203
+ @blueprint.route("/webhook", methods=["POST"])
204
+ @requires_basic_auth(self.username, self.password)
205
+ async def webhook(request: Request) -> HTTPResponse:
206
+ """Handle incoming webhook requests from Jambonz."""
207
+ data = request.json
208
+ logger.debug("jambonz.webhook.received", data=data)
209
+ return response.json(
210
+ [
211
+ {
212
+ "verb": "listen",
213
+ "url": self._websocket_stream_url(),
214
+ "sampleRate": 8000,
215
+ "passDtmf": True,
216
+ "bidirectionalAudio": {
217
+ "enabled": True,
218
+ "streaming": True,
219
+ "sampleRate": 8000,
220
+ },
221
+ }
222
+ ]
223
+ )
224
+
225
+ @blueprint.websocket("/websocket", subprotocols=["audio.jambonz.org"]) # type: ignore[misc]
226
+ async def handle_message(request: Request, ws: Websocket) -> None:
227
+ try:
228
+ await self.run_audio_streaming(on_new_message, ws)
229
+ except Exception as e:
230
+ logger.error("jambonz.handle_message.error", error=e)
231
+
232
+ return blueprint
@@ -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"]
@@ -19,7 +19,10 @@ from rasa.core.channels.channel import (
19
19
  create_auth_requested_response_provider,
20
20
  requires_basic_auth,
21
21
  )
22
- from rasa.core.channels.voice_ready.utils import CallParameters
22
+ from rasa.core.channels.voice_ready.utils import (
23
+ CallParameters,
24
+ validate_username_password_credentials,
25
+ )
23
26
  from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
24
27
  from rasa.core.channels.voice_stream.call_state import call_state
25
28
  from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine
@@ -31,7 +34,6 @@ from rasa.core.channels.voice_stream.voice_channel import (
31
34
  VoiceInputChannel,
32
35
  VoiceOutputChannel,
33
36
  )
34
- from rasa.shared.exceptions import RasaException
35
37
 
36
38
  if TYPE_CHECKING:
37
39
  from twilio.twiml.voice_response import VoiceResponse
@@ -122,11 +124,7 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
122
124
 
123
125
  username = credentials.get("username")
124
126
  password = credentials.get("password")
125
- if (username is None) != (password is None):
126
- raise RasaException(
127
- "In TwilioMediaStreams channel, either both username and password "
128
- "or neither should be provided. "
129
- )
127
+ validate_username_password_credentials(username, password, "TwilioMediaStreams")
130
128
 
131
129
  return cls(
132
130
  credentials["server_url"],
@@ -140,6 +138,13 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
140
138
  def name(cls) -> str:
141
139
  return "twilio_media_streams"
142
140
 
141
+ def get_sender_id(self, call_parameters: CallParameters) -> str:
142
+ """Get the sender ID for the channel.
143
+
144
+ Twilio Media Streams uses the Stream ID as Sender ID because
145
+ it is required in OutputChannel.send_text_message to send messages."""
146
+ return call_parameters.stream_id # type: ignore[return-value]
147
+
143
148
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
144
149
  return RasaAudioBytes(base64.b64decode(input_bytes))
145
150
 
@@ -35,7 +35,7 @@ from rasa.core.channels.voice_stream.util import (
35
35
  generate_silence,
36
36
  )
37
37
  from rasa.shared.core.constants import SILENCE_TIMEOUT_SLOT
38
- from rasa.shared.utils.cli import print_error_and_exit
38
+ from rasa.shared.exceptions import InvalidConfigException
39
39
  from rasa.shared.utils.common import (
40
40
  class_from_module_path,
41
41
  mark_as_beta_feature,
@@ -71,6 +71,14 @@ class ContinueConversationAction(VoiceChannelAction):
71
71
 
72
72
 
73
73
  def asr_engine_from_config(asr_config: Dict) -> ASREngine:
74
+ if not asr_config:
75
+ raise ValueError("ASR configuration dictionary cannot be empty")
76
+
77
+ if "name" not in asr_config:
78
+ raise ValueError(
79
+ "ASR configuration must contain 'name' key specifying the engine type"
80
+ )
81
+
74
82
  name = str(asr_config["name"])
75
83
  asr_config = copy.copy(asr_config)
76
84
  asr_config.pop("name")
@@ -84,12 +92,12 @@ def asr_engine_from_config(asr_config: Dict) -> ASREngine:
84
92
  asr_engine_class = class_from_module_path(name)
85
93
  return asr_engine_class.from_config_dict(asr_config)
86
94
  except NameError:
87
- print_error_and_exit(
95
+ raise InvalidConfigException(
88
96
  f"Failed to initialize ASR Engine with type '{name}'. "
89
97
  f"Please make sure the method `from_config_dict`is implemented."
90
98
  )
91
99
  except TypeError as e:
92
- print_error_and_exit(
100
+ raise InvalidConfigException(
93
101
  f"Failed to initialize ASR Engine with type '{name}'. "
94
102
  f"Invalid configuration provided. "
95
103
  f"Error: {e}"
@@ -97,6 +105,14 @@ def asr_engine_from_config(asr_config: Dict) -> ASREngine:
97
105
 
98
106
 
99
107
  def tts_engine_from_config(tts_config: Dict) -> TTSEngine:
108
+ if not tts_config:
109
+ raise ValueError("TTS configuration dictionary cannot be empty")
110
+
111
+ if "name" not in tts_config:
112
+ raise ValueError(
113
+ "TTS configuration must contain 'name' key specifying the engine type"
114
+ )
115
+
100
116
  name = str(tts_config["name"])
101
117
  tts_config = copy.copy(tts_config)
102
118
  tts_config.pop("name")
@@ -110,13 +126,13 @@ def tts_engine_from_config(tts_config: Dict) -> TTSEngine:
110
126
  tts_engine_class = class_from_module_path(name)
111
127
  return tts_engine_class.from_config_dict(tts_config)
112
128
  except NameError:
113
- print_error_and_exit(
129
+ raise InvalidConfigException(
114
130
  f"Failed to initialize TTS Engine with type '{name}'. "
115
131
  f"Please make sure the method `from_config_dict`is implemented."
116
132
  )
117
133
  except TypeError as e:
118
- print_error_and_exit(
119
- f"Failed to initialize ASR Engine with type '{name}'. "
134
+ raise InvalidConfigException(
135
+ f"Failed to initialize TTS Engine with type '{name}'. "
120
136
  f"Invalid configuration provided. "
121
137
  f"Error: {e}"
122
138
  )
@@ -286,13 +302,18 @@ class VoiceOutputChannel(OutputChannel):
286
302
 
287
303
 
288
304
  class VoiceInputChannel(InputChannel):
305
+ # All children of this class require a voice license to be used.
306
+ requires_voice_license = True
307
+
289
308
  def __init__(
290
309
  self,
291
310
  server_url: str,
292
311
  asr_config: Dict,
293
312
  tts_config: Dict,
294
313
  ):
295
- validate_voice_license_scope()
314
+ if self.requires_voice_license:
315
+ validate_voice_license_scope()
316
+
296
317
  self.server_url = server_url
297
318
  self.asr_config = asr_config
298
319
  self.tts_config = tts_config
@@ -305,6 +326,10 @@ class VoiceInputChannel(InputChannel):
305
326
  tts_config=self.tts_config,
306
327
  )
307
328
 
329
+ def get_sender_id(self, call_parameters: CallParameters) -> str:
330
+ """Get the sender ID for the channel."""
331
+ return call_parameters.call_id
332
+
308
333
  async def monitor_silence_timeout(self, asr_event_queue: asyncio.Queue) -> None:
309
334
  timeout = call_state.silence_timeout
310
335
  if not timeout:
@@ -327,11 +352,24 @@ class VoiceInputChannel(InputChannel):
327
352
  cls,
328
353
  credentials: Optional[Dict[str, Any]],
329
354
  ) -> InputChannel:
330
- credentials = credentials or {}
355
+ if not credentials:
356
+ 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"):
361
+ raise InvalidConfigException(
362
+ "No ASR configuration provided in credentials."
363
+ )
364
+ if not credentials.get("tts"):
365
+ raise InvalidConfigException(
366
+ "No TTS configuration provided in credentials."
367
+ )
368
+
331
369
  return cls(
332
- credentials["server_url"],
333
- credentials["asr"],
334
- credentials["tts"],
370
+ server_url=credentials["server_url"],
371
+ asr_config=credentials["asr"],
372
+ tts_config=credentials["tts"],
335
373
  )
336
374
 
337
375
  def channel_bytes_to_rasa_audio_bytes(self, input_bytes: bytes) -> RasaAudioBytes:
@@ -353,7 +391,7 @@ class VoiceInputChannel(InputChannel):
353
391
  message = UserMessage(
354
392
  text=USER_CONVERSATION_SESSION_START,
355
393
  output_channel=output_channel,
356
- sender_id=call_parameters.stream_id,
394
+ sender_id=self.get_sender_id(call_parameters),
357
395
  input_channel=self.name(),
358
396
  metadata=asdict(call_parameters),
359
397
  )
@@ -471,7 +509,7 @@ class VoiceInputChannel(InputChannel):
471
509
  message = UserMessage(
472
510
  text=e.text,
473
511
  output_channel=output_channel,
474
- sender_id=call_parameters.stream_id,
512
+ sender_id=self.get_sender_id(call_parameters),
475
513
  input_channel=self.name(),
476
514
  metadata=asdict(call_parameters),
477
515
  )
@@ -484,7 +522,7 @@ class VoiceInputChannel(InputChannel):
484
522
  message = UserMessage(
485
523
  text=USER_CONVERSATION_SILENCE_TIMEOUT,
486
524
  output_channel=output_channel,
487
- sender_id=call_parameters.stream_id,
525
+ sender_id=self.get_sender_id(call_parameters),
488
526
  input_channel=self.name(),
489
527
  metadata=asdict(call_parameters),
490
528
  )
@@ -502,7 +540,7 @@ class VoiceInputChannel(InputChannel):
502
540
  message = UserMessage(
503
541
  text=USER_CONVERSATION_SESSION_END,
504
542
  output_channel=output_channel,
505
- sender_id=call_parameters.stream_id,
543
+ sender_id=self.get_sender_id(call_parameters),
506
544
  input_channel=self.name(),
507
545
  )
508
546
  await on_new_message(message)
rasa/core/exporter.py CHANGED
@@ -16,6 +16,11 @@ from rasa.exceptions import (
16
16
  NoEventsToMigrateError,
17
17
  PublishingError,
18
18
  )
19
+ from rasa.shared.core.events import (
20
+ BotUttered,
21
+ SlotSet,
22
+ UserUttered,
23
+ )
19
24
  from rasa.shared.core.trackers import EventVerbosity
20
25
 
21
26
  logger = logging.getLogger(__name__)
@@ -43,6 +48,7 @@ class Exporter:
43
48
  tracker_store: TrackerStore,
44
49
  event_broker: EventBroker,
45
50
  endpoints_path: Text,
51
+ is_pii_enabled: bool = False,
46
52
  requested_conversation_ids: Optional[Text] = None,
47
53
  minimum_timestamp: Optional[float] = None,
48
54
  maximum_timestamp: Optional[float] = None,
@@ -52,6 +58,7 @@ class Exporter:
52
58
  self.tracker_store = tracker_store
53
59
 
54
60
  self.event_broker = event_broker
61
+ self.is_pii_enabled = is_pii_enabled
55
62
  self.requested_conversation_ids = requested_conversation_ids
56
63
  self.minimum_timestamp = minimum_timestamp
57
64
  self.maximum_timestamp = maximum_timestamp
@@ -72,10 +79,12 @@ class Exporter:
72
79
  current_timestamp = None
73
80
 
74
81
  headers = self._get_message_headers()
82
+ warned_sender_ids: Set[Text] = set()
75
83
 
76
84
  async for event in self._fetch_events_within_time_range():
77
85
  # noinspection PyBroadException
78
86
  try:
87
+ self._check_anonymization_status(event, warned_sender_ids)
79
88
  self._publish_with_message_headers(event, headers)
80
89
  published_events += 1
81
90
  current_timestamp = event["timestamp"]
@@ -282,3 +291,30 @@ class Exporter:
282
291
  events_with_conversation_id.append(event)
283
292
 
284
293
  return events_with_conversation_id
294
+
295
+ def _check_anonymization_status(
296
+ self, event: Dict[Text, Any], warned_sender_ids: Set[Text]
297
+ ) -> None:
298
+ """Check if the tracker store contains unanonymized events.
299
+
300
+ If it does, print a warning that these events will be published as is.
301
+
302
+ Args:
303
+ event: The event to check for anonymization status
304
+ warned_sender_ids: Set of sender IDs that have already been warned about
305
+ """
306
+ sender_id = event["sender_id"]
307
+ if (
308
+ self.is_pii_enabled
309
+ and sender_id not in warned_sender_ids
310
+ and event["event"]
311
+ in (UserUttered.type_name, BotUttered.type_name, SlotSet.type_name)
312
+ and not event.get("anonymized_at", None)
313
+ ):
314
+ rasa.shared.utils.cli.print_warning(
315
+ f"Retrieved un-anonymized event for sender_id {sender_id}. "
316
+ f"All events after this timestamp {event['timestamp']} "
317
+ "are not anonymized for this tracker. Proceeding with "
318
+ "publishing plaintext values for all events following this.",
319
+ )
320
+ warned_sender_ids.add(sender_id)
@@ -12,6 +12,7 @@ from rasa.core.information_retrieval import (
12
12
  InformationRetrievalException,
13
13
  SearchResultList,
14
14
  )
15
+ from rasa.core.information_retrieval.ingestion.faq_parser import _format_faq_documents
15
16
  from rasa.utils.endpoints import EndpointConfig
16
17
  from rasa.utils.ml_utils import persist_faiss_vector_store
17
18
 
@@ -31,10 +32,12 @@ class FAISS_Store(InformationRetrieval):
31
32
  index_path: str,
32
33
  docs_folder: Optional[str],
33
34
  create_index: Optional[bool] = False,
35
+ parse_as_faq_pairs: Optional[bool] = False,
34
36
  ):
35
37
  """Initializes the FAISS Store."""
36
38
  self.chunk_size = 1000
37
39
  self.chunk_overlap = 20
40
+ self.parse_as_faq_pairs = parse_as_faq_pairs
38
41
 
39
42
  path = Path(index_path) / "documents_faiss"
40
43
  if create_index:
@@ -86,21 +89,25 @@ class FAISS_Store(InformationRetrieval):
86
89
  if not docs_folder:
87
90
  raise ValueError("parameter `docs_folder` needs to be specified")
88
91
 
89
- docs = self.load_documents(docs_folder)
90
- splitter = RecursiveCharacterTextSplitter(
91
- chunk_size=self.chunk_size,
92
- chunk_overlap=self.chunk_overlap,
93
- length_function=len,
94
- )
95
- doc_chunks = splitter.split_documents(docs)
92
+ documents = self.load_documents(docs_folder)
93
+
94
+ if not self.parse_as_faq_pairs:
95
+ splitter = RecursiveCharacterTextSplitter(
96
+ chunk_size=self.chunk_size,
97
+ chunk_overlap=self.chunk_overlap,
98
+ length_function=len,
99
+ )
100
+ parsed_documents = splitter.split_documents(documents)
101
+ else:
102
+ parsed_documents = _format_faq_documents(documents)
96
103
 
97
104
  logger.info(
98
105
  "information_retrieval.faiss_store._create_document_index",
99
- len_chunks=len(doc_chunks),
106
+ len_chunks=len(parsed_documents),
100
107
  )
101
- if doc_chunks:
102
- texts = [chunk.page_content for chunk in doc_chunks]
103
- metadatas = [chunk.metadata for chunk in doc_chunks]
108
+ if parsed_documents:
109
+ texts = [document.page_content for document in parsed_documents]
110
+ metadatas = [document.metadata for document in parsed_documents]
104
111
  return FAISS.from_texts(texts, embedding, metadatas=metadatas, ids=None)
105
112
  else:
106
113
  raise ValueError(f"No documents found at '{docs_folder}'.")