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.

Files changed (156) hide show
  1. rasa/__main__.py +0 -3
  2. rasa/api.py +1 -1
  3. rasa/cli/dialogue_understanding_test.py +1 -1
  4. rasa/cli/e2e_test.py +1 -1
  5. rasa/cli/evaluate.py +1 -1
  6. rasa/cli/export.py +1 -1
  7. rasa/cli/llm_fine_tuning.py +12 -11
  8. rasa/cli/project_templates/defaults.py +133 -0
  9. rasa/cli/run.py +1 -1
  10. rasa/cli/studio/link.py +53 -0
  11. rasa/cli/studio/pull.py +78 -0
  12. rasa/cli/studio/push.py +78 -0
  13. rasa/cli/studio/studio.py +12 -0
  14. rasa/cli/studio/upload.py +8 -0
  15. rasa/cli/train.py +1 -1
  16. rasa/cli/utils.py +1 -1
  17. rasa/cli/x.py +1 -1
  18. rasa/constants.py +2 -0
  19. rasa/core/__init__.py +0 -16
  20. rasa/core/actions/action.py +5 -1
  21. rasa/core/actions/action_repeat_bot_messages.py +18 -22
  22. rasa/core/actions/action_run_slot_rejections.py +0 -1
  23. rasa/core/agent.py +16 -1
  24. rasa/core/available_endpoints.py +146 -0
  25. rasa/core/brokers/pika.py +1 -2
  26. rasa/core/channels/botframework.py +2 -2
  27. rasa/core/channels/channel.py +2 -2
  28. rasa/core/channels/hangouts.py +8 -5
  29. rasa/core/channels/mattermost.py +1 -1
  30. rasa/core/channels/rasa_chat.py +2 -4
  31. rasa/core/channels/rest.py +5 -4
  32. rasa/core/channels/studio_chat.py +3 -2
  33. rasa/core/channels/vier_cvg.py +1 -2
  34. rasa/core/channels/voice_ready/audiocodes.py +1 -8
  35. rasa/core/channels/voice_stream/audiocodes.py +7 -4
  36. rasa/core/channels/voice_stream/genesys.py +2 -2
  37. rasa/core/channels/voice_stream/twilio_media_streams.py +10 -5
  38. rasa/core/channels/voice_stream/voice_channel.py +33 -22
  39. rasa/core/http_interpreter.py +3 -7
  40. rasa/core/jobs.py +2 -1
  41. rasa/core/nlg/contextual_response_rephraser.py +38 -11
  42. rasa/core/nlg/generator.py +0 -1
  43. rasa/core/nlg/interpolator.py +2 -3
  44. rasa/core/nlg/summarize.py +39 -5
  45. rasa/core/policies/enterprise_search_policy.py +290 -66
  46. rasa/core/policies/enterprise_search_prompt_with_relevancy_check_and_citation_template.jinja2 +63 -0
  47. rasa/core/policies/flow_policy.py +1 -1
  48. rasa/core/policies/flows/flow_executor.py +96 -17
  49. rasa/core/policies/intentless_policy.py +24 -16
  50. rasa/core/processor.py +104 -51
  51. rasa/core/run.py +33 -11
  52. rasa/core/tracker_stores/tracker_store.py +1 -1
  53. rasa/core/training/interactive.py +1 -1
  54. rasa/core/utils.py +24 -97
  55. rasa/dialogue_understanding/coexistence/intent_based_router.py +2 -1
  56. rasa/dialogue_understanding/coexistence/llm_based_router.py +8 -3
  57. rasa/dialogue_understanding/commands/can_not_handle_command.py +2 -0
  58. rasa/dialogue_understanding/commands/cancel_flow_command.py +2 -0
  59. rasa/dialogue_understanding/commands/chit_chat_answer_command.py +2 -0
  60. rasa/dialogue_understanding/commands/clarify_command.py +5 -1
  61. rasa/dialogue_understanding/commands/command_syntax_manager.py +1 -0
  62. rasa/dialogue_understanding/commands/human_handoff_command.py +2 -0
  63. rasa/dialogue_understanding/commands/knowledge_answer_command.py +2 -0
  64. rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +2 -0
  65. rasa/dialogue_understanding/commands/set_slot_command.py +11 -1
  66. rasa/dialogue_understanding/commands/skip_question_command.py +2 -0
  67. rasa/dialogue_understanding/commands/start_flow_command.py +4 -0
  68. rasa/dialogue_understanding/commands/utils.py +26 -2
  69. rasa/dialogue_understanding/generator/__init__.py +7 -1
  70. rasa/dialogue_understanding/generator/command_generator.py +4 -2
  71. rasa/dialogue_understanding/generator/command_parser.py +2 -2
  72. rasa/dialogue_understanding/generator/command_parser_validator.py +63 -0
  73. rasa/dialogue_understanding/generator/constants.py +2 -2
  74. rasa/dialogue_understanding/generator/prompt_templates/command_prompt_v3_gpt_4o_2024_11_20_template.jinja2 +78 -0
  75. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +28 -463
  76. rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +147 -0
  77. rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +477 -0
  78. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +8 -58
  79. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +37 -25
  80. rasa/dialogue_understanding/patterns/domain_for_patterns.py +190 -0
  81. rasa/dialogue_understanding/processor/command_processor.py +3 -3
  82. rasa/dialogue_understanding/processor/command_processor_component.py +3 -3
  83. rasa/dialogue_understanding/stack/frames/flow_stack_frame.py +17 -4
  84. rasa/dialogue_understanding/utils.py +68 -12
  85. rasa/dialogue_understanding_test/du_test_case.py +1 -1
  86. rasa/dialogue_understanding_test/du_test_runner.py +4 -22
  87. rasa/dialogue_understanding_test/test_case_simulation/test_case_tracker_simulator.py +2 -6
  88. rasa/e2e_test/e2e_test_runner.py +1 -1
  89. rasa/engine/constants.py +1 -1
  90. rasa/engine/recipes/default_recipe.py +26 -2
  91. rasa/engine/validation.py +3 -2
  92. rasa/hooks.py +0 -28
  93. rasa/llm_fine_tuning/annotation_module.py +39 -9
  94. rasa/llm_fine_tuning/conversations.py +3 -0
  95. rasa/llm_fine_tuning/llm_data_preparation_module.py +66 -49
  96. rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +4 -2
  97. rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +52 -44
  98. rasa/llm_fine_tuning/paraphrasing_module.py +10 -12
  99. rasa/llm_fine_tuning/storage.py +4 -4
  100. rasa/llm_fine_tuning/utils.py +63 -1
  101. rasa/model_manager/model_api.py +88 -0
  102. rasa/model_manager/trainer_service.py +4 -4
  103. rasa/plugin.py +1 -11
  104. rasa/privacy/__init__.py +0 -0
  105. rasa/privacy/constants.py +83 -0
  106. rasa/privacy/event_broker_utils.py +77 -0
  107. rasa/privacy/privacy_config.py +281 -0
  108. rasa/privacy/privacy_config_schema.json +86 -0
  109. rasa/privacy/privacy_filter.py +340 -0
  110. rasa/privacy/privacy_manager.py +576 -0
  111. rasa/server.py +23 -2
  112. rasa/shared/constants.py +6 -0
  113. rasa/shared/core/constants.py +4 -3
  114. rasa/shared/core/domain.py +7 -0
  115. rasa/shared/core/events.py +37 -7
  116. rasa/shared/core/flows/flow.py +1 -2
  117. rasa/shared/core/flows/flows_yaml_schema.json +3 -0
  118. rasa/shared/core/flows/steps/collect.py +46 -2
  119. rasa/shared/core/slots.py +28 -0
  120. rasa/shared/exceptions.py +4 -0
  121. rasa/shared/providers/_configs/azure_openai_client_config.py +4 -0
  122. rasa/shared/providers/_configs/openai_client_config.py +4 -0
  123. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +3 -0
  124. rasa/shared/providers/llm/_base_litellm_client.py +5 -2
  125. rasa/shared/utils/llm.py +161 -6
  126. rasa/shared/utils/yaml.py +32 -0
  127. rasa/studio/data_handler.py +3 -3
  128. rasa/studio/download/download.py +37 -60
  129. rasa/studio/download/flows.py +23 -31
  130. rasa/studio/link.py +200 -0
  131. rasa/studio/pull.py +94 -0
  132. rasa/studio/push.py +131 -0
  133. rasa/studio/upload.py +117 -67
  134. rasa/telemetry.py +82 -25
  135. rasa/tracing/config.py +3 -4
  136. rasa/tracing/constants.py +19 -1
  137. rasa/tracing/instrumentation/attribute_extractors.py +10 -2
  138. rasa/tracing/instrumentation/instrumentation.py +53 -2
  139. rasa/tracing/instrumentation/metrics.py +98 -15
  140. rasa/tracing/metric_instrument_provider.py +75 -3
  141. rasa/utils/common.py +1 -27
  142. rasa/utils/log_utils.py +1 -45
  143. rasa/validator.py +2 -8
  144. rasa/version.py +1 -1
  145. {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/METADATA +5 -6
  146. {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/RECORD +149 -135
  147. rasa/anonymization/__init__.py +0 -2
  148. rasa/anonymization/anonymisation_rule_yaml_reader.py +0 -91
  149. rasa/anonymization/anonymization_pipeline.py +0 -286
  150. rasa/anonymization/anonymization_rule_executor.py +0 -266
  151. rasa/anonymization/anonymization_rule_orchestrator.py +0 -119
  152. rasa/anonymization/schemas/config.yml +0 -47
  153. rasa/anonymization/utils.py +0 -118
  154. {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/NOTICE +0 -0
  155. {rasa_pro-3.13.0.dev20250612.dist-info → rasa_pro-3.13.0.dev20250613.dist-info}/WHEEL +0 -0
  156. {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:
@@ -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, data_paths: List[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
- data_paths (List[Path]): List of paths to training data files
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=data_paths
338
+ domain_path=str(domain_path), training_data_paths=[str(data_path)]
339
339
  )
340
340
 
341
341
  data_paths = []
@@ -1,6 +1,6 @@
1
1
  import argparse
2
2
  from pathlib import Path
3
- from typing import Dict, List, Optional, Tuple
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, data_paths = _prepare_data_and_domain_paths(args)
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, data_paths)
81
+ _handle_download_no_overwrite(handler, domain_path, data_path)
82
82
  else:
83
- _handle_download_with_overwrite(handler, domain_path, data_paths)
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, List[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
- # Prepare data paths.
119
- data_paths: List[Path] = []
120
- for f in args.data:
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
- if data_path.is_file() or data_path.is_dir():
132
- data_paths.append(data_path)
133
- else:
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
- # Remove duplicates while preserving order.
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, data_paths: List[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, data_paths
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(data_paths, handler, data_from_studio, data_local)
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
- data_paths: List[Path],
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
- data_paths: The paths to the data files or directories.
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 data_paths:
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 len(data_paths) == 1:
288
- data_path = data_paths[0]
289
- if data_path.is_file():
290
- _merge_file_data_no_overwrite(
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
- # TODO: Handle multiple data paths.
306
- raise NotImplementedError("Multiple data paths are not supported yet.")
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, data_paths: List[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
- data_paths: The paths to the data files or directories.
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, data_paths
344
+ handler, domain_path, data_path
367
345
  )
368
346
  mapper = RasaPrimitiveStorageMapper(
369
- domain_path=domain_path, training_data_paths=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
- for item in data["nlu"]:
436
- if item.get("examples"):
437
- item["examples"] = LiteralScalarString(item["examples"])
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)
@@ -18,7 +18,7 @@ STUDIO_FLOWS_DIR_NAME = "studio_flows"
18
18
 
19
19
 
20
20
  def merge_flows_with_overwrite(
21
- data_paths: List[Path],
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
- data_paths: List of paths to the training data.
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
- pretty_write_nlu_yaml(read_yaml(nlu_data.nlu_as_yaml()), nlu_file_path)
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
- all_updated_flows: List[Flow] = []
210
+ all_updated_flows_ids: List[Text] = []
215
211
  for flow_file_path in local_flow_paths:
216
- updated_file_flows = _update_flow_file(flow_file_path, studio_flow_map)
217
- all_updated_flows.extend(updated_file_flows)
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 for flow in studio_flow_map.values() if flow not in all_updated_flows
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[Flow]:
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 flows from the updated flow file.
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
- updated_list: List[Any] = []
259
- has_changes = False
260
-
261
- for flow in file_flows.underlying_flows:
262
- studio_flow = studio_flows_map.get(flow.id)
263
- if studio_flow is not None and studio_flow != flow:
264
- updated_list.append(studio_flow)
265
- has_changes = True
266
- else:
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=new_flows_list.underlying_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 file_flows.underlying_flows
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)