rasa-pro 3.10.16__py3-none-any.whl → 3.11.0__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 +31 -15
- rasa/api.py +12 -2
- rasa/cli/arguments/default_arguments.py +24 -4
- rasa/cli/arguments/run.py +15 -0
- rasa/cli/arguments/shell.py +5 -1
- rasa/cli/arguments/train.py +17 -9
- rasa/cli/evaluate.py +7 -7
- rasa/cli/inspect.py +19 -7
- rasa/cli/interactive.py +1 -0
- rasa/cli/llm_fine_tuning.py +11 -14
- rasa/cli/project_templates/calm/config.yml +5 -7
- rasa/cli/project_templates/calm/endpoints.yml +15 -2
- rasa/cli/project_templates/tutorial/config.yml +8 -5
- rasa/cli/project_templates/tutorial/data/flows.yml +1 -1
- rasa/cli/project_templates/tutorial/data/patterns.yml +5 -0
- rasa/cli/project_templates/tutorial/domain.yml +14 -0
- rasa/cli/project_templates/tutorial/endpoints.yml +5 -0
- rasa/cli/run.py +7 -0
- rasa/cli/scaffold.py +4 -2
- rasa/cli/studio/upload.py +0 -15
- rasa/cli/train.py +14 -53
- rasa/cli/utils.py +14 -11
- rasa/cli/x.py +7 -7
- rasa/constants.py +3 -1
- rasa/core/actions/action.py +77 -33
- rasa/core/actions/action_hangup.py +29 -0
- rasa/core/actions/action_repeat_bot_messages.py +89 -0
- rasa/core/actions/e2e_stub_custom_action_executor.py +5 -1
- rasa/core/actions/http_custom_action_executor.py +4 -0
- rasa/core/agent.py +2 -2
- rasa/core/brokers/kafka.py +3 -1
- rasa/core/brokers/pika.py +3 -1
- rasa/core/channels/__init__.py +10 -6
- rasa/core/channels/channel.py +41 -4
- rasa/core/channels/development_inspector.py +150 -46
- rasa/core/channels/inspector/README.md +1 -1
- rasa/core/channels/inspector/dist/assets/{arc-b6e548fe.js → arc-bc141fb2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-d0fbc5ce-fa03ac9e.js → c4Diagram-d0fbc5ce-be2db283.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-936ed81e-ee67392a.js → classDiagram-936ed81e-55366915.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-c3cb15f1-9b283fae.js → classDiagram-v2-c3cb15f1-bb529518.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{createText-62fc7601-8b6fcc2a.js → createText-62fc7601-b0ec81d6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-f2ad444c-22e77f4f.js → edges-f2ad444c-6166330c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9d236eb7-60ffc87f.js → erDiagram-9d236eb7-5ccc6a8e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-1972c806-9dd802e4.js → flowDb-1972c806-fca3bfe4.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-7ea5b25a-5fa1912f.js → flowDiagram-7ea5b25a-4739080f.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-736177bf.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-abe16c3d-622a1fd2.js → flowchart-elk-definition-abe16c3d-7c1b0e0f.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-9b5ea136-e285a63a.js → ganttDiagram-9b5ea136-772fd050.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-99d0ae7c-f237bdca.js → gitGraphDiagram-99d0ae7c-8eae1dc9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-2c4b9a3b-4b03d70e.js → index-2c4b9a3b-f55afcdf.js} +1 -1
- rasa/core/channels/inspector/dist/assets/index-e7cef9de.js +1317 -0
- rasa/core/channels/inspector/dist/assets/{infoDiagram-736b4530-72a0fa5f.js → infoDiagram-736b4530-124d4a14.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-df861f2b-82218c41.js → journeyDiagram-df861f2b-7c4fae44.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-78cff630.js → layout-b9885fb6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-5038b469.js → line-7c59abb6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-c4fc4098.js → linear-4776f780.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-beec6740-c33c8ea6.js → mindmap-definition-beec6740-2332c46c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-dbbf0591-a8d03059.js → pieDiagram-dbbf0591-8fb39303.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-4d7f4fd6-6a0e56b2.js → quadrantDiagram-4d7f4fd6-3c7180a2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-6fc4c22a-2dc7c7bd.js → requirementDiagram-6fc4c22a-e910bcb8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-8f13d901-2360fe39.js → sankeyDiagram-8f13d901-ead16c89.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-b655622a-41b9f9ad.js → sequenceDiagram-b655622a-29a02a19.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-59f0c015-0aad326f.js → stateDiagram-59f0c015-042b3137.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-2b26beab-9847d984.js → stateDiagram-v2-2b26beab-2178c0f3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-080da4f6-564d890e.js → styles-080da4f6-23ffa4fc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-3dcbcfbf-38957613.js → styles-3dcbcfbf-94f59763.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9c745c82-f0fc6921.js → styles-9c745c82-78a6bebc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-4835440b-ef3c5a77.js → svgDrawCommon-4835440b-eae2a6f6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-5b62e21b-bf3e91c1.js → timeline-definition-5b62e21b-5c968d92.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-2b33534f-4d4026c0.js → xychartDiagram-2b33534f-fd3db0d5.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +18 -17
- rasa/core/channels/inspector/index.html +17 -16
- rasa/core/channels/inspector/package.json +5 -1
- rasa/core/channels/inspector/src/App.tsx +118 -68
- rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
- rasa/core/channels/inspector/src/components/DiagramFlow.tsx +11 -10
- rasa/core/channels/inspector/src/components/DialogueStack.tsx +10 -25
- rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -3
- rasa/core/channels/inspector/src/helpers/audiostream.ts +165 -0
- rasa/core/channels/inspector/src/helpers/formatters.test.ts +10 -0
- rasa/core/channels/inspector/src/helpers/formatters.ts +107 -41
- rasa/core/channels/inspector/src/helpers/utils.ts +92 -7
- rasa/core/channels/inspector/src/types.ts +21 -1
- rasa/core/channels/inspector/yarn.lock +94 -1
- rasa/core/channels/rest.py +51 -46
- rasa/core/channels/socketio.py +28 -1
- rasa/core/channels/telegram.py +1 -1
- rasa/core/channels/twilio.py +1 -1
- rasa/core/channels/{audiocodes.py → voice_ready/audiocodes.py} +122 -69
- rasa/core/channels/{voice_aware → voice_ready}/jambonz.py +26 -8
- rasa/core/channels/{voice_aware → voice_ready}/jambonz_protocol.py +57 -5
- rasa/core/channels/{twilio_voice.py → voice_ready/twilio_voice.py} +64 -28
- rasa/core/channels/voice_ready/utils.py +37 -0
- rasa/core/channels/voice_stream/asr/__init__.py +0 -0
- rasa/core/channels/voice_stream/asr/asr_engine.py +89 -0
- rasa/core/channels/voice_stream/asr/asr_event.py +18 -0
- rasa/core/channels/voice_stream/asr/azure.py +129 -0
- rasa/core/channels/voice_stream/asr/deepgram.py +90 -0
- rasa/core/channels/voice_stream/audio_bytes.py +8 -0
- rasa/core/channels/voice_stream/browser_audio.py +107 -0
- rasa/core/channels/voice_stream/call_state.py +23 -0
- rasa/core/channels/voice_stream/tts/__init__.py +0 -0
- rasa/core/channels/voice_stream/tts/azure.py +106 -0
- rasa/core/channels/voice_stream/tts/cartesia.py +118 -0
- rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
- rasa/core/channels/voice_stream/tts/tts_engine.py +58 -0
- rasa/core/channels/voice_stream/twilio_media_streams.py +173 -0
- rasa/core/channels/voice_stream/util.py +57 -0
- rasa/core/channels/voice_stream/voice_channel.py +427 -0
- rasa/core/information_retrieval/qdrant.py +1 -0
- rasa/core/nlg/contextual_response_rephraser.py +45 -17
- rasa/{nlu → core}/persistor.py +203 -68
- rasa/core/policies/enterprise_search_policy.py +119 -63
- rasa/core/policies/flows/flow_executor.py +15 -22
- rasa/core/policies/intentless_policy.py +83 -28
- rasa/core/processor.py +25 -0
- rasa/core/run.py +12 -2
- rasa/core/secrets_manager/constants.py +4 -0
- rasa/core/secrets_manager/factory.py +8 -0
- rasa/core/secrets_manager/vault.py +11 -1
- rasa/core/training/interactive.py +33 -34
- rasa/core/utils.py +47 -21
- rasa/dialogue_understanding/coexistence/llm_based_router.py +41 -14
- rasa/dialogue_understanding/commands/__init__.py +6 -0
- rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +60 -0
- rasa/dialogue_understanding/commands/session_end_command.py +61 -0
- rasa/dialogue_understanding/commands/user_silence_command.py +59 -0
- rasa/dialogue_understanding/commands/utils.py +5 -0
- rasa/dialogue_understanding/generator/constants.py +2 -0
- rasa/dialogue_understanding/generator/flow_retrieval.py +47 -9
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +38 -15
- rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +35 -13
- rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +3 -0
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +60 -13
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +53 -0
- rasa/dialogue_understanding/patterns/repeat.py +37 -0
- rasa/dialogue_understanding/patterns/user_silence.py +37 -0
- rasa/dialogue_understanding/processor/command_processor.py +21 -1
- rasa/e2e_test/aggregate_test_stats_calculator.py +1 -11
- rasa/e2e_test/assertions.py +136 -61
- rasa/e2e_test/assertions_schema.yml +23 -0
- rasa/e2e_test/e2e_test_case.py +85 -6
- rasa/e2e_test/e2e_test_runner.py +2 -3
- rasa/e2e_test/utils/e2e_yaml_utils.py +1 -1
- rasa/engine/graph.py +3 -10
- rasa/engine/loader.py +12 -0
- rasa/engine/recipes/config_files/default_config.yml +0 -3
- rasa/engine/recipes/default_recipe.py +0 -1
- rasa/engine/recipes/graph_recipe.py +0 -1
- rasa/engine/runner/dask.py +2 -2
- rasa/engine/storage/local_model_storage.py +12 -42
- rasa/engine/storage/storage.py +1 -5
- rasa/engine/validation.py +527 -74
- rasa/model_manager/__init__.py +0 -0
- rasa/model_manager/config.py +40 -0
- rasa/model_manager/model_api.py +559 -0
- rasa/model_manager/runner_service.py +286 -0
- rasa/model_manager/socket_bridge.py +146 -0
- rasa/model_manager/studio_jwt_auth.py +86 -0
- rasa/model_manager/trainer_service.py +325 -0
- rasa/model_manager/utils.py +87 -0
- rasa/model_manager/warm_rasa_process.py +187 -0
- rasa/model_service.py +112 -0
- rasa/model_training.py +42 -23
- rasa/nlu/tokenizers/whitespace_tokenizer.py +3 -14
- rasa/server.py +4 -2
- rasa/shared/constants.py +60 -8
- rasa/shared/core/constants.py +13 -0
- rasa/shared/core/domain.py +107 -50
- rasa/shared/core/events.py +29 -0
- rasa/shared/core/flows/flow.py +5 -0
- rasa/shared/core/flows/flows_list.py +19 -6
- rasa/shared/core/flows/flows_yaml_schema.json +10 -0
- rasa/shared/core/flows/utils.py +39 -0
- rasa/shared/core/flows/validation.py +121 -0
- rasa/shared/core/flows/yaml_flows_io.py +15 -27
- rasa/shared/core/slots.py +5 -0
- rasa/shared/importers/importer.py +59 -41
- rasa/shared/importers/multi_project.py +23 -11
- rasa/shared/importers/rasa.py +12 -3
- rasa/shared/importers/remote_importer.py +196 -0
- rasa/shared/importers/utils.py +3 -1
- rasa/shared/nlu/training_data/formats/rasa_yaml.py +18 -3
- rasa/shared/nlu/training_data/training_data.py +18 -19
- rasa/shared/providers/_configs/litellm_router_client_config.py +220 -0
- rasa/shared/providers/_configs/model_group_config.py +167 -0
- rasa/shared/providers/_configs/openai_client_config.py +1 -1
- rasa/shared/providers/_configs/rasa_llm_client_config.py +73 -0
- rasa/shared/providers/_configs/self_hosted_llm_client_config.py +1 -0
- rasa/shared/providers/_configs/utils.py +16 -0
- rasa/shared/providers/_utils.py +79 -0
- rasa/shared/providers/embedding/_base_litellm_embedding_client.py +13 -29
- rasa/shared/providers/embedding/azure_openai_embedding_client.py +54 -21
- rasa/shared/providers/embedding/default_litellm_embedding_client.py +24 -0
- rasa/shared/providers/embedding/litellm_router_embedding_client.py +135 -0
- rasa/shared/providers/llm/_base_litellm_client.py +34 -22
- rasa/shared/providers/llm/azure_openai_llm_client.py +50 -29
- rasa/shared/providers/llm/default_litellm_llm_client.py +24 -0
- rasa/shared/providers/llm/litellm_router_llm_client.py +182 -0
- rasa/shared/providers/llm/rasa_llm_client.py +112 -0
- rasa/shared/providers/llm/self_hosted_llm_client.py +5 -29
- rasa/shared/providers/mappings.py +19 -0
- rasa/shared/providers/router/__init__.py +0 -0
- rasa/shared/providers/router/_base_litellm_router_client.py +183 -0
- rasa/shared/providers/router/router_client.py +73 -0
- rasa/shared/utils/common.py +40 -24
- rasa/shared/utils/health_check/__init__.py +0 -0
- rasa/shared/utils/health_check/embeddings_health_check_mixin.py +31 -0
- rasa/shared/utils/health_check/health_check.py +258 -0
- rasa/shared/utils/health_check/llm_health_check_mixin.py +31 -0
- rasa/shared/utils/io.py +27 -6
- rasa/shared/utils/llm.py +354 -44
- rasa/shared/utils/schemas/events.py +2 -0
- rasa/shared/utils/schemas/model_config.yml +0 -10
- rasa/shared/utils/yaml.py +181 -38
- rasa/studio/data_handler.py +3 -1
- rasa/studio/upload.py +160 -74
- rasa/telemetry.py +94 -17
- rasa/tracing/config.py +3 -1
- rasa/tracing/instrumentation/attribute_extractors.py +95 -18
- rasa/tracing/instrumentation/instrumentation.py +121 -0
- rasa/utils/common.py +5 -0
- rasa/utils/endpoints.py +27 -1
- rasa/utils/io.py +8 -16
- rasa/utils/log_utils.py +9 -2
- rasa/utils/sanic_error_handler.py +32 -0
- rasa/validator.py +110 -16
- rasa/version.py +1 -1
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/METADATA +16 -14
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/RECORD +236 -185
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-1844e5a5.js +0 -1
- rasa/core/channels/inspector/dist/assets/index-a5d3e69d.js +0 -1040
- rasa/core/channels/voice_aware/utils.py +0 -20
- rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +0 -407
- /rasa/core/channels/{voice_aware → voice_ready}/__init__.py +0 -0
- /rasa/core/channels/{voice_native → voice_stream}/__init__.py +0 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/NOTICE +0 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/WHEEL +0 -0
- {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
import copy
|
|
2
|
-
import datetime
|
|
2
|
+
from datetime import datetime, timezone, timedelta
|
|
3
3
|
import json
|
|
4
|
-
import logging
|
|
5
4
|
import uuid
|
|
6
5
|
from typing import Any, Awaitable, Callable, Dict, List, Optional, Text, Union
|
|
6
|
+
from dataclasses import asdict
|
|
7
7
|
|
|
8
8
|
import structlog
|
|
9
9
|
from jsonschema import ValidationError, validate
|
|
10
10
|
from rasa.core import jobs
|
|
11
11
|
from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
|
|
12
|
-
from rasa.core.channels.
|
|
12
|
+
from rasa.core.channels.voice_ready.utils import (
|
|
13
|
+
validate_voice_license_scope,
|
|
14
|
+
CallParameters,
|
|
15
|
+
)
|
|
13
16
|
from rasa.shared.constants import INTENT_MESSAGE_PREFIX
|
|
17
|
+
from rasa.shared.core.constants import USER_INTENT_SESSION_START
|
|
14
18
|
from rasa.shared.exceptions import RasaException
|
|
15
19
|
from sanic import Blueprint, response
|
|
16
20
|
from sanic.exceptions import NotFound, SanicException, ServerError
|
|
17
21
|
from sanic.request import Request
|
|
18
22
|
from sanic.response import HTTPResponse
|
|
19
23
|
|
|
24
|
+
from rasa.utils.io import remove_emojis
|
|
20
25
|
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
26
|
structlogger = structlog.get_logger()
|
|
23
27
|
|
|
24
28
|
CHANNEL_NAME = "audiocodes"
|
|
25
29
|
KEEP_ALIVE_SECONDS = 120
|
|
26
30
|
KEEP_ALIVE_EXPIRATION_FACTOR = 1.5
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
EVENT_START = "start"
|
|
32
|
+
EVENT_DTMF = "DTMF"
|
|
33
|
+
ACTIVITY_MESSAGE = "message"
|
|
34
|
+
ACTIVITY_EVENT = "event"
|
|
35
|
+
INFO_UNKNOWN = "unknown"
|
|
36
|
+
ACTIVITY_ID_KEY = "id"
|
|
37
|
+
CREDENTIALS_TOKEN_KEY = "token"
|
|
38
|
+
CREDENTIALS_USE_WEBSOCKET_KEY = "use_websocket"
|
|
39
|
+
CREDENTIALS_KEEP_ALIVE_KEY = "keep_alive"
|
|
40
|
+
CREDENTIALS_KEEP_ALIVE_EXPIRATION_FACTOR_KEY = "keep_alive_expiration_factor"
|
|
41
|
+
CLEANUP_INTERVAL_MINUTES = 10
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def map_call_params(parameters: Dict[Text, Any]) -> CallParameters:
|
|
45
|
+
"""Map the Audiocodes parameters to the CallParameters dataclass."""
|
|
46
|
+
return CallParameters(
|
|
47
|
+
call_id=parameters.get("vaigConversationId"),
|
|
48
|
+
user_phone=parameters.get("callee"),
|
|
49
|
+
bot_phone=parameters.get("caller"),
|
|
50
|
+
user_name=parameters.get("callerDisplayName"),
|
|
51
|
+
user_host=parameters.get("callerHost"),
|
|
52
|
+
bot_host=parameters.get("calleeHost"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HttpUnauthorized(SanicException):
|
|
30
57
|
"""**Status**: 401 Not Authorized."""
|
|
31
58
|
|
|
32
59
|
status_code = 401
|
|
@@ -41,30 +68,43 @@ class Conversation:
|
|
|
41
68
|
self.update()
|
|
42
69
|
|
|
43
70
|
def update(self) -> None:
|
|
44
|
-
|
|
71
|
+
"""Update the last activity time."""
|
|
72
|
+
self.last_activity: datetime = datetime.now(timezone.utc)
|
|
45
73
|
|
|
46
74
|
@staticmethod
|
|
47
75
|
def get_metadata(activity: Dict[Text, Any]) -> Optional[Dict[Text, Any]]:
|
|
48
|
-
|
|
76
|
+
"""Get metadata from the activity."""
|
|
77
|
+
return asdict(map_call_params(activity["parameters"]))
|
|
49
78
|
|
|
50
79
|
@staticmethod
|
|
51
80
|
def _handle_event(event: Dict[Text, Any]) -> Text:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
"""Handle start and DTMF event and return the corresponding text."""
|
|
82
|
+
structlogger.debug("audiocodes.handle.event", event_payload=event)
|
|
83
|
+
if "name" not in event:
|
|
84
|
+
structlogger.warning(
|
|
85
|
+
"audiocodes.handle.event.no_name_key", event_payload=event
|
|
86
|
+
)
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
if event["name"] == EVENT_START:
|
|
90
|
+
text = f"{INTENT_MESSAGE_PREFIX}{USER_INTENT_SESSION_START}"
|
|
91
|
+
elif event["name"] == EVENT_DTMF:
|
|
92
|
+
text = f"{INTENT_MESSAGE_PREFIX}vaig_event_DTMF"
|
|
93
|
+
event_params = {"value": event["value"]}
|
|
59
94
|
text += json.dumps(event_params)
|
|
95
|
+
else:
|
|
96
|
+
structlogger.warning(
|
|
97
|
+
"audiocodes.handle.event.unknown_event", event_payload=event
|
|
98
|
+
)
|
|
99
|
+
return ""
|
|
100
|
+
|
|
60
101
|
return text
|
|
61
102
|
|
|
62
|
-
def is_active_conversation(
|
|
63
|
-
|
|
64
|
-
) -> bool:
|
|
103
|
+
def is_active_conversation(self, now: datetime, delta: timedelta) -> bool:
|
|
104
|
+
"""Check if the conversation is active."""
|
|
65
105
|
if now - self.last_activity > delta:
|
|
66
|
-
|
|
67
|
-
|
|
106
|
+
structlogger.warning(
|
|
107
|
+
"audiocodes.conversation.inactive", conversation=self.conversation_id
|
|
68
108
|
)
|
|
69
109
|
return False
|
|
70
110
|
return True
|
|
@@ -75,24 +115,25 @@ class Conversation:
|
|
|
75
115
|
output_channel: OutputChannel,
|
|
76
116
|
on_new_message: Callable[[UserMessage], Awaitable[Any]],
|
|
77
117
|
) -> None:
|
|
78
|
-
|
|
118
|
+
"""Handle activities sent by Audiocodes."""
|
|
119
|
+
structlogger.debug("audiocodes.handle.activities")
|
|
79
120
|
for activity in message["activities"]:
|
|
80
121
|
text = None
|
|
81
|
-
if activity[
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
122
|
+
if activity[ACTIVITY_ID_KEY] in self.activity_ids:
|
|
123
|
+
structlogger.warning(
|
|
124
|
+
"audiocodes.handle.activities.duplicate_activity",
|
|
125
|
+
activity_id=activity[ACTIVITY_ID_KEY],
|
|
85
126
|
)
|
|
86
127
|
continue
|
|
87
|
-
self.activity_ids.append(activity[
|
|
88
|
-
if activity["type"] ==
|
|
128
|
+
self.activity_ids.append(activity[ACTIVITY_ID_KEY])
|
|
129
|
+
if activity["type"] == ACTIVITY_MESSAGE:
|
|
89
130
|
text = activity["text"]
|
|
90
|
-
elif activity["type"] ==
|
|
131
|
+
elif activity["type"] == ACTIVITY_EVENT:
|
|
91
132
|
text = self._handle_event(activity)
|
|
92
133
|
else:
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
|
|
134
|
+
structlogger.warning(
|
|
135
|
+
"audiocodes.handle.activities.unknown_activity_type",
|
|
136
|
+
activity=activity,
|
|
96
137
|
)
|
|
97
138
|
if not text:
|
|
98
139
|
continue
|
|
@@ -111,13 +152,14 @@ class Conversation:
|
|
|
111
152
|
elif isinstance(user_msg.text, str):
|
|
112
153
|
anonymized_info = user_msg.text
|
|
113
154
|
else:
|
|
114
|
-
anonymized_info =
|
|
155
|
+
anonymized_info = INFO_UNKNOWN
|
|
115
156
|
|
|
116
157
|
structlogger.exception(
|
|
117
158
|
"audiocodes.handle.activities.failure",
|
|
118
159
|
user_message=copy.deepcopy(anonymized_info),
|
|
160
|
+
error=e,
|
|
161
|
+
exc_info=True,
|
|
119
162
|
)
|
|
120
|
-
logger.debug(e, exc_info=True)
|
|
121
163
|
|
|
122
164
|
await output_channel.send_custom_json(
|
|
123
165
|
self.conversation_id,
|
|
@@ -158,11 +200,12 @@ class AudiocodesInput(InputChannel):
|
|
|
158
200
|
raise RasaException(f"Invalid credentials: {e.message}")
|
|
159
201
|
|
|
160
202
|
return cls(
|
|
161
|
-
credentials.get(
|
|
162
|
-
credentials.get(
|
|
163
|
-
credentials.get(
|
|
203
|
+
credentials.get(CREDENTIALS_TOKEN_KEY, ""),
|
|
204
|
+
credentials.get(CREDENTIALS_USE_WEBSOCKET_KEY, True),
|
|
205
|
+
credentials.get(CREDENTIALS_KEEP_ALIVE_KEY, KEEP_ALIVE_SECONDS),
|
|
164
206
|
credentials.get(
|
|
165
|
-
|
|
207
|
+
CREDENTIALS_KEEP_ALIVE_EXPIRATION_FACTOR_KEY,
|
|
208
|
+
KEEP_ALIVE_EXPIRATION_FACTOR,
|
|
166
209
|
),
|
|
167
210
|
)
|
|
168
211
|
|
|
@@ -185,12 +228,12 @@ class AudiocodesInput(InputChannel):
|
|
|
185
228
|
if self.scheduler_job:
|
|
186
229
|
self.scheduler_job.remove()
|
|
187
230
|
self.scheduler_job = (await jobs.scheduler()).add_job(
|
|
188
|
-
self.clean_old_conversations, "interval", minutes=
|
|
231
|
+
self.clean_old_conversations, "interval", minutes=CLEANUP_INTERVAL_MINUTES
|
|
189
232
|
)
|
|
190
233
|
|
|
191
234
|
def _check_token(self, token: Optional[Text]) -> None:
|
|
192
235
|
if not token:
|
|
193
|
-
raise
|
|
236
|
+
raise HttpUnauthorized("Authentication token required.")
|
|
194
237
|
|
|
195
238
|
def _get_conversation(
|
|
196
239
|
self, token: Optional[Text], conversation_id: Text
|
|
@@ -203,14 +246,11 @@ class AudiocodesInput(InputChannel):
|
|
|
203
246
|
return conversation
|
|
204
247
|
|
|
205
248
|
def clean_old_conversations(self) -> None:
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
f" {len(self.conversations)}"
|
|
209
|
-
)
|
|
210
|
-
now = datetime.datetime.utcnow()
|
|
211
|
-
delta = datetime.timedelta(
|
|
212
|
-
seconds=self.keep_alive * self.keep_alive_expiration_factor
|
|
249
|
+
structlogger.debug(
|
|
250
|
+
"audiocodes.clean_old_conversations", current_number=len(self.conversations)
|
|
213
251
|
)
|
|
252
|
+
now = datetime.now(timezone.utc)
|
|
253
|
+
delta = timedelta(seconds=self.keep_alive * self.keep_alive_expiration_factor)
|
|
214
254
|
self.conversations = {
|
|
215
255
|
k: v
|
|
216
256
|
for k, v in self.conversations.items()
|
|
@@ -221,9 +261,8 @@ class AudiocodesInput(InputChannel):
|
|
|
221
261
|
conversation_id = body["conversation"]
|
|
222
262
|
if conversation_id in self.conversations:
|
|
223
263
|
raise ServerError("Conversation already exists")
|
|
224
|
-
|
|
225
|
-
"
|
|
226
|
-
f" Conversation: {conversation_id}"
|
|
264
|
+
structlogger.debug(
|
|
265
|
+
"audiocodes.handle_start_conversation", conversation=conversation_id
|
|
227
266
|
)
|
|
228
267
|
self.conversations[conversation_id] = Conversation(conversation_id)
|
|
229
268
|
urls = {
|
|
@@ -248,16 +287,15 @@ class AudiocodesInput(InputChannel):
|
|
|
248
287
|
"""Triggered on new websocket connection."""
|
|
249
288
|
if self.use_websocket is False:
|
|
250
289
|
raise ConnectionRefusedError("websocket is unavailable")
|
|
251
|
-
|
|
252
|
-
"
|
|
253
|
-
f" Conversation: {conversation_id}"
|
|
290
|
+
structlogger.debug(
|
|
291
|
+
"audiocodes.new_client_connection", conversation=conversation_id
|
|
254
292
|
)
|
|
255
293
|
conversation = self._get_conversation(request.token, conversation_id)
|
|
256
294
|
if conversation:
|
|
257
295
|
if conversation.ws:
|
|
258
|
-
|
|
259
|
-
"
|
|
260
|
-
|
|
296
|
+
structlogger.debug(
|
|
297
|
+
"audiocodes.new_client_connection.already_connected",
|
|
298
|
+
conversation=conversation_id,
|
|
261
299
|
)
|
|
262
300
|
else:
|
|
263
301
|
conversation.ws = ws
|
|
@@ -265,11 +303,9 @@ class AudiocodesInput(InputChannel):
|
|
|
265
303
|
try:
|
|
266
304
|
await ws.recv()
|
|
267
305
|
except Exception:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
f"{conversation_id}"
|
|
272
|
-
)
|
|
306
|
+
structlogger.warning(
|
|
307
|
+
"audiocodes.new_client_connection.closed",
|
|
308
|
+
conversation=conversation_id,
|
|
273
309
|
)
|
|
274
310
|
if conversation:
|
|
275
311
|
conversation.ws = None
|
|
@@ -308,10 +344,7 @@ class AudiocodesInput(InputChannel):
|
|
|
308
344
|
Example of payload:
|
|
309
345
|
{"conversation": <conversation_id>, "activities": List[Activity]}.
|
|
310
346
|
"""
|
|
311
|
-
|
|
312
|
-
"(on_activities) --- New activities from the user. Conversation: "
|
|
313
|
-
f"{conversation_id}"
|
|
314
|
-
)
|
|
347
|
+
structlogger.debug("audiocodes.on_activities", conversation=conversation_id)
|
|
315
348
|
conversation = self._get_conversation(request.token, conversation_id)
|
|
316
349
|
if conversation is None:
|
|
317
350
|
return response.json({})
|
|
@@ -350,16 +383,21 @@ class AudiocodesInput(InputChannel):
|
|
|
350
383
|
{"conversation": <conversation_id>, "reason": Optional[Text]}.
|
|
351
384
|
"""
|
|
352
385
|
self._get_conversation(request.token, conversation_id)
|
|
353
|
-
reason =
|
|
386
|
+
reason = {"reason": request.json.get("reason")}
|
|
354
387
|
await on_new_message(
|
|
355
388
|
UserMessage(
|
|
356
|
-
text=f"{INTENT_MESSAGE_PREFIX}
|
|
389
|
+
text=f"{INTENT_MESSAGE_PREFIX}session_end",
|
|
357
390
|
output_channel=None,
|
|
358
391
|
sender_id=conversation_id,
|
|
392
|
+
metadata=reason,
|
|
359
393
|
)
|
|
360
394
|
)
|
|
361
395
|
del self.conversations[conversation_id]
|
|
362
|
-
|
|
396
|
+
structlogger.debug(
|
|
397
|
+
"audiocodes.disconnect",
|
|
398
|
+
conversation=conversation_id,
|
|
399
|
+
request=request.json,
|
|
400
|
+
)
|
|
363
401
|
return response.json({})
|
|
364
402
|
|
|
365
403
|
@ac_webhook.route("/conversation/<conversation_id>/keepalive", methods=["POST"])
|
|
@@ -397,7 +435,7 @@ class AudiocodesOutput(OutputChannel):
|
|
|
397
435
|
)
|
|
398
436
|
message.update(
|
|
399
437
|
{
|
|
400
|
-
"timestamp": datetime.
|
|
438
|
+
"timestamp": datetime.now(timezone.utc).isoformat("T")[:-3] + "Z",
|
|
401
439
|
"id": str(uuid.uuid4()),
|
|
402
440
|
}
|
|
403
441
|
)
|
|
@@ -411,6 +449,7 @@ class AudiocodesOutput(OutputChannel):
|
|
|
411
449
|
self, recipient_id: Text, text: Text, **kwargs: Any
|
|
412
450
|
) -> None:
|
|
413
451
|
"""Send a text message."""
|
|
452
|
+
text = remove_emojis(text)
|
|
414
453
|
await self.add_message({"type": "message", "text": text})
|
|
415
454
|
|
|
416
455
|
async def send_image_url(
|
|
@@ -429,6 +468,20 @@ class AudiocodesOutput(OutputChannel):
|
|
|
429
468
|
"""Send an activity."""
|
|
430
469
|
await self.add_message(json_message)
|
|
431
470
|
|
|
471
|
+
async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
|
|
472
|
+
"""Indicate that the conversation should be ended."""
|
|
473
|
+
await self.add_message({"type": "event", "name": "hangup"})
|
|
474
|
+
|
|
475
|
+
async def send_text_with_buttons(
|
|
476
|
+
self,
|
|
477
|
+
recipient_id: str,
|
|
478
|
+
text: str,
|
|
479
|
+
buttons: List[Dict[str, Any]],
|
|
480
|
+
**kwargs: Any,
|
|
481
|
+
) -> None:
|
|
482
|
+
"""Uses the concise button output format for voice channels."""
|
|
483
|
+
await self.send_text_with_buttons_concise(recipient_id, text, buttons, **kwargs)
|
|
484
|
+
|
|
432
485
|
|
|
433
486
|
class WebsocketOutput(AudiocodesOutput):
|
|
434
487
|
def __init__(self, ws: Any, conversation_id: Text) -> None:
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
from typing import Any, Awaitable, Callable, Dict, Optional, Text
|
|
1
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Text
|
|
2
2
|
|
|
3
3
|
import structlog
|
|
4
4
|
from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
|
|
5
|
-
from rasa.core.channels.
|
|
5
|
+
from rasa.core.channels.voice_ready.jambonz_protocol import (
|
|
6
6
|
send_ws_text_message,
|
|
7
7
|
websocket_message_handler,
|
|
8
|
+
send_ws_hangup_message,
|
|
8
9
|
)
|
|
9
|
-
from rasa.core.channels.
|
|
10
|
+
from rasa.core.channels.voice_ready.utils import validate_voice_license_scope
|
|
10
11
|
from rasa.shared.exceptions import RasaException
|
|
11
12
|
from sanic import Blueprint, response, Websocket # type: ignore[attr-defined]
|
|
12
13
|
from sanic.request import Request
|
|
13
14
|
from sanic.response import HTTPResponse
|
|
14
15
|
|
|
15
|
-
from rasa.shared.utils.common import
|
|
16
|
-
|
|
16
|
+
from rasa.shared.utils.common import mark_as_beta_feature
|
|
17
|
+
from rasa.utils.io import remove_emojis
|
|
17
18
|
|
|
18
19
|
structlogger = structlog.get_logger()
|
|
19
20
|
|
|
20
21
|
CHANNEL_NAME = "jambonz"
|
|
21
22
|
|
|
23
|
+
DEFAULT_HANGUP_DELAY_SECONDS = 1
|
|
24
|
+
|
|
22
25
|
|
|
23
|
-
class
|
|
26
|
+
class JambonzVoiceReadyInput(InputChannel):
|
|
24
27
|
"""Connector for the Jambonz platform."""
|
|
25
28
|
|
|
26
29
|
@classmethod
|
|
@@ -32,8 +35,8 @@ class JambonzVoiceAwareInput(InputChannel):
|
|
|
32
35
|
return cls()
|
|
33
36
|
|
|
34
37
|
def __init__(self) -> None:
|
|
35
|
-
"""Initializes the
|
|
36
|
-
|
|
38
|
+
"""Initializes the JambonzVoiceReadyInput channel."""
|
|
39
|
+
mark_as_beta_feature("Jambonz Channel")
|
|
37
40
|
validate_voice_license_scope()
|
|
38
41
|
|
|
39
42
|
def blueprint(
|
|
@@ -84,6 +87,7 @@ class JambonzWebsocketOutput(OutputChannel):
|
|
|
84
87
|
self, recipient_id: Text, text: Text, **kwargs: Any
|
|
85
88
|
) -> None:
|
|
86
89
|
"""Send a text message."""
|
|
90
|
+
text = remove_emojis(text)
|
|
87
91
|
await self.add_message({"type": "message", "text": text})
|
|
88
92
|
|
|
89
93
|
async def send_image_url(
|
|
@@ -101,3 +105,17 @@ class JambonzWebsocketOutput(OutputChannel):
|
|
|
101
105
|
) -> None:
|
|
102
106
|
"""Send an activity."""
|
|
103
107
|
await self.add_message(json_message)
|
|
108
|
+
|
|
109
|
+
async def hangup(self, recipient_id: Text, **kwargs: Any) -> None:
|
|
110
|
+
"""Indicate that the conversation should be ended."""
|
|
111
|
+
await send_ws_hangup_message(DEFAULT_HANGUP_DELAY_SECONDS, self.ws)
|
|
112
|
+
|
|
113
|
+
async def send_text_with_buttons(
|
|
114
|
+
self,
|
|
115
|
+
recipient_id: str,
|
|
116
|
+
text: str,
|
|
117
|
+
buttons: List[Dict[str, Any]],
|
|
118
|
+
**kwargs: Any,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Uses the concise button output format for voice channels."""
|
|
121
|
+
await self.send_text_with_buttons_concise(recipient_id, text, buttons, **kwargs)
|
|
@@ -5,6 +5,8 @@ from typing import Any, Awaitable, Callable, Dict, List, Text
|
|
|
5
5
|
|
|
6
6
|
import structlog
|
|
7
7
|
from rasa.core.channels.channel import UserMessage
|
|
8
|
+
from rasa.core.channels.voice_ready.utils import CallParameters
|
|
9
|
+
from dataclasses import asdict
|
|
8
10
|
from sanic import Websocket # type: ignore[attr-defined]
|
|
9
11
|
|
|
10
12
|
|
|
@@ -17,12 +19,20 @@ class NewSessionMessage:
|
|
|
17
19
|
|
|
18
20
|
call_sid: str
|
|
19
21
|
message_id: str
|
|
22
|
+
call_params: CallParameters
|
|
20
23
|
|
|
21
24
|
@staticmethod
|
|
22
25
|
def from_message(message: Dict[str, Any]) -> "NewSessionMessage":
|
|
26
|
+
structlogger.debug("jambonz.websocket.message.new_session", message=message)
|
|
27
|
+
call_params = CallParameters(
|
|
28
|
+
call_id=message.get("call_sid"),
|
|
29
|
+
user_phone=message.get("data", {}).get("from"),
|
|
30
|
+
bot_phone=message.get("data", {}).get("to"),
|
|
31
|
+
)
|
|
23
32
|
return NewSessionMessage(
|
|
24
33
|
message.get("call_sid"),
|
|
25
34
|
message.get("msgid"),
|
|
35
|
+
call_params,
|
|
26
36
|
)
|
|
27
37
|
|
|
28
38
|
|
|
@@ -82,6 +92,10 @@ class CallStatusChanged:
|
|
|
82
92
|
|
|
83
93
|
@staticmethod
|
|
84
94
|
def from_message(message: Dict[str, Any]) -> "CallStatusChanged":
|
|
95
|
+
structlogger.debug(
|
|
96
|
+
"jambonz.websocket.message.call_status_changed",
|
|
97
|
+
message=message,
|
|
98
|
+
)
|
|
85
99
|
return CallStatusChanged(
|
|
86
100
|
message.get("call_sid"), message.get("data", {}).get("call_status")
|
|
87
101
|
)
|
|
@@ -145,7 +159,7 @@ async def websocket_message_handler(
|
|
|
145
159
|
await handle_session_reconnect(session_reconnect)
|
|
146
160
|
elif message.get("type") == "call:status":
|
|
147
161
|
call_status = CallStatusChanged.from_message(message)
|
|
148
|
-
await handle_call_status(call_status)
|
|
162
|
+
await handle_call_status(call_status, on_new_message, ws)
|
|
149
163
|
elif message.get("type") == "verb:hook" and message.get("hook") == "/gather":
|
|
150
164
|
hook_trigger_reason = message.get("data", {}).get("reason")
|
|
151
165
|
|
|
@@ -184,7 +198,7 @@ async def handle_new_session(
|
|
|
184
198
|
ws: Websocket,
|
|
185
199
|
) -> None:
|
|
186
200
|
"""Handle new session message."""
|
|
187
|
-
from rasa.core.channels.
|
|
201
|
+
from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
|
|
188
202
|
|
|
189
203
|
structlogger.debug("jambonz.websocket.message.new_call", call_sid=message.call_sid)
|
|
190
204
|
output_channel = JambonzWebsocketOutput(ws, message.call_sid)
|
|
@@ -192,7 +206,7 @@ async def handle_new_session(
|
|
|
192
206
|
text="/session_start",
|
|
193
207
|
output_channel=output_channel,
|
|
194
208
|
sender_id=message.call_sid,
|
|
195
|
-
metadata=
|
|
209
|
+
metadata=asdict(message.call_params),
|
|
196
210
|
)
|
|
197
211
|
await send_config_ack(message.message_id, ws)
|
|
198
212
|
await on_new_message(user_msg)
|
|
@@ -208,7 +222,7 @@ async def handle_gather_completed(
|
|
|
208
222
|
|
|
209
223
|
This includes results of gather calles with their transcription.
|
|
210
224
|
"""
|
|
211
|
-
from rasa.core.channels.
|
|
225
|
+
from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
|
|
212
226
|
|
|
213
227
|
if not transcript_result.is_final:
|
|
214
228
|
# in case of a non final transcript, we are going to wait for the final
|
|
@@ -256,7 +270,11 @@ async def handle_gather_timeout(gather_timeout: GatherTimeout, ws: Websocket) ->
|
|
|
256
270
|
await send_gather_input(ws)
|
|
257
271
|
|
|
258
272
|
|
|
259
|
-
async def handle_call_status(
|
|
273
|
+
async def handle_call_status(
|
|
274
|
+
call_status: CallStatusChanged,
|
|
275
|
+
on_new_message: Callable[[UserMessage], Awaitable[Any]],
|
|
276
|
+
ws: Websocket,
|
|
277
|
+
) -> None:
|
|
260
278
|
"""Handle changes in the call status."""
|
|
261
279
|
structlogger.debug(
|
|
262
280
|
"jambonz.websocket.message.call_status_changed",
|
|
@@ -264,6 +282,19 @@ async def handle_call_status(call_status: CallStatusChanged) -> None:
|
|
|
264
282
|
message=call_status.status,
|
|
265
283
|
)
|
|
266
284
|
|
|
285
|
+
if call_status.status == "completed":
|
|
286
|
+
structlogger.debug("jambonz.websocket.message.call_completed")
|
|
287
|
+
from rasa.core.channels.voice_ready.jambonz import JambonzWebsocketOutput
|
|
288
|
+
|
|
289
|
+
output_channel = JambonzWebsocketOutput(ws, call_status.call_sid)
|
|
290
|
+
user_msg = UserMessage(
|
|
291
|
+
text="/session_end",
|
|
292
|
+
output_channel=output_channel,
|
|
293
|
+
sender_id=call_status.call_sid,
|
|
294
|
+
metadata={},
|
|
295
|
+
)
|
|
296
|
+
await on_new_message(user_msg)
|
|
297
|
+
|
|
267
298
|
|
|
268
299
|
async def handle_session_reconnect(session_reconnect: SessionReconnect) -> None:
|
|
269
300
|
"""Handle session reconnect message."""
|
|
@@ -301,6 +332,7 @@ async def send_config_ack(message_id: str, ws: Websocket) -> None:
|
|
|
301
332
|
|
|
302
333
|
async def send_gather_input(ws: Websocket) -> None:
|
|
303
334
|
"""Send a gather input command to jambonz."""
|
|
335
|
+
structlogger.debug("jambonz.websocket.send.gather")
|
|
304
336
|
await ws.send(
|
|
305
337
|
json.dumps(
|
|
306
338
|
{
|
|
@@ -342,3 +374,23 @@ async def send_ws_text_message(ws: Websocket, text: Text) -> None:
|
|
|
342
374
|
}
|
|
343
375
|
)
|
|
344
376
|
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def send_ws_hangup_message(hangup_delay_seconds: int, ws: Websocket) -> None:
|
|
380
|
+
"""Send a hangup message to the websocket using the jambonz interface."""
|
|
381
|
+
structlogger.debug("jambonz.websocket.send.hangup")
|
|
382
|
+
await ws.send(
|
|
383
|
+
json.dumps(
|
|
384
|
+
{
|
|
385
|
+
"type": "command",
|
|
386
|
+
"command": "redirect",
|
|
387
|
+
"queueCommand": True,
|
|
388
|
+
"data": [
|
|
389
|
+
{"pause": {"length": hangup_delay_seconds}},
|
|
390
|
+
{
|
|
391
|
+
"hangup": {},
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
)
|