rasa-pro 3.11.7__py3-none-any.whl → 3.11.9__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 (72) hide show
  1. rasa/cli/inspect.py +8 -4
  2. rasa/cli/run.py +10 -6
  3. rasa/cli/utils.py +7 -0
  4. rasa/core/channels/channel.py +93 -0
  5. rasa/core/channels/inspector/dist/assets/{arc-f0f8bd46.js → arc-f09fea11.js} +1 -1
  6. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-7162c77d.js → blockDiagram-38ab4fdb-95518007.js} +1 -1
  7. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-b1d0d098.js → c4Diagram-3d4e48cf-c91a4a08.js} +1 -1
  8. rasa/core/channels/inspector/dist/assets/channel-cc7720dc.js +1 -0
  9. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-807a1b27.js → classDiagram-70f12bd4-27f7869b.js} +1 -1
  10. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-5238dcdb.js → classDiagram-v2-f2320105-1ab94cdb.js} +1 -1
  11. rasa/core/channels/inspector/dist/assets/clone-3688e1f7.js +1 -0
  12. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-75dfaa67.js → createText-2e5e7dd3-a7900089.js} +1 -1
  13. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-df20501d.js → edges-e0da2a9e-3d5b2697.js} +1 -1
  14. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-13cf4797.js → erDiagram-9861fffd-443cc11b.js} +1 -1
  15. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-a4991264.js → flowDb-956e92f1-8a6f8c52.js} +1 -1
  16. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-ccecf773.js → flowDiagram-66a62f08-06a0b4f3.js} +1 -1
  17. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-5055ec2d.js +1 -0
  18. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-b5801783.js → flowchart-elk-definition-4a651766-7a01e0b5.js} +1 -1
  19. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-161e079a.js → ganttDiagram-c361ad54-5f1289f2.js} +1 -1
  20. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-f38e86a4.js → gitGraphDiagram-72cf32ee-44409666.js} +1 -1
  21. rasa/core/channels/inspector/dist/assets/{graph-be6ef5d8.js → graph-3c393c89.js} +1 -1
  22. rasa/core/channels/inspector/dist/assets/{index-3862675e-d9ce8994.js → index-3862675e-4d0c4142.js} +1 -1
  23. rasa/core/channels/inspector/dist/assets/{index-7794b245.js → index-b208b2c3.js} +140 -127
  24. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-5000a3dc.js → infoDiagram-f8f76790-ae0fa7ff.js} +1 -1
  25. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-8ef0a17a.js → journeyDiagram-49397b02-5c3b08cc.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{layout-d649bc98.js → layout-b24c95cb.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{line-95add810.js → line-999a77c5.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/{linear-f6025094.js → linear-81a792fd.js} +1 -1
  29. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-2e8531c4.js → mindmap-definition-fc14e90a-c574f712.js} +1 -1
  30. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-918adfdb.js → pieDiagram-8a3498a8-1919891d.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-cbd01797.js → quadrantDiagram-120e2f19-26e43d09.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-6a8b877b.js → requirementDiagram-deff3bca-f4b22985.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-c377c3fe.js → sankeyDiagram-04a897e0-b957b472.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-ab9e9b7f.js → sequenceDiagram-704730f1-1d8ca073.js} +1 -1
  35. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-5e6ae67d.js → stateDiagram-587899a1-c67b1b71.js} +1 -1
  36. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-40643476.js → stateDiagram-v2-d93cdb3a-ee820f55.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-afb8d108.js → styles-6aaf32cf-b162bdf3.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-7edc9423.js → styles-9a916d00-67a7b254.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-c1d8f7e9.js → styles-c10674c1-81a8ac73.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-f494b2ef.js → svgDrawCommon-08f97a94-ede42905.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-11c7cdd0.js → timeline-definition-85554ec2-b0f41635.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-3f191ec1.js → xychartDiagram-e933f94c-d715dfb0.js} +1 -1
  43. rasa/core/channels/inspector/dist/index.html +1 -1
  44. rasa/core/channels/inspector/src/App.tsx +3 -2
  45. rasa/core/channels/inspector/src/components/Chat.tsx +23 -2
  46. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +2 -5
  47. rasa/core/channels/inspector/src/helpers/conversation.ts +16 -0
  48. rasa/core/channels/inspector/src/types.ts +1 -1
  49. rasa/core/channels/voice_ready/audiocodes.py +7 -4
  50. rasa/core/channels/voice_ready/jambonz.py +29 -8
  51. rasa/core/channels/voice_ready/jambonz_protocol.py +4 -0
  52. rasa/core/channels/voice_ready/twilio_voice.py +56 -8
  53. rasa/core/channels/voice_stream/asr/asr_event.py +5 -0
  54. rasa/core/channels/voice_stream/tts/azure.py +13 -5
  55. rasa/core/channels/voice_stream/twilio_media_streams.py +110 -32
  56. rasa/core/channels/voice_stream/voice_channel.py +30 -30
  57. rasa/e2e_test/utils/validation.py +3 -3
  58. rasa/shared/constants.py +1 -0
  59. rasa/shared/providers/_utils.py +84 -0
  60. rasa/shared/providers/llm/_base_litellm_client.py +5 -3
  61. rasa/shared/providers/llm/azure_openai_llm_client.py +6 -65
  62. rasa/shared/providers/router/_base_litellm_router_client.py +55 -1
  63. rasa/validator.py +1 -2
  64. rasa/version.py +1 -1
  65. {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/METADATA +5 -5
  66. {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/RECORD +69 -68
  67. rasa/core/channels/inspector/dist/assets/channel-e265ea59.js +0 -1
  68. rasa/core/channels/inspector/dist/assets/clone-21f8a43d.js +0 -1
  69. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-5c8ce12d.js +0 -1
  70. {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/NOTICE +0 -0
  71. {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/WHEEL +0 -0
  72. {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/entry_points.txt +0 -0
@@ -40,7 +40,7 @@ export const Chat = ({ sx, events, ...props }: Props) => {
40
40
 
41
41
  // collect user and bot messages
42
42
  const messages: MessageContent[] = events
43
- .filter((event: Event) => event.event === "user" || event.event === "bot")
43
+ .filter((event: Event) => event.event === "user" || event.event === "bot" || event.event === "session_ended")
44
44
  // @ts-expect-error
45
45
  .flatMap((event: Event) => {
46
46
  if (event.event === "user") {
@@ -58,7 +58,28 @@ export const Chat = ({ sx, events, ...props }: Props) => {
58
58
  html: `<div>${commands.join("")}</div>`,
59
59
  },
60
60
  ];
61
- } else {
61
+ } else if (event.event === "session_ended") {
62
+ return [
63
+ {
64
+ role: "system",
65
+ html: `<div>
66
+ session ended
67
+ <div style="margin-top: 8px;">
68
+ <button
69
+ onclick="(() => {
70
+ window.restartConversation();
71
+ return false;
72
+ })()"
73
+ style="background-color: transparent; border: 1px solid #ccc; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 14px;"
74
+ >
75
+ Start a new conversation
76
+ </button>
77
+ </div>
78
+ </div>`,
79
+ }
80
+ ]
81
+ }
82
+ else {
62
83
  return [
63
84
  {
64
85
  role: event.event,
@@ -2,6 +2,7 @@ import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react";
2
2
  import mermaid from "mermaid";
3
3
  import { useOurTheme } from "../theme";
4
4
  import { formatFlow } from "../helpers/formatters";
5
+ import { restartConversation } from "../helpers/conversation";
5
6
  import { useEffect, useRef, useState } from "react";
6
7
  import { Flow, Slot, Stack } from "../types";
7
8
  import { NoActiveFlow } from "./NoActiveFlow";
@@ -51,11 +52,7 @@ export const DiagramFlow = ({ stackFrame, stepTrail, flows, slots }: Props) => {
51
52
  }, [text, flow, slots, stackFrame]);
52
53
 
53
54
  const handleRestartConversation = () => {
54
- // unset the sender id from the query parameters
55
- const url = new URL(window.location.href);
56
- url.searchParams.delete("sender");
57
- window.history.pushState(null, "", url.toString());
58
- location.reload();
55
+ restartConversation();
59
56
  };
60
57
 
61
58
  const scrollSx = {
@@ -0,0 +1,16 @@
1
+ export const restartConversation = () => {
2
+ // unset the sender id from the query parameters
3
+ const url = new URL(window.location.href);
4
+ url.searchParams.delete("sender");
5
+ window.history.pushState(null, "", url.toString());
6
+ location.reload();
7
+ };
8
+
9
+ // Make the function available on the window object
10
+ declare global {
11
+ interface Window {
12
+ restartConversation: typeof restartConversation;
13
+ }
14
+ }
15
+
16
+ window.restartConversation = restartConversation;
@@ -5,7 +5,7 @@ export interface Slot {
5
5
  }
6
6
 
7
7
  export interface Event {
8
- event: "user" | "bot" | "flow_completed" | "flow_started" | "stack" | "restart";
8
+ event: "user" | "bot" | "flow_completed" | "flow_started" | "stack" | "restart" | "session_ended";
9
9
  text?: string;
10
10
  timestamp: string;
11
11
  update?: string;
@@ -96,10 +96,13 @@ class Conversation:
96
96
  event_params = {"value": event["value"]}
97
97
  text += json.dumps(event_params)
98
98
  else:
99
- structlogger.warning(
100
- "audiocodes.handle.event.unknown_event", event_payload=event
101
- )
102
- return ""
99
+ # handle other events described by Audiocodes
100
+ # https://techdocs.audiocodes.com/voice-ai-connect/#VAIG_Combined/inactivity-detection.htm?TocPath=Bot%2520integration%257CReceiving%2520notifications%257C_____3
101
+ text = f"{INTENT_MESSAGE_PREFIX}vaig_event_{event['name']}"
102
+ event_params = {**event.get("parameters", {})}
103
+ if "value" in event:
104
+ event_params["value"] = event["value"]
105
+ text += json.dumps(event_params)
103
106
 
104
107
  return text
105
108
 
@@ -1,25 +1,30 @@
1
1
  from typing import Any, Awaitable, Callable, Dict, List, Optional, Text
2
2
 
3
3
  import structlog
4
- from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
4
+ from sanic import Blueprint, Websocket, response # type: ignore[attr-defined]
5
+ from sanic.request import Request
6
+ from sanic.response import HTTPResponse
7
+
8
+ from rasa.core.channels.channel import (
9
+ InputChannel,
10
+ OutputChannel,
11
+ UserMessage,
12
+ requires_basic_auth,
13
+ )
5
14
  from rasa.core.channels.voice_ready.jambonz_protocol import (
15
+ CHANNEL_NAME,
6
16
  send_ws_text_message,
7
17
  websocket_message_handler,
8
18
  send_ws_hangup_message,
9
19
  )
10
20
  from rasa.core.channels.voice_ready.utils import validate_voice_license_scope
11
21
  from rasa.shared.exceptions import RasaException
12
- from sanic import Blueprint, response, Websocket # type: ignore[attr-defined]
13
- from sanic.request import Request
14
- from sanic.response import HTTPResponse
15
22
 
16
23
  from rasa.shared.utils.common import mark_as_beta_feature
17
24
  from rasa.utils.io import remove_emojis
18
25
 
19
26
  structlogger = structlog.get_logger()
20
27
 
21
- CHANNEL_NAME = "jambonz"
22
-
23
28
  DEFAULT_HANGUP_DELAY_SECONDS = 1
24
29
 
25
30
 
@@ -32,12 +37,27 @@ class JambonzVoiceReadyInput(InputChannel):
32
37
 
33
38
  @classmethod
34
39
  def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChannel:
35
- return cls()
40
+ if not credentials:
41
+ return cls()
42
+
43
+ username = credentials.get("username")
44
+ password = credentials.get("password")
45
+ if (username is None) != (password is None):
46
+ raise RasaException(
47
+ "In Jambonz channel, either both username and password "
48
+ "or neither should be provided. "
49
+ )
36
50
 
37
- def __init__(self) -> None:
51
+ return cls(username, password)
52
+
53
+ def __init__(
54
+ self, username: Optional[Text] = None, password: Optional[Text] = None
55
+ ) -> None:
38
56
  """Initializes the JambonzVoiceReadyInput channel."""
39
57
  mark_as_beta_feature("Jambonz Channel")
40
58
  validate_voice_license_scope()
59
+ self.username = username
60
+ self.password = password
41
61
 
42
62
  def blueprint(
43
63
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
@@ -50,6 +70,7 @@ class JambonzVoiceReadyInput(InputChannel):
50
70
  return response.json({"status": "ok"})
51
71
 
52
72
  @jambonz_webhook.websocket("/websocket", subprotocols=["ws.jambonz.org"]) # type: ignore
73
+ @requires_basic_auth(self.username, self.password)
53
74
  async def websocket(request: Request, ws: Websocket) -> None:
54
75
  """Triggered on new websocket connection."""
55
76
  async for message in ws:
@@ -11,6 +11,7 @@ from sanic import Websocket # type: ignore[attr-defined]
11
11
 
12
12
 
13
13
  structlogger = structlog.get_logger()
14
+ CHANNEL_NAME = "jambonz"
14
15
 
15
16
 
16
17
  @dataclass
@@ -207,6 +208,7 @@ async def handle_new_session(
207
208
  output_channel=output_channel,
208
209
  sender_id=message.call_sid,
209
210
  metadata=asdict(message.call_params),
211
+ input_channel=CHANNEL_NAME,
210
212
  )
211
213
  await send_config_ack(message.message_id, ws)
212
214
  await on_new_message(user_msg)
@@ -239,6 +241,7 @@ async def handle_gather_completed(
239
241
  output_channel = JambonzWebsocketOutput(ws, transcript_result.call_sid)
240
242
  user_msg = UserMessage(
241
243
  text=most_likely_transcript.text,
244
+ input_channel=CHANNEL_NAME,
242
245
  output_channel=output_channel,
243
246
  sender_id=transcript_result.call_sid,
244
247
  metadata={},
@@ -289,6 +292,7 @@ async def handle_call_status(
289
292
  output_channel = JambonzWebsocketOutput(ws, call_status.call_sid)
290
293
  user_msg = UserMessage(
291
294
  text="/session_end",
295
+ input_channel=CHANNEL_NAME,
292
296
  output_channel=output_channel,
293
297
  sender_id=call_status.call_sid,
294
298
  metadata={},
@@ -1,25 +1,31 @@
1
+ from dataclasses import asdict
2
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Text
3
+
4
+ import structlog
1
5
  from sanic import Blueprint, response
2
6
  from sanic.request import Request, RequestParameters
3
7
  from sanic.response import HTTPResponse
4
- from twilio.twiml.voice_response import VoiceResponse, Gather
5
- from typing import Text, Callable, Awaitable, List, Any, Dict, Optional
6
- from dataclasses import asdict
8
+ from twilio.twiml.voice_response import Gather, VoiceResponse
7
9
 
8
- import structlog
9
- import rasa.utils.io
10
10
  import rasa.shared.utils.io
11
- from rasa.shared.core.events import BotUttered
12
- from rasa.shared.exceptions import InvalidConfigException
11
+ import rasa.utils.io
13
12
  from rasa.core.channels.channel import (
14
- InputChannel,
15
13
  CollectingOutputChannel,
14
+ InputChannel,
16
15
  UserMessage,
16
+ create_auth_requested_response_provider,
17
+ requires_basic_auth,
17
18
  )
18
19
  from rasa.core.channels.voice_ready.utils import CallParameters
20
+ from rasa.shared.core.events import BotUttered
21
+ from rasa.shared.exceptions import InvalidConfigException, RasaException
19
22
 
20
23
  logger = structlog.get_logger(__name__)
21
24
 
22
25
 
26
+ TWILIO_VOICE_PATH = "webhooks/twilio_voice/webhook"
27
+
28
+
23
29
  def map_call_params(form: RequestParameters) -> CallParameters:
24
30
  """Map the Audiocodes parameters to the CallParameters dataclass."""
25
31
  return CallParameters(
@@ -119,6 +125,14 @@ class TwilioVoiceInput(InputChannel):
119
125
  """Load custom configurations."""
120
126
  credentials = credentials or {}
121
127
 
128
+ username = credentials.get("username")
129
+ password = credentials.get("password")
130
+ if (username is None) != (password is None):
131
+ raise RasaException(
132
+ "In TwilioVoice channel, either both username and password "
133
+ "or neither should be provided. "
134
+ )
135
+
122
136
  return cls(
123
137
  credentials.get(
124
138
  "reprompt_fallback_phrase",
@@ -128,6 +142,8 @@ class TwilioVoiceInput(InputChannel):
128
142
  credentials.get("speech_timeout", "5"),
129
143
  credentials.get("speech_model", "default"),
130
144
  credentials.get("enhanced", "false"),
145
+ username=username,
146
+ password=password,
131
147
  )
132
148
 
133
149
  def __init__(
@@ -137,6 +153,8 @@ class TwilioVoiceInput(InputChannel):
137
153
  speech_timeout: Text = "5",
138
154
  speech_model: Text = "default",
139
155
  enhanced: Text = "false",
156
+ username: Optional[Text] = None,
157
+ password: Optional[Text] = None,
140
158
  ) -> None:
141
159
  """Creates a connection to Twilio voice.
142
160
 
@@ -152,6 +170,8 @@ class TwilioVoiceInput(InputChannel):
152
170
  self.speech_timeout = speech_timeout
153
171
  self.speech_model = speech_model
154
172
  self.enhanced = enhanced
173
+ self.username = username
174
+ self.password = password
155
175
 
156
176
  self._validate_configuration()
157
177
 
@@ -160,6 +180,9 @@ class TwilioVoiceInput(InputChannel):
160
180
  if self.assistant_voice not in self.SUPPORTED_VOICES:
161
181
  self._raise_invalid_voice_exception()
162
182
 
183
+ if (self.username is None) != (self.password is None):
184
+ self._raise_invalid_credentials_exception()
185
+
163
186
  try:
164
187
  int(self.speech_timeout)
165
188
  except ValueError:
@@ -245,6 +268,13 @@ class TwilioVoiceInput(InputChannel):
245
268
  return response.json({"status": "ok"})
246
269
 
247
270
  @twilio_voice_webhook.route("/webhook", methods=["POST"])
271
+ @requires_basic_auth(
272
+ username=self.username,
273
+ password=self.password,
274
+ auth_request_provider=create_auth_requested_response_provider(
275
+ TWILIO_VOICE_PATH
276
+ ),
277
+ )
248
278
  async def receive(request: Request) -> HTTPResponse:
249
279
  sender_id = request.form.get("From")
250
280
  text = request.form.get("SpeechResult")
@@ -309,6 +339,11 @@ class TwilioVoiceInput(InputChannel):
309
339
  twilio_response = self._build_twilio_voice_response(
310
340
  [{"text": last_response_text}]
311
341
  )
342
+
343
+ logger.debug(
344
+ "twilio_voice.webhook.twilio_response",
345
+ twilio_response=str(twilio_response),
346
+ )
312
347
  return response.text(str(twilio_response), content_type="text/xml")
313
348
 
314
349
  return twilio_voice_webhook
@@ -328,6 +363,13 @@ class TwilioVoiceInput(InputChannel):
328
363
  enhanced=self.enhanced,
329
364
  )
330
365
 
366
+ if not messages:
367
+ # In case bot has a greet message disabled
368
+ # or if the bot is not configured to send an initial message
369
+ # we need to send a voice response with speech settings
370
+ voice_response.append(gather)
371
+ return voice_response
372
+
331
373
  # Add pauses between messages.
332
374
  # Add a listener to the last message to listen for user response.
333
375
  for i, message in enumerate(messages):
@@ -346,6 +388,12 @@ class TwilioVoiceInput(InputChannel):
346
388
 
347
389
  return voice_response
348
390
 
391
+ def _raise_invalid_credentials_exception(self) -> None:
392
+ raise InvalidConfigException(
393
+ "In TwilioVoice channel, either both username and password "
394
+ "or neither should be provided. "
395
+ )
396
+
349
397
 
350
398
  class TwilioVoiceCollectingOutputChannel(CollectingOutputChannel):
351
399
  """Output channel that collects send messages in a list.
@@ -16,3 +16,8 @@ class NewTranscript(ASREvent):
16
16
  @dataclass
17
17
  class UserIsSpeaking(ASREvent):
18
18
  pass
19
+
20
+
21
+ @dataclass
22
+ class UserSilence(ASREvent):
23
+ pass
@@ -1,6 +1,6 @@
1
1
  import os
2
- from typing import AsyncIterator, Dict, Optional
3
2
  from dataclasses import dataclass
3
+ from typing import AsyncIterator, Dict, Optional
4
4
 
5
5
  import aiohttp
6
6
  import structlog
@@ -15,7 +15,6 @@ from rasa.core.channels.voice_stream.tts.tts_engine import (
15
15
  from rasa.shared.constants import AZURE_SPEECH_API_KEY_ENV_VAR
16
16
  from rasa.shared.exceptions import ConnectionException
17
17
 
18
-
19
18
  structlogger = structlog.get_logger()
20
19
 
21
20
 
@@ -54,13 +53,22 @@ class AzureTTS(TTSEngine[AzureTTSConfig]):
54
53
  async for data in response.content.iter_chunked(1024):
55
54
  yield self.engine_bytes_to_rasa_audio_bytes(data)
56
55
  return
56
+ elif response.status == 401:
57
+ structlogger.error(
58
+ "azure.synthesize.rest.authentication_failed",
59
+ status_code=response.status,
60
+ )
61
+ raise TTSError(
62
+ f"Authentication failed. Please check your API key: {response.status}" # noqa: E501
63
+ )
57
64
  else:
65
+ response_text = await response.text()
58
66
  structlogger.error(
59
67
  "azure.synthesize.rest.failed",
60
68
  status_code=response.status,
61
- msg=response.text(),
69
+ msg=response_text,
62
70
  )
63
- raise TTSError(f"TTS failed: {response.text()}")
71
+ raise TTSError(f"TTS failed: {response_text}")
64
72
  except ClientConnectorError as e:
65
73
  raise TTSError(e)
66
74
  except TimeoutError as e:
@@ -98,7 +106,7 @@ class AzureTTS(TTSEngine[AzureTTSConfig]):
98
106
  language="en-US",
99
107
  voice="en-US-JennyNeural",
100
108
  timeout=10,
101
- speech_region="germanywestcentral",
109
+ speech_region="eastus",
102
110
  )
103
111
 
104
112
  @classmethod
@@ -1,31 +1,54 @@
1
+ from __future__ import annotations
2
+
1
3
  import base64
2
4
  import json
3
5
  import uuid
6
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Text, Tuple
4
7
 
5
8
  import structlog
6
- from typing import Any, Awaitable, Callable, Dict, Optional, Text, Tuple
7
-
8
- from sanic import Blueprint, HTTPResponse, Request, response
9
- from sanic import Websocket # type: ignore
10
-
9
+ from sanic import ( # type: ignore[attr-defined]
10
+ Blueprint,
11
+ HTTPResponse,
12
+ Request,
13
+ Websocket,
14
+ response,
15
+ )
11
16
 
12
- from rasa.core.channels import UserMessage
17
+ from rasa.core.channels import InputChannel, UserMessage
18
+ from rasa.core.channels.channel import (
19
+ create_auth_requested_response_provider,
20
+ requires_basic_auth,
21
+ )
13
22
  from rasa.core.channels.voice_ready.utils import CallParameters
23
+ from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
14
24
  from rasa.core.channels.voice_stream.call_state import call_state
15
25
  from rasa.core.channels.voice_stream.tts.tts_engine import TTSEngine
16
- from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
17
26
  from rasa.core.channels.voice_stream.voice_channel import (
27
+ ContinueConversationAction,
18
28
  EndConversationAction,
19
29
  NewAudioAction,
20
30
  VoiceChannelAction,
21
- ContinueConversationAction,
22
31
  VoiceInputChannel,
23
32
  VoiceOutputChannel,
24
33
  )
34
+ from rasa.shared.exceptions import RasaException
35
+
36
+ if TYPE_CHECKING:
37
+ from twilio.twiml.voice_response import VoiceResponse
25
38
 
26
39
  logger = structlog.get_logger(__name__)
27
40
 
28
41
 
42
+ TWILIO_MEDIA_STREAMS_WEBHOOK_PATH = "webhooks/twilio_media_streams/webhook"
43
+ TWILIO_MEDIA_STREAMS_WEBSOCKET_PATH = "webhooks/twilio_media_streams/websocket"
44
+
45
+
46
+ CALL_SID_REQUEST_KEY = "CallSid"
47
+ FROM_NUMBER_REQUEST_KEY = "From"
48
+ TO_NUMBER_REQUEST_KEY = "To"
49
+ DIRECTION_REQUEST_KEY = "Direction"
50
+
51
+
29
52
  def map_call_params(data: Dict[Text, Any]) -> CallParameters:
30
53
  """Map the twilio stream parameters to the CallParameters dataclass."""
31
54
  stream_sid = data["streamSid"]
@@ -74,6 +97,40 @@ class TwilioMediaStreamsOutputChannel(VoiceOutputChannel):
74
97
 
75
98
 
76
99
  class TwilioMediaStreamsInputChannel(VoiceInputChannel):
100
+ def __init__(
101
+ self,
102
+ server_url: str,
103
+ asr_config: Dict,
104
+ tts_config: Dict,
105
+ monitor_silence: bool = False,
106
+ username: Optional[Text] = None,
107
+ password: Optional[Text] = None,
108
+ ):
109
+ super().__init__(server_url, asr_config, tts_config, monitor_silence)
110
+ self.username = username
111
+ self.password = password
112
+
113
+ @classmethod
114
+ def from_credentials(cls, credentials: Optional[Dict[str, Any]]) -> InputChannel:
115
+ credentials = credentials or {}
116
+
117
+ username = credentials.get("username")
118
+ password = credentials.get("password")
119
+ if (username is None) != (password is None):
120
+ raise RasaException(
121
+ "In TwilioMediaStreams channel, either both username and password "
122
+ "or neither should be provided. "
123
+ )
124
+
125
+ return cls(
126
+ credentials["server_url"],
127
+ credentials["asr"],
128
+ credentials["tts"],
129
+ credentials.get("monitor_silence", False),
130
+ username=username,
131
+ password=password,
132
+ )
133
+
77
134
  @classmethod
78
135
  def name(cls) -> str:
79
136
  return "twilio_media_streams"
@@ -126,16 +183,6 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
126
183
  self.tts_cache,
127
184
  )
128
185
 
129
- def websocket_stream_url(self) -> str:
130
- """Returns the websocket stream URL."""
131
- # depending on the config value, the url might contain http as a
132
- # protocol or not - we'll make sure both work
133
- if self.server_url.startswith("http"):
134
- base_url = self.server_url.replace("http", "ws")
135
- else:
136
- base_url = f"wss://{self.server_url}"
137
- return f"{base_url}/webhooks/twilio_media_streams/websocket"
138
-
139
186
  def blueprint(
140
187
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
141
188
  ) -> Blueprint:
@@ -147,22 +194,20 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
147
194
  return response.json({"status": "ok"})
148
195
 
149
196
  @blueprint.route("/webhook", methods=["POST"])
197
+ @requires_basic_auth(
198
+ username=self.username,
199
+ password=self.password,
200
+ auth_request_provider=create_auth_requested_response_provider(
201
+ realm=TWILIO_MEDIA_STREAMS_WEBHOOK_PATH
202
+ ),
203
+ )
150
204
  async def receive(request: Request) -> HTTPResponse:
151
- from twilio.twiml.voice_response import Connect, VoiceResponse
152
-
153
- voice_response = VoiceResponse()
154
- start = Connect()
155
- stream = start.stream(url=self.websocket_stream_url())
156
- # pass information about the call to the webhook - so we can
157
- # store it in the input channel
158
- stream.parameter(name="call_id", value=request.form.get("CallSid", None))
159
- stream.parameter(name="user_phone", value=request.form.get("From", None))
160
- stream.parameter(name="bot_phone", value=request.form.get("To", None))
161
- stream.parameter(
162
- name="direction", value=request.form.get("Direction", None)
163
- )
205
+ voice_response = self._build_twilio_response(request)
164
206
 
165
- voice_response.append(start)
207
+ logger.debug(
208
+ "twilio_media_streams.webhook.twilio_response",
209
+ twilio_response=str(voice_response),
210
+ )
166
211
 
167
212
  return response.text(str(voice_response), content_type="text/xml")
168
213
 
@@ -171,3 +216,36 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
171
216
  await self.run_audio_streaming(on_new_message, ws)
172
217
 
173
218
  return blueprint
219
+
220
+ def _websocket_stream_url(self) -> str:
221
+ """Returns the websocket stream URL."""
222
+ # depending on the config value, the url might contain http as a
223
+ # protocol or not - we'll make sure both work
224
+ if self.server_url.startswith("http"):
225
+ base_url = self.server_url.replace("http", "ws")
226
+ else:
227
+ base_url = f"wss://{self.server_url}"
228
+ return f"{base_url}/{TWILIO_MEDIA_STREAMS_WEBSOCKET_PATH}"
229
+
230
+ def _build_twilio_response(self, request: Request) -> VoiceResponse:
231
+ from twilio.twiml.voice_response import Connect, VoiceResponse
232
+
233
+ voice_response = VoiceResponse()
234
+ start = Connect()
235
+ stream = start.stream(url=self._websocket_stream_url())
236
+ # pass information about the call to the webhook - so we can
237
+ # store it in the input channel
238
+ stream.parameter(
239
+ name="call_id", value=request.form.get(CALL_SID_REQUEST_KEY, None)
240
+ )
241
+ stream.parameter(
242
+ name="user_phone", value=request.form.get(FROM_NUMBER_REQUEST_KEY, None)
243
+ )
244
+ stream.parameter(
245
+ name="bot_phone", value=request.form.get(TO_NUMBER_REQUEST_KEY, None)
246
+ )
247
+ stream.parameter(
248
+ name="direction", value=request.form.get(DIRECTION_REQUEST_KEY, None)
249
+ )
250
+ voice_response.append(start)
251
+ return voice_response