rasa-pro 3.9.18__py3-none-any.whl → 3.10.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rasa-pro might be problematic. Click here for more details.

Files changed (189) hide show
  1. README.md +26 -57
  2. rasa/__init__.py +1 -2
  3. rasa/__main__.py +5 -0
  4. rasa/anonymization/anonymization_rule_executor.py +2 -2
  5. rasa/api.py +26 -22
  6. rasa/cli/arguments/data.py +27 -2
  7. rasa/cli/arguments/default_arguments.py +25 -3
  8. rasa/cli/arguments/run.py +9 -9
  9. rasa/cli/arguments/train.py +2 -0
  10. rasa/cli/data.py +70 -8
  11. rasa/cli/e2e_test.py +108 -433
  12. rasa/cli/interactive.py +1 -0
  13. rasa/cli/llm_fine_tuning.py +395 -0
  14. rasa/cli/project_templates/calm/endpoints.yml +1 -1
  15. rasa/cli/project_templates/tutorial/endpoints.yml +1 -1
  16. rasa/cli/run.py +14 -13
  17. rasa/cli/scaffold.py +10 -8
  18. rasa/cli/train.py +8 -7
  19. rasa/cli/utils.py +15 -0
  20. rasa/constants.py +7 -1
  21. rasa/core/actions/action.py +98 -49
  22. rasa/core/actions/action_run_slot_rejections.py +4 -1
  23. rasa/core/actions/custom_action_executor.py +9 -6
  24. rasa/core/actions/direct_custom_actions_executor.py +80 -0
  25. rasa/core/actions/e2e_stub_custom_action_executor.py +68 -0
  26. rasa/core/actions/grpc_custom_action_executor.py +2 -2
  27. rasa/core/actions/http_custom_action_executor.py +6 -5
  28. rasa/core/agent.py +21 -17
  29. rasa/core/channels/__init__.py +2 -0
  30. rasa/core/channels/audiocodes.py +1 -16
  31. rasa/core/channels/inspector/dist/index.html +0 -2
  32. rasa/core/channels/inspector/index.html +0 -2
  33. rasa/core/channels/voice_aware/__init__.py +0 -0
  34. rasa/core/channels/voice_aware/jambonz.py +103 -0
  35. rasa/core/channels/voice_aware/jambonz_protocol.py +344 -0
  36. rasa/core/channels/voice_aware/utils.py +20 -0
  37. rasa/core/channels/voice_native/__init__.py +0 -0
  38. rasa/core/constants.py +6 -1
  39. rasa/core/featurizers/single_state_featurizer.py +1 -22
  40. rasa/core/featurizers/tracker_featurizers.py +18 -115
  41. rasa/core/information_retrieval/faiss.py +7 -4
  42. rasa/core/information_retrieval/information_retrieval.py +8 -0
  43. rasa/core/information_retrieval/milvus.py +9 -2
  44. rasa/core/information_retrieval/qdrant.py +1 -1
  45. rasa/core/nlg/contextual_response_rephraser.py +32 -10
  46. rasa/core/nlg/summarize.py +4 -3
  47. rasa/core/policies/enterprise_search_policy.py +100 -44
  48. rasa/core/policies/flows/flow_executor.py +130 -94
  49. rasa/core/policies/intentless_policy.py +52 -28
  50. rasa/core/policies/ted_policy.py +33 -58
  51. rasa/core/policies/unexpected_intent_policy.py +7 -15
  52. rasa/core/processor.py +20 -53
  53. rasa/core/run.py +5 -4
  54. rasa/core/tracker_store.py +8 -4
  55. rasa/core/utils.py +45 -56
  56. rasa/dialogue_understanding/coexistence/llm_based_router.py +45 -12
  57. rasa/dialogue_understanding/commands/__init__.py +4 -0
  58. rasa/dialogue_understanding/commands/change_flow_command.py +0 -6
  59. rasa/dialogue_understanding/commands/session_start_command.py +59 -0
  60. rasa/dialogue_understanding/commands/set_slot_command.py +1 -5
  61. rasa/dialogue_understanding/commands/utils.py +38 -0
  62. rasa/dialogue_understanding/generator/constants.py +10 -3
  63. rasa/dialogue_understanding/generator/flow_retrieval.py +14 -5
  64. rasa/dialogue_understanding/generator/llm_based_command_generator.py +12 -2
  65. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +106 -87
  66. rasa/dialogue_understanding/generator/nlu_command_adapter.py +28 -6
  67. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +90 -37
  68. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +15 -15
  69. rasa/dialogue_understanding/patterns/session_start.py +37 -0
  70. rasa/dialogue_understanding/processor/command_processor.py +13 -14
  71. rasa/e2e_test/aggregate_test_stats_calculator.py +124 -0
  72. rasa/e2e_test/assertions.py +1181 -0
  73. rasa/e2e_test/assertions_schema.yml +106 -0
  74. rasa/e2e_test/constants.py +20 -0
  75. rasa/e2e_test/e2e_config.py +220 -0
  76. rasa/e2e_test/e2e_config_schema.yml +26 -0
  77. rasa/e2e_test/e2e_test_case.py +131 -8
  78. rasa/e2e_test/e2e_test_converter.py +363 -0
  79. rasa/e2e_test/e2e_test_converter_prompt.jinja2 +70 -0
  80. rasa/e2e_test/e2e_test_coverage_report.py +364 -0
  81. rasa/e2e_test/e2e_test_result.py +26 -6
  82. rasa/e2e_test/e2e_test_runner.py +491 -72
  83. rasa/e2e_test/e2e_test_schema.yml +96 -0
  84. rasa/e2e_test/pykwalify_extensions.py +39 -0
  85. rasa/e2e_test/stub_custom_action.py +70 -0
  86. rasa/e2e_test/utils/__init__.py +0 -0
  87. rasa/e2e_test/utils/e2e_yaml_utils.py +55 -0
  88. rasa/e2e_test/utils/io.py +596 -0
  89. rasa/e2e_test/utils/validation.py +80 -0
  90. rasa/engine/recipes/default_components.py +0 -2
  91. rasa/engine/storage/local_model_storage.py +0 -1
  92. rasa/env.py +9 -0
  93. rasa/llm_fine_tuning/__init__.py +0 -0
  94. rasa/llm_fine_tuning/annotation_module.py +241 -0
  95. rasa/llm_fine_tuning/conversations.py +144 -0
  96. rasa/llm_fine_tuning/llm_data_preparation_module.py +178 -0
  97. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +407 -0
  98. rasa/llm_fine_tuning/paraphrasing/__init__.py +0 -0
  99. rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +281 -0
  100. rasa/llm_fine_tuning/paraphrasing/default_rephrase_prompt_template.jina2 +44 -0
  101. rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +121 -0
  102. rasa/llm_fine_tuning/paraphrasing/rephrased_user_message.py +10 -0
  103. rasa/llm_fine_tuning/paraphrasing_module.py +128 -0
  104. rasa/llm_fine_tuning/storage.py +174 -0
  105. rasa/llm_fine_tuning/train_test_split_module.py +441 -0
  106. rasa/model_training.py +48 -16
  107. rasa/nlu/classifiers/diet_classifier.py +25 -38
  108. rasa/nlu/classifiers/logistic_regression_classifier.py +9 -44
  109. rasa/nlu/classifiers/sklearn_intent_classifier.py +16 -37
  110. rasa/nlu/extractors/crf_entity_extractor.py +50 -93
  111. rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py +45 -78
  112. rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py +17 -52
  113. rasa/nlu/featurizers/sparse_featurizer/regex_featurizer.py +3 -5
  114. rasa/nlu/persistor.py +129 -32
  115. rasa/server.py +45 -10
  116. rasa/shared/constants.py +63 -15
  117. rasa/shared/core/domain.py +15 -12
  118. rasa/shared/core/events.py +28 -2
  119. rasa/shared/core/flows/flow.py +208 -13
  120. rasa/shared/core/flows/flow_path.py +84 -0
  121. rasa/shared/core/flows/flows_list.py +28 -10
  122. rasa/shared/core/flows/flows_yaml_schema.json +269 -193
  123. rasa/shared/core/flows/validation.py +112 -25
  124. rasa/shared/core/flows/yaml_flows_io.py +149 -10
  125. rasa/shared/core/trackers.py +6 -0
  126. rasa/shared/core/training_data/visualization.html +2 -2
  127. rasa/shared/exceptions.py +4 -0
  128. rasa/shared/importers/importer.py +60 -11
  129. rasa/shared/importers/remote_importer.py +196 -0
  130. rasa/shared/nlu/constants.py +2 -0
  131. rasa/shared/nlu/training_data/features.py +2 -120
  132. rasa/shared/providers/_configs/__init__.py +0 -0
  133. rasa/shared/providers/_configs/azure_openai_client_config.py +181 -0
  134. rasa/shared/providers/_configs/client_config.py +57 -0
  135. rasa/shared/providers/_configs/default_litellm_client_config.py +130 -0
  136. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +234 -0
  137. rasa/shared/providers/_configs/openai_client_config.py +175 -0
  138. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +171 -0
  139. rasa/shared/providers/_configs/utils.py +101 -0
  140. rasa/shared/providers/_ssl_verification_utils.py +124 -0
  141. rasa/shared/providers/embedding/__init__.py +0 -0
  142. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +254 -0
  143. rasa/shared/providers/embedding/_langchain_embedding_client_adapter.py +74 -0
  144. rasa/shared/providers/embedding/azure_openai_embedding_client.py +277 -0
  145. rasa/shared/providers/embedding/default_litellm_embedding_client.py +102 -0
  146. rasa/shared/providers/embedding/embedding_client.py +90 -0
  147. rasa/shared/providers/embedding/embedding_response.py +41 -0
  148. rasa/shared/providers/embedding/huggingface_local_embedding_client.py +191 -0
  149. rasa/shared/providers/embedding/openai_embedding_client.py +172 -0
  150. rasa/shared/providers/llm/__init__.py +0 -0
  151. rasa/shared/providers/llm/_base_litellm_client.py +227 -0
  152. rasa/shared/providers/llm/azure_openai_llm_client.py +338 -0
  153. rasa/shared/providers/llm/default_litellm_llm_client.py +84 -0
  154. rasa/shared/providers/llm/llm_client.py +76 -0
  155. rasa/shared/providers/llm/llm_response.py +50 -0
  156. rasa/shared/providers/llm/openai_llm_client.py +155 -0
  157. rasa/shared/providers/llm/self_hosted_llm_client.py +169 -0
  158. rasa/shared/providers/mappings.py +75 -0
  159. rasa/shared/utils/cli.py +30 -0
  160. rasa/shared/utils/io.py +65 -3
  161. rasa/shared/utils/llm.py +223 -200
  162. rasa/shared/utils/yaml.py +122 -7
  163. rasa/studio/download.py +19 -13
  164. rasa/studio/train.py +2 -3
  165. rasa/studio/upload.py +2 -3
  166. rasa/telemetry.py +113 -58
  167. rasa/tracing/config.py +2 -3
  168. rasa/tracing/instrumentation/attribute_extractors.py +29 -17
  169. rasa/tracing/instrumentation/instrumentation.py +4 -47
  170. rasa/utils/common.py +18 -19
  171. rasa/utils/endpoints.py +7 -4
  172. rasa/utils/io.py +66 -0
  173. rasa/utils/json_utils.py +60 -0
  174. rasa/utils/licensing.py +9 -1
  175. rasa/utils/ml_utils.py +4 -2
  176. rasa/utils/tensorflow/model_data.py +193 -2
  177. rasa/validator.py +195 -1
  178. rasa/version.py +1 -1
  179. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.3.dist-info}/METADATA +47 -72
  180. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.3.dist-info}/RECORD +185 -121
  181. rasa/nlu/classifiers/llm_intent_classifier.py +0 -519
  182. rasa/shared/providers/openai/clients.py +0 -43
  183. rasa/shared/providers/openai/session_handler.py +0 -110
  184. rasa/utils/tensorflow/feature_array.py +0 -366
  185. /rasa/{shared/providers/openai → cli/project_templates/tutorial/actions}/__init__.py +0 -0
  186. /rasa/cli/project_templates/tutorial/{actions.py → actions/actions.py} +0 -0
  187. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.3.dist-info}/NOTICE +0 -0
  188. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.3.dist-info}/WHEEL +0 -0
  189. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.3.dist-info}/entry_points.txt +0 -0
