rasa-pro 3.11.0a3__py3-none-any.whl → 3.11.0a4.dev2__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 (51) hide show
  1. README.md +17 -396
  2. rasa/api.py +4 -0
  3. rasa/cli/arguments/train.py +14 -0
  4. rasa/cli/inspect.py +1 -1
  5. rasa/cli/interactive.py +1 -0
  6. rasa/cli/project_templates/calm/endpoints.yml +7 -2
  7. rasa/cli/project_templates/tutorial/endpoints.yml +7 -2
  8. rasa/cli/train.py +3 -0
  9. rasa/constants.py +2 -0
  10. rasa/core/actions/action.py +75 -33
  11. rasa/core/actions/action_repeat_bot_messages.py +72 -0
  12. rasa/core/actions/e2e_stub_custom_action_executor.py +5 -1
  13. rasa/core/actions/http_custom_action_executor.py +4 -0
  14. rasa/core/channels/socketio.py +5 -1
  15. rasa/core/channels/voice_ready/utils.py +6 -5
  16. rasa/core/channels/voice_stream/browser_audio.py +1 -1
  17. rasa/core/channels/voice_stream/twilio_media_streams.py +1 -1
  18. rasa/core/nlg/contextual_response_rephraser.py +19 -2
  19. rasa/core/persistor.py +87 -21
  20. rasa/core/utils.py +53 -22
  21. rasa/dialogue_understanding/commands/__init__.py +4 -0
  22. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +60 -0
  23. rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +3 -0
  24. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +19 -0
  25. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +5 -0
  26. rasa/dialogue_understanding/patterns/repeat.py +37 -0
  27. rasa/e2e_test/utils/io.py +2 -0
  28. rasa/model_manager/__init__.py +0 -0
  29. rasa/model_manager/config.py +18 -0
  30. rasa/model_manager/model_api.py +446 -0
  31. rasa/model_manager/runner_service.py +279 -0
  32. rasa/model_manager/socket_bridge.py +143 -0
  33. rasa/model_manager/studio_jwt_auth.py +86 -0
  34. rasa/model_manager/trainer_service.py +332 -0
  35. rasa/model_manager/utils.py +66 -0
  36. rasa/model_service.py +109 -0
  37. rasa/model_training.py +25 -7
  38. rasa/shared/constants.py +6 -0
  39. rasa/shared/core/constants.py +2 -0
  40. rasa/shared/providers/llm/self_hosted_llm_client.py +15 -3
  41. rasa/shared/utils/yaml.py +10 -1
  42. rasa/utils/endpoints.py +27 -1
  43. rasa/version.py +1 -1
  44. rasa_pro-3.11.0a4.dev2.dist-info/METADATA +197 -0
  45. {rasa_pro-3.11.0a3.dist-info → rasa_pro-3.11.0a4.dev2.dist-info}/RECORD +48 -38
  46. rasa/keys +0 -1
  47. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +0 -407
  48. rasa_pro-3.11.0a3.dist-info/METADATA +0 -576
  49. {rasa_pro-3.11.0a3.dist-info → rasa_pro-3.11.0a4.dev2.dist-info}/NOTICE +0 -0
  50. {rasa_pro-3.11.0a3.dist-info → rasa_pro-3.11.0a4.dev2.dist-info}/WHEEL +0 -0
  51. {rasa_pro-3.11.0a3.dist-info → rasa_pro-3.11.0a4.dev2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,446 @@
1
+ import asyncio
2
+ import os
3
+ from http import HTTPStatus
4
+ from typing import Any, Dict, Optional
5
+ import dotenv
6
+ from sanic import Blueprint, Sanic, response
7
+ from sanic.response import json
8
+ from sanic.exceptions import NotFound
9
+ from sanic.request import Request
10
+ import structlog
11
+ from socketio import AsyncServer
12
+
13
+ from rasa.exceptions import ModelNotFound
14
+ from rasa.model_manager import config
15
+ from rasa.model_manager.config import SERVER_BASE_URL
16
+ from rasa.constants import MODEL_ARCHIVE_EXTENSION
17
+ from rasa.model_manager.runner_service import (
18
+ BotSession,
19
+ fetch_remote_model_to_dir,
20
+ run_bot,
21
+ terminate_bot,
22
+ update_bot_status,
23
+ )
24
+ from rasa.model_manager.socket_bridge import create_bridge_server
25
+ from rasa.model_manager.trainer_service import (
26
+ TrainingSession,
27
+ run_training,
28
+ terminate_training,
29
+ update_training_status,
30
+ )
31
+ from rasa.model_manager.utils import (
32
+ get_logs_content,
33
+ logs_base_path,
34
+ models_base_path,
35
+ subpath,
36
+ )
37
+
38
+ dotenv.load_dotenv()
39
+
40
+ structlogger = structlog.get_logger()
41
+
42
+
43
+ # A simple in-memory store for training sessions and running bots
44
+ trainings: Dict[str, TrainingSession] = {}
45
+ running_bots: Dict[str, BotSession] = {}
46
+
47
+
48
+ def prepare_working_directories() -> None:
49
+ """Make sure all required directories exist."""
50
+ os.makedirs(logs_base_path(), exist_ok=True)
51
+ os.makedirs(models_base_path(), exist_ok=True)
52
+
53
+
54
+ def cleanup_training_processes() -> None:
55
+ """Terminate all running training processes."""
56
+ structlogger.debug("model_trainer.cleanup_processes.started")
57
+ running = list(trainings.values())
58
+ for training in running:
59
+ terminate_training(training)
60
+
61
+
62
+ def cleanup_bot_processes() -> None:
63
+ """Terminate all running bot processes."""
64
+ structlogger.debug("model_runner.cleanup_processes.started")
65
+ running = list(running_bots.values())
66
+ for bot in running:
67
+ terminate_bot(bot)
68
+
69
+
70
+ def update_status_of_all_trainings() -> None:
71
+ """Update the status of all training processes."""
72
+ running = list(trainings.values())
73
+ for training in running:
74
+ update_training_status(training)
75
+
76
+
77
+ async def update_status_of_all_bots() -> None:
78
+ """Update the status of all bot processes."""
79
+ # we need to get the values first, because (since we are async and waiting
80
+ # within the loop) some other job on the asyncio loop could change the dict
81
+ # (adding or removing). python doesn't like if you change the size of a dict
82
+ # while iterating over it and will raise a RuntimeError. so we get the values
83
+ # first and iterate over them to avoid that.
84
+ running = list(running_bots.values())
85
+ for bot in running:
86
+ await update_bot_status(bot)
87
+
88
+
89
+ def base_server_url(request: Request) -> str:
90
+ """Return the base URL of the server."""
91
+ if SERVER_BASE_URL:
92
+ return SERVER_BASE_URL.rstrip("/")
93
+ else:
94
+ return f"{request.scheme}://{request.host}"
95
+
96
+
97
+ async def continuously_update_process_status() -> None:
98
+ """Regularly Update the status of all training and bot processes."""
99
+ structlogger.debug("model_api.update_process_status.started")
100
+
101
+ while True:
102
+ try:
103
+ update_status_of_all_trainings()
104
+ await update_status_of_all_bots()
105
+ except asyncio.exceptions.CancelledError:
106
+ structlogger.debug("model_api.update_process_status.cancelled")
107
+ break
108
+ except Exception as e:
109
+ structlogger.error("model_api.update_process_status.error", error=str(e))
110
+ finally:
111
+ await asyncio.sleep(1)
112
+
113
+
114
+ def internal_blueprint() -> Blueprint:
115
+ """Create a blueprint for the model manager API."""
116
+ bp = Blueprint("model_api_internal")
117
+
118
+ @bp.before_server_stop
119
+ async def cleanup_processes(app: Sanic, loop: asyncio.AbstractEventLoop) -> None:
120
+ """Terminate all running processes before the server stops."""
121
+ structlogger.debug("model_api.cleanup_processes.started")
122
+ cleanup_training_processes()
123
+ cleanup_bot_processes()
124
+
125
+ @bp.on_request # type: ignore[misc]
126
+ async def limit_parallel_training_requests(request: Request) -> Any:
127
+ """Limit the number of parallel training requests."""
128
+ from rasa.model_manager.config import MAX_PARALLEL_TRAININGS
129
+
130
+ if not request.url.endswith("/training"):
131
+ return None
132
+
133
+ running_requests = len(
134
+ [
135
+ training
136
+ for training in trainings.values()
137
+ if training.status == "running" and training.process.poll() is None
138
+ ]
139
+ )
140
+
141
+ if running_requests >= int(MAX_PARALLEL_TRAININGS):
142
+ return response.json(
143
+ {
144
+ "message": f"Too many parallel training requests, above "
145
+ f"the limit of {MAX_PARALLEL_TRAININGS}. "
146
+ f"Retry later or increase your server's "
147
+ f"memory and CPU resources."
148
+ },
149
+ status=HTTPStatus.TOO_MANY_REQUESTS,
150
+ )
151
+
152
+ @bp.on_request # type: ignore[misc]
153
+ async def limit_parallel_bot_runs(request: Request) -> Any:
154
+ """Limit the number of parallel bot runs."""
155
+ from rasa.model_manager.config import MAX_PARALLEL_TRAININGS
156
+
157
+ if not request.url.endswith("/bot"):
158
+ return None
159
+
160
+ running_requests = len(
161
+ [
162
+ bot
163
+ for bot in running_bots.values()
164
+ if bot.status in {"running", "queued"}
165
+ ]
166
+ )
167
+
168
+ if running_requests >= int(MAX_PARALLEL_TRAININGS):
169
+ return response.json(
170
+ {
171
+ "message": f"Too many parallel bot runs, above "
172
+ f"the limit of {MAX_PARALLEL_TRAININGS}. "
173
+ f"Retry later or increase your server's "
174
+ f"memory and CPU resources."
175
+ },
176
+ status=HTTPStatus.TOO_MANY_REQUESTS,
177
+ )
178
+
179
+ @bp.get("/")
180
+ async def health(request: Request) -> response.HTTPResponse:
181
+ return json(
182
+ {
183
+ "status": "ok",
184
+ "bots": [
185
+ {
186
+ "deployment_id": bot.deployment_id,
187
+ "status": bot.status,
188
+ "internal_url": bot.internal_url,
189
+ "url": bot.url,
190
+ }
191
+ for bot in running_bots.values()
192
+ ],
193
+ "trainings": [
194
+ {
195
+ "training_id": training.training_id,
196
+ "assistant_id": training.assistant_id,
197
+ "client_id": training.client_id,
198
+ "progress": training.progress,
199
+ "status": training.status,
200
+ }
201
+ for training in trainings.values()
202
+ ],
203
+ }
204
+ )
205
+
206
+ @bp.get("/training")
207
+ async def get_training_list(request: Request) -> response.HTTPResponse:
208
+ """Return a list of all training sessions for an assistant."""
209
+ assistant_id = request.args.get("assistant_id")
210
+ sessions = [
211
+ {
212
+ "training_id": session.training_id,
213
+ "assistant_id": session.assistant_id,
214
+ "client_id": session.client_id,
215
+ "progress": session.progress,
216
+ "status": session.status,
217
+ "model_name": session.model_name,
218
+ "runtime_metadata": None,
219
+ }
220
+ for session in trainings.values()
221
+ if session.assistant_id == assistant_id
222
+ ]
223
+ return json({"training_sessions": sessions, "total_number": len(sessions)})
224
+
225
+ @bp.post("/training")
226
+ async def start_training(request: Request) -> response.HTTPResponse:
227
+ """Start a new training session."""
228
+ data = request.json
229
+ training_id: Optional[str] = data.get("id")
230
+ assistant_id: Optional[str] = data.get("assistant_id")
231
+ client_id: Optional[str] = data.get("client_id")
232
+ encoded_training_data: Dict[str, str] = data.get("bot_config", {}).get(
233
+ "data", {}
234
+ )
235
+
236
+ if training_id in trainings:
237
+ # fail, because there apparently is already a training with this id
238
+ return json({"message": "Training with this id already exists"}, status=409)
239
+
240
+ if not assistant_id:
241
+ return json({"message": "Assistant id is required"}, status=400)
242
+
243
+ if not training_id:
244
+ return json({"message": "Training id is required"}, status=400)
245
+
246
+ try:
247
+ training_session = run_training(
248
+ training_id=training_id,
249
+ assistant_id=assistant_id,
250
+ client_id=client_id,
251
+ encoded_training_data=encoded_training_data,
252
+ )
253
+ trainings[training_id] = training_session
254
+ return json(
255
+ {"training_id": training_id, "model_name": training_session.model_name}
256
+ )
257
+ except Exception as e:
258
+ return json({"message": str(e)}, status=500)
259
+
260
+ @bp.get("/training/<training_id>")
261
+ async def get_training(request: Request, training_id: str) -> response.HTTPResponse:
262
+ """Return the status of a training session."""
263
+ if training := trainings.get(training_id):
264
+ return json(
265
+ {
266
+ "training_id": training_id,
267
+ "assistant_id": training.assistant_id,
268
+ "client_id": training.client_id,
269
+ "progress": training.progress,
270
+ "model_name": training.model_name,
271
+ "status": training.status,
272
+ "logs": get_logs_content(training_id),
273
+ }
274
+ )
275
+ else:
276
+ return json({"message": "Training not found"}, status=404)
277
+
278
+ @bp.delete("/training/<training_id>")
279
+ async def stop_training(
280
+ request: Request, training_id: str
281
+ ) -> response.HTTPResponse:
282
+ # this is a no-op if the training is already done
283
+ if not (training := trainings.get(training_id)):
284
+ return json({"message": "Training session not found"}, status=404)
285
+
286
+ terminate_training(training)
287
+ return json({"training_id": training_id})
288
+
289
+ @bp.post("/bot")
290
+ async def start_bot(request: Request) -> response.HTTPResponse:
291
+ data = request.json
292
+ deployment_id: Optional[str] = data.get("deployment_id")
293
+ model_name: Optional[str] = data.get("model_name")
294
+ encoded_configs: Dict[str, str] = data.get("bot_config", {})
295
+
296
+ if deployment_id in running_bots:
297
+ # fail, because there apparently is already a bot running with this id
298
+ return json(
299
+ {"message": "Bot with this deployment id already exists"}, status=409
300
+ )
301
+
302
+ if not deployment_id:
303
+ return json({"message": "Deployment id is required"}, status=400)
304
+
305
+ if not model_name:
306
+ return json({"message": "Model name is required"}, status=400)
307
+
308
+ base_url_path = base_server_url(request)
309
+ try:
310
+ bot_session = run_bot(
311
+ deployment_id,
312
+ model_name,
313
+ base_url_path,
314
+ encoded_configs,
315
+ )
316
+ running_bots[deployment_id] = bot_session
317
+ return json(
318
+ {
319
+ "deployment_id": deployment_id,
320
+ "status": bot_session.status,
321
+ "url": bot_session.url,
322
+ }
323
+ )
324
+ except ModelNotFound:
325
+ return json(
326
+ {"message": f"Model with name '{model_name}' could not be found."},
327
+ status=404,
328
+ )
329
+ except Exception as e:
330
+ return json({"message": str(e)}, status=500)
331
+
332
+ @bp.delete("/bot/<deployment_id>")
333
+ async def stop_bot(request: Request, deployment_id: str) -> response.HTTPResponse:
334
+ bot = running_bots.get(deployment_id)
335
+ if bot is None:
336
+ return json({"message": "Bot not found"}, status=404)
337
+
338
+ terminate_bot(bot)
339
+
340
+ return json(
341
+ {"deployment_id": deployment_id, "status": bot.status, "url": bot.url}
342
+ )
343
+
344
+ @bp.get("/bot/<deployment_id>")
345
+ async def get_bot(request: Request, deployment_id: str) -> response.HTTPResponse:
346
+ bot = running_bots.get(deployment_id)
347
+ if bot is None:
348
+ return json({"message": "Bot not found"}, status=404)
349
+
350
+ return json(
351
+ {
352
+ "deployment_id": deployment_id,
353
+ "status": bot.status,
354
+ "url": bot.url,
355
+ "logs": get_logs_content(deployment_id),
356
+ }
357
+ )
358
+
359
+ @bp.get("/bot")
360
+ async def list_bots(request: Request) -> response.HTTPResponse:
361
+ bots = [
362
+ {
363
+ "deployment_id": bot.deployment_id,
364
+ "status": bot.status,
365
+ "url": bot.url,
366
+ }
367
+ for bot in running_bots.values()
368
+ ]
369
+ return json({"deployment_sessions": bots, "total_number": len(bots)})
370
+
371
+ @bp.route("/models/<model_name>")
372
+ async def send_model(request: Request, model_name: str) -> response.HTTPResponse:
373
+ try:
374
+ model_path = path_to_model(model_name)
375
+
376
+ if not model_path:
377
+ return json({"message": "Model not found"}, status=404)
378
+
379
+ return await response.file(model_path)
380
+ except NotFound:
381
+ return json({"message": "Model not found"}, status=404)
382
+ except ModelNotFound:
383
+ return json({"message": "Model not found"}, status=404)
384
+
385
+ return bp
386
+
387
+
388
+ def external_blueprint() -> Blueprint:
389
+ """Create a blueprint for the model manager API."""
390
+ from rasa.core.channels.socketio import SocketBlueprint
391
+
392
+ sio = AsyncServer(async_mode="sanic", cors_allowed_origins=[])
393
+ bp = SocketBlueprint(sio, "", "model_api_external")
394
+
395
+ create_bridge_server(sio, running_bots)
396
+
397
+ @bp.get("/health")
398
+ async def health(request: Request) -> response.HTTPResponse:
399
+ return json(
400
+ {
401
+ "status": "ok",
402
+ "bots": [
403
+ {
404
+ "deployment_id": bot.deployment_id,
405
+ "status": bot.status,
406
+ "internal_url": bot.internal_url,
407
+ "url": bot.url,
408
+ }
409
+ for bot in running_bots.values()
410
+ ],
411
+ "trainings": [
412
+ {
413
+ "training_id": training.training_id,
414
+ "assistant_id": training.assistant_id,
415
+ "client_id": training.client_id,
416
+ "progress": training.progress,
417
+ "status": training.status,
418
+ }
419
+ for training in trainings.values()
420
+ ],
421
+ }
422
+ )
423
+
424
+ return bp
425
+
426
+
427
+ def path_to_model(model_name: str) -> Optional[str]:
428
+ """Return the path to a local model."""
429
+ model_file_name = f"{model_name}.{MODEL_ARCHIVE_EXTENSION}"
430
+ model_path = subpath(models_base_path(), model_file_name)
431
+
432
+ if os.path.exists(model_path):
433
+ return model_path
434
+
435
+ if config.SERVER_MODEL_REMOTE_STORAGE:
436
+ structlogger.info(
437
+ "model_api.storage.fetching_remote_model",
438
+ model_name=model_file_name,
439
+ )
440
+ return fetch_remote_model_to_dir(
441
+ model_file_name,
442
+ models_base_path(),
443
+ config.SERVER_MODEL_REMOTE_STORAGE,
444
+ )
445
+
446
+ return None