rasa-pro 3.13.0.dev3__py3-none-any.whl → 3.13.0.dev5__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 (132) hide show
  1. rasa/__main__.py +3 -1
  2. rasa/cli/inspect.py +8 -4
  3. rasa/cli/project_templates/default/config.yml +5 -32
  4. rasa/cli/project_templates/{calm → default}/e2e_tests/cancelations/user_cancels_during_a_correction.yml +1 -1
  5. rasa/cli/project_templates/{calm → default}/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml +1 -1
  6. rasa/cli/project_templates/{calm → default}/e2e_tests/corrections/user_corrects_contact_handle.yml +1 -1
  7. rasa/cli/project_templates/{calm → default}/e2e_tests/corrections/user_corrects_contact_name.yml +1 -1
  8. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml +1 -1
  9. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_lists_contacts.yml +1 -1
  10. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_removes_contact.yml +1 -1
  11. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_removes_contact_from_list.yml +1 -1
  12. rasa/cli/project_templates/default/endpoints.yml +18 -2
  13. rasa/cli/scaffold.py +3 -4
  14. rasa/cli/studio/download.py +1 -1
  15. rasa/cli/studio/upload.py +0 -6
  16. rasa/core/channels/channel.py +68 -5
  17. rasa/core/channels/inspector/dist/assets/{arc-c7691751.js → arc-9f75cc3b.js} +1 -1
  18. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-ab99dff7.js → blockDiagram-38ab4fdb-7f34db23.js} +1 -1
  19. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-08c35a6b.js → c4Diagram-3d4e48cf-948bab2c.js} +1 -1
  20. rasa/core/channels/inspector/dist/assets/channel-dfa68278.js +1 -0
  21. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-9e9c71c9.js → classDiagram-70f12bd4-53b0dd0e.js} +1 -1
  22. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-15e7e2bf.js → classDiagram-v2-f2320105-fdf789e7.js} +1 -1
  23. rasa/core/channels/inspector/dist/assets/clone-edb7f119.js +1 -0
  24. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-9c105cb1.js → createText-2e5e7dd3-87c4ece5.js} +1 -1
  25. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-77e89e48.js → edges-e0da2a9e-5a8b0749.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-7a011646.js → erDiagram-9861fffd-66da90e2.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-b6f105ac.js → flowDb-956e92f1-10044f05.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-ce4f18c2.js → flowDiagram-66a62f08-f338f66a.js} +1 -1
  29. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-65e7c670.js +1 -0
  30. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-cb5f6da4.js → flowchart-elk-definition-4a651766-b13140aa.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-e4d19e28.js → ganttDiagram-c361ad54-f2b4a55a.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-727b1c33.js → gitGraphDiagram-72cf32ee-dedc298d.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{graph-6e2ab9a7.js → graph-4ede11ff.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{index-3862675e-84ec700f.js → index-3862675e-65549d37.js} +1 -1
  35. rasa/core/channels/inspector/dist/assets/{index-098a1a24.js → index-3a23e736.js} +142 -129
  36. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-78dda442.js → infoDiagram-f8f76790-65439671.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-f1cc6dd1.js → journeyDiagram-49397b02-56d03d98.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{layout-d98dcd0c.js → layout-dd48f7f4.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{line-838e3d82.js → line-1569ad2c.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{linear-eae72406.js → linear-48bf4935.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-c96fd84b.js → mindmap-definition-fc14e90a-688504c1.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-c936d4e2.js → pieDiagram-8a3498a8-78b6d7e6.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-b338eb8f.js → quadrantDiagram-120e2f19-048b84b3.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-c6b6c0d5.js → requirementDiagram-deff3bca-dd67f107.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-b9372e19.js → sankeyDiagram-04a897e0-8128436e.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-479e0a3f.js → sequenceDiagram-704730f1-1a0d1461.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-fd26eebc.js → stateDiagram-587899a1-46d388ed.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-3233e0ae.js → stateDiagram-v2-d93cdb3a-ea42951a.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-1fdd392b.js → styles-6aaf32cf-7427ed0c.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-6d7bfa1b.js → styles-9a916d00-ff5e5a16.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-f86aab11.js → styles-c10674c1-7b3680cf.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-e3e49d7a.js → svgDrawCommon-08f97a94-f860f2ad.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-6fe08b4d.js → timeline-definition-85554ec2-2eebf0c8.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-c2e06fd6.js → xychartDiagram-e933f94c-5d7f4e96.js} +1 -1
  55. rasa/core/channels/inspector/dist/index.html +1 -1
  56. rasa/core/channels/inspector/src/App.tsx +3 -2
  57. rasa/core/channels/inspector/src/components/Chat.tsx +23 -2
  58. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +2 -5
  59. rasa/core/channels/inspector/src/helpers/conversation.ts +16 -0
  60. rasa/core/channels/inspector/src/types.ts +1 -1
  61. rasa/core/channels/voice_ready/audiocodes.py +41 -15
  62. rasa/core/channels/voice_ready/twilio_voice.py +48 -1
  63. rasa/core/channels/voice_stream/tts/azure.py +11 -2
  64. rasa/core/channels/voice_stream/twilio_media_streams.py +101 -26
  65. rasa/core/channels/voice_stream/voice_channel.py +28 -2
  66. rasa/core/concurrent_lock_store.py +24 -10
  67. rasa/core/lock_store.py +151 -60
  68. rasa/dialogue_understanding_test/du_test_case.py +16 -8
  69. rasa/plugin.py +0 -3
  70. rasa/shared/constants.py +1 -0
  71. rasa/shared/core/domain.py +165 -11
  72. rasa/shared/core/flows/flow.py +155 -131
  73. rasa/shared/core/flows/flow_step.py +19 -3
  74. rasa/shared/core/flows/flow_step_links.py +15 -0
  75. rasa/shared/core/flows/flow_step_sequence.py +6 -0
  76. rasa/shared/core/flows/nlu_trigger.py +13 -0
  77. rasa/shared/core/flows/steps/action.py +7 -4
  78. rasa/shared/core/flows/steps/call.py +11 -4
  79. rasa/shared/core/flows/steps/collect.py +27 -6
  80. rasa/shared/core/flows/steps/internal.py +6 -1
  81. rasa/shared/core/flows/steps/link.py +7 -4
  82. rasa/shared/core/flows/steps/no_operation.py +7 -4
  83. rasa/shared/core/flows/steps/set_slots.py +8 -4
  84. rasa/shared/core/flows/yaml_flows_io.py +106 -5
  85. rasa/shared/importers/importer.py +8 -0
  86. rasa/shared/providers/_utils.py +83 -0
  87. rasa/shared/providers/llm/_base_litellm_client.py +6 -3
  88. rasa/shared/providers/llm/azure_openai_llm_client.py +6 -68
  89. rasa/shared/providers/router/_base_litellm_router_client.py +53 -1
  90. rasa/shared/utils/common.py +42 -0
  91. rasa/studio/download/domains.py +49 -0
  92. rasa/studio/download/download.py +439 -0
  93. rasa/studio/download/flows.py +359 -0
  94. rasa/studio/results_logger.py +6 -1
  95. rasa/studio/upload.py +69 -5
  96. rasa/utils/common.py +36 -0
  97. rasa/utils/endpoints.py +22 -1
  98. rasa/utils/licensing.py +1 -1
  99. rasa/validator.py +1 -2
  100. rasa/version.py +1 -1
  101. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/METADATA +8 -8
  102. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/RECORD +119 -125
  103. rasa/cli/project_templates/calm/config.yml +0 -10
  104. rasa/cli/project_templates/calm/credentials.yml +0 -33
  105. rasa/cli/project_templates/calm/endpoints.yml +0 -58
  106. rasa/cli/project_templates/default/actions/actions.py +0 -27
  107. rasa/cli/project_templates/default/data/nlu.yml +0 -91
  108. rasa/cli/project_templates/default/data/rules.yml +0 -13
  109. rasa/cli/project_templates/default/data/stories.yml +0 -30
  110. rasa/cli/project_templates/default/domain.yml +0 -34
  111. rasa/cli/project_templates/default/tests/test_stories.yml +0 -91
  112. rasa/core/channels/inspector/dist/assets/channel-11268142.js +0 -1
  113. rasa/core/channels/inspector/dist/assets/clone-ff7f2ce7.js +0 -1
  114. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-cba7ae20.js +0 -1
  115. rasa/studio/download.py +0 -489
  116. /rasa/cli/project_templates/{calm → default}/actions/action_template.py +0 -0
  117. /rasa/cli/project_templates/{calm → default}/actions/add_contact.py +0 -0
  118. /rasa/cli/project_templates/{calm → default}/actions/db.py +0 -0
  119. /rasa/cli/project_templates/{calm → default}/actions/list_contacts.py +0 -0
  120. /rasa/cli/project_templates/{calm → default}/actions/remove_contact.py +0 -0
  121. /rasa/cli/project_templates/{calm → default}/data/flows/add_contact.yml +0 -0
  122. /rasa/cli/project_templates/{calm → default}/data/flows/list_contacts.yml +0 -0
  123. /rasa/cli/project_templates/{calm → default}/data/flows/remove_contact.yml +0 -0
  124. /rasa/cli/project_templates/{calm → default}/db/contacts.json +0 -0
  125. /rasa/cli/project_templates/{calm → default}/domain/add_contact.yml +0 -0
  126. /rasa/cli/project_templates/{calm → default}/domain/list_contacts.yml +0 -0
  127. /rasa/cli/project_templates/{calm → default}/domain/remove_contact.yml +0 -0
  128. /rasa/cli/project_templates/{calm → default}/domain/shared.yml +0 -0
  129. /rasa/{cli/project_templates/calm/actions → studio/download}/__init__.py +0 -0
  130. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/NOTICE +0 -0
  131. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/WHEEL +0 -0
  132. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.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;
@@ -6,7 +6,18 @@ import uuid
6
6
  from collections import defaultdict
7
7
  from dataclasses import asdict
8
8
  from datetime import datetime, timedelta, timezone
9
- from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Text, Union
9
+ from typing import (
10
+ Any,
11
+ Awaitable,
12
+ Callable,
13
+ Dict,
14
+ List,
15
+ Optional,
16
+ Set,
17
+ Text,
18
+ Tuple,
19
+ Union,
20
+ )
10
21
 
11
22
  import structlog
12
23
  from jsonschema import ValidationError, validate
@@ -76,32 +87,45 @@ class Conversation:
76
87
 
77
88
  @staticmethod
78
89
  def get_metadata(activity: Dict[Text, Any]) -> Optional[Dict[Text, Any]]:
79
- """Get metadata from the activity."""
80
- return asdict(map_call_params(activity["parameters"]))
90
+ """Get metadata from the activity.
91
+
92
+ ONLY used for activities NOT for events (see _handle_event)."""
93
+ return activity.get("parameters")
81
94
 
82
95
  @staticmethod
83
- def _handle_event(event: Dict[Text, Any]) -> Text:
84
- """Handle start and DTMF event and return the corresponding text."""
96
+ def _handle_event(event: Dict[Text, Any]) -> Tuple[Text, Dict[Text, Any]]:
97
+ """Handle events and return a tuple of text and metadata.
98
+
99
+ Args:
100
+ event: The event to handle.
101
+
102
+ Returns:
103
+ Tuple of text and metadata.
104
+ text is either /session_start or /vaig_event_<event_name>
105
+ metadata is a dictionary with the event parameters.
106
+ """
85
107
  structlogger.debug("audiocodes.handle.event", event_payload=event)
86
108
  if "name" not in event:
87
109
  structlogger.warning(
88
110
  "audiocodes.handle.event.no_name_key", event_payload=event
89
111
  )
90
- return ""
112
+ return "", {}
91
113
 
92
114
  if event["name"] == EVENT_START:
93
115
  text = f"{INTENT_MESSAGE_PREFIX}{USER_INTENT_SESSION_START}"
116
+ metadata = asdict(map_call_params(event.get("parameters", {})))
94
117
  elif event["name"] == EVENT_DTMF:
95
118
  text = f"{INTENT_MESSAGE_PREFIX}vaig_event_DTMF"
96
- event_params = {"value": event["value"]}
97
- text += json.dumps(event_params)
119
+ metadata = {"value": event["value"]}
98
120
  else:
99
- structlogger.warning(
100
- "audiocodes.handle.event.unknown_event", event_payload=event
101
- )
102
- return ""
121
+ # handle other events described by Audiocodes
122
+ # https://techdocs.audiocodes.com/voice-ai-connect/#VAIG_Combined/inactivity-detection.htm?TocPath=Bot%2520integration%257CReceiving%2520notifications%257C_____3
123
+ text = f"{INTENT_MESSAGE_PREFIX}vaig_event_{event['name']}"
124
+ metadata = {**event.get("parameters", {})}
125
+ if "value" in event:
126
+ metadata["value"] = event["value"]
103
127
 
104
- return text
128
+ return text, metadata
105
129
 
106
130
  def is_active_conversation(self, now: datetime, delta: timedelta) -> bool:
107
131
  """Check if the conversation is active."""
@@ -141,16 +165,18 @@ class Conversation:
141
165
  self.activity_ids.append(activity[ACTIVITY_ID_KEY])
142
166
  if activity["type"] == ACTIVITY_MESSAGE:
143
167
  text = activity["text"]
168
+ metadata = self.get_metadata(activity)
144
169
  elif activity["type"] == ACTIVITY_EVENT:
145
- text = self._handle_event(activity)
170
+ text, metadata = self._handle_event(activity)
146
171
  else:
147
172
  structlogger.warning(
148
173
  "audiocodes.handle.activities.unknown_activity_type",
149
174
  activity=activity,
150
175
  )
176
+ continue
177
+
151
178
  if not text:
152
179
  continue
153
- metadata = self.get_metadata(activity)
154
180
  user_msg = UserMessage(
155
181
  text=text,
156
182
  input_channel=input_channel_name,
@@ -13,14 +13,19 @@ from rasa.core.channels.channel import (
13
13
  CollectingOutputChannel,
14
14
  InputChannel,
15
15
  UserMessage,
16
+ create_auth_requested_response_provider,
17
+ requires_basic_auth,
16
18
  )
17
19
  from rasa.core.channels.voice_ready.utils import CallParameters
18
20
  from rasa.shared.core.events import BotUttered
19
- from rasa.shared.exceptions import InvalidConfigException
21
+ from rasa.shared.exceptions import InvalidConfigException, RasaException
20
22
 
21
23
  logger = structlog.get_logger(__name__)
22
24
 
23
25
 
26
+ TWILIO_VOICE_PATH = "webhooks/twilio_voice/webhook"
27
+
28
+
24
29
  def map_call_params(form: RequestParameters) -> CallParameters:
25
30
  """Map the Audiocodes parameters to the CallParameters dataclass."""
26
31
  return CallParameters(
@@ -120,6 +125,14 @@ class TwilioVoiceInput(InputChannel):
120
125
  """Load custom configurations."""
121
126
  credentials = credentials or {}
122
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
+
123
136
  return cls(
124
137
  credentials.get(
125
138
  "reprompt_fallback_phrase",
@@ -129,6 +142,8 @@ class TwilioVoiceInput(InputChannel):
129
142
  credentials.get("speech_timeout", "5"),
130
143
  credentials.get("speech_model", "default"),
131
144
  credentials.get("enhanced", "false"),
145
+ username=username,
146
+ password=password,
132
147
  )
133
148
 
134
149
  def __init__(
@@ -138,6 +153,8 @@ class TwilioVoiceInput(InputChannel):
138
153
  speech_timeout: Text = "5",
139
154
  speech_model: Text = "default",
140
155
  enhanced: Text = "false",
156
+ username: Optional[Text] = None,
157
+ password: Optional[Text] = None,
141
158
  ) -> None:
142
159
  """Creates a connection to Twilio voice.
143
160
 
@@ -153,6 +170,8 @@ class TwilioVoiceInput(InputChannel):
153
170
  self.speech_timeout = speech_timeout
154
171
  self.speech_model = speech_model
155
172
  self.enhanced = enhanced
173
+ self.username = username
174
+ self.password = password
156
175
 
157
176
  self._validate_configuration()
158
177
 
@@ -161,6 +180,9 @@ class TwilioVoiceInput(InputChannel):
161
180
  if self.assistant_voice not in self.SUPPORTED_VOICES:
162
181
  self._raise_invalid_voice_exception()
163
182
 
183
+ if (self.username is None) != (self.password is None):
184
+ self._raise_invalid_credentials_exception()
185
+
164
186
  try:
165
187
  int(self.speech_timeout)
166
188
  except ValueError:
@@ -246,6 +268,13 @@ class TwilioVoiceInput(InputChannel):
246
268
  return response.json({"status": "ok"})
247
269
 
248
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
+ )
249
278
  async def receive(request: Request) -> HTTPResponse:
250
279
  sender_id = request.form.get("From")
251
280
  text = request.form.get("SpeechResult")
@@ -310,6 +339,11 @@ class TwilioVoiceInput(InputChannel):
310
339
  twilio_response = self._build_twilio_voice_response(
311
340
  [{"text": last_response_text}]
312
341
  )
342
+
343
+ logger.debug(
344
+ "twilio_voice.webhook.twilio_response",
345
+ twilio_response=str(twilio_response),
346
+ )
313
347
  return response.text(str(twilio_response), content_type="text/xml")
314
348
 
315
349
  return twilio_voice_webhook
@@ -329,6 +363,13 @@ class TwilioVoiceInput(InputChannel):
329
363
  enhanced=self.enhanced,
330
364
  )
331
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
+
332
373
  # Add pauses between messages.
333
374
  # Add a listener to the last message to listen for user response.
334
375
  for i, message in enumerate(messages):
@@ -347,6 +388,12 @@ class TwilioVoiceInput(InputChannel):
347
388
 
348
389
  return voice_response
349
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
+
350
397
 
351
398
  class TwilioVoiceCollectingOutputChannel(CollectingOutputChannel):
352
399
  """Output channel that collects send messages in a list.
@@ -54,13 +54,22 @@ class AzureTTS(TTSEngine[AzureTTSConfig]):
54
54
  async for data in response.content.iter_chunked(1024):
55
55
  yield self.engine_bytes_to_rasa_audio_bytes(data)
56
56
  return
57
+ elif response.status == 401:
58
+ structlogger.error(
59
+ "azure.synthesize.rest.authentication_failed",
60
+ status_code=response.status,
61
+ )
62
+ raise TTSError(
63
+ f"Authentication failed. Please check your API key: {response.status}" # noqa: E501
64
+ )
57
65
  else:
66
+ response_text = await response.text()
58
67
  structlogger.error(
59
68
  "azure.synthesize.rest.failed",
60
69
  status_code=response.status,
61
- msg=response.text(),
70
+ msg=response_text,
62
71
  )
63
- raise TTSError(f"TTS failed: {response.text()}")
72
+ raise TTSError(f"TTS failed: {response_text}")
64
73
  except ClientConnectorError as e:
65
74
  raise TTSError(e)
66
75
  except TimeoutError as e:
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import base64
2
4
  import json
3
5
  import uuid
4
- from typing import Any, Awaitable, Callable, Dict, Optional, Text, Tuple
6
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Text, Tuple
5
7
 
6
8
  import structlog
7
9
  from sanic import ( # type: ignore[attr-defined]
@@ -12,7 +14,11 @@ from sanic import ( # type: ignore[attr-defined]
12
14
  response,
13
15
  )
14
16
 
15
- 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
+ )
16
22
  from rasa.core.channels.voice_ready.utils import CallParameters
17
23
  from rasa.core.channels.voice_stream.audio_bytes import RasaAudioBytes
18
24
  from rasa.core.channels.voice_stream.call_state import call_state
@@ -25,10 +31,24 @@ from rasa.core.channels.voice_stream.voice_channel import (
25
31
  VoiceInputChannel,
26
32
  VoiceOutputChannel,
27
33
  )
34
+ from rasa.shared.exceptions import RasaException
35
+
36
+ if TYPE_CHECKING:
37
+ from twilio.twiml.voice_response import VoiceResponse
28
38
 
29
39
  logger = structlog.get_logger(__name__)
30
40
 
31
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
+
32
52
  def map_call_params(data: Dict[Text, Any]) -> CallParameters:
33
53
  """Map the twilio stream parameters to the CallParameters dataclass."""
34
54
  stream_sid = data["streamSid"]
@@ -77,6 +97,40 @@ class TwilioMediaStreamsOutputChannel(VoiceOutputChannel):
77
97
 
78
98
 
79
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
+
80
134
  @classmethod
81
135
  def name(cls) -> str:
82
136
  return "twilio_media_streams"
@@ -130,16 +184,6 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
130
184
  self.tts_cache,
131
185
  )
132
186
 
133
- def websocket_stream_url(self) -> str:
134
- """Returns the websocket stream URL."""
135
- # depending on the config value, the url might contain http as a
136
- # protocol or not - we'll make sure both work
137
- if self.server_url.startswith("http"):
138
- base_url = self.server_url.replace("http", "ws")
139
- else:
140
- base_url = f"wss://{self.server_url}"
141
- return f"{base_url}/webhooks/twilio_media_streams/websocket"
142
-
143
187
  def blueprint(
144
188
  self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
145
189
  ) -> Blueprint:
@@ -151,22 +195,20 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
151
195
  return response.json({"status": "ok"})
152
196
 
153
197
  @blueprint.route("/webhook", methods=["POST"])
198
+ @requires_basic_auth(
199
+ username=self.username,
200
+ password=self.password,
201
+ auth_request_provider=create_auth_requested_response_provider(
202
+ realm=TWILIO_MEDIA_STREAMS_WEBHOOK_PATH
203
+ ),
204
+ )
154
205
  async def receive(request: Request) -> HTTPResponse:
155
- from twilio.twiml.voice_response import Connect, VoiceResponse
156
-
157
- voice_response = VoiceResponse()
158
- start = Connect()
159
- stream = start.stream(url=self.websocket_stream_url())
160
- # pass information about the call to the webhook - so we can
161
- # store it in the input channel
162
- stream.parameter(name="call_id", value=request.form.get("CallSid", None))
163
- stream.parameter(name="user_phone", value=request.form.get("From", None))
164
- stream.parameter(name="bot_phone", value=request.form.get("To", None))
165
- stream.parameter(
166
- name="direction", value=request.form.get("Direction", None)
167
- )
206
+ voice_response = self._build_twilio_response(request)
168
207
 
169
- voice_response.append(start)
208
+ logger.debug(
209
+ "twilio_media_streams.webhook.twilio_response",
210
+ twilio_response=str(voice_response),
211
+ )
170
212
 
171
213
  return response.text(str(voice_response), content_type="text/xml")
172
214
 
@@ -175,3 +217,36 @@ class TwilioMediaStreamsInputChannel(VoiceInputChannel):
175
217
  await self.run_audio_streaming(on_new_message, ws)
176
218
 
177
219
  return blueprint
220
+
221
+ def _websocket_stream_url(self) -> str:
222
+ """Returns the websocket stream URL."""
223
+ # depending on the config value, the url might contain http as a
224
+ # protocol or not - we'll make sure both work
225
+ if self.server_url.startswith("http"):
226
+ base_url = self.server_url.replace("http", "ws")
227
+ else:
228
+ base_url = f"wss://{self.server_url}"
229
+ return f"{base_url}/{TWILIO_MEDIA_STREAMS_WEBSOCKET_PATH}"
230
+
231
+ def _build_twilio_response(self, request: Request) -> VoiceResponse:
232
+ from twilio.twiml.voice_response import Connect, VoiceResponse
233
+
234
+ voice_response = VoiceResponse()
235
+ start = Connect()
236
+ stream = start.stream(url=self._websocket_stream_url())
237
+ # pass information about the call to the webhook - so we can
238
+ # store it in the input channel
239
+ stream.parameter(
240
+ name="call_id", value=request.form.get(CALL_SID_REQUEST_KEY, None)
241
+ )
242
+ stream.parameter(
243
+ name="user_phone", value=request.form.get(FROM_NUMBER_REQUEST_KEY, None)
244
+ )
245
+ stream.parameter(
246
+ name="bot_phone", value=request.form.get(TO_NUMBER_REQUEST_KEY, None)
247
+ )
248
+ stream.parameter(
249
+ name="direction", value=request.form.get(DIRECTION_REQUEST_KEY, None)
250
+ )
251
+ voice_response.append(start)
252
+ return voice_response
@@ -42,6 +42,11 @@ from rasa.utils.io import remove_emojis
42
42
 
43
43
  logger = structlog.get_logger(__name__)
44
44
 
45
+ # define constants for the voice channel
46
+ USER_CONVERSATION_SESSION_END = "/session_end"
47
+ USER_CONVERSATION_SESSION_START = "/session_start"
48
+ USER_CONVERSATION_SILENCE_TIMEOUT = "/silence_timeout"
49
+
45
50
 
46
51
  @dataclass
47
52
  class VoiceChannelAction:
@@ -189,6 +194,7 @@ class VoiceOutputChannel(OutputChannel):
189
194
  collected_audio_bytes = RasaAudioBytes(b"")
190
195
  seconds_marker = -1
191
196
  last_sent_offset = 0
197
+ logger.debug("voice_channel.sending_audio", text=text)
192
198
 
193
199
  # Send start marker before first chunk
194
200
  try:
@@ -334,7 +340,7 @@ class VoiceInputChannel(InputChannel):
334
340
  ) -> None:
335
341
  output_channel = self.create_output_channel(channel_websocket, tts_engine)
336
342
  message = UserMessage(
337
- "/session_start",
343
+ USER_CONVERSATION_SESSION_START,
338
344
  output_channel,
339
345
  call_parameters.stream_id,
340
346
  input_channel=self.name(),
@@ -393,6 +399,9 @@ class VoiceInputChannel(InputChannel):
393
399
  await asr_engine.send_audio_chunks(channel_action.audio_bytes)
394
400
  elif isinstance(channel_action, EndConversationAction):
395
401
  # end stream event came from the other side
402
+ await self.handle_disconnect(
403
+ channel_websocket, on_new_message, tts_engine, call_parameters
404
+ )
396
405
  break
397
406
 
398
407
  async def receive_asr_events() -> None:
@@ -462,10 +471,27 @@ class VoiceInputChannel(InputChannel):
462
471
  elif isinstance(e, UserSilence):
463
472
  output_channel = self.create_output_channel(voice_websocket, tts_engine)
464
473
  message = UserMessage(
465
- "/silence_timeout",
474
+ USER_CONVERSATION_SILENCE_TIMEOUT,
466
475
  output_channel,
467
476
  call_parameters.stream_id,
468
477
  input_channel=self.name(),
469
478
  metadata=asdict(call_parameters),
470
479
  )
471
480
  await on_new_message(message)
481
+
482
+ async def handle_disconnect(
483
+ self,
484
+ channel_websocket: Websocket,
485
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
486
+ tts_engine: TTSEngine,
487
+ call_parameters: CallParameters,
488
+ ) -> None:
489
+ """Handle disconnection from the channel."""
490
+ output_channel = self.create_output_channel(channel_websocket, tts_engine)
491
+ message = UserMessage(
492
+ text=USER_CONVERSATION_SESSION_END,
493
+ output_channel=output_channel,
494
+ sender_id=call_parameters.stream_id,
495
+ input_channel=self.name(),
496
+ )
497
+ await on_new_message(message)
@@ -1,9 +1,10 @@
1
1
  import json
2
- import logging
3
2
  import time
4
3
  from collections import deque
5
4
  from typing import Deque, Optional, Text
6
5
 
6
+ import structlog
7
+
7
8
  from rasa.core.lock import Ticket, TicketLock
8
9
  from rasa.core.lock_store import (
9
10
  DEFAULT_SOCKET_TIMEOUT_IN_SECONDS,
@@ -19,7 +20,7 @@ DEFAULT_PORT = 6379
19
20
 
20
21
  DEFAULT_HOSTNAME = "localhost"
21
22
 
22
- logger = logging.getLogger(__name__)
23
+ structlogger = structlog.getLogger(__name__)
23
24
 
24
25
  LAST_ISSUED_TICKET_NUMBER_SUFFIX = "last_issued_ticket_number"
25
26
 
@@ -105,7 +106,10 @@ class ConcurrentRedisLockStore(LockStore):
105
106
 
106
107
  self.key_prefix = DEFAULT_CONCURRENT_REDIS_LOCK_STORE_KEY_PREFIX
107
108
  if key_prefix:
108
- logger.debug(f"Setting non-default redis key prefix: '{key_prefix}'.")
109
+ structlogger.debug(
110
+ "concurrent_redis_lock_store._set_key_prefix.non_default_key_prefix",
111
+ event_info=f"Setting non-default redis key prefix: '{key_prefix}'.",
112
+ )
109
113
  self._set_key_prefix(key_prefix)
110
114
 
111
115
  super().__init__()
@@ -116,9 +120,13 @@ class ConcurrentRedisLockStore(LockStore):
116
120
  key_prefix + ":" + DEFAULT_CONCURRENT_REDIS_LOCK_STORE_KEY_PREFIX
117
121
  )
118
122
  else:
119
- logger.warning(
120
- f"Omitting provided non-alphanumeric redis key prefix: '{key_prefix}'. "
121
- f"Using default '{self.key_prefix}' instead."
123
+ structlogger.warning(
124
+ "concurrent_redis_lock_store._set_key_prefix.default_instead_of_invalid_key_prefix",
125
+ event_info=(
126
+ f"Omitting provided non-alphanumeric "
127
+ f"redis key prefix: '{key_prefix}'. "
128
+ f"Using default '{self.key_prefix}' instead."
129
+ ),
122
130
  )
123
131
 
124
132
  def issue_ticket(
@@ -129,7 +137,10 @@ class ConcurrentRedisLockStore(LockStore):
129
137
  It's configured with `lock_lifetime` and associated with `conversation_id`.
130
138
  Creates a new lock if none is found.
131
139
  """
132
- logger.debug(f"Issuing ticket for conversation '{conversation_id}'.")
140
+ structlogger.debug(
141
+ "concurrent_redis_lock_store.issue_ticket",
142
+ event_info=f"Issuing ticket for conversation '{conversation_id}'.",
143
+ )
133
144
  try:
134
145
  lock = self.get_or_create_lock(conversation_id)
135
146
  lock.remove_expired_tickets()
@@ -164,9 +175,12 @@ class ConcurrentRedisLockStore(LockStore):
164
175
  redis_keys = self.red.keys(pattern)
165
176
 
166
177
  if not redis_keys:
167
- logger.debug(
168
- f"The lock store does not contain any key-value "
169
- f"items for conversation '{conversation_id}'."
178
+ structlogger.debug(
179
+ "concurrent_redis_lock_store.delete_lock_key_not_found",
180
+ event_info=(
181
+ f"The lock store does not contain any key-value "
182
+ f"items for conversation '{conversation_id}'."
183
+ ),
170
184
  )
171
185
  return None
172
186