rasa/core/agent.py CHANGED
@@ -1,38 +1,42 @@
1
1
  from __future__ import annotations
2
- from asyncio import AbstractEventLoop, CancelledError
2
+
3
3
  import functools
4
4
  import logging
5
5
  import os
6
+ import uuid
7
+ from asyncio import AbstractEventLoop, CancelledError
6
8
  from pathlib import Path
7
9
  from typing import Any, Callable, Dict, List, Optional, Text, Union
8
- import uuid
9
10
 
10
11
  import aiohttp
11
12
  from aiohttp import ClientError
12
13
 
14
+ import rasa.shared.utils.io
13
15
  from rasa.core import jobs
14
16
  from rasa.core.channels.channel import OutputChannel, UserMessage
15
17
  from rasa.core.constants import DEFAULT_REQUEST_TIMEOUT
16
- from rasa.core.http_interpreter import RasaNLUHttpInterpreter
17
- from rasa.shared.core.domain import Domain
18
18
  from rasa.core.exceptions import AgentNotReady
19
- from rasa.shared.constants import DEFAULT_SENDER_ID
19
+ from rasa.core.http_interpreter import RasaNLUHttpInterpreter
20
20
  from rasa.core.lock_store import InMemoryLockStore, LockStore
21
21
  from rasa.core.nlg import NaturalLanguageGenerator, TemplatedNaturalLanguageGenerator
