rasa-pro 3.13.0.dev20250612__py3-none-any.whl → 3.13.0.dev20250613__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/__main__.py +0 -3
- rasa/api.py +1 -1
- rasa/cli/dialogue_understanding_test.py +1 -1
- rasa/cli/e2e_test.py +1 -1
- rasa/cli/evaluate.py +1 -1
- rasa/cli/export.py +1 -1
- rasa/cli/llm_fine_tuning.py +12 -11
- rasa/cli/project_templates/defaults.py +133 -0
- rasa/cli/run.py +1 -1
- rasa/cli/studio/link.py +53 -0
- rasa/cli/studio/pull.py +78 -0
- rasa/cli/studio/push.py +78 -0
- rasa/cli/studio/studio.py +12 -0
- rasa/cli/studio/upload.py +8 -0
- rasa/cli/train.py +1 -1
- rasa/cli/utils.py +1 -1
- rasa/cli/x.py +1 -1
- rasa/constants.py +2 -0
- rasa/core/__init__.py +0 -16
- rasa/core/actions/action.py +5 -1
- rasa/core/actions/action_repeat_bot_messages.py +18 -22
- rasa/core/actions/action_run_slot_rejections.py +0 -1
- rasa/core/agent.py +16 -1
- rasa/core/available_endpoints.py +146 -0
- rasa/core/brokers/pika.py +1 -2
- rasa/core/channels/botframework.py +2 -2
- rasa/core/channels/channel.py +2 -2
- rasa/core/channels/hangouts.py +8 -5
- rasa/core/channels/mattermost.py +1 -1
- rasa/core/channels/rasa_chat.py +2 -4
- rasa/core/channels/rest.py +5 -4
- rasa/core/channels/studio_chat.py +3 -2
- rasa/core/channels/vier_cvg.py +1 -2
- rasa/core/channels/voice_ready/audiocodes.py +1 -8
- rasa/core/channels/voice_stream/audiocodes.py +7 -4
- rasa/core/channels/voice_stream/genesys.py +2 -2
- rasa/core/channels/voice_stream/twilio_media_streams.py +10 -5
- rasa/core/channels/voice_stream/voice_channel.py +33 -22
- rasa/core/http_interpreter.py +3 -7
- rasa/core/jobs.py +2 -1
- rasa/core/nlg/contextual_response_rephraser.py +38 -11
- rasa/core/nlg/generator.py +0 -1
- rasa/core/nlg/interpolator.py +2 -3
- rasa/core/nlg/summarize.py +39 -5
- rasa/core/policies/enterprise_search_policy.py +290 -66
- rasa/core/policies/enterprise_search_prompt_with_relevancy_check_and_citation_template.jinja2 +63 -0
- rasa/core/policies/flow_policy.py +1 -1
- rasa/core/policies/flows/flow_executor.py +96 -17
- rasa/core/policies/intentless_policy.py +24 -16
- rasa/core/processor.py +104 -51
- rasa/core/run.py +33 -11
- rasa/core/tracker_stores/tracker_store.py +1 -1
- rasa/core/training/interactive.py +1 -1
- rasa/core/utils.py +24 -97
- rasa/dialogue_understanding/coexistence/intent_based_router.py +2 -1
- rasa/dialogue_understanding/coexistence/llm_based_router.py +8 -3
- rasa/dialogue_understanding/commands/can_not_handle_command.py +2 -0
- rasa/dialogue_understanding/commands/cancel_flow_command.py +2 -0
- rasa/dialogue_understanding/commands/chit_chat_answer_command.py +2 -0
- rasa/dialogue_understanding/commands/clarify_command.py +5 -1
- rasa/dialogue_understanding/commands/command_syntax_manager.py +1 -0
- rasa/dialogue_understanding/commands/human_handoff_command.py +2 -0
- rasa/dialogue_understanding/commands/knowledge_answer_command.py +2 -0
- rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +2 -0
- rasa/dialogue_understanding/commands/set_slot_command.py +11 -1
- rasa/dialogue_understanding/commands/skip_question_command.py +2 -0
- rasa/dialogue_understanding/commands/start_flow_command.py +4 -0
- rasa/dialogue_understanding/commands/utils.py +26 -2
- rasa/dialogue_understanding/generator/__init__.py +7 -1
- rasa/dialogue_understanding/generator/command_generator.py +4 -2
- rasa/dialogue_understanding/generator/command_parser.py +2 -2
- rasa/dialogue_understanding/generator/command_parser_validator.py +63 -0
- rasa/dialogue_understanding/generator/constants.py +2 -2
- rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v3_gpt_4o_2024_11_20_template.jinja2 +78 -0
- rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +28 -463
- rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +147 -0
- rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +477 -0
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +8 -58
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +37 -25
- rasa/dialogue_understanding/patterns/domain_for_patterns.py +190 -0
- rasa/dialogue_understanding/processor/command_processor.py +3 -3
- rasa/dialogue_understanding/processor/command_processor_component.py +3 -3
- rasa/dialogue_understanding/stack/frames/flow_stack_frame.py +17 -4
- rasa/dialogue_understanding/utils.py +68 -12
- rasa/dialogue_understanding_test/du_test_case.py +1 -1
- rasa/dialogue_understanding_test/du_test_runner.py +4 -22
- rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +2 -6
- rasa/e2e_test/e2e_test_runner.py +1 -1
- rasa/engine/constants.py +1 -1
- rasa/engine/recipes/default_recipe.py +26 -2
- rasa/engine/validation.py +3 -2
- rasa/hooks.py +0 -28
- rasa/llm_fine_tuning/annotation_module.py +39 -9
- rasa/llm_fine_tuning/conversations.py +3 -0
- rasa/llm_fine_tuning/llm_data_preparation_module.py +66 -49
- rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +4 -2
- rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +52 -44
- rasa/llm_fine_tuning/paraphrasing_module.py +10 -12
- rasa/llm_fine_tuning/storage.py +4 -4
- rasa/llm_fine_tuning/utils.py +63 -1
- rasa/model_manager/model_api.py +88 -0
- rasa/model_manager/trainer_service.py +4 -4
- rasa/plugin.py +1 -11
- rasa/privacy/__init__.py +0 -0
- rasa/privacy/constants.py +83 -0
- rasa/privacy/event_broker_utils.py +77 -0
- rasa/privacy/privacy_config.py +281 -0
- rasa/privacy/privacy_config_schema.json +86 -0
- rasa/privacy/privacy_filter.py +340 -0
- rasa/privacy/privacy_manager.py +576 -0
- rasa/server.py +23 -2
- rasa/shared/constants.py +6 -0
- rasa/shared/core/constants.py +4 -3
- rasa/shared/core/domain.py +7 -0
- rasa/shared/core/events.py +37 -7
- rasa/shared/core/flows/flow.py +1 -2
- rasa/shared/core/flows/flows_yaml_schema.json +3 -0
- rasa/shared/core/flows/steps/collect.py +46 -2
- rasa/shared/core/slots.py +28 -0
- rasa/shared/exceptions.py +4 -0
- rasa/shared/providers/_configs/azure_openai_client_config.py +4 -0
- rasa/shared/providers/_configs/openai_client_config.py +4 -0
- rasa/shared/providers/embedding/_base_litellm_embedding_client.py +3 -0
- rasa/shared/providers/llm/_base_litellm_client.py +5 -2
- rasa/shared/utils/llm.py +161 -6
- rasa/shared/utils/yaml.py +32 -0
- rasa/studio/data_handler.py +3 -3
- rasa/studio/download/download.py +37 -60
- rasa/studio/download/flows.py +23 -31
- rasa/studio/link.py +200 -0
- rasa/studio/pull.py +94 -0
- rasa/studio/push.py +131 -0
- rasa/studio/upload.py +117 -67
- rasa/telemetry.py +82 -25
- rasa/tracing/config.py +3 -4
- rasa/tracing/constants.py +19 -1
- rasa/tracing/instrumentation/attribute_extractors.py +10 -2
- rasa/tracing/instrumentation/instrumentation.py +53 -2
- rasa/tracing/instrumentation/metrics.py +98 -15
- rasa/tracing/metric_instrument_provider.py +75 -3
- rasa/utils/common.py +1 -27
- rasa/utils/log_utils.py +1 -45
- rasa/validator.py +2 -8
- rasa/version.py +1 -1
- {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/METADATA +5 -6
- {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/RECORD +149 -135
- rasa/anonymization/__init__.py +0 -2
- rasa/anonymization/anonymisation_rule_yaml_reader.py +0 -91
- rasa/anonymization/anonymization_pipeline.py +0 -286
- rasa/anonymization/anonymization_rule_executor.py +0 -266
- rasa/anonymization/anonymization_rule_orchestrator.py +0 -119
- rasa/anonymization/schemas/config.yml +0 -47
- rasa/anonymization/utils.py +0 -118
- {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/entry_points.txt +0 -0
rasa/shared/utils/yaml.py
CHANGED
|
@@ -21,6 +21,7 @@ from ruamel.yaml import YAML, RoundTripRepresenter, YAMLError
|
|
|
21
21
|
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
22
22
|
from ruamel.yaml.constructor import BaseConstructor, DuplicateKeyError, ScalarNode
|
|
23
23
|
from ruamel.yaml.loader import SafeLoader
|
|
24
|
+
from ruamel.yaml.scalarstring import LiteralScalarString
|
|
24
25
|
|
|
25
26
|
from rasa.shared.constants import (
|
|
26
27
|
ASSERTIONS_SCHEMA_EXTENSIONS_FILE,
|
|
@@ -794,6 +795,25 @@ def write_yaml(
|
|
|
794
795
|
should_preserve_key_order: Whether to force preserve key order in `data`.
|
|
795
796
|
transform: A function to transform the data before writing it to the file.
|
|
796
797
|
"""
|
|
798
|
+
|
|
799
|
+
def multiline_str_representer(self: Any, value: str) -> Any:
|
|
800
|
+
"""Dump multi-line strings as readable YAML block scalars where possible."""
|
|
801
|
+
if "\n" in value:
|
|
802
|
+
# First line after the newline decides: paragraph vs. snippet
|
|
803
|
+
first_line = value.split("\n", 1)[1]
|
|
804
|
+
|
|
805
|
+
# If the first line after the newline is not indented, treat the value
|
|
806
|
+
# as plain text. Indented text is likely pre-formatted YAML/JSON/etc.
|
|
807
|
+
if not first_line.startswith((" ", "\t")):
|
|
808
|
+
return self.represent_scalar(
|
|
809
|
+
"tag:yaml.org,2002:str",
|
|
810
|
+
LiteralScalarString(value),
|
|
811
|
+
style="|",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# Fallback: keep default YAML scalar style (plain/quoted)
|
|
815
|
+
return self.represent_scalar("tag:yaml.org,2002:str", value)
|
|
816
|
+
|
|
797
817
|
_enable_ordered_dict_yaml_dumping()
|
|
798
818
|
|
|
799
819
|
if should_preserve_key_order:
|
|
@@ -808,6 +828,7 @@ def write_yaml(
|
|
|
808
828
|
type(None),
|
|
809
829
|
lambda self, _: self.represent_scalar("tag:yaml.org,2002:null", "null"),
|
|
810
830
|
)
|
|
831
|
+
dumper.representer.add_representer(str, multiline_str_representer)
|
|
811
832
|
|
|
812
833
|
if isinstance(target, StringIO):
|
|
813
834
|
dumper.dump(data, target, transform=transform)
|
|
@@ -1025,6 +1046,17 @@ def validate_yaml_with_jsonschema(
|
|
|
1025
1046
|
except (YAMLError, DuplicateKeyError) as e:
|
|
1026
1047
|
raise YamlSyntaxException(underlying_yaml_exception=e)
|
|
1027
1048
|
|
|
1049
|
+
validate_data_with_jsonschema(source_data, schema_content, humanize_error)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def validate_data_with_jsonschema(
|
|
1053
|
+
source_data: Any,
|
|
1054
|
+
schema_content: Any,
|
|
1055
|
+
humanize_error: Callable[
|
|
1056
|
+
[jsonschema.ValidationError], str
|
|
1057
|
+
] = default_error_humanizer,
|
|
1058
|
+
) -> None:
|
|
1059
|
+
"""Validate Python object against the provided jsonschema content."""
|
|
1028
1060
|
try:
|
|
1029
1061
|
jsonschema.validate(source_data, schema_content)
|
|
1030
1062
|
except jsonschema.ValidationError as error:
|
rasa/studio/data_handler.py
CHANGED
|
@@ -320,14 +320,14 @@ def create_new_flows_from_diff(
|
|
|
320
320
|
|
|
321
321
|
|
|
322
322
|
def import_data_from_studio(
|
|
323
|
-
handler: StudioDataHandler, domain_path: Path,
|
|
323
|
+
handler: StudioDataHandler, domain_path: Path, data_path: Path
|
|
324
324
|
) -> Tuple[TrainingDataImporter, TrainingDataImporter]:
|
|
325
325
|
"""Construct TrainingDataImporter from Studio data and original data.
|
|
326
326
|
|
|
327
327
|
Args:
|
|
328
328
|
handler (StudioDataHandler): handler with data from studio
|
|
329
329
|
domain_path (Path): Path to a domain file
|
|
330
|
-
|
|
330
|
+
data_path (List[Path]): List of paths to training data files
|
|
331
331
|
|
|
332
332
|
Returns:
|
|
333
333
|
Tuple[TrainingDataImporter, TrainingDataImporter]:
|
|
@@ -335,7 +335,7 @@ def import_data_from_studio(
|
|
|
335
335
|
"""
|
|
336
336
|
tmp_dir = get_temp_dir_name()
|
|
337
337
|
data_original = TrainingDataImporter.load_from_dict(
|
|
338
|
-
domain_path=domain_path, training_data_paths=
|
|
338
|
+
domain_path=str(domain_path), training_data_paths=[str(data_path)]
|
|
339
339
|
)
|
|
340
340
|
|
|
341
341
|
data_paths = []
|
rasa/studio/download/download.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Dict,
|
|
3
|
+
from typing import Dict, Optional, Tuple
|
|
4
4
|
|
|
5
5
|
import questionary
|
|
6
6
|
import structlog
|
|
@@ -46,7 +46,7 @@ def handle_download(args: argparse.Namespace) -> None:
|
|
|
46
46
|
)
|
|
47
47
|
handler.request_all_data()
|
|
48
48
|
|
|
49
|
-
domain_path,
|
|
49
|
+
domain_path, data_path = _prepare_data_and_domain_paths(args)
|
|
50
50
|
|
|
51
51
|
# Handle config and endpoints.
|
|
52
52
|
config_path, write_config = _handle_file_overwrite(
|
|
@@ -78,12 +78,12 @@ def handle_download(args: argparse.Namespace) -> None:
|
|
|
78
78
|
structlogger.info("studio.download.config_endpoints", event_info=message)
|
|
79
79
|
|
|
80
80
|
if not args.overwrite:
|
|
81
|
-
_handle_download_no_overwrite(handler, domain_path,
|
|
81
|
+
_handle_download_no_overwrite(handler, domain_path, data_path)
|
|
82
82
|
else:
|
|
83
|
-
_handle_download_with_overwrite(handler, domain_path,
|
|
83
|
+
_handle_download_with_overwrite(handler, domain_path, data_path)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def _prepare_data_and_domain_paths(args: argparse.Namespace) -> Tuple[Path,
|
|
86
|
+
def _prepare_data_and_domain_paths(args: argparse.Namespace) -> Tuple[Path, Path]:
|
|
87
87
|
"""Prepars the domain and data paths based on the provided arguments.
|
|
88
88
|
|
|
89
89
|
Args:
|
|
@@ -115,28 +115,15 @@ def _prepare_data_and_domain_paths(args: argparse.Namespace) -> Tuple[Path, List
|
|
|
115
115
|
domain_path = domain_path / STUDIO_DOMAIN_FILENAME
|
|
116
116
|
domain_path.touch()
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
data_path = rasa.cli.utils.get_validated_path(
|
|
122
|
-
f, "data", DEFAULT_DATA_PATH, none_is_valid=True
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if data_path is None:
|
|
126
|
-
data_path = Path(f)
|
|
127
|
-
data_path.mkdir(parents=True, exist_ok=True)
|
|
128
|
-
else:
|
|
129
|
-
data_path = Path(data_path)
|
|
118
|
+
data_path = rasa.cli.utils.get_validated_path(
|
|
119
|
+
args.data[0], "data", DEFAULT_DATA_PATH, none_is_valid=True
|
|
120
|
+
)
|
|
130
121
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
data_path.mkdir(parents=True, exist_ok=True)
|
|
135
|
-
data_paths.append(data_path)
|
|
122
|
+
data_path = Path(data_path or args.data[0])
|
|
123
|
+
if not (data_path.is_file() or data_path.is_dir()):
|
|
124
|
+
data_path.mkdir(parents=True, exist_ok=True)
|
|
136
125
|
|
|
137
|
-
|
|
138
|
-
data_paths = list(dict.fromkeys(data_paths))
|
|
139
|
-
return domain_path, data_paths
|
|
126
|
+
return domain_path, data_path
|
|
140
127
|
|
|
141
128
|
|
|
142
129
|
def _handle_file_overwrite(
|
|
@@ -177,7 +164,7 @@ def _handle_file_overwrite(
|
|
|
177
164
|
|
|
178
165
|
|
|
179
166
|
def _handle_download_no_overwrite(
|
|
180
|
-
handler: StudioDataHandler, domain_path: Path,
|
|
167
|
+
handler: StudioDataHandler, domain_path: Path, data_path: Path
|
|
181
168
|
) -> None:
|
|
182
169
|
"""Handles downloading without overwriting existing files.
|
|
183
170
|
|
|
@@ -187,10 +174,10 @@ def _handle_download_no_overwrite(
|
|
|
187
174
|
data_paths: The paths to the data files or directories.
|
|
188
175
|
"""
|
|
189
176
|
data_from_studio, data_local = import_data_from_studio(
|
|
190
|
-
handler, domain_path,
|
|
177
|
+
handler, domain_path, data_path
|
|
191
178
|
)
|
|
192
179
|
_merge_domain_no_overwrite(domain_path, data_from_studio, data_local)
|
|
193
|
-
_merge_data_no_overwrite(
|
|
180
|
+
_merge_data_no_overwrite(data_path, handler, data_from_studio, data_local)
|
|
194
181
|
|
|
195
182
|
|
|
196
183
|
def _merge_domain_no_overwrite(
|
|
@@ -264,7 +251,7 @@ def _merge_file_domain(
|
|
|
264
251
|
|
|
265
252
|
|
|
266
253
|
def _merge_data_no_overwrite(
|
|
267
|
-
|
|
254
|
+
data_path: Path,
|
|
268
255
|
handler: StudioDataHandler,
|
|
269
256
|
data_from_studio: TrainingDataImporter,
|
|
270
257
|
data_local: TrainingDataImporter,
|
|
@@ -272,38 +259,29 @@ def _merge_data_no_overwrite(
|
|
|
272
259
|
"""Merges NLU and flow data without overwriting existing data.
|
|
273
260
|
|
|
274
261
|
Args:
|
|
275
|
-
|
|
262
|
+
data_path: The paths to the data files or directories.
|
|
276
263
|
handler: The StudioDataHandler instance.
|
|
277
264
|
data_from_studio: The Studio data importer.
|
|
278
265
|
data_local: The local data importer.
|
|
279
266
|
"""
|
|
280
|
-
if not
|
|
267
|
+
if not data_path:
|
|
281
268
|
structlogger.warning(
|
|
282
269
|
"studio.download.merge_data_no_overwrite.no_path",
|
|
283
270
|
event_info="No data paths provided. Skipping data merge.",
|
|
284
271
|
)
|
|
285
272
|
return
|
|
286
273
|
|
|
287
|
-
if
|
|
288
|
-
data_path
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
data_path, handler, data_from_studio, data_local
|
|
292
|
-
)
|
|
293
|
-
elif data_path.is_dir():
|
|
294
|
-
_merge_dir_data_no_overwrite(
|
|
295
|
-
data_path, handler, data_from_studio, data_local
|
|
296
|
-
)
|
|
297
|
-
else:
|
|
298
|
-
structlogger.warning(
|
|
299
|
-
"studio.download.merge_data_no_overwrite.invalid_path",
|
|
300
|
-
event_info=(
|
|
301
|
-
f"Provided path '{data_path}' is neither a file nor a directory."
|
|
302
|
-
),
|
|
303
|
-
)
|
|
274
|
+
if data_path.is_file():
|
|
275
|
+
_merge_file_data_no_overwrite(data_path, handler, data_from_studio, data_local)
|
|
276
|
+
elif data_path.is_dir():
|
|
277
|
+
_merge_dir_data_no_overwrite(data_path, handler, data_from_studio, data_local)
|
|
304
278
|
else:
|
|
305
|
-
|
|
306
|
-
|
|
279
|
+
structlogger.warning(
|
|
280
|
+
"studio.download.merge_data_no_overwrite.invalid_path",
|
|
281
|
+
event_info=(
|
|
282
|
+
f"Provided path '{data_path}' is neither a file nor a directory."
|
|
283
|
+
),
|
|
284
|
+
)
|
|
307
285
|
|
|
308
286
|
|
|
309
287
|
def _merge_file_data_no_overwrite(
|
|
@@ -353,25 +331,23 @@ def _merge_dir_data_no_overwrite(
|
|
|
353
331
|
|
|
354
332
|
|
|
355
333
|
def _handle_download_with_overwrite(
|
|
356
|
-
handler: StudioDataHandler, domain_path: Path,
|
|
334
|
+
handler: StudioDataHandler, domain_path: Path, data_path: Path
|
|
357
335
|
) -> None:
|
|
358
336
|
"""Handles downloading and merging data when the user opts for overwrite.
|
|
359
337
|
|
|
360
338
|
Args:
|
|
361
339
|
handler: The StudioDataHandler instance.
|
|
362
340
|
domain_path: The path to the domain file or directory.
|
|
363
|
-
|
|
341
|
+
data_path: The paths to the data files or directories.
|
|
364
342
|
"""
|
|
365
343
|
data_from_studio, data_local = import_data_from_studio(
|
|
366
|
-
handler, domain_path,
|
|
344
|
+
handler, domain_path, data_path
|
|
367
345
|
)
|
|
368
346
|
mapper = RasaPrimitiveStorageMapper(
|
|
369
|
-
domain_path=domain_path, training_data_paths=
|
|
347
|
+
domain_path=domain_path, training_data_paths=[data_path]
|
|
370
348
|
)
|
|
371
349
|
merge_domain_with_overwrite(data_from_studio, data_local, domain_path)
|
|
372
|
-
merge_flows_with_overwrite(
|
|
373
|
-
data_paths, handler, data_from_studio, data_local, mapper
|
|
374
|
-
)
|
|
350
|
+
merge_flows_with_overwrite(data_path, handler, data_from_studio, data_local, mapper)
|
|
375
351
|
|
|
376
352
|
|
|
377
353
|
def _persist_nlu_diff(
|
|
@@ -432,8 +408,9 @@ def pretty_write_nlu_yaml(data: Dict, file: Path) -> None:
|
|
|
432
408
|
file: The file to write to.
|
|
433
409
|
"""
|
|
434
410
|
dumper = yaml.YAML()
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
item
|
|
411
|
+
if nlu_data := data.get("nlu"):
|
|
412
|
+
for item in nlu_data:
|
|
413
|
+
if item.get("examples"):
|
|
414
|
+
item["examples"] = LiteralScalarString(item["examples"])
|
|
438
415
|
with file.open("w", encoding="utf-8") as outfile:
|
|
439
416
|
dumper.dump(data, outfile)
|
rasa/studio/download/flows.py
CHANGED
|
@@ -18,7 +18,7 @@ STUDIO_FLOWS_DIR_NAME = "studio_flows"
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def merge_flows_with_overwrite(
|
|
21
|
-
|
|
21
|
+
data_path: Path,
|
|
22
22
|
handler: Any,
|
|
23
23
|
data_from_studio: TrainingDataImporter,
|
|
24
24
|
data_local: TrainingDataImporter,
|
|
@@ -28,17 +28,12 @@ def merge_flows_with_overwrite(
|
|
|
28
28
|
Merges flows data from a file or directory when overwrite is enabled.
|
|
29
29
|
|
|
30
30
|
Args:
|
|
31
|
-
|
|
31
|
+
data_path: List of paths to the training data.
|
|
32
32
|
handler: The StudioDataHandler instance.
|
|
33
33
|
data_from_studio: The TrainingDataImporter instance for Studio data.
|
|
34
34
|
data_local: The TrainingDataImporter instance for local data.
|
|
35
35
|
mapper: The RasaPrimitiveStorageMapper instance for mapping.
|
|
36
36
|
"""
|
|
37
|
-
if len(data_paths) != 1:
|
|
38
|
-
# TODO: Handle multiple data paths.
|
|
39
|
-
raise NotImplementedError("Multiple data paths are not supported yet.")
|
|
40
|
-
|
|
41
|
-
data_path = data_paths[0]
|
|
42
37
|
if data_path.is_file():
|
|
43
38
|
merge_training_data_file(handler, data_from_studio, data_local, data_path)
|
|
44
39
|
elif data_path.is_dir():
|
|
@@ -132,7 +127,8 @@ def merge_nlu_in_directory(
|
|
|
132
127
|
)
|
|
133
128
|
nlu_data = nlu_data.merge(local_nlu.get_nlu_data())
|
|
134
129
|
|
|
135
|
-
|
|
130
|
+
if nlu_yaml := nlu_data.nlu_as_yaml():
|
|
131
|
+
pretty_write_nlu_yaml(read_yaml(nlu_yaml), nlu_file_path)
|
|
136
132
|
|
|
137
133
|
|
|
138
134
|
def get_nlu_path(
|
|
@@ -211,14 +207,16 @@ def merge_flows_in_directory(
|
|
|
211
207
|
local_flow_paths: Set[Path] = _get_local_flow_paths(local_flows, mapper)
|
|
212
208
|
|
|
213
209
|
# Track updated flows and update local files with Studio flow data.
|
|
214
|
-
|
|
210
|
+
all_updated_flows_ids: List[Text] = []
|
|
215
211
|
for flow_file_path in local_flow_paths:
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
updated_flows_ids = _update_flow_file(flow_file_path, studio_flow_map)
|
|
213
|
+
all_updated_flows_ids.extend(updated_flows_ids)
|
|
218
214
|
|
|
219
215
|
# Identify new Studio flows and save them as separate files in the directory.
|
|
220
216
|
new_flows = [
|
|
221
|
-
flow
|
|
217
|
+
flow
|
|
218
|
+
for flow_id, flow in studio_flow_map.items()
|
|
219
|
+
if flow_id not in all_updated_flows_ids
|
|
222
220
|
]
|
|
223
221
|
_dump_flows_as_separate_files(new_flows, data_path)
|
|
224
222
|
|
|
@@ -243,7 +241,7 @@ def _get_local_flow_paths(
|
|
|
243
241
|
|
|
244
242
|
def _update_flow_file(
|
|
245
243
|
flow_file_path: Path, studio_flows_map: Dict[Text, Any]
|
|
246
|
-
) -> List[
|
|
244
|
+
) -> List[Text]:
|
|
247
245
|
"""
|
|
248
246
|
Reads a flow file, updates outdated flows, and replaces them with studio versions.
|
|
249
247
|
|
|
@@ -252,31 +250,25 @@ def _update_flow_file(
|
|
|
252
250
|
studio_flows_map: A dictionary mapping flow IDs to their updated versions.
|
|
253
251
|
|
|
254
252
|
Returns:
|
|
255
|
-
A list of
|
|
253
|
+
A list of Flows IDs from the updated flow file.
|
|
256
254
|
"""
|
|
257
255
|
file_flows = YAMLFlowsReader.read_from_file(flow_file_path, False)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
updated_list.append(flow)
|
|
268
|
-
|
|
269
|
-
if has_changes:
|
|
270
|
-
new_flows_list = FlowsList(underlying_flows=updated_list)
|
|
271
|
-
new_flows_list = strip_default_next_references(new_flows_list)
|
|
256
|
+
|
|
257
|
+
# Build a list of flows, replacing any outdated flow with its studio version
|
|
258
|
+
updated_flows = [
|
|
259
|
+
studio_flows_map.get(flow.id, flow) or flow
|
|
260
|
+
for flow in file_flows.underlying_flows
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
# If the updated flows differ from the original file flows, write them back
|
|
264
|
+
if updated_flows != file_flows.underlying_flows:
|
|
272
265
|
YamlFlowsWriter.dump(
|
|
273
|
-
flows=
|
|
266
|
+
flows=updated_flows,
|
|
274
267
|
filename=flow_file_path,
|
|
275
268
|
should_clean_json=True,
|
|
276
269
|
)
|
|
277
|
-
return new_flows_list.underlying_flows
|
|
278
270
|
|
|
279
|
-
return
|
|
271
|
+
return [flow.id for flow in updated_flows]
|
|
280
272
|
|
|
281
273
|
|
|
282
274
|
def _dump_flows_as_separate_files(flows: List[Any], data_path: Path) -> None:
|
rasa/studio/link.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Text, Union
|
|
8
|
+
|
|
9
|
+
import questionary
|
|
10
|
+
import structlog
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
import rasa.shared.utils.cli
|
|
14
|
+
from rasa.constants import RASA_DIR_NAME
|
|
15
|
+
from rasa.shared.utils.yaml import read_yaml_file, write_yaml
|
|
16
|
+
from rasa.studio.config import StudioConfig
|
|
17
|
+
from rasa.studio.upload import (
|
|
18
|
+
check_if_assistant_already_exists,
|
|
19
|
+
handle_upload,
|
|
20
|
+
is_auth_working,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
structlogger = structlog.get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
_LINK_FILE_NAME: Text = "studio.yml"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AssistantLinkPayload(BaseModel):
|
|
29
|
+
assistant_name: Text
|
|
30
|
+
studio_url: Text
|
|
31
|
+
linked_at: Text = Field(
|
|
32
|
+
default_factory=lambda: datetime.datetime.utcnow().isoformat() + "Z"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _link_file(project_root: Path) -> Path:
|
|
37
|
+
"""Return `<project-root>/.rasa/studio.yml`.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
project_root: The path to the project root.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The path to the link file.
|
|
44
|
+
"""
|
|
45
|
+
return project_root / RASA_DIR_NAME / _LINK_FILE_NAME
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _write_link_file(
|
|
49
|
+
project_root: Path, assistant_name: Text, studio_url: Text
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Persist assistant information inside the project.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
project_root: The path to the project root.
|
|
55
|
+
assistant_name: The name of the assistant.
|
|
56
|
+
studio_url: The URL of the Rasa Studio instance.
|
|
57
|
+
"""
|
|
58
|
+
file_path = _link_file(project_root)
|
|
59
|
+
file_path.parent.mkdir(exist_ok=True, parents=True)
|
|
60
|
+
|
|
61
|
+
payload = AssistantLinkPayload(
|
|
62
|
+
assistant_name=assistant_name,
|
|
63
|
+
studio_url=studio_url,
|
|
64
|
+
)
|
|
65
|
+
write_yaml(payload.model_dump(), file_path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _read_link_file(
|
|
69
|
+
project_root: Path = Path.cwd(),
|
|
70
|
+
) -> Optional[Union[List[Any], Dict[Text, Any]]]:
|
|
71
|
+
"""Reads the link configuration file.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
project_root: The path to the project root.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The assistant information if the file exists, otherwise None.
|
|
78
|
+
"""
|
|
79
|
+
file_path = _link_file(project_root)
|
|
80
|
+
if not file_path.is_file():
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
return read_yaml_file(file_path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_assistant_name(project_root: Path = Path.cwd()) -> Optional[Text]:
|
|
87
|
+
"""Reads the assistant_name from the linked configuration file.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
project_root: The path to the project root.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The assistant name if the file exists, otherwise None.
|
|
94
|
+
"""
|
|
95
|
+
linked = _read_link_file(project_root)
|
|
96
|
+
assistant_name = (
|
|
97
|
+
linked.get("assistant_name") if linked and isinstance(linked, dict) else None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not assistant_name:
|
|
101
|
+
rasa.shared.utils.cli.print_error_and_exit(
|
|
102
|
+
"This project is not linked to any Rasa Studio assistant.\n"
|
|
103
|
+
"Run `rasa studio link <assistant-name>` first."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return assistant_name
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_studio_config() -> StudioConfig:
|
|
110
|
+
"""Get the StudioConfig object or exit with an error message.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A valid StudioConfig object.
|
|
114
|
+
"""
|
|
115
|
+
config = StudioConfig.read_config()
|
|
116
|
+
if not config.is_valid():
|
|
117
|
+
rasa.shared.utils.cli.print_error_and_exit(
|
|
118
|
+
"Rasa Studio is not configured correctly. Run `rasa studio config` first."
|
|
119
|
+
)
|
|
120
|
+
if not is_auth_working(config.studio_url, not config.disable_verify):
|
|
121
|
+
rasa.shared.utils.cli.print_error_and_exit(
|
|
122
|
+
"Authentication invalid or expired. Please run `rasa studio login`."
|
|
123
|
+
)
|
|
124
|
+
return config
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _ensure_assistant_exists(
|
|
128
|
+
assistant_name: Text,
|
|
129
|
+
studio_cfg: StudioConfig,
|
|
130
|
+
args: argparse.Namespace,
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Create the assistant on Studio if it does not yet exist.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
assistant_name: The name the user provided on the CLI.
|
|
136
|
+
studio_cfg: The validated Studio configuration.
|
|
137
|
+
args: The original CLI args (needed for `handle_upload`).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if the assistant already exists or was created, False otherwise.
|
|
141
|
+
"""
|
|
142
|
+
verify_ssl = not studio_cfg.disable_verify
|
|
143
|
+
assistant_already_exists = check_if_assistant_already_exists(
|
|
144
|
+
assistant_name, studio_cfg.studio_url, verify_ssl
|
|
145
|
+
)
|
|
146
|
+
if not assistant_already_exists:
|
|
147
|
+
should_create_assistant = questionary.confirm(
|
|
148
|
+
f"Assistant '{assistant_name}' was not found on Rasa Studio. "
|
|
149
|
+
f"Do you want to create it?"
|
|
150
|
+
).ask()
|
|
151
|
+
if should_create_assistant:
|
|
152
|
+
# `handle_upload` expects the name to live in `args.assistant_name`
|
|
153
|
+
args.assistant_name = assistant_name
|
|
154
|
+
handle_upload(args)
|
|
155
|
+
|
|
156
|
+
rasa.shared.utils.cli.print_info(
|
|
157
|
+
f"Assistant {assistant_name} successfully created."
|
|
158
|
+
)
|
|
159
|
+
return should_create_assistant
|
|
160
|
+
|
|
161
|
+
return assistant_already_exists
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def handle_link(args: argparse.Namespace) -> None:
|
|
165
|
+
"""Implementation of `rasa studio link <assistant-name>` CLI command.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
args: The command line arguments.
|
|
169
|
+
"""
|
|
170
|
+
assistant_name: Text = args.assistant_name[0]
|
|
171
|
+
studio_cfg = get_studio_config()
|
|
172
|
+
assistant_exists = _ensure_assistant_exists(assistant_name, studio_cfg, args)
|
|
173
|
+
if not assistant_exists:
|
|
174
|
+
rasa.shared.utils.cli.print_error_and_exit(
|
|
175
|
+
"Project has not been linked with Studio assistant."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
project_root = Path.cwd()
|
|
179
|
+
link_file = _link_file(project_root)
|
|
180
|
+
|
|
181
|
+
if link_file.exists():
|
|
182
|
+
overwrite = questionary.confirm(
|
|
183
|
+
f"This project is already linked " f"(link file '{link_file}').\nOverwrite?"
|
|
184
|
+
).ask()
|
|
185
|
+
if not overwrite:
|
|
186
|
+
rasa.shared.utils.cli.print_info(
|
|
187
|
+
"Existing link kept – nothing was changed."
|
|
188
|
+
)
|
|
189
|
+
sys.exit(0)
|
|
190
|
+
|
|
191
|
+
_write_link_file(project_root, assistant_name, studio_cfg.studio_url)
|
|
192
|
+
|
|
193
|
+
structlogger.info(
|
|
194
|
+
"studio.link.success",
|
|
195
|
+
event_info=f"Project linked to Studio assistant '{assistant_name}'.",
|
|
196
|
+
assistant_name=assistant_name,
|
|
197
|
+
)
|
|
198
|
+
rasa.shared.utils.cli.print_success(
|
|
199
|
+
f"Project successfully linked to assistant '{assistant_name}'."
|
|
200
|
+
)
|
rasa/studio/pull.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Text, Union
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
import rasa.cli.utils
|
|
10
|
+
import rasa.shared.utils.cli
|
|
11
|
+
from rasa.shared.constants import DEFAULT_CONFIG_PATH, DEFAULT_ENDPOINTS_PATH
|
|
12
|
+
from rasa.shared.utils.io import write_text_file
|
|
13
|
+
from rasa.studio.data_handler import StudioDataHandler
|
|
14
|
+
from rasa.studio.download.download import handle_download
|
|
15
|
+
from rasa.studio.link import get_studio_config, read_assistant_name
|
|
16
|
+
|
|
17
|
+
structlogger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _write_to_file(
|
|
21
|
+
content: Text, file_type: Text, file_path: Text, default_path: Text
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Write the content to a file.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
content: The content to write.
|
|
27
|
+
file_type: The type of file (e.g., "config" or "endpoints".).
|
|
28
|
+
file_path: The path to the file.
|
|
29
|
+
default_path: The default path to use file_path is not valid.
|
|
30
|
+
"""
|
|
31
|
+
path: Union[Path, str, None] = rasa.cli.utils.get_validated_path(
|
|
32
|
+
file_path, file_type, default_path, none_is_valid=True
|
|
33
|
+
)
|
|
34
|
+
write_text_file(content, path, encoding="utf-8")
|
|
35
|
+
rasa.shared.utils.cli.print_success(f"Pulled {file_type} data from assistant.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def handle_pull(args: argparse.Namespace) -> None:
|
|
39
|
+
"""Pull all data and overwrite the local assistant.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
args: The parsed arguments.
|
|
43
|
+
"""
|
|
44
|
+
assistant_name = read_assistant_name()
|
|
45
|
+
|
|
46
|
+
# Use the CLI command logic to download with overwrite
|
|
47
|
+
download_args = argparse.Namespace(**vars(args))
|
|
48
|
+
download_args.assistant_name = [assistant_name]
|
|
49
|
+
download_args.overwrite = True
|
|
50
|
+
|
|
51
|
+
handle_download(download_args)
|
|
52
|
+
rasa.shared.utils.cli.print_success("Pulled the data from assistant.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handle_pull_config(args: argparse.Namespace) -> None:
|
|
56
|
+
"""Pull just the assistant's `config.yml`.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
args: The parsed arguments.
|
|
60
|
+
"""
|
|
61
|
+
studio_cfg = get_studio_config()
|
|
62
|
+
assistant_name = read_assistant_name()
|
|
63
|
+
|
|
64
|
+
handler = StudioDataHandler(studio_cfg, assistant_name)
|
|
65
|
+
handler.request_all_data()
|
|
66
|
+
|
|
67
|
+
config_yaml = handler.get_config()
|
|
68
|
+
if not config_yaml:
|
|
69
|
+
rasa.shared.utils.cli.print_error_and_exit(
|
|
70
|
+
"No configuration data was found in the Studio assistant."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
_write_to_file(config_yaml, "config", args.config, DEFAULT_CONFIG_PATH)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_pull_endpoints(args: argparse.Namespace) -> None:
|
|
77
|
+
"""Pull just the assistant's `endpoints.yml`.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
args: The parsed arguments.
|
|
81
|
+
"""
|
|
82
|
+
studio_cfg = get_studio_config()
|
|
83
|
+
assistant_name = read_assistant_name()
|
|
84
|
+
|
|
85
|
+
handler = StudioDataHandler(studio_cfg, assistant_name)
|
|
86
|
+
handler.request_all_data()
|
|
87
|
+
|
|
88
|
+
endpoints_yaml = handler.get_endpoints()
|
|
89
|
+
if not endpoints_yaml:
|
|
90
|
+
rasa.shared.utils.cli.print_error_and_exit(
|
|
91
|
+
"No endpoints data was found in the Studio assistant."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
_write_to_file(endpoints_yaml, "endpoints", args.endpoints, DEFAULT_ENDPOINTS_PATH)
|