rasa-pro 3.13.0rc4__py3-none-any.whl → 3.13.1a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rasa-pro might be problematic. Click here for more details.
- rasa/builder/README.md +120 -0
- rasa/builder/__init__.py +0 -0
- rasa/builder/config.py +69 -0
- rasa/builder/create_openai_vector_store.py +228 -0
- rasa/builder/exceptions.py +49 -0
- rasa/builder/llm-helper-schema.json +69 -0
- rasa/builder/llm_context.py +81 -0
- rasa/builder/llm_helper_prompt.jinja2 +245 -0
- rasa/builder/llm_service.py +327 -0
- rasa/builder/logging_utils.py +51 -0
- rasa/builder/main.py +61 -0
- rasa/builder/models.py +174 -0
- rasa/builder/project_generator.py +264 -0
- rasa/builder/scrape_rasa_docs.py +97 -0
- rasa/builder/service.py +447 -0
- rasa/builder/skill_to_bot_prompt.jinja2 +164 -0
- rasa/builder/training_service.py +123 -0
- rasa/builder/validation_service.py +79 -0
- rasa/cli/project_templates/finance/config.yml +17 -0
- rasa/cli/project_templates/finance/credentials.yml +33 -0
- rasa/cli/project_templates/finance/data/flows/transfer_money.yml +5 -0
- rasa/cli/project_templates/finance/data/patterns/pattern_session_start.yml +7 -0
- rasa/cli/project_templates/finance/domain.yml +7 -0
- rasa/cli/project_templates/finance/endpoints.yml +58 -0
- rasa/cli/project_templates/plain/config.yml +17 -0
- rasa/cli/project_templates/plain/credentials.yml +33 -0
- rasa/cli/project_templates/plain/data/patterns/pattern_session_start.yml +7 -0
- rasa/cli/project_templates/plain/domain.yml +5 -0
- rasa/cli/project_templates/plain/endpoints.yml +58 -0
- rasa/cli/project_templates/telecom/config.yml +17 -0
- rasa/cli/project_templates/telecom/credentials.yml +33 -0
- rasa/cli/project_templates/telecom/data/flows/upgrade_contract.yml +5 -0
- rasa/cli/project_templates/telecom/data/patterns/pattern_session_start.yml +7 -0
- rasa/cli/project_templates/telecom/domain.yml +7 -0
- rasa/cli/project_templates/telecom/endpoints.yml +58 -0
- rasa/cli/scaffold.py +20 -3
- rasa/core/actions/action.py +5 -3
- rasa/core/channels/studio_chat.py +29 -8
- rasa/core/policies/flows/flow_executor.py +8 -1
- rasa/model_manager/model_api.py +2 -2
- rasa/model_manager/runner_service.py +1 -1
- rasa/model_manager/trainer_service.py +12 -9
- rasa/model_manager/utils.py +1 -29
- rasa/shared/core/domain.py +62 -15
- rasa/shared/core/flows/flow_step.py +7 -1
- rasa/shared/core/flows/yaml_flows_io.py +16 -8
- rasa/shared/core/slots.py +4 -0
- rasa/shared/importers/importer.py +6 -0
- rasa/shared/importers/static.py +63 -0
- rasa/telemetry.py +2 -1
- rasa/utils/io.py +27 -9
- rasa/utils/log_utils.py +5 -1
- rasa/validator.py +7 -3
- rasa/version.py +1 -1
- {rasa_pro-3.13.0rc4.dist-info → rasa_pro-3.13.1a3.dist-info}/METADATA +3 -3
- {rasa_pro-3.13.0rc4.dist-info → rasa_pro-3.13.1a3.dist-info}/RECORD +59 -23
- {rasa_pro-3.13.0rc4.dist-info → rasa_pro-3.13.1a3.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.0rc4.dist-info → rasa_pro-3.13.1a3.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.0rc4.dist-info → rasa_pro-3.13.1a3.dist-info}/entry_points.txt +0 -0
|
@@ -49,7 +49,7 @@ if TYPE_CHECKING:
|
|
|
49
49
|
structlogger = structlog.get_logger()
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def tracker_as_dump(tracker: "DialogueStateTracker") -> str:
|
|
52
|
+
def tracker_as_dump(tracker: "DialogueStateTracker") -> Dict[str, Any]:
|
|
53
53
|
"""Create a dump of the tracker state."""
|
|
54
54
|
from rasa.shared.core.trackers import get_trackers_for_conversation_sessions
|
|
55
55
|
|
|
@@ -60,8 +60,9 @@ def tracker_as_dump(tracker: "DialogueStateTracker") -> str:
|
|
|
60
60
|
else:
|
|
61
61
|
last_tracker = multiple_tracker_sessions[-1]
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
# TODO: this is a bug: the bridge converts this back to json, but it
|
|
64
|
+
# should be json in the first place
|
|
65
|
+
return last_tracker.current_state(EventVerbosity.AFTER_RESTART)
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
def does_need_action_prediction(tracker: "DialogueStateTracker") -> bool:
|
|
@@ -148,6 +149,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
|
148
149
|
from rasa.core.agent import Agent
|
|
149
150
|
|
|
150
151
|
self.agent: Optional[Agent] = None
|
|
152
|
+
self.latest_tracker_session_id = None
|
|
151
153
|
|
|
152
154
|
# Initialize the SocketIO input channel
|
|
153
155
|
SocketIOInput.__init__(
|
|
@@ -211,6 +213,11 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
|
211
213
|
if not self.sio:
|
|
212
214
|
structlogger.error("studio_chat.on_tracker_updated.sio_not_initialized")
|
|
213
215
|
return
|
|
216
|
+
|
|
217
|
+
# we need the latest session id to use it for the llm helper to get the
|
|
218
|
+
# most recent conversation the user had with the bot.
|
|
219
|
+
self.latest_tracker_session_id = sender_id
|
|
220
|
+
|
|
214
221
|
await self.sio.emit("tracker", tracker_dump, room=sender_id)
|
|
215
222
|
|
|
216
223
|
async def on_message_proxy(
|
|
@@ -222,7 +229,14 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
|
222
229
|
|
|
223
230
|
Triggers a tracker update notification after processing the message.
|
|
224
231
|
"""
|
|
225
|
-
|
|
232
|
+
try:
|
|
233
|
+
await on_new_message(message)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
structlogger.exception(
|
|
236
|
+
"studio_chat.on_new_message.error",
|
|
237
|
+
error=str(e),
|
|
238
|
+
sender_id=message.sender_id,
|
|
239
|
+
)
|
|
226
240
|
|
|
227
241
|
if not self.agent:
|
|
228
242
|
structlogger.error("studio_chat.on_message_proxy.agent_not_initialized")
|
|
@@ -275,7 +289,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
|
275
289
|
async def collect_call_parameters(
|
|
276
290
|
self, channel_websocket: "Websocket"
|
|
277
291
|
) -> Optional[CallParameters]:
|
|
278
|
-
"""Voice method to collect call parameters"""
|
|
292
|
+
"""Voice method to collect call parameters."""
|
|
279
293
|
session_id = channel_websocket.session_id
|
|
280
294
|
return CallParameters(session_id, "local", "local", stream_id=session_id)
|
|
281
295
|
|
|
@@ -305,7 +319,7 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
|
305
319
|
def create_output_channel(
|
|
306
320
|
self, voice_websocket: "Websocket", tts_engine: TTSEngine
|
|
307
321
|
) -> VoiceOutputChannel:
|
|
308
|
-
"""Create a voice output channel"""
|
|
322
|
+
"""Create a voice output channel."""
|
|
309
323
|
return StudioVoiceOutputChannel(
|
|
310
324
|
voice_websocket,
|
|
311
325
|
tts_engine,
|
|
@@ -432,8 +446,15 @@ class StudioChatInput(SocketIOInput, VoiceInputChannel):
|
|
|
432
446
|
ws.put_message(data)
|
|
433
447
|
return
|
|
434
448
|
|
|
435
|
-
|
|
436
|
-
|
|
449
|
+
try:
|
|
450
|
+
# Handle text messages
|
|
451
|
+
await self.handle_user_message(sid, data, on_new_message)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
structlogger.exception(
|
|
454
|
+
"studio_chat.sio.handle_message.error",
|
|
455
|
+
error=str(e),
|
|
456
|
+
sid=sid,
|
|
457
|
+
)
|
|
437
458
|
|
|
438
459
|
@self.sio.on("update_tracker", namespace=self.namespace)
|
|
439
460
|
async def on_update_tracker(sid: Text, data: Dict) -> None:
|
|
@@ -740,7 +740,14 @@ def _run_action_step(
|
|
|
740
740
|
# do not log about non-existing validation actions of collect steps
|
|
741
741
|
utter_action_name = render_template_variables("{{context.utter}}", context)
|
|
742
742
|
if utter_action_name not in available_actions:
|
|
743
|
-
structlogger.warning(
|
|
743
|
+
structlogger.warning(
|
|
744
|
+
"flow.step.run.action.unknown",
|
|
745
|
+
action=action_name,
|
|
746
|
+
event_info=(
|
|
747
|
+
f"The action '{action_name}' is not defined in the domain but "
|
|
748
|
+
f"getting triggered by the flow '{step.flow_id}'."
|
|
749
|
+
),
|
|
750
|
+
)
|
|
744
751
|
return ContinueFlowWithNextStep(events=initial_events)
|
|
745
752
|
|
|
746
753
|
|
rasa/model_manager/model_api.py
CHANGED
|
@@ -43,7 +43,6 @@ from rasa.model_manager.utils import (
|
|
|
43
43
|
get_logs_content,
|
|
44
44
|
logs_base_path,
|
|
45
45
|
models_base_path,
|
|
46
|
-
subpath,
|
|
47
46
|
)
|
|
48
47
|
from rasa.model_manager.warm_rasa_process import (
|
|
49
48
|
initialize_warm_rasa_process,
|
|
@@ -53,6 +52,7 @@ from rasa.server import ErrorResponse
|
|
|
53
52
|
from rasa.shared.exceptions import InvalidConfigException
|
|
54
53
|
from rasa.shared.utils.yaml import dump_obj_as_yaml_to_string
|
|
55
54
|
from rasa.studio.upload import build_calm_import_parts
|
|
55
|
+
from rasa.utils.io import subpath
|
|
56
56
|
|
|
57
57
|
dotenv.load_dotenv()
|
|
58
58
|
|
|
@@ -105,7 +105,7 @@ async def update_status_of_all_bots() -> None:
|
|
|
105
105
|
await update_bot_status(bot)
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def base_server_url(request: Request) -> str:
|
|
108
|
+
def base_server_url(request: Optional[Request]) -> str:
|
|
109
109
|
"""Return the base URL of the server."""
|
|
110
110
|
if SERVER_BASE_URL:
|
|
111
111
|
return SERVER_BASE_URL.rstrip("/")
|
|
@@ -15,11 +15,11 @@ from rasa.model_manager import config
|
|
|
15
15
|
from rasa.model_manager.utils import (
|
|
16
16
|
logs_path,
|
|
17
17
|
models_base_path,
|
|
18
|
-
subpath,
|
|
19
18
|
write_encoded_data_to_file,
|
|
20
19
|
)
|
|
21
20
|
from rasa.model_manager.warm_rasa_process import start_rasa_process
|
|
22
21
|
from rasa.studio.prompts import handle_prompts
|
|
22
|
+
from rasa.utils.io import subpath
|
|
23
23
|
|
|
24
24
|
structlogger = structlog.get_logger()
|
|
25
25
|
|
|
@@ -14,7 +14,6 @@ from rasa.model_manager.utils import (
|
|
|
14
14
|
ensure_base_directory_exists,
|
|
15
15
|
logs_path,
|
|
16
16
|
models_base_path,
|
|
17
|
-
subpath,
|
|
18
17
|
write_encoded_data_to_file,
|
|
19
18
|
)
|
|
20
19
|
from rasa.model_manager.warm_rasa_process import (
|
|
@@ -22,6 +21,7 @@ from rasa.model_manager.warm_rasa_process import (
|
|
|
22
21
|
)
|
|
23
22
|
from rasa.model_training import generate_random_model_name
|
|
24
23
|
from rasa.studio.prompts import handle_prompts
|
|
24
|
+
from rasa.utils.io import subpath
|
|
25
25
|
|
|
26
26
|
structlogger = structlog.get_logger()
|
|
27
27
|
|
|
@@ -53,6 +53,15 @@ class TrainingSession(BaseModel):
|
|
|
53
53
|
"""Check if the training is running."""
|
|
54
54
|
return self.status == TrainingSessionStatus.RUNNING
|
|
55
55
|
|
|
56
|
+
def has_just_finished(self) -> bool:
|
|
57
|
+
if not self.is_status_indicating_alive():
|
|
58
|
+
# skip if the training is not running
|
|
59
|
+
return False
|
|
60
|
+
if self.process.poll() is None:
|
|
61
|
+
# process is still running
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
|
|
56
65
|
def model_path(self) -> str:
|
|
57
66
|
"""Return the path to the model."""
|
|
58
67
|
return subpath(models_base_path(), f"{self.model_name}.tar.gz")
|
|
@@ -89,14 +98,8 @@ def terminate_training(training: TrainingSession) -> None:
|
|
|
89
98
|
|
|
90
99
|
|
|
91
100
|
def update_training_status(training: TrainingSession) -> None:
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
return
|
|
95
|
-
if training.process.poll() is None:
|
|
96
|
-
# process is still running
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
complete_training(training)
|
|
101
|
+
if training.has_just_finished():
|
|
102
|
+
complete_training(training)
|
|
100
103
|
|
|
101
104
|
|
|
102
105
|
def complete_training(training: TrainingSession) -> None:
|
rasa/model_manager/utils.py
CHANGED
|
@@ -5,15 +5,11 @@ from typing import Optional
|
|
|
5
5
|
import structlog
|
|
6
6
|
|
|
7
7
|
from rasa.model_manager import config
|
|
8
|
-
from rasa.
|
|
8
|
+
from rasa.utils.io import subpath
|
|
9
9
|
|
|
10
10
|
structlogger = structlog.get_logger()
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class InvalidPathException(RasaException):
|
|
14
|
-
"""Raised if a path is invalid - e.g. path traversal is detected."""
|
|
15
|
-
|
|
16
|
-
|
|
17
13
|
def write_encoded_data_to_file(encoded_data: bytes, file: str) -> None:
|
|
18
14
|
"""Write base64 encoded data to a file."""
|
|
19
15
|
# create the directory if it does not exist of the parent directory
|
|
@@ -53,30 +49,6 @@ def logs_path(action_id: str) -> str:
|
|
|
53
49
|
return subpath(logs_base_path(), f"{action_id}.txt")
|
|
54
50
|
|
|
55
51
|
|
|
56
|
-
def subpath(parent: str, child: str) -> str:
|
|
57
|
-
"""Return the path to the child directory of the parent directory.
|
|
58
|
-
|
|
59
|
-
Ensures, that child doesn't navigate to parent directories. Prevents
|
|
60
|
-
path traversal. Raises an InvalidPathException if the path is invalid.
|
|
61
|
-
|
|
62
|
-
Based on Snyk's directory traversal mitigation:
|
|
63
|
-
https://learn.snyk.io/lesson/directory-traversal/
|
|
64
|
-
"""
|
|
65
|
-
safe_path = os.path.abspath(os.path.join(parent, child))
|
|
66
|
-
parent = os.path.abspath(parent)
|
|
67
|
-
|
|
68
|
-
common_base = os.path.commonpath([parent, safe_path])
|
|
69
|
-
if common_base != parent:
|
|
70
|
-
raise InvalidPathException(f"Invalid path: {safe_path}")
|
|
71
|
-
|
|
72
|
-
if os.path.basename(safe_path) != child:
|
|
73
|
-
raise InvalidPathException(
|
|
74
|
-
f"Invalid path - path traversal detected: {safe_path}"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return safe_path
|
|
78
|
-
|
|
79
|
-
|
|
80
52
|
def get_logs_content(action_id: str) -> Optional[str]:
|
|
81
53
|
"""Return the content of the log file for a given action id."""
|
|
82
54
|
try:
|
rasa/shared/core/domain.py
CHANGED
|
@@ -98,6 +98,8 @@ IS_RETRIEVAL_INTENT_KEY = "is_retrieval_intent"
|
|
|
98
98
|
ENTITY_ROLES_KEY = "roles"
|
|
99
99
|
ENTITY_GROUPS_KEY = "groups"
|
|
100
100
|
ENTITY_FEATURIZATION_KEY = "influence_conversation"
|
|
101
|
+
STORE_ENTITIES_AS_SLOTS_KEY = "store_entities_as_slots"
|
|
102
|
+
DOMAIN_CONFIG_KEY = "config"
|
|
101
103
|
|
|
102
104
|
KEY_SLOTS = "slots"
|
|
103
105
|
KEY_INTENTS = "intents"
|
|
@@ -146,6 +148,8 @@ MERGE_FUNC_MAPPING: Dict[Text, Callable[..., Any]] = {
|
|
|
146
148
|
KEY_FORMS: rasa.shared.utils.common.merge_dicts,
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
DEFAULT_STORE_ENTITIES_AS_SLOTS = True
|
|
152
|
+
|
|
149
153
|
DICT_DATA_KEYS = [
|
|
150
154
|
key
|
|
151
155
|
for key, value in MERGE_FUNC_MAPPING.items()
|
|
@@ -318,7 +322,7 @@ class Domain:
|
|
|
318
322
|
actions = cls._collect_action_names(domain_actions)
|
|
319
323
|
|
|
320
324
|
additional_arguments = {
|
|
321
|
-
**data.get(
|
|
325
|
+
**data.get(DOMAIN_CONFIG_KEY, {}),
|
|
322
326
|
"actions_which_explicitly_need_domain": (
|
|
323
327
|
cls._collect_actions_which_explicitly_need_domain(domain_actions)
|
|
324
328
|
),
|
|
@@ -468,9 +472,9 @@ class Domain:
|
|
|
468
472
|
return domain_dict
|
|
469
473
|
|
|
470
474
|
if override:
|
|
471
|
-
config = domain_dict.get(
|
|
475
|
+
config = domain_dict.get(DOMAIN_CONFIG_KEY, {})
|
|
472
476
|
for key, val in config.items():
|
|
473
|
-
combined[
|
|
477
|
+
combined[DOMAIN_CONFIG_KEY][key] = val
|
|
474
478
|
|
|
475
479
|
if (
|
|
476
480
|
override
|
|
@@ -508,10 +512,10 @@ class Domain:
|
|
|
508
512
|
return combined
|
|
509
513
|
|
|
510
514
|
def partial_merge(self, other: Domain) -> Domain:
|
|
511
|
-
"""
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
+
"""Returns a new Domain with intersection-based merging.
|
|
516
|
+
|
|
517
|
+
For each domain section only overwrite items that already exist in self.
|
|
518
|
+
Brand-new items in `other` are ignored.
|
|
515
519
|
|
|
516
520
|
Args:
|
|
517
521
|
other: The domain to merge with.
|
|
@@ -543,9 +547,9 @@ class Domain:
|
|
|
543
547
|
return Domain.from_dict(updated_self)
|
|
544
548
|
|
|
545
549
|
def difference(self, other: Domain) -> Domain:
|
|
546
|
-
"""
|
|
547
|
-
|
|
548
|
-
|
|
550
|
+
"""Returns a new Domain containing items in `self` that are NOT in `other`.
|
|
551
|
+
|
|
552
|
+
Uses simple equality checks for dict/list items.
|
|
549
553
|
|
|
550
554
|
Args:
|
|
551
555
|
other: The domain to compare with.
|
|
@@ -598,9 +602,16 @@ class Domain:
|
|
|
598
602
|
) -> Dict:
|
|
599
603
|
# add the config, session_config and training data version defaults
|
|
600
604
|
# if not included in the original domain dict
|
|
601
|
-
if
|
|
605
|
+
if (
|
|
606
|
+
DOMAIN_CONFIG_KEY not in data
|
|
607
|
+
and store_entities_as_slots != DEFAULT_STORE_ENTITIES_AS_SLOTS
|
|
608
|
+
):
|
|
602
609
|
data.update(
|
|
603
|
-
{
|
|
610
|
+
{
|
|
611
|
+
DOMAIN_CONFIG_KEY: {
|
|
612
|
+
STORE_ENTITIES_AS_SLOTS_KEY: store_entities_as_slots
|
|
613
|
+
}
|
|
614
|
+
}
|
|
604
615
|
)
|
|
605
616
|
|
|
606
617
|
if SESSION_CONFIG_KEY not in data:
|
|
@@ -937,7 +948,7 @@ class Domain:
|
|
|
937
948
|
forms: Union[Dict[Text, Any], List[Text]],
|
|
938
949
|
data: Dict,
|
|
939
950
|
action_texts: Optional[List[Text]] = None,
|
|
940
|
-
store_entities_as_slots: bool =
|
|
951
|
+
store_entities_as_slots: bool = DEFAULT_STORE_ENTITIES_AS_SLOTS,
|
|
941
952
|
session_config: SessionConfig = SessionConfig.default(),
|
|
942
953
|
**kwargs: Any,
|
|
943
954
|
) -> None:
|
|
@@ -1711,9 +1722,45 @@ class Domain:
|
|
|
1711
1722
|
else:
|
|
1712
1723
|
return True
|
|
1713
1724
|
|
|
1714
|
-
def
|
|
1725
|
+
def _uses_custom_session_config(self) -> bool:
|
|
1726
|
+
"""Check if the domain uses a custom session config."""
|
|
1727
|
+
return self._data.get(SESSION_CONFIG_KEY) != SessionConfig.default().as_dict()
|
|
1728
|
+
|
|
1729
|
+
def _uses_custom_domain_config(self) -> bool:
|
|
1730
|
+
"""Check if the domain uses a custom domain config."""
|
|
1731
|
+
return self._data.get(DOMAIN_CONFIG_KEY) != {
|
|
1732
|
+
STORE_ENTITIES_AS_SLOTS_KEY: DEFAULT_STORE_ENTITIES_AS_SLOTS
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
def _cleaned_json_data(self) -> Dict[Text, Any]:
|
|
1736
|
+
"""Remove default values from the domain data.
|
|
1737
|
+
|
|
1738
|
+
Only retains data that was customized by the user.
|
|
1739
|
+
|
|
1740
|
+
Returns:
|
|
1741
|
+
A cleaned dictionary version of the domain.
|
|
1742
|
+
"""
|
|
1743
|
+
cleaned_data = copy.deepcopy(self._data)
|
|
1744
|
+
|
|
1745
|
+
# Remove default config if it only contains store_entities_as_slots: False
|
|
1746
|
+
if DOMAIN_CONFIG_KEY in cleaned_data and not self._uses_custom_domain_config():
|
|
1747
|
+
del cleaned_data[DOMAIN_CONFIG_KEY]
|
|
1748
|
+
|
|
1749
|
+
# Remove default session config if it matches the default values
|
|
1750
|
+
if (
|
|
1751
|
+
SESSION_CONFIG_KEY in cleaned_data
|
|
1752
|
+
and not self._uses_custom_session_config()
|
|
1753
|
+
):
|
|
1754
|
+
del cleaned_data[SESSION_CONFIG_KEY]
|
|
1755
|
+
|
|
1756
|
+
return cleaned_data
|
|
1757
|
+
|
|
1758
|
+
def as_dict(self, should_clean_json: bool = False) -> Dict[Text, Any]:
|
|
1715
1759
|
"""Return serialized `Domain`."""
|
|
1716
|
-
|
|
1760
|
+
if should_clean_json:
|
|
1761
|
+
return self._cleaned_json_data()
|
|
1762
|
+
else:
|
|
1763
|
+
return self._data
|
|
1717
1764
|
|
|
1718
1765
|
@staticmethod
|
|
1719
1766
|
def get_responses_with_multilines(
|
|
@@ -52,7 +52,13 @@ def step_from_json(flow_id: Text, data: Dict[Text, Any]) -> FlowStep:
|
|
|
52
52
|
return SetSlotsFlowStep.from_json(flow_id, data)
|
|
53
53
|
if "noop" in data:
|
|
54
54
|
return NoOperationFlowStep.from_json(flow_id, data)
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
required_properties = ["action", "collect", "link", "call", "set_slots", "noop"]
|
|
57
|
+
raise RasaException(
|
|
58
|
+
f"Failed to parse step from json. Unknown type for {data}. "
|
|
59
|
+
f"At lest one of the following properties is required: "
|
|
60
|
+
f"{', '.join(required_properties)}"
|
|
61
|
+
)
|
|
56
62
|
|
|
57
63
|
|
|
58
64
|
@dataclass
|
|
@@ -262,12 +262,9 @@ class YamlFlowsWriter:
|
|
|
262
262
|
Returns:
|
|
263
263
|
The dumped YAML.
|
|
264
264
|
"""
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
del dumped_flow["id"]
|
|
269
|
-
dump[flow.id] = dumped_flow
|
|
270
|
-
return dump_obj_as_yaml_to_string({KEY_FLOWS: dump})
|
|
265
|
+
return dump_obj_as_yaml_to_string(
|
|
266
|
+
{KEY_FLOWS: get_flows_as_json(flows, should_clean_json)}
|
|
267
|
+
)
|
|
271
268
|
|
|
272
269
|
@staticmethod
|
|
273
270
|
def dump(
|
|
@@ -424,9 +421,20 @@ def process_yaml_content(yaml_content: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
424
421
|
return yaml_content
|
|
425
422
|
|
|
426
423
|
|
|
424
|
+
def get_flows_as_json(
|
|
425
|
+
flows: FlowsList, should_clean_json: bool = False
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
"""Get the flows as a JSON dictionary."""
|
|
428
|
+
dump = {}
|
|
429
|
+
for flow in flows:
|
|
430
|
+
dumped_flow = get_flow_as_json(flow, should_clean_json)
|
|
431
|
+
del dumped_flow["id"]
|
|
432
|
+
dump[flow.id] = dumped_flow
|
|
433
|
+
return dump
|
|
434
|
+
|
|
435
|
+
|
|
427
436
|
def get_flow_as_json(flow: Flow, should_clean_json: bool = False) -> Dict[str, Any]:
|
|
428
|
-
"""
|
|
429
|
-
Clean the Flow JSON by removing default values and empty fields.
|
|
437
|
+
"""Clean the Flow JSON by removing default values and empty fields.
|
|
430
438
|
|
|
431
439
|
Args:
|
|
432
440
|
flow: The Flow object to clean.
|
rasa/shared/core/slots.py
CHANGED
|
@@ -273,10 +273,14 @@ class Slot(ABC):
|
|
|
273
273
|
try:
|
|
274
274
|
return rasa.shared.utils.common.class_from_module_path(type_name)
|
|
275
275
|
except (ImportError, AttributeError):
|
|
276
|
+
known_types = [
|
|
277
|
+
cls.type_name for cls in rasa.shared.utils.common.all_subclasses(Slot)
|
|
278
|
+
]
|
|
276
279
|
raise InvalidSlotTypeException(
|
|
277
280
|
f"Failed to find slot type, '{type_name}' is neither a known type nor "
|
|
278
281
|
f"user-defined. If you are creating your own slot type, make "
|
|
279
282
|
f"sure its module path is correct. "
|
|
283
|
+
f"Known types: {', '.join(known_types)} "
|
|
280
284
|
f"You can find all build in types at {DOCS_URL_SLOTS}"
|
|
281
285
|
)
|
|
282
286
|
|
|
@@ -207,6 +207,12 @@ class TrainingDataImporter(ABC):
|
|
|
207
207
|
)
|
|
208
208
|
]
|
|
209
209
|
|
|
210
|
+
return TrainingDataImporter.wrap_in_builtins(importers)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def wrap_in_builtins(
|
|
214
|
+
importers: List["TrainingDataImporter"],
|
|
215
|
+
) -> "TrainingDataImporter":
|
|
210
216
|
return LanguageImporter(
|
|
211
217
|
E2EImporter(
|
|
212
218
|
FlowSyncImporter(ResponsesSyncImporter(CombinedDataImporter(importers)))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Optional, Text
|
|
3
|
+
|
|
4
|
+
from rasa.shared.core.domain import Domain
|
|
5
|
+
from rasa.shared.core.flows import FlowsList
|
|
6
|
+
from rasa.shared.core.training_data.structures import StoryGraph
|
|
7
|
+
from rasa.shared.importers.importer import TrainingDataImporter
|
|
8
|
+
from rasa.shared.nlu.training_data.training_data import TrainingData
|
|
9
|
+
from rasa.shared.utils.common import cached_method
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StaticTrainingDataImporter(TrainingDataImporter):
|
|
15
|
+
"""Static `TrainingFileImporter` implementation."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
domain: Domain,
|
|
20
|
+
stories: Optional[StoryGraph] = None,
|
|
21
|
+
flows: Optional[FlowsList] = None,
|
|
22
|
+
nlu_data: Optional[TrainingData] = None,
|
|
23
|
+
config: Optional[Dict] = None,
|
|
24
|
+
):
|
|
25
|
+
self.domain = domain
|
|
26
|
+
self.stories = stories or StoryGraph([])
|
|
27
|
+
self.flows = flows or FlowsList(underlying_flows=[])
|
|
28
|
+
self.nlu_data = nlu_data or TrainingData()
|
|
29
|
+
self.config = config or {}
|
|
30
|
+
|
|
31
|
+
@cached_method
|
|
32
|
+
def get_config(self) -> Dict:
|
|
33
|
+
"""Retrieves model config (see parent class for full docstring)."""
|
|
34
|
+
return self.config
|
|
35
|
+
|
|
36
|
+
def get_config_file_for_auto_config(self) -> Optional[Text]:
|
|
37
|
+
"""Returns config file path for auto-config only if there is a single one."""
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
@cached_method
|
|
41
|
+
def get_stories(self, exclusion_percentage: Optional[int] = None) -> StoryGraph:
|
|
42
|
+
"""Retrieves training stories / rules (see parent class for full docstring)."""
|
|
43
|
+
return self.stories
|
|
44
|
+
|
|
45
|
+
@cached_method
|
|
46
|
+
def get_flows(self) -> FlowsList:
|
|
47
|
+
"""Retrieves training stories / rules (see parent class for full docstring)."""
|
|
48
|
+
return self.flows
|
|
49
|
+
|
|
50
|
+
@cached_method
|
|
51
|
+
def get_conversation_tests(self) -> StoryGraph:
|
|
52
|
+
"""Retrieves conversation test stories (see parent class for full docstring)."""
|
|
53
|
+
return StoryGraph([])
|
|
54
|
+
|
|
55
|
+
@cached_method
|
|
56
|
+
def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData:
|
|
57
|
+
"""Retrieves NLU training data (see parent class for full docstring)."""
|
|
58
|
+
return self.nlu_data
|
|
59
|
+
|
|
60
|
+
@cached_method
|
|
61
|
+
def get_domain(self) -> Domain:
|
|
62
|
+
"""Retrieves model domain (see parent class for full docstring)."""
|
|
63
|
+
return self.domain
|
rasa/telemetry.py
CHANGED
|
@@ -1426,6 +1426,7 @@ def track_shell_started(model_type: Text, assistant_id: Text) -> None:
|
|
|
1426
1426
|
|
|
1427
1427
|
Args:
|
|
1428
1428
|
model_type: Type of the model, core / nlu or rasa.
|
|
1429
|
+
assistant_id: ID of the assistant being inspected.
|
|
1429
1430
|
"""
|
|
1430
1431
|
_track(
|
|
1431
1432
|
TELEMETRY_SHELL_STARTED_EVENT,
|
|
@@ -1997,7 +1998,7 @@ def _extract_stream_pii(event_broker: Optional["EventBroker"]) -> bool:
|
|
|
1997
1998
|
def track_privacy_enabled(
|
|
1998
1999
|
privacy_config: "PrivacyConfig", event_broker: Optional["EventBroker"]
|
|
1999
2000
|
) -> None:
|
|
2000
|
-
"""Track when PII management capability is enabled"""
|
|
2001
|
+
"""Track when PII management capability is enabled."""
|
|
2001
2002
|
stream_pii = _extract_stream_pii(event_broker)
|
|
2002
2003
|
privacy_properties = _extract_privacy_enabled_event_properties(
|
|
2003
2004
|
privacy_config, stream_pii
|
rasa/utils/io.py
CHANGED
|
@@ -26,6 +26,7 @@ from typing_extensions import Protocol
|
|
|
26
26
|
|
|
27
27
|
import rasa.shared.constants
|
|
28
28
|
import rasa.shared.utils.io
|
|
29
|
+
from rasa.shared.exceptions import RasaException
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from prompt_toolkit.validation import Validator
|
|
@@ -124,9 +125,7 @@ def create_path(file_path: Text) -> None:
|
|
|
124
125
|
def file_type_validator(
|
|
125
126
|
valid_file_types: List[Text], error_message: Text
|
|
126
127
|
) -> Type["Validator"]:
|
|
127
|
-
"""Creates a
|
|
128
|
-
file paths.
|
|
129
|
-
"""
|
|
128
|
+
"""Creates a file type validator class for the questionary package."""
|
|
130
129
|
|
|
131
130
|
def is_valid(path: Text) -> bool:
|
|
132
131
|
return path is not None and any(
|
|
@@ -137,9 +136,7 @@ def file_type_validator(
|
|
|
137
136
|
|
|
138
137
|
|
|
139
138
|
def not_empty_validator(error_message: Text) -> Type["Validator"]:
|
|
140
|
-
"""Creates a
|
|
141
|
-
that the user entered something other than whitespace.
|
|
142
|
-
"""
|
|
139
|
+
"""Creates a not empty validator class for the questionary package."""
|
|
143
140
|
|
|
144
141
|
def is_valid(input: Text) -> bool:
|
|
145
142
|
return input is not None and input.strip() != ""
|
|
@@ -150,9 +147,7 @@ def not_empty_validator(error_message: Text) -> Type["Validator"]:
|
|
|
150
147
|
def create_validator(
|
|
151
148
|
function: Callable[[Text], bool], error_message: Text
|
|
152
149
|
) -> Type["Validator"]:
|
|
153
|
-
"""Helper method to create
|
|
154
|
-
removed when questionary supports `Validator` objects.
|
|
155
|
-
"""
|
|
150
|
+
"""Helper method to create a validator class from a callable function."""
|
|
156
151
|
from prompt_toolkit.document import Document
|
|
157
152
|
from prompt_toolkit.validation import ValidationError, Validator
|
|
158
153
|
|
|
@@ -250,3 +245,26 @@ def write_yaml(
|
|
|
250
245
|
|
|
251
246
|
with Path(target).open("w", encoding="utf-8") as outfile:
|
|
252
247
|
dumper.dump(data, outfile, transform=transform)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class InvalidPathException(RasaException):
|
|
251
|
+
"""Raised if a path is invalid - e.g. path traversal is detected."""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def subpath(parent: str, child: str) -> str:
|
|
255
|
+
"""Return the path to the child directory of the parent directory.
|
|
256
|
+
|
|
257
|
+
Ensures, that child doesn't navigate to parent directories. Prevents
|
|
258
|
+
path traversal. Raises an InvalidPathException if the path is invalid.
|
|
259
|
+
|
|
260
|
+
Based on Snyk's directory traversal mitigation:
|
|
261
|
+
https://learn.snyk.io/lesson/directory-traversal/
|
|
262
|
+
"""
|
|
263
|
+
safe_path = os.path.abspath(os.path.join(parent, child))
|
|
264
|
+
parent = os.path.abspath(parent)
|
|
265
|
+
|
|
266
|
+
common_base = os.path.commonpath([parent, safe_path])
|
|
267
|
+
if common_base != parent:
|
|
268
|
+
raise InvalidPathException(f"Invalid path: {safe_path}")
|
|
269
|
+
|
|
270
|
+
return safe_path
|
rasa/utils/log_utils.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
|
-
from typing import Any, Optional
|
|
6
|
+
from typing import Any, List, Optional
|
|
7
7
|
|
|
8
8
|
import structlog
|
|
9
9
|
from structlog.dev import ConsoleRenderer
|
|
@@ -37,6 +37,7 @@ class HumanConsoleRenderer(ConsoleRenderer):
|
|
|
37
37
|
def configure_structlog(
|
|
38
38
|
log_level: Optional[int] = None,
|
|
39
39
|
include_time: bool = False,
|
|
40
|
+
additional_processors: Optional[List[structlog.processors.Processor]] = None,
|
|
40
41
|
) -> None:
|
|
41
42
|
"""Configure logging of the server."""
|
|
42
43
|
if log_level is None: # Log level NOTSET is 0 so we use `is None` here
|
|
@@ -75,6 +76,9 @@ def configure_structlog(
|
|
|
75
76
|
if include_time:
|
|
76
77
|
shared_processors.append(structlog.processors.TimeStamper(fmt="iso"))
|
|
77
78
|
|
|
79
|
+
if additional_processors:
|
|
80
|
+
shared_processors.extend(additional_processors)
|
|
81
|
+
|
|
78
82
|
if not FORCE_JSON_LOGGING and sys.stderr.isatty():
|
|
79
83
|
# Pretty printing when we run in a terminal session.
|
|
80
84
|
# Automatically prints pretty tracebacks when "rich" is installed
|
rasa/validator.py
CHANGED
|
@@ -630,11 +630,14 @@ class Validator:
|
|
|
630
630
|
flow_id: str,
|
|
631
631
|
) -> bool:
|
|
632
632
|
"""Validates that a collect step can have either an action or an utterance.
|
|
633
|
+
|
|
633
634
|
Also logs an error if neither an action nor an utterance is defined.
|
|
634
635
|
|
|
635
636
|
Args:
|
|
636
637
|
collect: the name of the slot to collect
|
|
637
638
|
all_good: boolean value indicating the validation status
|
|
639
|
+
domain_slots: the slots of the domain
|
|
640
|
+
flow_id: the id of the flow
|
|
638
641
|
|
|
639
642
|
Returns:
|
|
640
643
|
False, if validation failed, true, otherwise
|
|
@@ -683,9 +686,10 @@ class Validator:
|
|
|
683
686
|
has_action_defined=has_action_defined,
|
|
684
687
|
flow=flow_id,
|
|
685
688
|
event_info=(
|
|
686
|
-
f"The collect step '{collect.collect}' has neither
|
|
687
|
-
f"nor an action defined,
|
|
688
|
-
f"You
|
|
689
|
+
f"The collect step '{collect.collect}' has neither a response "
|
|
690
|
+
f"nor an action defined, nor an initial value defined in the "
|
|
691
|
+
f"domain. You can fix this by adding a response named "
|
|
692
|
+
f"'{collect.utter}' used in the collect step."
|
|
689
693
|
),
|
|
690
694
|
)
|
|
691
695
|
all_good = False
|