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.
- rasa/cli/inspect.py +8 -4
- rasa/cli/run.py +10 -6
- rasa/cli/utils.py +7 -0
- rasa/core/channels/channel.py +93 -0
- rasa/core/channels/inspector/dist/assets/{arc-f0f8bd46.js → arc-f09fea11.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-7162c77d.js → blockDiagram-38ab4fdb-95518007.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-b1d0d098.js → c4Diagram-3d4e48cf-c91a4a08.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-cc7720dc.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-807a1b27.js → classDiagram-70f12bd4-27f7869b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-5238dcdb.js → classDiagram-v2-f2320105-1ab94cdb.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-3688e1f7.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-75dfaa67.js → createText-2e5e7dd3-a7900089.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-df20501d.js → edges-e0da2a9e-3d5b2697.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-13cf4797.js → erDiagram-9861fffd-443cc11b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-a4991264.js → flowDb-956e92f1-8a6f8c52.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-ccecf773.js → flowDiagram-66a62f08-06a0b4f3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-5055ec2d.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-b5801783.js → flowchart-elk-definition-4a651766-7a01e0b5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-161e079a.js → ganttDiagram-c361ad54-5f1289f2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-f38e86a4.js → gitGraphDiagram-72cf32ee-44409666.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-be6ef5d8.js → graph-3c393c89.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-3862675e-d9ce8994.js → index-3862675e-4d0c4142.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-7794b245.js → index-b208b2c3.js} +140 -127
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-5000a3dc.js → infoDiagram-f8f76790-ae0fa7ff.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-8ef0a17a.js → journeyDiagram-49397b02-5c3b08cc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-d649bc98.js → layout-b24c95cb.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-95add810.js → line-999a77c5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-f6025094.js → linear-81a792fd.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-2e8531c4.js → mindmap-definition-fc14e90a-c574f712.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-918adfdb.js → pieDiagram-8a3498a8-1919891d.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-cbd01797.js → quadrantDiagram-120e2f19-26e43d09.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-6a8b877b.js → requirementDiagram-deff3bca-f4b22985.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-c377c3fe.js → sankeyDiagram-04a897e0-b957b472.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-ab9e9b7f.js → sequenceDiagram-704730f1-1d8ca073.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-5e6ae67d.js → stateDiagram-587899a1-c67b1b71.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-40643476.js → stateDiagram-v2-d93cdb3a-ee820f55.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-afb8d108.js → styles-6aaf32cf-b162bdf3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-7edc9423.js → styles-9a916d00-67a7b254.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-c1d8f7e9.js → styles-c10674c1-81a8ac73.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-f494b2ef.js → svgDrawCommon-08f97a94-ede42905.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-11c7cdd0.js → timeline-definition-85554ec2-b0f41635.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-3f191ec1.js → xychartDiagram-e933f94c-d715dfb0.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +1 -1
- rasa/core/channels/inspector/src/App.tsx +3 -2
- rasa/core/channels/inspector/src/components/Chat.tsx +23 -2
- rasa/core/channels/inspector/src/components/DiagramFlow.tsx +2 -5
- rasa/core/channels/inspector/src/helpers/conversation.ts +16 -0
- rasa/core/channels/inspector/src/types.ts +1 -1
- rasa/core/channels/voice_ready/audiocodes.py +7 -4
- rasa/core/channels/voice_ready/jambonz.py +29 -8
- rasa/core/channels/voice_ready/jambonz_protocol.py +4 -0
- rasa/core/channels/voice_ready/twilio_voice.py +56 -8
- rasa/core/channels/voice_stream/asr/asr_event.py +5 -0
- rasa/core/channels/voice_stream/tts/azure.py +13 -5
- rasa/core/channels/voice_stream/twilio_media_streams.py +110 -32
- rasa/core/channels/voice_stream/voice_channel.py +30 -30
- rasa/e2e_test/utils/validation.py +3 -3
- rasa/shared/constants.py +1 -0
- rasa/shared/providers/_utils.py +84 -0
- rasa/shared/providers/llm/_base_litellm_client.py +5 -3
- rasa/shared/providers/llm/azure_openai_llm_client.py +6 -65
- rasa/shared/providers/router/_base_litellm_router_client.py +55 -1
- rasa/validator.py +1 -2
- rasa/version.py +1 -1
- {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/METADATA +5 -5
- {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/RECORD +69 -68
- rasa/core/channels/inspector/dist/assets/channel-e265ea59.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-21f8a43d.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-5c8ce12d.js +0 -1
- {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/NOTICE +0 -0
- {rasa_pro-3.11.7.dist-info → rasa_pro-3.11.9.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -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=
|
|
69
|
+
msg=response_text,
|
|
62
70
|
)
|
|
63
|
-
raise TTSError(f"TTS failed: {
|
|
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="
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|