rasa-pro 3.13.0.dev2__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.
- rasa/__main__.py +3 -1
- rasa/cli/inspect.py +8 -4
- rasa/cli/project_templates/default/config.yml +5 -32
- rasa/cli/project_templates/{calm → default}/e2e_tests/cancelations/user_cancels_during_a_correction.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/corrections/user_corrects_contact_handle.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/corrections/user_corrects_contact_name.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_lists_contacts.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_removes_contact.yml +1 -1
- rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_removes_contact_from_list.yml +1 -1
- rasa/cli/project_templates/default/endpoints.yml +18 -2
- rasa/cli/run.py +10 -6
- rasa/cli/scaffold.py +3 -4
- rasa/cli/studio/download.py +1 -1
- rasa/cli/studio/upload.py +0 -6
- rasa/cli/utils.py +7 -0
- rasa/core/channels/channel.py +93 -0
- rasa/core/channels/inspector/dist/assets/{arc-c7691751.js → arc-9f75cc3b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-ab99dff7.js → blockDiagram-38ab4fdb-7f34db23.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-08c35a6b.js → c4Diagram-3d4e48cf-948bab2c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-dfa68278.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-9e9c71c9.js → classDiagram-70f12bd4-53b0dd0e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-15e7e2bf.js → classDiagram-v2-f2320105-fdf789e7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-edb7f119.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-9c105cb1.js → createText-2e5e7dd3-87c4ece5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-77e89e48.js → edges-e0da2a9e-5a8b0749.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-7a011646.js → erDiagram-9861fffd-66da90e2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-b6f105ac.js → flowDb-956e92f1-10044f05.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-ce4f18c2.js → flowDiagram-66a62f08-f338f66a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-65e7c670.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-cb5f6da4.js → flowchart-elk-definition-4a651766-b13140aa.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-e4d19e28.js → ganttDiagram-c361ad54-f2b4a55a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-727b1c33.js → gitGraphDiagram-72cf32ee-dedc298d.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-6e2ab9a7.js → graph-4ede11ff.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-3862675e-84ec700f.js → index-3862675e-65549d37.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-098a1a24.js → index-3a23e736.js} +142 -129
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-78dda442.js → infoDiagram-f8f76790-65439671.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-f1cc6dd1.js → journeyDiagram-49397b02-56d03d98.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-d98dcd0c.js → layout-dd48f7f4.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-838e3d82.js → line-1569ad2c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-eae72406.js → linear-48bf4935.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-c96fd84b.js → mindmap-definition-fc14e90a-688504c1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-c936d4e2.js → pieDiagram-8a3498a8-78b6d7e6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-b338eb8f.js → quadrantDiagram-120e2f19-048b84b3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-c6b6c0d5.js → requirementDiagram-deff3bca-dd67f107.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-b9372e19.js → sankeyDiagram-04a897e0-8128436e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-479e0a3f.js → sequenceDiagram-704730f1-1a0d1461.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-fd26eebc.js → stateDiagram-587899a1-46d388ed.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-3233e0ae.js → stateDiagram-v2-d93cdb3a-ea42951a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-1fdd392b.js → styles-6aaf32cf-7427ed0c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-6d7bfa1b.js → styles-9a916d00-ff5e5a16.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-f86aab11.js → styles-c10674c1-7b3680cf.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-e3e49d7a.js → svgDrawCommon-08f97a94-f860f2ad.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-6fe08b4d.js → timeline-definition-85554ec2-2eebf0c8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-c2e06fd6.js → xychartDiagram-e933f94c-5d7f4e96.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 +41 -15
- rasa/core/channels/voice_ready/jambonz.py +25 -5
- rasa/core/channels/voice_ready/jambonz_protocol.py +4 -0
- rasa/core/channels/voice_ready/twilio_voice.py +48 -1
- rasa/core/channels/voice_stream/tts/azure.py +11 -2
- rasa/core/channels/voice_stream/twilio_media_streams.py +101 -26
- rasa/core/channels/voice_stream/voice_channel.py +28 -2
- rasa/core/concurrent_lock_store.py +24 -10
- rasa/core/information_retrieval/faiss.py +7 -68
- rasa/core/information_retrieval/information_retrieval.py +2 -40
- rasa/core/information_retrieval/milvus.py +2 -7
- rasa/core/information_retrieval/qdrant.py +2 -7
- rasa/core/lock_store.py +151 -60
- rasa/core/nlg/contextual_response_rephraser.py +3 -0
- rasa/core/policies/enterprise_search_policy.py +310 -61
- rasa/core/policies/intentless_policy.py +3 -0
- rasa/dialogue_understanding/coexistence/llm_based_router.py +8 -0
- rasa/dialogue_understanding/commands/knowledge_answer_command.py +2 -2
- rasa/dialogue_understanding/generator/command_parser.py +1 -1
- rasa/dialogue_understanding/generator/flow_retrieval.py +1 -4
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +1 -2
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +13 -0
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_template.jinja2 +1 -1
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_claude_3_5_sonnet_20240620_template.jinja2 +1 -1
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v2_gpt_4o_2024_11_20_template.jinja2 +2 -24
- rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +22 -17
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +27 -12
- rasa/dialogue_understanding_test/du_test_case.py +16 -8
- rasa/dialogue_understanding_test/io.py +8 -13
- rasa/e2e_test/utils/validation.py +3 -3
- rasa/engine/recipes/default_components.py +0 -2
- rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +3 -0
- rasa/plugin.py +0 -3
- rasa/shared/constants.py +1 -0
- rasa/shared/core/domain.py +165 -11
- rasa/shared/core/flows/flow.py +155 -131
- rasa/shared/core/flows/flow_step.py +19 -3
- rasa/shared/core/flows/flow_step_links.py +15 -0
- rasa/shared/core/flows/flow_step_sequence.py +6 -0
- rasa/shared/core/flows/nlu_trigger.py +13 -0
- rasa/shared/core/flows/steps/action.py +7 -4
- rasa/shared/core/flows/steps/call.py +11 -4
- rasa/shared/core/flows/steps/collect.py +27 -6
- rasa/shared/core/flows/steps/internal.py +6 -1
- rasa/shared/core/flows/steps/link.py +7 -4
- rasa/shared/core/flows/steps/no_operation.py +7 -4
- rasa/shared/core/flows/steps/set_slots.py +8 -4
- rasa/shared/core/flows/yaml_flows_io.py +106 -5
- rasa/shared/importers/importer.py +8 -0
- rasa/shared/providers/_utils.py +83 -0
- rasa/shared/providers/llm/_base_litellm_client.py +6 -3
- rasa/shared/providers/llm/azure_openai_llm_client.py +6 -68
- rasa/shared/providers/router/_base_litellm_router_client.py +53 -1
- rasa/shared/utils/common.py +42 -0
- rasa/shared/utils/constants.py +3 -0
- rasa/shared/utils/llm.py +70 -24
- rasa/studio/download/domains.py +49 -0
- rasa/studio/download/download.py +439 -0
- rasa/studio/download/flows.py +359 -0
- rasa/studio/results_logger.py +6 -1
- rasa/studio/upload.py +69 -5
- rasa/tracing/instrumentation/attribute_extractors.py +7 -10
- rasa/tracing/instrumentation/instrumentation.py +12 -12
- rasa/utils/common.py +36 -0
- rasa/utils/endpoints.py +22 -1
- rasa/utils/licensing.py +1 -1
- rasa/validator.py +1 -2
- rasa/version.py +1 -1
- {rasa_pro-3.13.0.dev2.dist-info → rasa_pro-3.13.0.dev5.dist-info}/METADATA +7 -7
- {rasa_pro-3.13.0.dev2.dist-info → rasa_pro-3.13.0.dev5.dist-info}/RECORD +149 -166
- rasa/cli/project_templates/calm/config.yml +0 -10
- rasa/cli/project_templates/calm/credentials.yml +0 -33
- rasa/cli/project_templates/calm/endpoints.yml +0 -58
- rasa/cli/project_templates/default/actions/actions.py +0 -27
- rasa/cli/project_templates/default/data/nlu.yml +0 -91
- rasa/cli/project_templates/default/data/rules.yml +0 -13
- rasa/cli/project_templates/default/data/stories.yml +0 -30
- rasa/cli/project_templates/default/domain.yml +0 -34
- rasa/cli/project_templates/default/tests/test_stories.yml +0 -91
- rasa/core/channels/inspector/dist/assets/channel-11268142.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-ff7f2ce7.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-cba7ae20.js +0 -1
- rasa/document_retrieval/__init__.py +0 -0
- rasa/document_retrieval/constants.py +0 -32
- rasa/document_retrieval/document_post_processor.py +0 -351
- rasa/document_retrieval/document_post_processor_prompt_template.jinja2 +0 -0
- rasa/document_retrieval/document_retriever.py +0 -333
- rasa/document_retrieval/knowledge_base_connectors/__init__.py +0 -0
- rasa/document_retrieval/knowledge_base_connectors/api_connector.py +0 -39
- rasa/document_retrieval/knowledge_base_connectors/knowledge_base_connector.py +0 -34
- rasa/document_retrieval/knowledge_base_connectors/vector_store_connector.py +0 -226
- rasa/document_retrieval/query_rewriter.py +0 -234
- rasa/document_retrieval/query_rewriter_prompt_template.jinja2 +0 -8
- rasa/studio/download.py +0 -489
- /rasa/cli/project_templates/{calm → default}/actions/action_template.py +0 -0
- /rasa/cli/project_templates/{calm → default}/actions/add_contact.py +0 -0
- /rasa/cli/project_templates/{calm → default}/actions/db.py +0 -0
- /rasa/cli/project_templates/{calm → default}/actions/list_contacts.py +0 -0
- /rasa/cli/project_templates/{calm → default}/actions/remove_contact.py +0 -0
- /rasa/cli/project_templates/{calm → default}/data/flows/add_contact.yml +0 -0
- /rasa/cli/project_templates/{calm → default}/data/flows/list_contacts.yml +0 -0
- /rasa/cli/project_templates/{calm → default}/data/flows/remove_contact.yml +0 -0
- /rasa/cli/project_templates/{calm → default}/db/contacts.json +0 -0
- /rasa/cli/project_templates/{calm → default}/domain/add_contact.yml +0 -0
- /rasa/cli/project_templates/{calm → default}/domain/list_contacts.yml +0 -0
- /rasa/cli/project_templates/{calm → default}/domain/remove_contact.yml +0 -0
- /rasa/cli/project_templates/{calm → default}/domain/shared.yml +0 -0
- /rasa/{cli/project_templates/calm/actions → studio/download}/__init__.py +0 -0
- {rasa_pro-3.13.0.dev2.dist-info → rasa_pro-3.13.0.dev5.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.0.dev2.dist-info → rasa_pro-3.13.0.dev5.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.0.dev2.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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
97
|
-
text += json.dumps(event_params)
|
|
119
|
+
metadata = {"value": event["value"]}
|
|
98
120
|
else:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
@@ -5,8 +5,14 @@ from sanic import Blueprint, Websocket, response # type: ignore[attr-defined]
|
|
|
5
5
|
from sanic.request import Request
|
|
6
6
|
from sanic.response import HTTPResponse
|
|
7
7
|
|
|
8
|
-
from rasa.core.channels.channel import
|
|
8
|
+
from rasa.core.channels.channel import (
|
|
9
|
+
InputChannel,
|
|
10
|
+
OutputChannel,
|
|
11
|
+
UserMessage,
|
|
12
|
+
requires_basic_auth,
|
|
13
|
+
)
|
|
9
14
|
from rasa.core.channels.voice_ready.jambonz_protocol import (
|
|
15
|
+
CHANNEL_NAME,
|
|
10
16
|
send_ws_hangup_message,
|
|
11
17
|
send_ws_text_message,
|
|
12
18
|
websocket_message_handler,
|
|
@@ -18,8 +24,6 @@ from rasa.utils.io import remove_emojis
|
|
|
18
24
|
|
|
19
25
|
structlogger = structlog.get_logger()
|
|
20
26
|
|
|
21
|
-
CHANNEL_NAME = "jambonz"
|
|
22
|
-
|
|
23
27
|
DEFAULT_HANGUP_DELAY_SECONDS = 1
|
|
24
28
|
|
|
25
29
|
|
|
@@ -32,12 +36,27 @@ class JambonzVoiceReadyInput(InputChannel):
|
|
|
32
36
|
|
|
33
37
|
@classmethod
|
|
34
38
|
def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChannel:
|
|
35
|
-
|
|
39
|
+
if not credentials:
|
|
40
|
+
return cls()
|
|
41
|
+
|
|
42
|
+
username = credentials.get("username")
|
|
43
|
+
password = credentials.get("password")
|
|
44
|
+
if (username is None) != (password is None):
|
|
45
|
+
raise RasaException(
|
|
46
|
+
"In Jambonz channel, either both username and password "
|
|
47
|
+
"or neither should be provided. "
|
|
48
|
+
)
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
return cls(username, password)
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self, username: Optional[Text] = None, password: Optional[Text] = None
|
|
54
|
+
) -> None:
|
|
38
55
|
"""Initializes the JambonzVoiceReadyInput channel."""
|
|
39
56
|
mark_as_beta_feature("Jambonz Channel")
|
|
40
57
|
validate_voice_license_scope()
|
|
58
|
+
self.username = username
|
|
59
|
+
self.password = password
|
|
41
60
|
|
|
42
61
|
def blueprint(
|
|
43
62
|
self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
|
|
@@ -50,6 +69,7 @@ class JambonzVoiceReadyInput(InputChannel):
|
|
|
50
69
|
return response.json({"status": "ok"})
|
|
51
70
|
|
|
52
71
|
@jambonz_webhook.websocket("/websocket", subprotocols=["ws.jambonz.org"]) # type: ignore
|
|
72
|
+
@requires_basic_auth(self.username, self.password)
|
|
53
73
|
async def websocket(request: Request, ws: Websocket) -> None:
|
|
54
74
|
"""Triggered on new websocket connection."""
|
|
55
75
|
async for message in ws:
|
|
@@ -10,6 +10,7 @@ from rasa.core.channels.channel import UserMessage
|
|
|
10
10
|
from rasa.core.channels.voice_ready.utils import CallParameters
|
|
11
11
|
|
|
12
12
|
structlogger = structlog.get_logger()
|
|
13
|
+
CHANNEL_NAME = "jambonz"
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@dataclass
|
|
@@ -206,6 +207,7 @@ async def handle_new_session(
|
|
|
206
207
|
output_channel=output_channel,
|
|
207
208
|
sender_id=message.call_sid,
|
|
208
209
|
metadata=asdict(message.call_params),
|
|
210
|
+
input_channel=CHANNEL_NAME,
|
|
209
211
|
)
|
|
210
212
|
await send_config_ack(message.message_id, ws)
|
|
211
213
|
await on_new_message(user_msg)
|
|
@@ -238,6 +240,7 @@ async def handle_gather_completed(
|
|
|
238
240
|
output_channel = JambonzWebsocketOutput(ws, transcript_result.call_sid)
|
|
239
241
|
user_msg = UserMessage(
|
|
240
242
|
text=most_likely_transcript.text,
|
|
243
|
+
input_channel=CHANNEL_NAME,
|
|
241
244
|
output_channel=output_channel,
|
|
242
245
|
sender_id=transcript_result.call_sid,
|
|
243
246
|
metadata={},
|
|
@@ -288,6 +291,7 @@ async def handle_call_status(
|
|
|
288
291
|
output_channel = JambonzWebsocketOutput(ws, call_status.call_sid)
|
|
289
292
|
user_msg = UserMessage(
|
|
290
293
|
text="/session_end",
|
|
294
|
+
input_channel=CHANNEL_NAME,
|
|
291
295
|
output_channel=output_channel,
|
|
292
296
|
sender_id=call_status.call_sid,
|
|
293
297
|
metadata={},
|
|
@@ -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=
|
|
70
|
+
msg=response_text,
|
|
62
71
|
)
|
|
63
|
-
raise TTSError(f"TTS failed: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|