rasa-pro 3.10.7.dev5__py3-none-any.whl → 3.10.8__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.
- README.md +37 -1
- rasa/api.py +2 -8
- rasa/cli/arguments/default_arguments.py +2 -23
- rasa/cli/arguments/run.py +0 -2
- rasa/cli/e2e_test.py +8 -10
- rasa/cli/inspect.py +2 -5
- rasa/cli/run.py +0 -7
- rasa/cli/studio/studio.py +21 -1
- rasa/cli/train.py +4 -9
- rasa/cli/utils.py +3 -3
- rasa/core/agent.py +2 -2
- rasa/core/brokers/kafka.py +1 -3
- rasa/core/brokers/pika.py +1 -3
- rasa/core/channels/socketio.py +1 -5
- rasa/core/channels/voice_aware/utils.py +5 -6
- rasa/core/nlg/contextual_response_rephraser.py +2 -11
- rasa/core/policies/enterprise_search_policy.py +2 -11
- rasa/core/policies/intentless_policy.py +2 -9
- rasa/core/run.py +1 -2
- rasa/core/secrets_manager/constants.py +0 -4
- rasa/core/secrets_manager/factory.py +0 -8
- rasa/core/secrets_manager/vault.py +1 -11
- rasa/core/utils.py +19 -30
- rasa/dialogue_understanding/coexistence/llm_based_router.py +2 -9
- rasa/dialogue_understanding/commands/__init__.py +2 -0
- rasa/dialogue_understanding/commands/restart_command.py +58 -0
- rasa/dialogue_understanding/commands/set_slot_command.py +5 -1
- rasa/dialogue_understanding/commands/utils.py +3 -1
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +2 -11
- rasa/dialogue_understanding/generator/llm_command_generator.py +1 -1
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +15 -15
- rasa/dialogue_understanding/patterns/restart.py +37 -0
- rasa/e2e_test/e2e_test_runner.py +1 -1
- rasa/engine/graph.py +1 -0
- rasa/engine/recipes/config_files/default_config.yml +3 -0
- rasa/engine/recipes/default_recipe.py +1 -0
- rasa/engine/recipes/graph_recipe.py +1 -0
- rasa/engine/storage/local_model_storage.py +1 -0
- rasa/engine/storage/storage.py +5 -1
- rasa/model_training.py +6 -11
- rasa/{core → nlu}/persistor.py +1 -1
- rasa/server.py +1 -1
- rasa/shared/constants.py +3 -2
- rasa/shared/core/domain.py +47 -101
- rasa/shared/core/flows/flows_list.py +6 -19
- rasa/shared/core/flows/validation.py +0 -25
- rasa/shared/core/flows/yaml_flows_io.py +24 -3
- rasa/shared/importers/importer.py +32 -32
- rasa/shared/importers/multi_project.py +11 -23
- rasa/shared/importers/rasa.py +2 -7
- rasa/shared/importers/remote_importer.py +2 -2
- rasa/shared/importers/utils.py +1 -3
- rasa/shared/nlu/training_data/training_data.py +19 -18
- rasa/shared/providers/_configs/azure_openai_client_config.py +5 -3
- rasa/shared/providers/llm/_base_litellm_client.py +26 -10
- rasa/shared/providers/llm/self_hosted_llm_client.py +15 -3
- rasa/shared/utils/common.py +22 -3
- rasa/shared/utils/llm.py +5 -29
- rasa/shared/utils/schemas/model_config.yml +10 -0
- rasa/studio/auth.py +4 -0
- rasa/tracing/instrumentation/attribute_extractors.py +1 -1
- rasa/validator.py +5 -2
- rasa/version.py +1 -1
- {rasa_pro-3.10.7.dev5.dist-info → rasa_pro-3.10.8.dist-info}/METADATA +43 -7
- {rasa_pro-3.10.7.dev5.dist-info → rasa_pro-3.10.8.dist-info}/RECORD +68 -74
- rasa/model_manager/__init__.py +0 -0
- rasa/model_manager/config.py +0 -12
- rasa/model_manager/model_api.py +0 -467
- rasa/model_manager/runner_service.py +0 -185
- rasa/model_manager/socket_bridge.py +0 -44
- rasa/model_manager/trainer_service.py +0 -240
- rasa/model_manager/utils.py +0 -27
- rasa/model_service.py +0 -66
- {rasa_pro-3.10.7.dev5.dist-info → rasa_pro-3.10.8.dist-info}/NOTICE +0 -0
- {rasa_pro-3.10.7.dev5.dist-info → rasa_pro-3.10.8.dist-info}/WHEEL +0 -0
- {rasa_pro-3.10.7.dev5.dist-info → rasa_pro-3.10.8.dist-info}/entry_points.txt +0 -0
rasa/model_manager/model_api.py
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
from typing import Any, Dict, Optional
|
|
4
|
-
import dotenv
|
|
5
|
-
from sanic import Blueprint, Sanic, response
|
|
6
|
-
from sanic.response import json
|
|
7
|
-
from sanic.exceptions import NotFound
|
|
8
|
-
from sanic.request import Request
|
|
9
|
-
import structlog
|
|
10
|
-
from socketio import AsyncServer
|
|
11
|
-
|
|
12
|
-
from rasa.model_manager.config import SERVER_BASE_URL
|
|
13
|
-
from rasa.model_manager.runner_service import (
|
|
14
|
-
BotSession,
|
|
15
|
-
run_bot,
|
|
16
|
-
terminate_bot,
|
|
17
|
-
update_bot_status,
|
|
18
|
-
)
|
|
19
|
-
from rasa.model_manager.socket_bridge import create_bridge_client
|
|
20
|
-
from rasa.model_manager.trainer_service import (
|
|
21
|
-
TrainingSession,
|
|
22
|
-
run_training,
|
|
23
|
-
terminate_training,
|
|
24
|
-
train_path,
|
|
25
|
-
update_training_status,
|
|
26
|
-
)
|
|
27
|
-
from rasa.model_manager.utils import (
|
|
28
|
-
logs_base_path,
|
|
29
|
-
logs_path,
|
|
30
|
-
models_base_path,
|
|
31
|
-
models_path,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
dotenv.load_dotenv()
|
|
35
|
-
|
|
36
|
-
structlogger = structlog.get_logger()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# A simple in-memory store for training sessions and running bots
|
|
40
|
-
trainings: Dict[str, TrainingSession] = {}
|
|
41
|
-
running_bots: Dict[str, BotSession] = {}
|
|
42
|
-
|
|
43
|
-
# A simple in-memory store for active chat connections to studio frontend
|
|
44
|
-
socket_proxy_clients = {}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def prepare_working_directories() -> None:
|
|
48
|
-
"""Make sure all required directories exist."""
|
|
49
|
-
os.makedirs(logs_base_path(), exist_ok=True)
|
|
50
|
-
os.makedirs(models_base_path(), exist_ok=True)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def cleanup_training_processes() -> None:
|
|
54
|
-
"""Terminate all running training processes."""
|
|
55
|
-
structlogger.debug("model_trainer.cleanup_processes.started")
|
|
56
|
-
running = list(trainings.values())
|
|
57
|
-
for training in running:
|
|
58
|
-
terminate_training(training)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def cleanup_bot_processes() -> None:
|
|
62
|
-
"""Terminate all running bot processes."""
|
|
63
|
-
structlogger.debug("model_runner.cleanup_processes.started")
|
|
64
|
-
running = list(running_bots.values())
|
|
65
|
-
for bot in running:
|
|
66
|
-
terminate_bot(bot)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def update_status_of_all_trainings() -> None:
|
|
70
|
-
"""Update the status of all training processes."""
|
|
71
|
-
running = list(trainings.values())
|
|
72
|
-
for training in running:
|
|
73
|
-
update_training_status(training)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
async def update_status_of_all_bots() -> None:
|
|
77
|
-
"""Update the status of all bot processes."""
|
|
78
|
-
running = list(running_bots.values())
|
|
79
|
-
for bot in running:
|
|
80
|
-
await update_bot_status(bot)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def base_server_url(request: Request) -> str:
|
|
84
|
-
"""Return the base URL of the server."""
|
|
85
|
-
if SERVER_BASE_URL:
|
|
86
|
-
return SERVER_BASE_URL
|
|
87
|
-
else:
|
|
88
|
-
return f"{request.scheme}://{request.host}"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def get_log_url(request: Request, action_id: str) -> response.HTTPResponse:
|
|
92
|
-
"""Return a URL for downloading the log file for training / deployment."""
|
|
93
|
-
if not os.path.exists(logs_path(action_id)):
|
|
94
|
-
return json({"message": "Log not found"}, status=404)
|
|
95
|
-
|
|
96
|
-
return json(
|
|
97
|
-
{
|
|
98
|
-
"url": f"{base_server_url(request)}/logs/{action_id}.txt",
|
|
99
|
-
"expires_in_seconds": 60 * 60 * 24,
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def get_training_model_url(request: Request, training_id: str) -> response.HTTPResponse:
|
|
105
|
-
"""Return a URL for downloading the model file for a training session."""
|
|
106
|
-
if not os.path.exists(f"{train_path(training_id)}/models"):
|
|
107
|
-
return json({"message": "Model not found"}, status=404)
|
|
108
|
-
|
|
109
|
-
# pick the first model in the directory, link it to models/
|
|
110
|
-
# and provide the download link
|
|
111
|
-
models = os.listdir(f"{train_path(training_id)}/models")
|
|
112
|
-
if not models:
|
|
113
|
-
return json({"message": "Model not found"}, status=404)
|
|
114
|
-
|
|
115
|
-
# there should really be only one model
|
|
116
|
-
model = models[0]
|
|
117
|
-
|
|
118
|
-
if not os.path.exists(models_path(model)):
|
|
119
|
-
os.symlink(f"{train_path(training_id)}/models/{model}", models_path(model))
|
|
120
|
-
|
|
121
|
-
return json(
|
|
122
|
-
{
|
|
123
|
-
"url": f"{base_server_url(request)}/models/{model}",
|
|
124
|
-
"expires_in_seconds": 60 * 60 * 24,
|
|
125
|
-
}
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
async def continuously_update_process_status() -> None:
|
|
130
|
-
"""Regularly Update the status of all training and bot processes."""
|
|
131
|
-
structlogger.debug("model_api.update_process_status.started")
|
|
132
|
-
|
|
133
|
-
while True:
|
|
134
|
-
try:
|
|
135
|
-
update_status_of_all_trainings()
|
|
136
|
-
await update_status_of_all_bots()
|
|
137
|
-
await asyncio.sleep(1)
|
|
138
|
-
except Exception as e:
|
|
139
|
-
structlogger.error("model_api.update_process_status.error", error=str(e))
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def internal_blueprint() -> Blueprint:
|
|
143
|
-
"""Create a blueprint for the model manager API."""
|
|
144
|
-
bp = Blueprint("model_api_internal")
|
|
145
|
-
|
|
146
|
-
@bp.before_server_stop
|
|
147
|
-
async def cleanup_processes(app: Sanic, loop: asyncio.AbstractEventLoop) -> None:
|
|
148
|
-
"""Terminate all running processes before the server stops."""
|
|
149
|
-
structlogger.debug("model_api.cleanup_processes.started")
|
|
150
|
-
cleanup_training_processes()
|
|
151
|
-
cleanup_bot_processes()
|
|
152
|
-
|
|
153
|
-
@bp.get("/")
|
|
154
|
-
async def health(request: Request) -> response.HTTPResponse:
|
|
155
|
-
return json(
|
|
156
|
-
{
|
|
157
|
-
"status": "ok",
|
|
158
|
-
"bots": [
|
|
159
|
-
{
|
|
160
|
-
"deployment_id": bot.deployment_id,
|
|
161
|
-
"status": bot.status,
|
|
162
|
-
"internal_url": bot.internal_url,
|
|
163
|
-
"url": bot.url,
|
|
164
|
-
}
|
|
165
|
-
for bot in running_bots.values()
|
|
166
|
-
],
|
|
167
|
-
"trainings": [
|
|
168
|
-
{
|
|
169
|
-
"training_id": training.training_id,
|
|
170
|
-
"assistant_id": training.assistant_id,
|
|
171
|
-
"client_id": training.client_id,
|
|
172
|
-
"progress": training.progress,
|
|
173
|
-
"status": training.status,
|
|
174
|
-
}
|
|
175
|
-
for training in trainings.values()
|
|
176
|
-
],
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
@bp.get("/training")
|
|
181
|
-
async def get_training_list(request: Request) -> response.HTTPResponse:
|
|
182
|
-
"""Return a list of all training sessions for an assistant."""
|
|
183
|
-
assistant_id = request.args.get("assistant_id")
|
|
184
|
-
sessions = [
|
|
185
|
-
{
|
|
186
|
-
"training_id": session.training_id,
|
|
187
|
-
"assistant_id": session.assistant_id,
|
|
188
|
-
"client_id": session.client_id,
|
|
189
|
-
"training_runtime": "self",
|
|
190
|
-
"status": session.status,
|
|
191
|
-
"bot_config": None,
|
|
192
|
-
"logs": None,
|
|
193
|
-
"metadata": None,
|
|
194
|
-
"model": None,
|
|
195
|
-
"runtime_metadata": None,
|
|
196
|
-
}
|
|
197
|
-
for session in trainings.values()
|
|
198
|
-
if session.assistant_id == assistant_id
|
|
199
|
-
]
|
|
200
|
-
return json({"training_sessions": sessions, "total_number": len(sessions)})
|
|
201
|
-
|
|
202
|
-
@bp.post("/training")
|
|
203
|
-
async def start_training(request: Request) -> response.HTTPResponse:
|
|
204
|
-
"""Start a new training session."""
|
|
205
|
-
data = request.json
|
|
206
|
-
training_id: Optional[str] = data.get("id")
|
|
207
|
-
assistant_id: Optional[str] = data.get("assistant_id")
|
|
208
|
-
client_id: Optional[str] = data.get("client_id")
|
|
209
|
-
|
|
210
|
-
if training_id in trainings:
|
|
211
|
-
# fail, because there apparently is already a training with this id
|
|
212
|
-
return json({"message": "Training with this id already exists"}, status=409)
|
|
213
|
-
|
|
214
|
-
if not assistant_id:
|
|
215
|
-
return json({"message": "Assistant id is required"}, status=400)
|
|
216
|
-
|
|
217
|
-
if not training_id:
|
|
218
|
-
return json({"message": "Training id is required"}, status=400)
|
|
219
|
-
|
|
220
|
-
try:
|
|
221
|
-
training_session = run_training(
|
|
222
|
-
training_id=training_id,
|
|
223
|
-
assistant_id=assistant_id,
|
|
224
|
-
client_id=client_id,
|
|
225
|
-
data=data,
|
|
226
|
-
)
|
|
227
|
-
trainings[training_id] = training_session
|
|
228
|
-
return json({"training_id": training_id})
|
|
229
|
-
except Exception as e:
|
|
230
|
-
return json({"message": str(e)}, status=500)
|
|
231
|
-
|
|
232
|
-
@bp.get("/training/<training_id>")
|
|
233
|
-
async def get_training(request: Request, training_id: str) -> response.HTTPResponse:
|
|
234
|
-
"""Return the status of a training session."""
|
|
235
|
-
if training := trainings.get(training_id):
|
|
236
|
-
return json(
|
|
237
|
-
{
|
|
238
|
-
"training_id": training_id,
|
|
239
|
-
"assistant_id": training.assistant_id,
|
|
240
|
-
"client_id": training.client_id,
|
|
241
|
-
"progress": training.progress,
|
|
242
|
-
"status": training.status,
|
|
243
|
-
}
|
|
244
|
-
)
|
|
245
|
-
else:
|
|
246
|
-
return json({"message": "Training not found"}, status=404)
|
|
247
|
-
|
|
248
|
-
@bp.delete("/training/<training_id>")
|
|
249
|
-
async def stop_training(
|
|
250
|
-
request: Request, training_id: str
|
|
251
|
-
) -> response.HTTPResponse:
|
|
252
|
-
# this is a no-op if the training is already done
|
|
253
|
-
if not (training := trainings.get(training_id)):
|
|
254
|
-
return json({"message": "Training session not found"}, status=404)
|
|
255
|
-
|
|
256
|
-
terminate_training(training)
|
|
257
|
-
return json({"training_id": training_id})
|
|
258
|
-
|
|
259
|
-
@bp.get("/training/<training_id>/download_url")
|
|
260
|
-
async def get_training_download_url(
|
|
261
|
-
request: Request, training_id: str
|
|
262
|
-
) -> response.HTTPResponse:
|
|
263
|
-
# Provide a URL for downloading the training log
|
|
264
|
-
# check object key that is passed in as a query parameter
|
|
265
|
-
key = request.args.get("object_key")
|
|
266
|
-
if "model.tar.gz" in key:
|
|
267
|
-
return get_training_model_url(request, training_id)
|
|
268
|
-
return get_log_url(request, training_id)
|
|
269
|
-
|
|
270
|
-
@bp.post("/bot")
|
|
271
|
-
async def start_bot(request: Request) -> response.HTTPResponse:
|
|
272
|
-
data = request.json
|
|
273
|
-
deployment_id: Optional[str] = data.get("deployment_id")
|
|
274
|
-
assumed_model_path: Optional[str] = data.get("model_path")
|
|
275
|
-
|
|
276
|
-
if deployment_id in running_bots:
|
|
277
|
-
# fail, because there apparently is already a bot running with this id
|
|
278
|
-
return json(
|
|
279
|
-
{"message": "Bot with this deployment id already exists"}, status=409
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
if not deployment_id:
|
|
283
|
-
return json({"message": "Deployment id is required"}, status=400)
|
|
284
|
-
|
|
285
|
-
if not assumed_model_path:
|
|
286
|
-
return json({"message": "Model path is required"}, status=400)
|
|
287
|
-
|
|
288
|
-
training_id = assumed_model_path.split("/")[-3]
|
|
289
|
-
training_base_path = train_path(training_id)
|
|
290
|
-
if not os.path.exists(f"{training_base_path}/models"):
|
|
291
|
-
return json(
|
|
292
|
-
{"message": "Model not found, for the given training id"},
|
|
293
|
-
status=404,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
base_url_path = base_server_url(request)
|
|
297
|
-
try:
|
|
298
|
-
bot_session = run_bot(deployment_id, training_base_path, base_url_path)
|
|
299
|
-
running_bots[deployment_id] = bot_session
|
|
300
|
-
return json(
|
|
301
|
-
{
|
|
302
|
-
"deployment_id": deployment_id,
|
|
303
|
-
"status": bot_session.status,
|
|
304
|
-
"url": bot_session.url,
|
|
305
|
-
}
|
|
306
|
-
)
|
|
307
|
-
except Exception as e:
|
|
308
|
-
return json({"message": str(e)}, status=500)
|
|
309
|
-
|
|
310
|
-
@bp.delete("/bot/<deployment_id>")
|
|
311
|
-
async def stop_bot(request: Request, deployment_id: str) -> response.HTTPResponse:
|
|
312
|
-
bot = running_bots.get(deployment_id)
|
|
313
|
-
if bot is None:
|
|
314
|
-
return json({"message": "Bot not found"}, status=404)
|
|
315
|
-
|
|
316
|
-
terminate_bot(bot)
|
|
317
|
-
|
|
318
|
-
return json(
|
|
319
|
-
{"deployment_id": deployment_id, "status": bot.status, "url": bot.url}
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
@bp.get("/bot/<deployment_id>")
|
|
323
|
-
async def get_bot(request: Request, deployment_id: str) -> response.HTTPResponse:
|
|
324
|
-
bot = running_bots.get(deployment_id)
|
|
325
|
-
if bot is None:
|
|
326
|
-
return json({"message": "Bot not found"}, status=404)
|
|
327
|
-
|
|
328
|
-
return json(
|
|
329
|
-
{
|
|
330
|
-
"deployment_id": deployment_id,
|
|
331
|
-
"status": bot.status,
|
|
332
|
-
"url": bot.url,
|
|
333
|
-
}
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
@bp.get("/bot/<deployment_id>/download_url")
|
|
337
|
-
async def get_bot_download_url(
|
|
338
|
-
request: Request, deployment_id: str
|
|
339
|
-
) -> response.HTTPResponse:
|
|
340
|
-
return get_log_url(request, deployment_id)
|
|
341
|
-
|
|
342
|
-
@bp.get("/bot/<deployment_id>/logs")
|
|
343
|
-
async def get_bot_logs(
|
|
344
|
-
request: Request, deployment_id: str
|
|
345
|
-
) -> response.HTTPResponse:
|
|
346
|
-
return get_log_url(request, deployment_id)
|
|
347
|
-
|
|
348
|
-
@bp.get("/bot")
|
|
349
|
-
async def list_bots(request: Request) -> response.HTTPResponse:
|
|
350
|
-
bots = [
|
|
351
|
-
{
|
|
352
|
-
"deployment_id": bot.deployment_id,
|
|
353
|
-
"status": bot.status,
|
|
354
|
-
"url": bot.url,
|
|
355
|
-
}
|
|
356
|
-
for bot in running_bots.values()
|
|
357
|
-
]
|
|
358
|
-
return json({"deployment_sessions": bots, "total_number": len(bots)})
|
|
359
|
-
|
|
360
|
-
return bp
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def external_blueprint() -> Blueprint:
|
|
364
|
-
"""Create a blueprint for the model manager API."""
|
|
365
|
-
from rasa.core.channels.socketio import SocketBlueprint
|
|
366
|
-
|
|
367
|
-
sio = AsyncServer(async_mode="sanic", cors_allowed_origins=[])
|
|
368
|
-
bp = SocketBlueprint(sio, "", "model_api_external")
|
|
369
|
-
|
|
370
|
-
@bp.get("/health")
|
|
371
|
-
async def health(request: Request) -> response.HTTPResponse:
|
|
372
|
-
return json(
|
|
373
|
-
{
|
|
374
|
-
"status": "ok",
|
|
375
|
-
"bots": [
|
|
376
|
-
{
|
|
377
|
-
"deployment_id": bot.deployment_id,
|
|
378
|
-
"status": bot.status,
|
|
379
|
-
"internal_url": bot.internal_url,
|
|
380
|
-
"url": bot.url,
|
|
381
|
-
}
|
|
382
|
-
for bot in running_bots.values()
|
|
383
|
-
],
|
|
384
|
-
"trainings": [
|
|
385
|
-
{
|
|
386
|
-
"training_id": training.training_id,
|
|
387
|
-
"assistant_id": training.assistant_id,
|
|
388
|
-
"client_id": training.client_id,
|
|
389
|
-
"progress": training.progress,
|
|
390
|
-
"status": training.status,
|
|
391
|
-
}
|
|
392
|
-
for training in trainings.values()
|
|
393
|
-
],
|
|
394
|
-
}
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
@bp.route("/logs/<path:path>")
|
|
398
|
-
async def get_training_logs(request: Request, path: str) -> response.HTTPResponse:
|
|
399
|
-
try:
|
|
400
|
-
headers = {"Content-Disposition": 'attachment; filename="log.txt"'}
|
|
401
|
-
return await response.file(
|
|
402
|
-
os.path.join(logs_base_path(), path), headers=headers
|
|
403
|
-
)
|
|
404
|
-
except NotFound:
|
|
405
|
-
return json({"message": "Log not found"}, status=404)
|
|
406
|
-
|
|
407
|
-
@bp.route("/models/<path:path>")
|
|
408
|
-
async def send_model(request: Request, path: str) -> response.HTTPResponse:
|
|
409
|
-
try:
|
|
410
|
-
return await response.file(models_path(path))
|
|
411
|
-
except NotFound:
|
|
412
|
-
return json({"message": "Model not found"}, status=404)
|
|
413
|
-
|
|
414
|
-
@sio.on("connect")
|
|
415
|
-
async def socketio_websocket_traffic(
|
|
416
|
-
sid: str, environ: Dict, auth: Optional[Dict]
|
|
417
|
-
) -> bool:
|
|
418
|
-
"""Bridge websockets between user chat socket and bot server."""
|
|
419
|
-
structlogger.debug("model_runner.user_connected", sid=sid)
|
|
420
|
-
deployment_id = auth.get("deployment_id") if auth else None
|
|
421
|
-
|
|
422
|
-
if deployment_id is None:
|
|
423
|
-
structlogger.error("model_runner.bot_no_deployment_id", sid=sid)
|
|
424
|
-
return False
|
|
425
|
-
|
|
426
|
-
bot = running_bots.get(deployment_id)
|
|
427
|
-
if bot is None:
|
|
428
|
-
structlogger.error(
|
|
429
|
-
"model_runner.bot_not_found", deployment_id=deployment_id
|
|
430
|
-
)
|
|
431
|
-
return False
|
|
432
|
-
|
|
433
|
-
client = await create_bridge_client(sio, bot.internal_url, sid, deployment_id)
|
|
434
|
-
|
|
435
|
-
if client.sid is not None:
|
|
436
|
-
structlogger.debug(
|
|
437
|
-
"model_runner.bot_connection_established", deployment_id=deployment_id
|
|
438
|
-
)
|
|
439
|
-
socket_proxy_clients[sid] = client
|
|
440
|
-
return True
|
|
441
|
-
else:
|
|
442
|
-
structlogger.error(
|
|
443
|
-
"model_runner.bot_connection_failed", deployment_id=deployment_id
|
|
444
|
-
)
|
|
445
|
-
return False
|
|
446
|
-
|
|
447
|
-
@sio.on("disconnect")
|
|
448
|
-
async def disconnect(sid: str) -> None:
|
|
449
|
-
structlogger.debug("model_runner.bot_disconnect", sid=sid)
|
|
450
|
-
if sid in socket_proxy_clients:
|
|
451
|
-
await socket_proxy_clients[sid].disconnect()
|
|
452
|
-
del socket_proxy_clients[sid]
|
|
453
|
-
|
|
454
|
-
@sio.on("*")
|
|
455
|
-
async def handle_message(event: str, sid: str, data: Dict[str, Any]) -> None:
|
|
456
|
-
# bridge both, incoming messages to the bot_url but also
|
|
457
|
-
# send the response back to the client. both need to happen
|
|
458
|
-
# in parallel in an async way
|
|
459
|
-
|
|
460
|
-
client = socket_proxy_clients.get(sid)
|
|
461
|
-
if client is None:
|
|
462
|
-
structlogger.error("model_runner.bot_not_connected", sid=sid)
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
await client.emit(event, data)
|
|
466
|
-
|
|
467
|
-
return bp
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
import aiohttp
|
|
4
|
-
import structlog
|
|
5
|
-
import subprocess
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
|
|
8
|
-
from rasa.model_manager.config import RASA_PYTHON_PATH, SERVER_BASE_WORKING_DIRECTORY
|
|
9
|
-
from rasa.model_manager.utils import logs_path
|
|
10
|
-
|
|
11
|
-
structlogger = structlog.get_logger()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class BotSession:
|
|
16
|
-
"""Store information about a running bot."""
|
|
17
|
-
|
|
18
|
-
deployment_id: str
|
|
19
|
-
status: str
|
|
20
|
-
process: subprocess.Popen
|
|
21
|
-
url: str
|
|
22
|
-
internal_url: str
|
|
23
|
-
port: int
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def bot_path(deployment_id: str) -> str:
|
|
27
|
-
"""Return the path to the bot directory for a given deployment id."""
|
|
28
|
-
return os.path.abspath(f"{SERVER_BASE_WORKING_DIRECTORY}/bots/{deployment_id}")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
async def is_bot_startup_finished(bot: BotSession) -> bool:
|
|
32
|
-
"""Send a request to the bot to see if the bot is up and running."""
|
|
33
|
-
health_timeout = aiohttp.ClientTimeout(total=5, sock_connect=2, sock_read=3)
|
|
34
|
-
try:
|
|
35
|
-
async with aiohttp.ClientSession(timeout=health_timeout) as session:
|
|
36
|
-
async with session.get(f"{bot.internal_url}/status") as resp:
|
|
37
|
-
return resp.status == 200
|
|
38
|
-
except aiohttp.client_exceptions.ClientConnectorError:
|
|
39
|
-
structlogger.debug(
|
|
40
|
-
"model_runner.bot.not_running_yet", deployment_id=bot.deployment_id
|
|
41
|
-
)
|
|
42
|
-
return False
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def update_bot_to_stopped(bot: BotSession) -> None:
|
|
46
|
-
"""Set a bots state to stopped."""
|
|
47
|
-
structlogger.info(
|
|
48
|
-
"model_runner.bot_stopped",
|
|
49
|
-
deployment_id=bot.deployment_id,
|
|
50
|
-
status=bot.process.returncode,
|
|
51
|
-
)
|
|
52
|
-
bot.status = "stopped"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def update_bot_to_running(bot: BotSession) -> None:
|
|
56
|
-
"""Set a bots state to running."""
|
|
57
|
-
structlogger.info(
|
|
58
|
-
"model_runner.bot_running",
|
|
59
|
-
deployment_id=bot.deployment_id,
|
|
60
|
-
)
|
|
61
|
-
bot.status = "running"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def get_open_port() -> int:
|
|
65
|
-
"""Get an open port on the system that is not in use yet."""
|
|
66
|
-
# from https://stackoverflow.com/questions/2838244/get-open-tcp-port-in-python/2838309#2838309
|
|
67
|
-
import socket
|
|
68
|
-
|
|
69
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
70
|
-
s.bind(("", 0))
|
|
71
|
-
s.listen(1)
|
|
72
|
-
port = s.getsockname()[1]
|
|
73
|
-
s.close()
|
|
74
|
-
return port
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def prepare_bot_directory(bot_base_path: str, training_base_path: str) -> None:
|
|
78
|
-
"""Prepare the bot directory for a new bot session."""
|
|
79
|
-
if not os.path.exists(bot_base_path):
|
|
80
|
-
os.makedirs(bot_base_path, exist_ok=True)
|
|
81
|
-
else:
|
|
82
|
-
shutil.rmtree(bot_base_path, ignore_errors=True)
|
|
83
|
-
|
|
84
|
-
shutil.copytree(f"{training_base_path}/models", f"{bot_base_path}/models")
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
shutil.copy(
|
|
88
|
-
f"{training_base_path}/endpoints.yml", f"{bot_base_path}/endpoints.yml"
|
|
89
|
-
)
|
|
90
|
-
except FileNotFoundError:
|
|
91
|
-
structlogger.warning("model_runner.bot.prepare.no_endpoints")
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
shutil.copy(
|
|
95
|
-
f"{training_base_path}/credentials.yml", f"{bot_base_path}/credentials.yml"
|
|
96
|
-
)
|
|
97
|
-
except FileNotFoundError:
|
|
98
|
-
structlogger.warning("model_runner.bot.prepare.no_credentials")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def start_bot_process(
|
|
102
|
-
deployment_id: str, bot_base_path: str, base_url_path: str
|
|
103
|
-
) -> BotSession:
|
|
104
|
-
port = get_open_port()
|
|
105
|
-
log_path = logs_path(deployment_id)
|
|
106
|
-
|
|
107
|
-
process = subprocess.Popen(
|
|
108
|
-
[
|
|
109
|
-
RASA_PYTHON_PATH,
|
|
110
|
-
"-m",
|
|
111
|
-
"rasa.__main__",
|
|
112
|
-
"run",
|
|
113
|
-
"--endpoints",
|
|
114
|
-
f"{bot_base_path}/endpoints.yml",
|
|
115
|
-
"--credentials",
|
|
116
|
-
f"{bot_base_path}/credentials.yml",
|
|
117
|
-
"--enable-api",
|
|
118
|
-
"--debug",
|
|
119
|
-
f"--port={port}",
|
|
120
|
-
"--cors",
|
|
121
|
-
"*",
|
|
122
|
-
# absolute path to models as positional arg
|
|
123
|
-
f"{bot_base_path}/models",
|
|
124
|
-
],
|
|
125
|
-
cwd=bot_base_path,
|
|
126
|
-
stdout=open(log_path, "w"),
|
|
127
|
-
stderr=subprocess.STDOUT,
|
|
128
|
-
env=os.environ.copy(),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
internal_bot_url = f"http://localhost:{port}"
|
|
132
|
-
|
|
133
|
-
structlogger.info(
|
|
134
|
-
"model_runner.bot.starting",
|
|
135
|
-
deployment_id=deployment_id,
|
|
136
|
-
log=log_path,
|
|
137
|
-
url=internal_bot_url,
|
|
138
|
-
port=port,
|
|
139
|
-
pid=process.pid,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return BotSession(
|
|
143
|
-
deployment_id=deployment_id,
|
|
144
|
-
status="queued",
|
|
145
|
-
process=process,
|
|
146
|
-
url=f"{base_url_path}?deployment_id={deployment_id}",
|
|
147
|
-
internal_url=internal_bot_url,
|
|
148
|
-
port=port,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def run_bot(
|
|
153
|
-
deployment_id: str, training_base_path: str, base_url_path: str
|
|
154
|
-
) -> BotSession:
|
|
155
|
-
"""Deploy a bot based on a given training id."""
|
|
156
|
-
bot_base_path = bot_path(deployment_id)
|
|
157
|
-
prepare_bot_directory(bot_base_path, training_base_path)
|
|
158
|
-
|
|
159
|
-
return start_bot_process(deployment_id, bot_base_path, base_url_path)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
async def update_bot_status(bot: BotSession) -> None:
|
|
163
|
-
"""Update the status of a bot based on the process return code."""
|
|
164
|
-
if bot.status == "running" and bot.process.poll() is not None:
|
|
165
|
-
update_bot_to_stopped(bot)
|
|
166
|
-
if bot.status == "queued" and await is_bot_startup_finished(bot):
|
|
167
|
-
update_bot_to_running(bot)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def terminate_bot(bot: BotSession) -> None:
|
|
171
|
-
"""Terminate the bot process."""
|
|
172
|
-
if bot.status in {"running", "queued"}:
|
|
173
|
-
try:
|
|
174
|
-
bot.process.terminate()
|
|
175
|
-
structlogger.info(
|
|
176
|
-
"model_runner.stopping_bot",
|
|
177
|
-
deployment_id=bot.deployment_id,
|
|
178
|
-
status=bot.process.returncode,
|
|
179
|
-
)
|
|
180
|
-
bot.status = "stopped"
|
|
181
|
-
except ProcessLookupError:
|
|
182
|
-
structlogger.debug(
|
|
183
|
-
"model_runner.stop_bot.process_not_found",
|
|
184
|
-
deployment_id=bot.deployment_id,
|
|
185
|
-
)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from typing import Any, Dict
|
|
2
|
-
from socketio import AsyncServer
|
|
3
|
-
import structlog
|
|
4
|
-
from socketio.asyncio_client import AsyncClient
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
structlogger = structlog.get_logger()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
async def create_bridge_client(
|
|
11
|
-
sio: AsyncServer, url: str, sid: str, deployment_id: str
|
|
12
|
-
) -> AsyncClient:
|
|
13
|
-
"""Create a new socket bridge client."""
|
|
14
|
-
client = AsyncClient()
|
|
15
|
-
|
|
16
|
-
await client.connect(url)
|
|
17
|
-
|
|
18
|
-
@client.event # type: ignore[misc]
|
|
19
|
-
async def session_confirm(data: Dict[str, Any]) -> None:
|
|
20
|
-
structlogger.debug(
|
|
21
|
-
"model_runner.bot_session_confirmed", deployment_id=deployment_id
|
|
22
|
-
)
|
|
23
|
-
await sio.emit("session_confirm", room=sid)
|
|
24
|
-
|
|
25
|
-
@client.event # type: ignore[misc]
|
|
26
|
-
async def bot_message(data: Dict[str, Any]) -> None:
|
|
27
|
-
structlogger.debug("model_runner.bot_message", deployment_id=deployment_id)
|
|
28
|
-
await sio.emit("bot_message", data, room=sid)
|
|
29
|
-
|
|
30
|
-
@client.event # type: ignore[misc]
|
|
31
|
-
async def disconnect() -> None:
|
|
32
|
-
structlogger.debug(
|
|
33
|
-
"model_runner.bot_connection_closed", deployment_id=deployment_id
|
|
34
|
-
)
|
|
35
|
-
await sio.emit("disconnect", room=sid)
|
|
36
|
-
|
|
37
|
-
@client.event # type: ignore[misc]
|
|
38
|
-
async def connect_error() -> None:
|
|
39
|
-
structlogger.error(
|
|
40
|
-
"model_runner.bot_connection_error", deployment_id=deployment_id
|
|
41
|
-
)
|
|
42
|
-
await sio.emit("disconnect", room=sid)
|
|
43
|
-
|
|
44
|
-
return client
|