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