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.

Files changed (240) hide show
  1. rasa/__main__.py +31 -15
  2. rasa/api.py +12 -2
  3. rasa/cli/arguments/default_arguments.py +24 -4
  4. rasa/cli/arguments/run.py +15 -0
  5. rasa/cli/arguments/shell.py +5 -1
  6. rasa/cli/arguments/train.py +17 -9
  7. rasa/cli/evaluate.py +7 -7
  8. rasa/cli/inspect.py +19 -7
  9. rasa/cli/interactive.py +1 -0
  10. rasa/cli/llm_fine_tuning.py +11 -14
  11. rasa/cli/project_templates/calm/config.yml +5 -7
  12. rasa/cli/project_templates/calm/endpoints.yml +15 -2
  13. rasa/cli/project_templates/tutorial/config.yml +8 -5
  14. rasa/cli/project_templates/tutorial/data/flows.yml +1 -1
  15. rasa/cli/project_templates/tutorial/data/patterns.yml +5 -0
  16. rasa/cli/project_templates/tutorial/domain.yml +14 -0
  17. rasa/cli/project_templates/tutorial/endpoints.yml +5 -0
  18. rasa/cli/run.py +7 -0
  19. rasa/cli/scaffold.py +4 -2
  20. rasa/cli/studio/upload.py +0 -15
  21. rasa/cli/train.py +14 -53
  22. rasa/cli/utils.py +14 -11
  23. rasa/cli/x.py +7 -7
  24. rasa/constants.py +3 -1
  25. rasa/core/actions/action.py +77 -33
  26. rasa/core/actions/action_hangup.py +29 -0
  27. rasa/core/actions/action_repeat_bot_messages.py +89 -0
  28. rasa/core/actions/e2e_stub_custom_action_executor.py +5 -1
  29. rasa/core/actions/http_custom_action_executor.py +4 -0
  30. rasa/core/agent.py +2 -2
  31. rasa/core/brokers/kafka.py +3 -1
  32. rasa/core/brokers/pika.py +3 -1
  33. rasa/core/channels/__init__.py +10 -6
  34. rasa/core/channels/channel.py +41 -4
  35. rasa/core/channels/development_inspector.py +150 -46
  36. rasa/core/channels/inspector/README.md +1 -1
  37. rasa/core/channels/inspector/dist/assets/{arc-b6e548fe.js → arc-bc141fb2.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{c4Diagram-d0fbc5ce-fa03ac9e.js → c4Diagram-d0fbc5ce-be2db283.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{classDiagram-936ed81e-ee67392a.js → classDiagram-936ed81e-55366915.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-c3cb15f1-9b283fae.js → classDiagram-v2-c3cb15f1-bb529518.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{createText-62fc7601-8b6fcc2a.js → createText-62fc7601-b0ec81d6.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{edges-f2ad444c-22e77f4f.js → edges-f2ad444c-6166330c.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{erDiagram-9d236eb7-60ffc87f.js → erDiagram-9d236eb7-5ccc6a8e.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{flowDb-1972c806-9dd802e4.js → flowDb-1972c806-fca3bfe4.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/{flowDiagram-7ea5b25a-5fa1912f.js → flowDiagram-7ea5b25a-4739080f.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-736177bf.js +1 -0
  47. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-abe16c3d-622a1fd2.js → flowchart-elk-definition-abe16c3d-7c1b0e0f.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{ganttDiagram-9b5ea136-e285a63a.js → ganttDiagram-9b5ea136-772fd050.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-99d0ae7c-f237bdca.js → gitGraphDiagram-99d0ae7c-8eae1dc9.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{index-2c4b9a3b-4b03d70e.js → index-2c4b9a3b-f55afcdf.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/index-e7cef9de.js +1317 -0
  52. rasa/core/channels/inspector/dist/assets/{infoDiagram-736b4530-72a0fa5f.js → infoDiagram-736b4530-124d4a14.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{journeyDiagram-df861f2b-82218c41.js → journeyDiagram-df861f2b-7c4fae44.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{layout-78cff630.js → layout-b9885fb6.js} +1 -1
  55. rasa/core/channels/inspector/dist/assets/{line-5038b469.js → line-7c59abb6.js} +1 -1
  56. rasa/core/channels/inspector/dist/assets/{linear-c4fc4098.js → linear-4776f780.js} +1 -1
  57. rasa/core/channels/inspector/dist/assets/{mindmap-definition-beec6740-c33c8ea6.js → mindmap-definition-beec6740-2332c46c.js} +1 -1
  58. rasa/core/channels/inspector/dist/assets/{pieDiagram-dbbf0591-a8d03059.js → pieDiagram-dbbf0591-8fb39303.js} +1 -1
  59. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-4d7f4fd6-6a0e56b2.js → quadrantDiagram-4d7f4fd6-3c7180a2.js} +1 -1
  60. rasa/core/channels/inspector/dist/assets/{requirementDiagram-6fc4c22a-2dc7c7bd.js → requirementDiagram-6fc4c22a-e910bcb8.js} +1 -1
  61. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-8f13d901-2360fe39.js → sankeyDiagram-8f13d901-ead16c89.js} +1 -1
  62. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-b655622a-41b9f9ad.js → sequenceDiagram-b655622a-29a02a19.js} +1 -1
  63. rasa/core/channels/inspector/dist/assets/{stateDiagram-59f0c015-0aad326f.js → stateDiagram-59f0c015-042b3137.js} +1 -1
  64. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-2b26beab-9847d984.js → stateDiagram-v2-2b26beab-2178c0f3.js} +1 -1
  65. rasa/core/channels/inspector/dist/assets/{styles-080da4f6-564d890e.js → styles-080da4f6-23ffa4fc.js} +1 -1
  66. rasa/core/channels/inspector/dist/assets/{styles-3dcbcfbf-38957613.js → styles-3dcbcfbf-94f59763.js} +1 -1
  67. rasa/core/channels/inspector/dist/assets/{styles-9c745c82-f0fc6921.js → styles-9c745c82-78a6bebc.js} +1 -1
  68. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-4835440b-ef3c5a77.js → svgDrawCommon-4835440b-eae2a6f6.js} +1 -1
  69. rasa/core/channels/inspector/dist/assets/{timeline-definition-5b62e21b-bf3e91c1.js → timeline-definition-5b62e21b-5c968d92.js} +1 -1
  70. rasa/core/channels/inspector/dist/assets/{xychartDiagram-2b33534f-4d4026c0.js → xychartDiagram-2b33534f-fd3db0d5.js} +1 -1
  71. rasa/core/channels/inspector/dist/index.html +18 -17
  72. rasa/core/channels/inspector/index.html +17 -16
  73. rasa/core/channels/inspector/package.json +5 -1
  74. rasa/core/channels/inspector/src/App.tsx +118 -68
  75. rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
  76. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +11 -10
  77. rasa/core/channels/inspector/src/components/DialogueStack.tsx +10 -25
  78. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -3
  79. rasa/core/channels/inspector/src/helpers/audiostream.ts +165 -0
  80. rasa/core/channels/inspector/src/helpers/formatters.test.ts +10 -0
  81. rasa/core/channels/inspector/src/helpers/formatters.ts +107 -41
  82. rasa/core/channels/inspector/src/helpers/utils.ts +92 -7
  83. rasa/core/channels/inspector/src/types.ts +21 -1
  84. rasa/core/channels/inspector/yarn.lock +94 -1
  85. rasa/core/channels/rest.py +51 -46
  86. rasa/core/channels/socketio.py +28 -1
  87. rasa/core/channels/telegram.py +1 -1
  88. rasa/core/channels/twilio.py +1 -1
  89. rasa/core/channels/{audiocodes.py → voice_ready/audiocodes.py} +122 -69
  90. rasa/core/channels/{voice_aware → voice_ready}/jambonz.py +26 -8
  91. rasa/core/channels/{voice_aware → voice_ready}/jambonz_protocol.py +57 -5
  92. rasa/core/channels/{twilio_voice.py → voice_ready/twilio_voice.py} +64 -28
  93. rasa/core/channels/voice_ready/utils.py +37 -0
  94. rasa/core/channels/voice_stream/asr/__init__.py +0 -0
  95. rasa/core/channels/voice_stream/asr/asr_engine.py +89 -0
  96. rasa/core/channels/voice_stream/asr/asr_event.py +18 -0
  97. rasa/core/channels/voice_stream/asr/azure.py +129 -0
  98. rasa/core/channels/voice_stream/asr/deepgram.py +90 -0
  99. rasa/core/channels/voice_stream/audio_bytes.py +8 -0
  100. rasa/core/channels/voice_stream/browser_audio.py +107 -0
  101. rasa/core/channels/voice_stream/call_state.py +23 -0
  102. rasa/core/channels/voice_stream/tts/__init__.py +0 -0
  103. rasa/core/channels/voice_stream/tts/azure.py +106 -0
  104. rasa/core/channels/voice_stream/tts/cartesia.py +118 -0
  105. rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
  106. rasa/core/channels/voice_stream/tts/tts_engine.py +58 -0
  107. rasa/core/channels/voice_stream/twilio_media_streams.py +173 -0
  108. rasa/core/channels/voice_stream/util.py +57 -0
  109. rasa/core/channels/voice_stream/voice_channel.py +427 -0
  110. rasa/core/information_retrieval/qdrant.py +1 -0
  111. rasa/core/nlg/contextual_response_rephraser.py +45 -17
  112. rasa/{nlu → core}/persistor.py +203 -68
  113. rasa/core/policies/enterprise_search_policy.py +119 -63
  114. rasa/core/policies/flows/flow_executor.py +15 -22
  115. rasa/core/policies/intentless_policy.py +83 -28
  116. rasa/core/processor.py +25 -0
  117. rasa/core/run.py +12 -2
  118. rasa/core/secrets_manager/constants.py +4 -0
  119. rasa/core/secrets_manager/factory.py +8 -0
  120. rasa/core/secrets_manager/vault.py +11 -1
  121. rasa/core/training/interactive.py +33 -34
  122. rasa/core/utils.py +47 -21
  123. rasa/dialogue_understanding/coexistence/llm_based_router.py +41 -14
  124. rasa/dialogue_understanding/commands/__init__.py +6 -0
  125. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +60 -0
  126. rasa/dialogue_understanding/commands/session_end_command.py +61 -0
  127. rasa/dialogue_understanding/commands/user_silence_command.py +59 -0
  128. rasa/dialogue_understanding/commands/utils.py +5 -0
  129. rasa/dialogue_understanding/generator/constants.py +2 -0
  130. rasa/dialogue_understanding/generator/flow_retrieval.py +47 -9
  131. rasa/dialogue_understanding/generator/llm_based_command_generator.py +38 -15
  132. rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
  133. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +35 -13
  134. rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +3 -0
  135. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +60 -13
  136. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +53 -0
  137. rasa/dialogue_understanding/patterns/repeat.py +37 -0
  138. rasa/dialogue_understanding/patterns/user_silence.py +37 -0
  139. rasa/dialogue_understanding/processor/command_processor.py +21 -1
  140. rasa/e2e_test/aggregate_test_stats_calculator.py +1 -11
  141. rasa/e2e_test/assertions.py +136 -61
  142. rasa/e2e_test/assertions_schema.yml +23 -0
  143. rasa/e2e_test/e2e_test_case.py +85 -6
  144. rasa/e2e_test/e2e_test_runner.py +2 -3
  145. rasa/e2e_test/utils/e2e_yaml_utils.py +1 -1
  146. rasa/engine/graph.py +3 -10
  147. rasa/engine/loader.py +12 -0
  148. rasa/engine/recipes/config_files/default_config.yml +0 -3
  149. rasa/engine/recipes/default_recipe.py +0 -1
  150. rasa/engine/recipes/graph_recipe.py +0 -1
  151. rasa/engine/runner/dask.py +2 -2
  152. rasa/engine/storage/local_model_storage.py +12 -42
  153. rasa/engine/storage/storage.py +1 -5
  154. rasa/engine/validation.py +527 -74
  155. rasa/model_manager/__init__.py +0 -0
  156. rasa/model_manager/config.py +40 -0
  157. rasa/model_manager/model_api.py +559 -0
  158. rasa/model_manager/runner_service.py +286 -0
  159. rasa/model_manager/socket_bridge.py +146 -0
  160. rasa/model_manager/studio_jwt_auth.py +86 -0
  161. rasa/model_manager/trainer_service.py +325 -0
  162. rasa/model_manager/utils.py +87 -0
  163. rasa/model_manager/warm_rasa_process.py +187 -0
  164. rasa/model_service.py +112 -0
  165. rasa/model_training.py +42 -23
  166. rasa/nlu/tokenizers/whitespace_tokenizer.py +3 -14
  167. rasa/server.py +4 -2
  168. rasa/shared/constants.py +60 -8
  169. rasa/shared/core/constants.py +13 -0
  170. rasa/shared/core/domain.py +107 -50
  171. rasa/shared/core/events.py +29 -0
  172. rasa/shared/core/flows/flow.py +5 -0
  173. rasa/shared/core/flows/flows_list.py +19 -6
  174. rasa/shared/core/flows/flows_yaml_schema.json +10 -0
  175. rasa/shared/core/flows/utils.py +39 -0
  176. rasa/shared/core/flows/validation.py +121 -0
  177. rasa/shared/core/flows/yaml_flows_io.py +15 -27
  178. rasa/shared/core/slots.py +5 -0
  179. rasa/shared/importers/importer.py +59 -41
  180. rasa/shared/importers/multi_project.py +23 -11
  181. rasa/shared/importers/rasa.py +12 -3
  182. rasa/shared/importers/remote_importer.py +196 -0
  183. rasa/shared/importers/utils.py +3 -1
  184. rasa/shared/nlu/training_data/formats/rasa_yaml.py +18 -3
  185. rasa/shared/nlu/training_data/training_data.py +18 -19
  186. rasa/shared/providers/_configs/litellm_router_client_config.py +220 -0
  187. rasa/shared/providers/_configs/model_group_config.py +167 -0
  188. rasa/shared/providers/_configs/openai_client_config.py +1 -1
  189. rasa/shared/providers/_configs/rasa_llm_client_config.py +73 -0
  190. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +1 -0
  191. rasa/shared/providers/_configs/utils.py +16 -0
  192. rasa/shared/providers/_utils.py +79 -0
  193. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +13 -29
  194. rasa/shared/providers/embedding/azure_openai_embedding_client.py +54 -21
  195. rasa/shared/providers/embedding/default_litellm_embedding_client.py +24 -0
  196. rasa/shared/providers/embedding/litellm_router_embedding_client.py +135 -0
  197. rasa/shared/providers/llm/_base_litellm_client.py +34 -22
  198. rasa/shared/providers/llm/azure_openai_llm_client.py +50 -29
  199. rasa/shared/providers/llm/default_litellm_llm_client.py +24 -0
  200. rasa/shared/providers/llm/litellm_router_llm_client.py +182 -0
  201. rasa/shared/providers/llm/rasa_llm_client.py +112 -0
  202. rasa/shared/providers/llm/self_hosted_llm_client.py +5 -29
  203. rasa/shared/providers/mappings.py +19 -0
  204. rasa/shared/providers/router/__init__.py +0 -0
  205. rasa/shared/providers/router/_base_litellm_router_client.py +183 -0
  206. rasa/shared/providers/router/router_client.py +73 -0
  207. rasa/shared/utils/common.py +40 -24
  208. rasa/shared/utils/health_check/__init__.py +0 -0
  209. rasa/shared/utils/health_check/embeddings_health_check_mixin.py +31 -0
  210. rasa/shared/utils/health_check/health_check.py +258 -0
  211. rasa/shared/utils/health_check/llm_health_check_mixin.py +31 -0
  212. rasa/shared/utils/io.py +27 -6
  213. rasa/shared/utils/llm.py +354 -44
  214. rasa/shared/utils/schemas/events.py +2 -0
  215. rasa/shared/utils/schemas/model_config.yml +0 -10
  216. rasa/shared/utils/yaml.py +181 -38
  217. rasa/studio/data_handler.py +3 -1
  218. rasa/studio/upload.py +160 -74
  219. rasa/telemetry.py +94 -17
  220. rasa/tracing/config.py +3 -1
  221. rasa/tracing/instrumentation/attribute_extractors.py +95 -18
  222. rasa/tracing/instrumentation/instrumentation.py +121 -0
  223. rasa/utils/common.py +5 -0
  224. rasa/utils/endpoints.py +27 -1
  225. rasa/utils/io.py +8 -16
  226. rasa/utils/log_utils.py +9 -2
  227. rasa/utils/sanic_error_handler.py +32 -0
  228. rasa/validator.py +110 -16
  229. rasa/version.py +1 -1
  230. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/METADATA +16 -14
  231. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/RECORD +236 -185
  232. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-1844e5a5.js +0 -1
  233. rasa/core/channels/inspector/dist/assets/index-a5d3e69d.js +0 -1040
  234. rasa/core/channels/voice_aware/utils.py +0 -20
  235. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +0 -407
  236. /rasa/core/channels/{voice_aware → voice_ready}/__init__.py +0 -0
  237. /rasa/core/channels/{voice_native → voice_stream}/__init__.py +0 -0
  238. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/NOTICE +0 -0
  239. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/WHEEL +0 -0
  240. {rasa_pro-3.10.16.dist-info → rasa_pro-3.11.0.dist-info}/entry_points.txt +0 -0
File without changes
@@ -0,0 +1,40 @@
1
+ import sys
2
+ import os
3
+
4
+ DEFAULT_SERVER_BASE_WORKING_DIRECTORY = "working-data"
5
+
6
+ SERVER_BASE_WORKING_DIRECTORY = os.environ.get(
7
+ "RASA_MODEL_SERVER_BASE_DIRECTORY", DEFAULT_SERVER_BASE_WORKING_DIRECTORY
8
+ )
9
+
10
+ SERVER_PORT = os.environ.get("RASA_MODEL_SERVER_PORT", 8000)
11
+
12
+ SERVER_BASE_URL = os.environ.get("RASA_MODEL_SERVER_BASE_URL", None)
13
+
14
+ # defaults to storing on the local hard drive
15
+ SERVER_MODEL_REMOTE_STORAGE = os.environ.get("RASA_REMOTE_STORAGE", None)
16
+
17
+ # The path to the python executable that is running this script
18
+ # we will use the same python to run training / bots
19
+ RASA_PYTHON_PATH = sys.executable
20
+
21
+ # the max limit for parallel training requests
22
+ DEFAULT_MAX_PARALLEL_TRAININGS = 10
23
+
24
+ MAX_PARALLEL_TRAININGS = os.getenv(
25
+ "MAX_PARALLEL_TRAININGS", DEFAULT_MAX_PARALLEL_TRAININGS
26
+ )
27
+ # the max limit for parallel running bots
28
+ DEFAULT_MAX_PARALLEL_BOT_RUNS = 10
29
+
30
+ MAX_PARALLEL_BOT_RUNS = os.getenv(
31
+ "MAX_PARALLEL_BOT_RUNS", DEFAULT_MAX_PARALLEL_BOT_RUNS
32
+ )
33
+
34
+ DEFAULT_SERVER_PATH_PREFIX = "talk"
35
+
36
+ DEFAULT_MIN_REQUIRED_DISCSPACE_MB = 1
37
+
38
+ MIN_REQUIRED_DISCSPACE_MB = int(
39
+ os.getenv("MIN_REQUIRED_DISCSPACE_MB", DEFAULT_MIN_REQUIRED_DISCSPACE_MB)
40
+ )
@@ -0,0 +1,559 @@
1
+ import asyncio
2
+ from functools import wraps
3
+ import os
4
+ from http import HTTPStatus
5
+ from typing import Any, Callable, Dict, Optional, Union
6
+ import dotenv
7
+ import psutil
8
+ from sanic import Blueprint, Sanic, response
9
+ from sanic.response import json
10
+ from sanic.exceptions import NotFound
11
+ from sanic.request import Request
12
+ import structlog
13
+ from socketio import AsyncServer
14
+
15
+ from rasa.exceptions import ModelNotFound
16
+ from rasa.model_manager import config
17
+ from rasa.model_manager.config import SERVER_BASE_URL
18
+ from rasa.constants import MODEL_ARCHIVE_EXTENSION
19
+ from rasa.model_manager.runner_service import (
20
+ BotSession,
21
+ BotSessionStatus,
22
+ fetch_remote_model_to_dir,
23
+ fetch_size_of_remote_model,
24
+ run_bot,
25
+ terminate_bot,
26
+ update_bot_status,
27
+ )
28
+ from rasa.model_manager.socket_bridge import create_bridge_server
29
+ from rasa.model_manager.trainer_service import (
30
+ TrainingSession,
31
+ TrainingSessionStatus,
32
+ run_training,
33
+ terminate_training,
34
+ update_training_status,
35
+ )
36
+ from rasa.model_manager.utils import (
37
+ InvalidPathException,
38
+ get_logs_content,
39
+ logs_base_path,
40
+ models_base_path,
41
+ subpath,
42
+ )
43
+ from rasa.model_manager.warm_rasa_process import (
44
+ initialize_warm_rasa_process,
45
+ shutdown_warm_rasa_processes,
46
+ )
47
+
48
+ dotenv.load_dotenv()
49
+
50
+ structlogger = structlog.get_logger()
51
+
52
+
53
+ # A simple in-memory store for training sessions and running bots
54
+ trainings: Dict[str, TrainingSession] = {}
55
+ running_bots: Dict[str, BotSession] = {}
56
+
57
+
58
+ def prepare_working_directories() -> None:
59
+ """Make sure all required directories exist."""
60
+ os.makedirs(logs_base_path(), exist_ok=True)
61
+ os.makedirs(models_base_path(), exist_ok=True)
62
+
63
+
64
+ def cleanup_training_processes() -> None:
65
+ """Terminate all running training processes."""
66
+ structlogger.debug("model_trainer.cleanup_processes.started")
67
+ running = list(trainings.values())
68
+ for training in running:
69
+ terminate_training(training)
70
+
71
+
72
+ def cleanup_bot_processes() -> None:
73
+ """Terminate all running bot processes."""
74
+ structlogger.debug("model_runner.cleanup_processes.started")
75
+ running = list(running_bots.values())
76
+ for bot in running:
77
+ terminate_bot(bot)
78
+
79
+
80
+ def update_status_of_all_trainings() -> None:
81
+ """Update the status of all training processes."""
82
+ running = list(trainings.values())
83
+ for training in running:
84
+ update_training_status(training)
85
+
86
+
87
+ async def update_status_of_all_bots() -> None:
88
+ """Update the status of all bot processes."""
89
+ # we need to get the values first, because (since we are async and waiting
90
+ # within the loop) some other job on the asyncio loop could change the dict
91
+ # (adding or removing). python doesn't like if you change the size of a dict
92
+ # while iterating over it and will raise a RuntimeError. so we get the values
93
+ # first and iterate over them to avoid that.
94
+ running = list(running_bots.values())
95
+ for bot in running:
96
+ await update_bot_status(bot)
97
+
98
+
99
+ def base_server_url(request: Request) -> str:
100
+ """Return the base URL of the server."""
101
+ if SERVER_BASE_URL:
102
+ return SERVER_BASE_URL.rstrip("/")
103
+ else:
104
+ return f"{request.scheme}://{request.host}/{config.DEFAULT_SERVER_PATH_PREFIX}"
105
+
106
+
107
+ async def continuously_update_process_status() -> None:
108
+ """Regularly Update the status of all training and bot processes."""
109
+ structlogger.debug("model_api.update_process_status.started")
110
+
111
+ while True:
112
+ try:
113
+ update_status_of_all_trainings()
114
+ await update_status_of_all_bots()
115
+ except asyncio.exceptions.CancelledError:
116
+ structlogger.debug("model_api.update_process_status.cancelled")
117
+ break
118
+ except Exception as e:
119
+ structlogger.error("model_api.update_process_status.error", error=str(e))
120
+ finally:
121
+ await asyncio.sleep(0.1)
122
+
123
+
124
+ def internal_blueprint() -> Blueprint:
125
+ """Create a blueprint for the model manager API."""
126
+ bp = Blueprint("model_api_internal")
127
+
128
+ @bp.before_server_stop
129
+ async def cleanup_processes(app: Sanic, loop: asyncio.AbstractEventLoop) -> None:
130
+ """Terminate all running processes before the server stops."""
131
+ structlogger.debug("model_api.cleanup_processes.started")
132
+ cleanup_training_processes()
133
+ cleanup_bot_processes()
134
+ shutdown_warm_rasa_processes()
135
+
136
+ @bp.after_server_start
137
+ async def create_warm_rasa_processes(
138
+ app: Sanic, loop: asyncio.AbstractEventLoop
139
+ ) -> None:
140
+ """Create warm Rasa processes to speed up future training and bot runs."""
141
+ structlogger.debug("model_api.create_warm_rasa_processes.started")
142
+ initialize_warm_rasa_process()
143
+
144
+ def limit_parallel_training_requests() -> Callable[[Callable], Callable[..., Any]]:
145
+ """Limit the number of parallel training requests."""
146
+
147
+ def decorator(f: Callable) -> Callable:
148
+ @wraps(f)
149
+ def decorated(*args: Any, **kwargs: Any) -> Any:
150
+ running_requests = len(
151
+ [
152
+ training
153
+ for training in trainings.values()
154
+ if training.status == TrainingSessionStatus.RUNNING
155
+ and training.process.poll() is None
156
+ ]
157
+ )
158
+
159
+ if running_requests >= int(config.MAX_PARALLEL_TRAININGS):
160
+ return response.json(
161
+ {
162
+ "message": f"Too many parallel training requests, above "
163
+ f"the limit of {config.MAX_PARALLEL_TRAININGS}. "
164
+ f"Retry later or increase your server's "
165
+ f"memory and CPU resources."
166
+ },
167
+ status=HTTPStatus.TOO_MANY_REQUESTS,
168
+ )
169
+ return f(*args, **kwargs)
170
+
171
+ return decorated
172
+
173
+ return decorator
174
+
175
+ def limit_parallel_bot_runs() -> Callable[[Callable], Callable[..., Any]]:
176
+ """Limit the number of parallel training requests."""
177
+
178
+ def decorator(f: Callable) -> Callable:
179
+ @wraps(f)
180
+ def decorated(*args: Any, **kwargs: Any) -> Any:
181
+ running_requests = len(
182
+ [
183
+ bot
184
+ for bot in running_bots.values()
185
+ if bot.status
186
+ in {BotSessionStatus.RUNNING, BotSessionStatus.QUEUED}
187
+ ]
188
+ )
189
+
190
+ if running_requests >= int(config.MAX_PARALLEL_BOT_RUNS):
191
+ return response.json(
192
+ {
193
+ "message": f"Too many parallel bot runs, above "
194
+ f"the limit of {config.MAX_PARALLEL_BOT_RUNS}. "
195
+ f"Retry later or increase your server's "
196
+ f"memory and CPU resources."
197
+ },
198
+ status=HTTPStatus.TOO_MANY_REQUESTS,
199
+ )
200
+
201
+ return f(*args, **kwargs)
202
+
203
+ return decorated
204
+
205
+ return decorator
206
+
207
+ def ensure_minimum_disk_space() -> Callable[[Callable], Callable[..., Any]]:
208
+ """Ensure that there is enough disk space before starting a new process."""
209
+ min_required_disk_space = 1024 * 1024 * config.MIN_REQUIRED_DISCSPACE_MB
210
+
211
+ def decorator(f: Callable) -> Callable:
212
+ @wraps(f)
213
+ def decorated(*args: Any, **kwargs: Any) -> Any:
214
+ if os.path.exists(config.SERVER_BASE_WORKING_DIRECTORY):
215
+ free_space_bytes = psutil.disk_usage(
216
+ config.SERVER_BASE_WORKING_DIRECTORY
217
+ ).free
218
+ structlogger.debug(
219
+ "model_api.storage.available_disk_space",
220
+ available_space_mb=free_space_bytes / 1024 / 1024,
221
+ )
222
+
223
+ if free_space_bytes < min_required_disk_space:
224
+ return response.json(
225
+ {
226
+ "message": (
227
+ f"Less than {config.MIN_REQUIRED_DISCSPACE_MB} MB "
228
+ f"of free disk space available. "
229
+ f"Please free up some space on the model service."
230
+ )
231
+ },
232
+ status=HTTPStatus.INSUFFICIENT_STORAGE,
233
+ )
234
+
235
+ return f(*args, **kwargs)
236
+
237
+ return decorated
238
+
239
+ return decorator
240
+
241
+ @bp.get("/")
242
+ async def health(request: Request) -> response.HTTPResponse:
243
+ return json(
244
+ {
245
+ "status": "ok",
246
+ "bots": [
247
+ {
248
+ "deployment_id": bot.deployment_id,
249
+ "status": bot.status,
250
+ "internal_url": bot.internal_url,
251
+ "returncode": bot.returncode,
252
+ "url": bot.url,
253
+ }
254
+ for bot in running_bots.values()
255
+ ],
256
+ "trainings": [
257
+ {
258
+ "training_id": training.training_id,
259
+ "assistant_id": training.assistant_id,
260
+ "client_id": training.client_id,
261
+ "progress": training.progress,
262
+ "status": training.status,
263
+ }
264
+ for training in trainings.values()
265
+ ],
266
+ }
267
+ )
268
+
269
+ @bp.get("/training")
270
+ async def get_training_list(request: Request) -> response.HTTPResponse:
271
+ """Return a list of all training sessions for an assistant."""
272
+ assistant_id = request.args.get("assistant_id")
273
+ sessions = [
274
+ {
275
+ "training_id": session.training_id,
276
+ "assistant_id": session.assistant_id,
277
+ "client_id": session.client_id,
278
+ "progress": session.progress,
279
+ "status": session.status,
280
+ "model_name": session.model_name,
281
+ "runtime_metadata": None,
282
+ }
283
+ for session in trainings.values()
284
+ if session.assistant_id == assistant_id
285
+ ]
286
+ return json({"training_sessions": sessions, "total_number": len(sessions)})
287
+
288
+ @bp.post("/training")
289
+ @limit_parallel_training_requests()
290
+ @ensure_minimum_disk_space()
291
+ async def start_training(request: Request) -> response.HTTPResponse:
292
+ """Start a new training session."""
293
+ data = request.json
294
+ training_id: Optional[str] = data.get("id")
295
+ assistant_id: Optional[str] = data.get("assistant_id")
296
+ client_id: Optional[str] = data.get("client_id")
297
+ encoded_training_data: Dict[str, str] = data.get("bot_config", {}).get(
298
+ "data", {}
299
+ )
300
+
301
+ if training_id in trainings:
302
+ # fail, because there apparently is already a training with this id
303
+ return json({"message": "Training with this id already exists"}, status=409)
304
+
305
+ if not assistant_id:
306
+ return json({"message": "Assistant id is required"}, status=400)
307
+
308
+ if not training_id:
309
+ return json({"message": "Training id is required"}, status=400)
310
+
311
+ try:
312
+ # file deepcode ignore PT: path traversal is prevented
313
+ # by the `subpath` function found in the `rasa.model_manager.utils` module
314
+ training_session = run_training(
315
+ training_id=training_id,
316
+ assistant_id=assistant_id,
317
+ client_id=client_id,
318
+ encoded_training_data=encoded_training_data,
319
+ )
320
+ trainings[training_id] = training_session
321
+ return json(
322
+ {"training_id": training_id, "model_name": training_session.model_name}
323
+ )
324
+ except InvalidPathException as exc:
325
+ return json({"message": str(exc)}, status=403)
326
+ except Exception as exc:
327
+ return json({"message": str(exc)}, status=500)
328
+
329
+ @bp.get("/training/<training_id>")
330
+ async def get_training(request: Request, training_id: str) -> response.HTTPResponse:
331
+ """Return the status of a training session."""
332
+ if training := trainings.get(training_id):
333
+ return json(
334
+ {
335
+ "training_id": training_id,
336
+ "assistant_id": training.assistant_id,
337
+ "client_id": training.client_id,
338
+ "progress": training.progress,
339
+ "model_name": training.model_name,
340
+ "status": training.status,
341
+ "logs": get_logs_content(training.log_id),
342
+ }
343
+ )
344
+ else:
345
+ return json({"message": "Training not found"}, status=404)
346
+
347
+ @bp.delete("/training/<training_id>")
348
+ async def stop_training(
349
+ request: Request, training_id: str
350
+ ) -> response.HTTPResponse:
351
+ # this is a no-op if the training is already done
352
+ if not (training := trainings.get(training_id)):
353
+ return json({"message": "Training session not found"}, status=404)
354
+
355
+ terminate_training(training)
356
+ return json({"training_id": training_id})
357
+
358
+ @bp.post("/bot")
359
+ @limit_parallel_bot_runs()
360
+ @ensure_minimum_disk_space()
361
+ async def start_bot(request: Request) -> response.HTTPResponse:
362
+ data = request.json
363
+ deployment_id: Optional[str] = data.get("deployment_id")
364
+ model_name: Optional[str] = data.get("model_name")
365
+ encoded_configs: Dict[str, str] = data.get("bot_config", {})
366
+
367
+ if deployment_id in running_bots:
368
+ # fail, because there apparently is already a bot running with this id
369
+ return json(
370
+ {"message": "Bot with this deployment id already exists"}, status=409
371
+ )
372
+
373
+ if not deployment_id:
374
+ return json({"message": "Deployment id is required"}, status=400)
375
+
376
+ if not model_name:
377
+ return json({"message": "Model name is required"}, status=400)
378
+
379
+ base_url_path = base_server_url(request)
380
+ try:
381
+ bot_session = run_bot(
382
+ deployment_id,
383
+ model_name,
384
+ base_url_path,
385
+ encoded_configs,
386
+ )
387
+ running_bots[deployment_id] = bot_session
388
+ return json(
389
+ {
390
+ "deployment_id": deployment_id,
391
+ "status": bot_session.status,
392
+ "url": bot_session.url,
393
+ }
394
+ )
395
+ except ModelNotFound:
396
+ return json(
397
+ {"message": f"Model with name '{model_name}' could not be found."},
398
+ status=404,
399
+ )
400
+ except Exception as e:
401
+ return json({"message": str(e)}, status=500)
402
+
403
+ @bp.delete("/bot/<deployment_id>")
404
+ async def stop_bot(request: Request, deployment_id: str) -> response.HTTPResponse:
405
+ bot = running_bots.get(deployment_id)
406
+ if bot is None:
407
+ return json({"message": "Bot not found"}, status=404)
408
+
409
+ terminate_bot(bot)
410
+
411
+ return json(
412
+ {"deployment_id": deployment_id, "status": bot.status, "url": bot.url}
413
+ )
414
+
415
+ @bp.get("/bot/<deployment_id>")
416
+ async def get_bot(request: Request, deployment_id: str) -> response.HTTPResponse:
417
+ bot = running_bots.get(deployment_id)
418
+ if bot is None:
419
+ return json({"message": "Bot not found"}, status=404)
420
+
421
+ return json(
422
+ {
423
+ "deployment_id": deployment_id,
424
+ "status": bot.status,
425
+ "returncode": bot.returncode,
426
+ "url": bot.url,
427
+ "logs": get_logs_content(bot.log_id),
428
+ }
429
+ )
430
+
431
+ @bp.get("/bot")
432
+ async def list_bots(request: Request) -> response.HTTPResponse:
433
+ bots = [
434
+ {
435
+ "deployment_id": bot.deployment_id,
436
+ "status": bot.status,
437
+ "returncode": bot.returncode,
438
+ "url": bot.url,
439
+ }
440
+ for bot in running_bots.values()
441
+ ]
442
+ return json({"deployment_sessions": bots, "total_number": len(bots)})
443
+
444
+ @bp.route("/models/<model_name>", methods=["GET"])
445
+ async def send_model(
446
+ request: Request, model_name: str
447
+ ) -> Union[response.ResponseStream, response.HTTPResponse]:
448
+ try:
449
+ model_path = path_to_model(model_name)
450
+
451
+ # get size of model file
452
+ model_size = os.stat(model_path)
453
+
454
+ return await response.file_stream(
455
+ model_path, headers={"Content-Length": str(model_size.st_size)}
456
+ )
457
+ except NotFound:
458
+ return json({"message": "Model not found"}, status=404)
459
+ except ModelNotFound:
460
+ return json({"message": "Model not found"}, status=404)
461
+
462
+ @bp.route("/models/<model_name>", methods=["HEAD"])
463
+ async def head_model(request: Request, model_name: str) -> response.HTTPResponse:
464
+ try:
465
+ model_size = size_of_model(model_name)
466
+
467
+ structlogger.debug(
468
+ "model_api.internal.head_model",
469
+ model_name=model_name,
470
+ size=model_size,
471
+ )
472
+ return response.raw(
473
+ b"", status=200, headers={"Content-Length": str(model_size)}
474
+ )
475
+ except ModelNotFound:
476
+ return response.raw(b"", status=404)
477
+
478
+ return bp
479
+
480
+
481
+ def external_blueprint() -> Blueprint:
482
+ """Create a blueprint for the model manager API."""
483
+ from rasa.core.channels.socketio import SocketBlueprint
484
+
485
+ sio = AsyncServer(async_mode="sanic", cors_allowed_origins=[])
486
+ bp = SocketBlueprint(sio, "", "model_api_external")
487
+
488
+ create_bridge_server(sio, running_bots)
489
+
490
+ @bp.get("/health")
491
+ async def health(request: Request) -> response.HTTPResponse:
492
+ return json(
493
+ {
494
+ "status": "ok",
495
+ "bots": [
496
+ {
497
+ "deployment_id": bot.deployment_id,
498
+ "status": bot.status,
499
+ "internal_url": bot.internal_url,
500
+ "url": bot.url,
501
+ }
502
+ for bot in running_bots.values()
503
+ ],
504
+ "trainings": [
505
+ {
506
+ "training_id": training.training_id,
507
+ "assistant_id": training.assistant_id,
508
+ "client_id": training.client_id,
509
+ "progress": training.progress,
510
+ "status": training.status,
511
+ }
512
+ for training in trainings.values()
513
+ ],
514
+ }
515
+ )
516
+
517
+ return bp
518
+
519
+
520
+ def size_of_model(model_name: str) -> Optional[int]:
521
+ """Return the size of a model."""
522
+ model_file_name = f"{model_name}.{MODEL_ARCHIVE_EXTENSION}"
523
+ model_path = subpath(models_base_path(), model_file_name)
524
+
525
+ if os.path.exists(model_path):
526
+ return os.path.getsize(model_path)
527
+
528
+ if config.SERVER_MODEL_REMOTE_STORAGE:
529
+ structlogger.debug(
530
+ "model_api.storage.fetching_remote_model_size",
531
+ model_name=model_file_name,
532
+ )
533
+ return fetch_size_of_remote_model(
534
+ model_file_name,
535
+ config.SERVER_MODEL_REMOTE_STORAGE,
536
+ )
537
+ raise ModelNotFound("Model not found.")
538
+
539
+
540
+ def path_to_model(model_name: str) -> Optional[str]:
541
+ """Return the path to a local model."""
542
+ model_file_name = f"{model_name}.{MODEL_ARCHIVE_EXTENSION}"
543
+ model_path = subpath(models_base_path(), model_file_name)
544
+
545
+ if os.path.exists(model_path):
546
+ return model_path
547
+
548
+ if config.SERVER_MODEL_REMOTE_STORAGE:
549
+ structlogger.info(
550
+ "model_api.storage.fetching_remote_model",
551
+ model_name=model_file_name,
552
+ )
553
+ return fetch_remote_model_to_dir(
554
+ model_file_name,
555
+ models_base_path(),
556
+ config.SERVER_MODEL_REMOTE_STORAGE,
557
+ )
558
+
559
+ raise ModelNotFound("Model not found.")