22
22
  from rasa.core.policies.policy import PolicyPrediction
23
23
  from rasa.core.processor import MessageProcessor
24
- from rasa.core.tracker_store import FailSafeTrackerStore, InMemoryTrackerStore
25
- from rasa.shared.core.trackers import DialogueStateTracker, EventVerbosity
24
+ from rasa.core.tracker_store import (
25
+ FailSafeTrackerStore,
26
+ InMemoryTrackerStore,
27
+ TrackerStore,
28
+ )
29
+ from rasa.core.utils import AvailableEndpoints
26
30
  from rasa.exceptions import ModelNotFound
31
+ from rasa.nlu.persistor import StorageType
27
32
  from rasa.nlu.utils import is_url
33
+ from rasa.shared.constants import DEFAULT_SENDER_ID
34
+ from rasa.shared.core.domain import Domain
35
+ from rasa.shared.core.trackers import DialogueStateTracker, EventVerbosity
28
36
  from rasa.shared.exceptions import RasaException
29
- import rasa.shared.utils.io
30
37
  from rasa.utils.common import TempDirectoryPath, get_temp_dir_name
31
38
  from rasa.utils.endpoints import EndpointConfig
32
39
 
33
- from rasa.core.tracker_store import TrackerStore
34
- from rasa.core.utils import AvailableEndpoints
35
-
36
40
  logger = logging.getLogger(__name__)
