rasa-pro 3.13.0.dev8__py3-none-any.whl → 3.13.0.dev10__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.
- rasa/cli/export.py +2 -0
- rasa/core/channels/development_inspector.py +1 -1
- rasa/core/channels/facebook.py +1 -4
- rasa/core/channels/inspector/README.md +3 -3
- rasa/core/channels/inspector/dist/assets/{arc-c4b064fc.js → arc-02053cc1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-215b5026.js → blockDiagram-38ab4fdb-008b6289.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-2b54a0a3.js → c4Diagram-3d4e48cf-fb2597be.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-078dada8.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-daacea5f.js → classDiagram-70f12bd4-7f847e00.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-930d4dc2.js → classDiagram-v2-f2320105-ba1d689b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-5b4516de.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-83c206ba.js → createText-2e5e7dd3-dd8e67c4.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-b0eb01d0.js → edges-e0da2a9e-10784939.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-17586500.js → erDiagram-9861fffd-24947ae6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-be2a1776.js → flowDb-956e92f1-a9ced505.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-c2120ebd.js → flowDiagram-66a62f08-afda9c7c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-f9613071.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-a6ab5c48.js → flowchart-elk-definition-4a651766-6ef530b8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-ef613457.js → ganttDiagram-c361ad54-0c7dd39a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-d59185b3.js → gitGraphDiagram-72cf32ee-b57239d6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-0f155405.js → graph-9ed57cec.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-3862675e-d5f1d1b7.js → index-3862675e-233090de.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-47737d3a.js → index-72184470.js} +3 -3
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-b07d141f.js → infoDiagram-f8f76790-aa116649.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-1936d429.js → journeyDiagram-49397b02-e51877cc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-dde8d0f3.js → layout-3ca3798c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-0c2c7ee0.js → line-26ee10d3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-35dd89a4.js → linear-aedded32.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-56192851.js → mindmap-definition-fc14e90a-d8957261.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-fc21ed78.js → pieDiagram-8a3498a8-d771f885.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-25e98518.js → quadrantDiagram-120e2f19-09fdf50c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-546ff1f5.js → requirementDiagram-deff3bca-9f0af02e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-02d8b82d.js → sankeyDiagram-04a897e0-84415b37.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-3ca5a92e.js → sequenceDiagram-704730f1-8dec4055.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-128ea07c.js → stateDiagram-587899a1-c5431d07.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-95f290af.js → stateDiagram-v2-d93cdb3a-274e77d9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-4984898a.js → styles-6aaf32cf-e364a1d7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-1bf266ba.js → styles-9a916d00-0dae36f6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-60521c63.js → styles-c10674c1-c4641675.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-a25b6e12.js → svgDrawCommon-08f97a94-831fe9a1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-0fc086bf.js → timeline-definition-85554ec2-c3304b3a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-44ee592e.js → xychartDiagram-e933f94c-da799369.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +1 -1
- rasa/core/channels/inspector/src/components/RecruitmentPanel.tsx +1 -1
- rasa/core/channels/socketio.py +56 -41
- rasa/core/channels/studio_chat.py +311 -8
- rasa/core/channels/voice_ready/audiocodes.py +1 -1
- rasa/core/channels/voice_stream/audiocodes.py +1 -1
- rasa/core/channels/voice_stream/browser_audio.py +1 -1
- rasa/core/channels/voice_stream/tts/__init__.py +8 -0
- rasa/core/channels/voice_stream/voice_channel.py +6 -1
- rasa/core/exporter.py +36 -0
- rasa/core/information_retrieval/faiss.py +18 -11
- rasa/core/information_retrieval/ingestion/__init__.py +0 -0
- rasa/core/information_retrieval/ingestion/faq_parser.py +158 -0
- rasa/core/nlg/contextual_response_rephraser.py +10 -1
- rasa/core/policies/enterprise_search_policy.py +151 -258
- rasa/core/policies/enterprise_search_policy_config.py +242 -0
- rasa/core/policies/intentless_policy.py +47 -10
- rasa/dialogue_understanding/coexistence/llm_based_router.py +9 -6
- rasa/dialogue_understanding/commands/cancel_flow_command.py +3 -1
- rasa/dialogue_understanding/commands/correct_slots_command.py +1 -3
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +2 -2
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v3_claude_3_5_sonnet_20240620_template.jinja2 +78 -0
- rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +2 -2
- rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +2 -2
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +8 -11
- rasa/dialogue_understanding/patterns/cancel.py +1 -2
- rasa/dialogue_understanding/patterns/clarify.py +1 -1
- rasa/dialogue_understanding/patterns/correction.py +2 -2
- rasa/dialogue_understanding/processor/command_processor.py +3 -4
- rasa/dialogue_understanding/stack/utils.py +3 -1
- rasa/engine/graph.py +2 -2
- rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +1 -5
- rasa/shared/constants.py +11 -0
- rasa/shared/core/command_payload_reader.py +1 -5
- rasa/shared/core/events.py +1 -3
- rasa/shared/core/flows/validation.py +25 -5
- rasa/shared/core/training_data/story_reader/yaml_story_reader.py +1 -4
- rasa/shared/providers/_configs/azure_openai_client_config.py +2 -2
- rasa/shared/providers/_configs/default_litellm_client_config.py +1 -1
- rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +1 -1
- rasa/shared/providers/_configs/openai_client_config.py +1 -1
- rasa/shared/providers/_configs/rasa_llm_client_config.py +1 -1
- rasa/shared/providers/_configs/self_hosted_llm_client_config.py +1 -1
- rasa/shared/providers/_configs/utils.py +0 -99
- rasa/shared/utils/common.py +1 -1
- rasa/shared/utils/configs.py +110 -0
- rasa/shared/utils/llm.py +30 -0
- rasa/tracing/instrumentation/attribute_extractors.py +10 -5
- rasa/version.py +1 -1
- {rasa_pro-3.13.0.dev8.dist-info → rasa_pro-3.13.0.dev10.dist-info}/METADATA +3 -3
- {rasa_pro-3.13.0.dev8.dist-info → rasa_pro-3.13.0.dev10.dist-info}/RECORD +96 -91
- rasa/core/channels/inspector/dist/assets/channel-3730f5fd.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-e847561e.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-efbbfe00.js +0 -1
- {rasa_pro-3.13.0.dev8.dist-info → rasa_pro-3.13.0.dev10.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.0.dev8.dist-info → rasa_pro-3.13.0.dev10.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.0.dev8.dist-info → rasa_pro-3.13.0.dev10.dist-info}/entry_points.txt +0 -0
rasa/core/channels/socketio.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -102,26 +121,84 @@ class StudioTrackerUpdatePlugin:
|
|
|
102
121
|
self._cancel_tasks()
|
|
103
122
|
|
|
104
123
|
|
|
105
|
-
class StudioChatInput(SocketIOInput):
|
|
124
|
+
class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
106
125
|
"""Input channel for the communication between Rasa Studio and Rasa Pro."""
|
|
107
126
|
|
|
127
|
+
requires_voice_license = False
|
|
128
|
+
|
|
108
129
|
@classmethod
|
|
109
130
|
def name(cls) -> Text:
|
|
110
131
|
return "studio_chat"
|
|
111
132
|
|
|
112
133
|
def __init__(
|
|
113
134
|
self,
|
|
114
|
-
|
|
115
|
-
|
|
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",
|
|
116
146
|
) -> None:
|
|
117
|
-
"""Creates a
|
|
147
|
+
"""Creates a `StudioChatInput` object."""
|
|
118
148
|
from rasa.core.agent import Agent
|
|
119
149
|
|
|
120
|
-
super().__init__(*args, **kwargs)
|
|
121
150
|
self.agent: Optional[Agent] = None
|
|
122
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
|
+
|
|
123
179
|
self._register_tracker_update_hook()
|
|
124
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
|
+
|
|
125
202
|
def _register_tracker_update_hook(self) -> None:
|
|
126
203
|
plugin_manager().register(StudioTrackerUpdatePlugin(self))
|
|
127
204
|
|
|
@@ -191,6 +268,117 @@ class StudioChatInput(SocketIOInput):
|
|
|
191
268
|
|
|
192
269
|
await self.on_tracker_updated(tracker)
|
|
193
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
|
+
|
|
194
382
|
def blueprint(
|
|
195
383
|
self, on_new_message: Callable[["UserMessage"], Awaitable[Any]]
|
|
196
384
|
) -> SocketBlueprint:
|
|
@@ -203,11 +391,126 @@ class StudioChatInput(SocketIOInput):
|
|
|
203
391
|
return socket_blueprint
|
|
204
392
|
|
|
205
393
|
@socket_blueprint.listener("after_server_start") # type: ignore[misc]
|
|
206
|
-
async def after_server_start(
|
|
394
|
+
async def after_server_start(
|
|
395
|
+
app: "Sanic", _: asyncio.AbstractEventLoop
|
|
396
|
+
) -> None:
|
|
207
397
|
self.agent = app.ctx.agent
|
|
208
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
|
+
|
|
209
438
|
@self.sio.on("update_tracker", namespace=self.namespace)
|
|
210
439
|
async def on_update_tracker(sid: Text, data: Dict) -> None:
|
|
211
440
|
await self.handle_tracker_update(sid, data)
|
|
212
441
|
|
|
213
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
|
|
@@ -295,7 +295,7 @@ class AudiocodesVoiceInputChannel(VoiceInputChannel):
|
|
|
295
295
|
def blueprint(
|
|
296
296
|
self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
|
|
297
297
|
) -> Blueprint:
|
|
298
|
-
"""Defines a Sanic
|
|
298
|
+
"""Defines a Sanic blueprint"""
|
|
299
299
|
blueprint = Blueprint("audiocodes_stream", __name__)
|
|
300
300
|
|
|
301
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
|
|
100
|
+
"""Defines a Sanic blueprint"""
|
|
101
101
|
blueprint = Blueprint("browser_audio", __name__)
|
|
102
102
|
|
|
103
103
|
@blueprint.route("/", methods=["GET"])
|
|
@@ -286,13 +286,18 @@ class VoiceOutputChannel(OutputChannel):
|
|
|
286
286
|
|
|
287
287
|
|
|
288
288
|
class VoiceInputChannel(InputChannel):
|
|
289
|
+
# All children of this class require a voice license to be used.
|
|
290
|
+
requires_voice_license = True
|
|
291
|
+
|
|
289
292
|
def __init__(
|
|
290
293
|
self,
|
|
291
294
|
server_url: str,
|
|
292
295
|
asr_config: Dict,
|
|
293
296
|
tts_config: Dict,
|
|
294
297
|
):
|
|
295
|
-
|
|
298
|
+
if self.requires_voice_license:
|
|
299
|
+
validate_voice_license_scope()
|
|
300
|
+
|
|
296
301
|
self.server_url = server_url
|
|
297
302
|
self.asr_config = asr_config
|
|
298
303
|
self.tts_config = tts_config
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
106
|
+
len_chunks=len(parsed_documents),
|
|
100
107
|
)
|
|
101
|
-
if
|
|
102
|
-
texts = [
|
|
103
|
-
metadatas = [
|
|
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}'.")
|
|
File without changes
|