37
41
 
38
42
 
@@ -194,7 +198,7 @@ async def _schedule_model_pulling(
194
198
  async def load_agent(
195
199
  model_path: Optional[Text] = None,
196
200
  model_server: Optional[EndpointConfig] = None,
197
- remote_storage: Optional[Text] = None,
201
+ remote_storage: Optional[StorageType] = None,
198
202
  endpoints: Optional[AvailableEndpoints] = None,
199
203
  loop: Optional[AbstractEventLoop] = None,
200
204
  ) -> Agent:
@@ -203,15 +207,15 @@ async def load_agent(
203
207
  Args:
204
208
  model_path: Path to the model if it's on disk.
205
209
  model_server: Configuration for a potential server which serves the model.
206
- remote_storage: URL of remote storage for model.
210
+ remote_storage: Remote storage to use for loading the model.
207
211
  endpoints: Endpoint configuration.
208
212
  loop: Optional async loop to pass to broker creation.
209
213
 
210
214
  Returns:
211
215
  The instantiated `Agent` or `None`.
212
216
  """
213
- from rasa.core.tracker_store import TrackerStore
214
217
  from rasa.core.brokers.broker import EventBroker
218
+ from rasa.core.tracker_store import TrackerStore
215
219
 
216
220
  tracker_store = None
217
221
  lock_store = None
@@ -299,7 +303,7 @@ class Agent:
299
303
  action_endpoint: Optional[EndpointConfig] = None,
300
304
  fingerprint: Optional[Text] = None,
301
305
  model_server: Optional[EndpointConfig] = None,
302
- remote_storage: Optional[Text] = None,
306
+ remote_storage: Optional[StorageType] = None,
303
307
  http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
304
308
  endpoints: Optional[AvailableEndpoints] = None,
305
309
  ):
@@ -329,11 +333,11 @@ class Agent:
329
333
  action_endpoint: Optional[EndpointConfig] = None,
330
334
  fingerprint: Optional[Text] = None,
331
335
  model_server: Optional[EndpointConfig] = None,
332
- remote_storage: Optional[Text] = None,
336
+ remote_storage: Optional[StorageType] = None,
333
337
  http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
334
338
  endpoints: Optional[AvailableEndpoints] = None,
335
339
  ) -> Agent:
336
- """Constructs a new agent and loads the processer and model."""
340
+ """Constructs a new agent and loads the processor and model."""
337
341
  agent = Agent(
338
342
  domain=domain,
339
343
  generator=generator,
@@ -22,6 +22,7 @@ from rasa.core.channels.slack import SlackInput
22
22
  from rasa.core.channels.telegram import TelegramInput
23
23
  from rasa.core.channels.twilio import TwilioInput
24
24
  from rasa.core.channels.twilio_voice import TwilioVoiceInput
25
+ from rasa.core.channels.voice_aware.jambonz import JambonzVoiceAwareInput
25
26
  from rasa.core.channels.webexteams import WebexTeamsInput
26
27
  from rasa.core.channels.hangouts import HangoutsInput
27
28
  from rasa.core.channels.audiocodes import AudiocodesInput
@@ -47,6 +48,7 @@ input_channel_classes: List[Type[InputChannel]] = [
47
48
  AudiocodesInput,
48
49
  DevelopmentInspectInput,
49
50
  CVGInput,
51
+ JambonzVoiceAwareInput,
50
52
  ]
51
53
 
52
54
  # Mapping from an input channel name to its class to allow name based lookup.
@@ -9,6 +9,7 @@ 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.voice_aware.utils import validate_voice_license_scope
12
13
  from rasa.shared.constants import INTENT_MESSAGE_PREFIX
13
14
  from rasa.shared.exceptions import RasaException
14
15
  from sanic import Blueprint, response
@@ -16,11 +17,6 @@ from sanic.exceptions import NotFound, SanicException, ServerError
16
17
  from sanic.request import Request
17
18
  from sanic.response import HTTPResponse
18
19
 
19
- from rasa.utils.licensing import (
20
- PRODUCT_AREA,
21
- VOICE_SCOPE,
22
- validate_license_from_env,
23
- )
24
20
 
25
21
  logger = logging.getLogger(__name__)
26
22
  structlogger = structlog.get_logger()
@@ -30,17 +26,6 @@ KEEP_ALIVE_SECONDS = 120
30
26
  KEEP_ALIVE_EXPIRATION_FACTOR = 1.5
31
27
 
32
28
 
33
- def validate_voice_license_scope() -> None:
34
- """Validate that the correct license scope is present."""
35
- logger.info(
36
- f"Validating current Rasa Pro license scope which must include "
37
- f"the '{VOICE_SCOPE}' scope to use the voice channel."
38
- )
39
-
40
- voice_product_scope = PRODUCT_AREA + " " + VOICE_SCOPE
41
- validate_license_from_env(product_area=voice_product_scope)
42
-
43
-
44
29
  class Unauthorized(SanicException):
45
30
  """**Status**: 401 Not Authorized."""
46
31
 
@@ -17,7 +17,6 @@
17
17
  <script>
18
18
  const chatDiv = document.getElementById("rasa-chat-widget");
19
19
  const websocketUrl = window.location.origin.replace("http", "ws");
20
- const initialPayload = "/session_start";
21
20
  const maxHeight = document.documentElement.scrollHeight - 130;
22
21
  // 21 and 25 are the rem number we're using for the columns. We add 0.75rem for the padding
23
22
  // A potential improvement would be to add a onresize event for both width and height
@@ -29,7 +28,6 @@
29
28
  const columnWidth = remReference * parseFloat(getComputedStyle(document.documentElement).fontSize);
30
29
 
31
30
  chatDiv.setAttribute("data-websocket-url", websocketUrl);
32
- chatDiv.setAttribute("data-initial-payload", initialPayload);
33
31
  chatDiv.setAttribute("data-close-on-outside-click", false);
34
32
  chatDiv.setAttribute("data-height", maxHeight);
35
33
  chatDiv.setAttribute("data-width", columnWidth);
@@ -15,7 +15,6 @@
15
15
  <script>
16
16
  const chatDiv = document.getElementById("rasa-chat-widget");
17
17
  const websocketUrl = window.location.origin.replace("http", "ws");
18
- const initialPayload = "/session_start";
19
18
  const maxHeight = document.documentElement.scrollHeight - 130;
20
19
  // 21 and 25 are the rem number we're using for the columns. We add 0.75rem for the padding
21
20
  // A potential improvement would be to add a onresize event for both width and height
@@ -27,7 +26,6 @@
27
26
  const columnWidth = remReference * parseFloat(getComputedStyle(document.documentElement).fontSize);
28
27
 
29
28
  chatDiv.setAttribute("data-websocket-url", websocketUrl);
30
- chatDiv.setAttribute("data-initial-payload", initialPayload);
31
29
  chatDiv.setAttribute("data-close-on-outside-click", false);
32
30
  chatDiv.setAttribute("data-height", maxHeight);
33
31
  chatDiv.setAttribute("data-width", columnWidth);
File without changes
@@ -0,0 +1,103 @@
1
+ from typing import Any, Awaitable, Callable, Dict, Optional, Text
2
+
3
+ import structlog
4
+ from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
5
+ from rasa.core.channels.voice_aware.jambonz_protocol import (
6
+ send_ws_text_message,
7
+ websocket_message_handler,
8
+ )
9
+ from rasa.core.channels.voice_aware.utils import validate_voice_license_scope
10
+ from rasa.shared.exceptions import RasaException
11
+ from sanic import Blueprint, response, Websocket # type: ignore[attr-defined]
12
+ from sanic.request import Request
13
+ from sanic.response import HTTPResponse
14
+
15
+ from rasa.shared.utils.common import mark_as_experimental_feature
16
+
17
+
18
+ structlogger = structlog.get_logger()
19
+
20
+ CHANNEL_NAME = "jambonz"
21
+
22
+
23
+ class JambonzVoiceAwareInput(InputChannel):
24
+ """Connector for the Jambonz platform."""
25
+
26
+ @classmethod
27
+ def name(cls) -> Text:
28
+ return CHANNEL_NAME
29
+
30
+ @classmethod
31
+ def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChannel:
32
+ return cls()
33
+
34
+ def __init__(self) -> None:
35
+ """Initializes the JambonzVoiceAwareInput channel."""
36
+ mark_as_experimental_feature("Jambonz Channel")
37
+ validate_voice_license_scope()
38
+
39
+ def blueprint(
40
+ self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
41
+ ) -> Blueprint:
42
+ jambonz_webhook = Blueprint("jambonz_webhook", __name__)
43
+
44
+ @jambonz_webhook.route("/", methods=["GET"])
45
+ async def health(request: Request) -> HTTPResponse:
46
+ """Server health route."""
47
+ return response.json({"status": "ok"})
48
+
49
+ @jambonz_webhook.websocket("/websocket", subprotocols=["ws.jambonz.org"]) # type: ignore
50
+ async def websocket(request: Request, ws: Websocket) -> None:
51
+ """Triggered on new websocket connection."""
52
+ async for message in ws:
53
+ await websocket_message_handler(message, on_new_message, ws)
54
+
55
+ return jambonz_webhook
56
+
57
+
58
+ class JambonzWebsocketOutput(OutputChannel):
59
+ @classmethod
60
+ def name(cls) -> Text:
61
+ return CHANNEL_NAME
62
+
63
+ def __init__(self, ws: Any, conversation_id: Text) -> None:
64
+ self.ws = ws
65
+ self.conversation_id = conversation_id
66
+
67
+ async def add_message(self, message: Dict) -> None:
68
+ """Add metadata and add message.
69
+
70
+ Message is added to the list of
71
+ activities to be sent to the Jambonz Websocket server.
72
+ """
73
+ text_message = message.get("text", "")
74
+ structlogger.debug(
75
+ "jambonz.add.message",
76
+ class_name=self.__class__.__name__,
77
+ message=text_message,
78
+ )
79
+
80
+ # send message to jambonz
81
+ await send_ws_text_message(self.ws, message.get("text"))
82
+
83
+ async def send_text_message(
84
+ self, recipient_id: Text, text: Text, **kwargs: Any
85
+ ) -> None:
86
+ """Send a text message."""
87
+ await self.add_message({"type": "message", "text": text})
88
+
89
+ async def send_image_url(
90
+ self, recipient_id: Text, image: Text, **kwargs: Any
91
+ ) -> None:
92
+ raise RasaException("Images are not supported by this channel")
93
+
94
+ async def send_attachment(
95
+ self, recipient_id: Text, attachment: Text, **kwargs: Any
96
+ ) -> None:
97
+ raise RasaException("Attachments are not supported by this channel")
98
+
99
+ async def send_custom_json(
100
+ self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
101
+ ) -> None:
102
+ """Send an activity."""
103
+ await self.add_message(json_message)
@@ -0,0 +1,344 @@
1
+ from dataclasses import dataclass, field
2
+ import json
3
+ import uuid
4
+ from typing import Any, Awaitable, Callable, Dict, List, Text
5
+
6
+ import structlog
7
+ from rasa.core.channels.channel import UserMessage
8
+ from sanic import Websocket # type: ignore[attr-defined]
9
+
10
+
11
+ structlogger = structlog.get_logger()
12
+
13
+
14
+ @dataclass
15
+ class NewSessionMessage:
16
+ """Message indicating a new session has been started."""
17
+
18
+ call_sid: str
19
+ message_id: str
20
+
21
+ @staticmethod
22
+ def from_message(message: Dict[str, Any]) -> "NewSessionMessage":
23
+ return NewSessionMessage(
24
+ message.get("call_sid"),
25
+ message.get("msgid"),
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class Transcript:
31
+ """Transcript of a spoken utterance."""
32
+
33
+ text: str
34
+ confidence: float
35
+
36
+
37
+ @dataclass
38
+ class TranscriptResult:
39
+ """Result of an ASR call with potential transcripts."""
40
+
41
+ call_sid: str
42
+ message_id: str
43
+ is_final: bool
44
+ transcripts: List[Transcript] = field(default_factory=list)
45
+
46
+ @staticmethod
47
+ def from_speech_result(message: Dict[str, Any]) -> "TranscriptResult":
48
+ return TranscriptResult(
49
+ message.get("call_sid"),
50
+ message.get("msgid"),
51
+ message.get("data", {}).get("speech", {}).get("is_final", True),
52
+ transcripts=[
53
+ Transcript(t.get("transcript", ""), t.get("confidence", 1.0))
54
+ for t in message.get("data", {})
55
+ .get("speech", {})
56
+ .get("alternatives", [])
57
+ ],
58
+ )
59
+
60
+ @staticmethod
61
+ def from_dtmf_result(message: Dict[str, Any]) -> "TranscriptResult":
62
+ """Create a transcript result from a DTMF result.
63
+
64
+ We use the dtmf as the text with confidence 1.0
65
+ """
66
+ return TranscriptResult(
67
+ message.get("call_sid"),
68
+ message.get("msgid"),
69
+ is_final=True,
70
+ transcripts=[
71
+ Transcript(str(message.get("data", {}).get("digits", "")), 1.0)
72
+ ],
73
+ )
74
+
75
+
76
+ @dataclass
77
+ class CallStatusChanged:
78
+ """Message indicating a change in the call status."""
79
+
80
+ call_sid: str
81
+ status: str
82
+
83
+ @staticmethod
84
+ def from_message(message: Dict[str, Any]) -> "CallStatusChanged":
85
+ return CallStatusChanged(
86
+ message.get("call_sid"), message.get("data", {}).get("call_status")
87
+ )
88
+
89
+
90
+ @dataclass
91
+ class SessionReconnect:
92
+ """Message indicating a session has reconnected."""
93
+
94
+ call_sid: str
95
+
96
+ @staticmethod
97
+ def from_message(message: Dict[str, Any]) -> "SessionReconnect":
98
+ return SessionReconnect(message.get("call_sid"))
99
+
100
+
101
+ @dataclass
102
+ class VerbStatusChanged:
103
+ """Message indicating a change in the status of a verb."""
104
+
105
+ call_sid: str
106
+ event: str
107
+ id: str
108
+ name: str
109
+
110
+ @staticmethod
111
+ def from_message(message: Dict[str, Any]) -> "VerbStatusChanged":
112
+ return VerbStatusChanged(
113
+ message.get("call_sid"),
114
+ message.get("data", {}).get("event"),
115
+ message.get("data", {}).get("id"),
116
+ message.get("data", {}).get("name"),
117
+ )
118
+
119
+
120
+ @dataclass
121
+ class GatherTimeout:
122
+ """Message indicating a gather timeout."""
123
+
124
+ call_sid: str
125
+
126
+ @staticmethod
127
+ def from_message(message: Dict[str, Any]) -> "GatherTimeout":
128
+ return GatherTimeout(message.get("call_sid"))
129
+
130
+
131
+ async def websocket_message_handler(
132
+ message_dump: str,
133
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
134
+ ws: Websocket,
135
+ ) -> None:
136
+ """Handle incoming messages from the websocket."""
137
+ message = json.loads(message_dump)
138
+
139
+ # parse and handle the different message types
140
+ if message.get("type") == "session:new":
141
+ new_session = NewSessionMessage.from_message(message)
142
+ await handle_new_session(new_session, on_new_message, ws)
143
+ elif message.get("type") == "session:reconnect":
144
+ session_reconnect = SessionReconnect.from_message(message)
145
+ await handle_session_reconnect(session_reconnect)
146
+ elif message.get("type") == "call:status":
147
+ call_status = CallStatusChanged.from_message(message)
148
+ await handle_call_status(call_status)
149
+ elif message.get("type") == "verb:hook" and message.get("hook") == "/gather":
150
+ hook_trigger_reason = message.get("data", {}).get("reason")
151
+
152
+ if hook_trigger_reason == "speechDetected":
153
+ transcript = TranscriptResult.from_speech_result(message)
154
+ await handle_gather_completed(transcript, on_new_message, ws)
155
+ elif hook_trigger_reason == "timeout":
156
+ gather_timeout = GatherTimeout.from_message(message)
157
+ await handle_gather_timeout(gather_timeout, ws)
158
+ elif hook_trigger_reason == "dtmfDetected":
159
+ # for now, let's handle it as normal user input with a
160
+ # confidence of 1.0
161
+ transcript = TranscriptResult.from_dtmf_result(message)
162
+ await handle_gather_completed(transcript, on_new_message, ws)
163
+ else:
164
+ structlogger.debug(
165
+ "jambonz.websocket.message.verb_hook",
166
+ call_sid=message.get("call_sid"),
167
+ reason=hook_trigger_reason,
168
+ message=message,
169
+ )
170
+ elif message.get("type") == "verb:status":
171
+ verb_status = VerbStatusChanged.from_message(message)
172
+ await handle_verb_status(verb_status)
173
+ elif message.get("type") == "jambonz:error":
174
+ # jambonz ran into a fatal error handling the call. the call will be
175
+ # terminated.
176
+ structlogger.error("jambonz.websocket.message.error", message=message)
177
+ else:
178
+ structlogger.warning("jambonz.websocket.message.unknown_type", message=message)
179
+
180
+
181
+ async def handle_new_session(
182
+ message: NewSessionMessage,
183
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
184
+ ws: Websocket,
185
+ ) -> None:
186
+ """Handle new session message."""
187
+ from rasa.core.channels.voice_aware.jambonz import JambonzWebsocketOutput
188
+
189
+ structlogger.debug("jambonz.websocket.message.new_call", call_sid=message.call_sid)
190
+ output_channel = JambonzWebsocketOutput(ws, message.call_sid)
191
+ user_msg = UserMessage(
192
+ text="/session_start",
193
+ output_channel=output_channel,
194
+ sender_id=message.call_sid,
195
+ metadata={},
196
+ )
197
+ await send_config_ack(message.message_id, ws)
198
+ await on_new_message(user_msg)
199
+ await send_gather_input(ws)
200
+
201
+
202
+ async def handle_gather_completed(
203
+ transcript_result: TranscriptResult,
204
+ on_new_message: Callable[[UserMessage], Awaitable[Any]],
205
+ ws: Websocket,
206
+ ) -> None:
207
+ """Handle changes to commands we have send to jambonz.
208
+
209
+ This includes results of gather calles with their transcription.
210
+ """
211
+ from rasa.core.channels.voice_aware.jambonz import JambonzWebsocketOutput
212
+
213
+ if not transcript_result.is_final:
214
+ # in case of a non final transcript, we are going to wait for the final
215
+ # one and ignore the partial one
216
+ structlogger.debug(
217
+ "jambonz.websocket.message.transcript_partial",
218
+ call_sid=transcript_result.call_sid,
219
+ number_of_transcripts=len(transcript_result.transcripts),
220
+ )
221
+ return
222
+
223
+ if transcript_result.transcripts:
224
+ most_likely_transcript = transcript_result.transcripts[0]
225
+ output_channel = JambonzWebsocketOutput(ws, transcript_result.call_sid)
226
+ user_msg = UserMessage(
227
+ text=most_likely_transcript.text,
228
+ output_channel=output_channel,
229
+ sender_id=transcript_result.call_sid,
230
+ metadata={},
231
+ )
232
+ structlogger.debug(
233
+ "jambonz.websocket.message.transcript",
234
+ call_sid=transcript_result.call_sid,
235
+ transcript=most_likely_transcript.text,
236
+ confidence=most_likely_transcript.confidence,
237
+ number_of_transcripts=len(transcript_result.transcripts),
238
+ )
239
+ await on_new_message(user_msg)
240
+ else:
241
+ structlogger.warning(
242
+ "jambonz.websocket.message.no_transcript",
243
+ call_sid=transcript_result.call_sid,
244
+ )
245
+ await send_gather_input(ws)
246
+
247
+
248
+ async def handle_gather_timeout(gather_timeout: GatherTimeout, ws: Websocket) -> None:
249
+ """Handle gather timeout."""
250
+ structlogger.debug(
251
+ "jambonz.websocket.message.gather_timeout",
252
+ call_sid=gather_timeout.call_sid,
253
+ )
254
+ # TODO: figure out how to handle timeouts
255
+ await send_ws_text_message(ws, "I'm sorry, I didn't catch that.")
256
+ await send_gather_input(ws)
257
+
258
+
259
+ async def handle_call_status(call_status: CallStatusChanged) -> None:
260
+ """Handle changes in the call status."""
261
+ structlogger.debug(
262
+ "jambonz.websocket.message.call_status_changed",
263
+ call_sid=call_status.call_sid,
264
+ message=call_status.status,
265
+ )
266
+
267
+
268
+ async def handle_session_reconnect(session_reconnect: SessionReconnect) -> None:
269
+ """Handle session reconnect message."""
270
+ # there is nothing we need to do atm when a session reconnects.
271
+ # this happens if jambonz looses the websocket connection and reconnects
272
+ structlogger.debug(
273
+ "jambonz.websocket.message.session_reconnect",
274
+ call_sid=session_reconnect.call_sid,
275
+ )
276
+
277
+
278
+ async def handle_verb_status(verb_status: VerbStatusChanged) -> None:
279
+ """Handle changes in the status of a verb."""
280
+ structlogger.debug(
281
+ "jambonz.websocket.message.verb_status_changed",
282
+ call_sid=verb_status.call_sid,
283
+ event_type=verb_status.event,
284
+ id=verb_status.id,
285
+ name=verb_status.name,
286
+ )
287
+
288
+
289
+ async def send_config_ack(message_id: str, ws: Websocket) -> None:
290
+ """Send an ack message to jambonz including the configuration."""
291
+ await ws.send(
292
+ json.dumps(
293
+ {
294
+ "type": "ack",
295
+ "msgid": message_id,
296
+ "data": [{"config": {"notifyEvents": True}}],
297
+ }
298
+ )
299
+ )
300
+
301
+
302
+ async def send_gather_input(ws: Websocket) -> None:
303
+ """Send a gather input command to jambonz."""
304
+ await ws.send(
305
+ json.dumps(
306
+ {
307
+ "type": "command",
308
+ "command": "redirect",
309
+ "queueCommand": True,
310
+ "data": [
311
+ {
312
+ "gather": {
313
+ "input": ["speech", "digits"],
314
+ "minDigits": 1,
315
+ "id": uuid.uuid4().hex,
316
+ "actionHook": "/gather",
317
+ }
318
+ }
319
+ ],
320
+ }
321
+ )
322
+ )
323
+
324
+
325
+ async def send_ws_text_message(ws: Websocket, text: Text) -> None:
326
+ """Send a text message to the websocket using the jambonz interface."""
327
+ await ws.send(
328
+ json.dumps(
329
+ {
330
+ "type": "command",
331
+ "command": "redirect",
332
+ "queueCommand": True,
333
+ "data": [
334
+ {
335
+ "say": {
336
+ # id can be used for status notifications
337
+ "id": uuid.uuid4().hex,
338
+ "text": text,
339
+ }
340
+ }
341
+ ],
342
+ }
343
+ )
344
+ )
@@ -0,0 +1,20 @@
1
+ import structlog
2
+
3
+ from rasa.utils.licensing import (
4
+ PRODUCT_AREA,
5
+ VOICE_SCOPE,
6
+ validate_license_from_env,
7
+ )
8
+
9
+ structlogger = structlog.get_logger()
10
+
11
+
12
+ def validate_voice_license_scope() -> None:
13
+ """Validate that the correct license scope is present."""
14
+ structlogger.info(
15
+ f"Validating current Rasa Pro license scope which must include "
16
+ f"the '{VOICE_SCOPE}' scope to use the voice channel."
17
+ )
18
+
19
+ voice_product_scope = PRODUCT_AREA + " " + VOICE_SCOPE
20
+ validate_license_from_env(product_area=voice_product_scope)
File without changes
rasa/core/constants.py CHANGED
@@ -61,7 +61,6 @@ SEARCH_POLICY_PRIORITY = CHAT_POLICY_PRIORITY + 1
61
61
  # flow policy priority
62
62
  FLOW_POLICY_PRIORITY = SEARCH_POLICY_PRIORITY + 1
63
63
 
64
-
65
64
  DIALOGUE = "dialogue"
66
65
 
67
66
  # RabbitMQ message property header added to events published using `rasa export`
@@ -105,3 +104,9 @@ DEFAULT_TEMPLATE_ENGINE = RASA_FORMAT_TEMPLATE_ENGINE
105
104
  # configuration parameter used to specify the template engine to use
106
105
  # for a response
107
106
  TEMPLATE_ENGINE_CONFIG_KEY = "template"
107
+
108
+ # metadata keys for bot utterance events
109
+ UTTER_SOURCE_METADATA_KEY = "utter_source"
110
+ DOMAIN_GROUND_TRUTH_METADATA_KEY = "domain_ground_truth"
111
+ ACTIVE_FLOW_METADATA_KEY = "active_flow"
112
+ STEP_ID_METADATA_KEY = "step_id"