rasa-pro 3.13.0.dev3__py3-none-any.whl → 3.13.0.dev5__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 (132) hide show
  1. rasa/__main__.py +3 -1
  2. rasa/cli/inspect.py +8 -4
  3. rasa/cli/project_templates/default/config.yml +5 -32
  4. rasa/cli/project_templates/{calm → default}/e2e_tests/cancelations/user_cancels_during_a_correction.yml +1 -1
  5. rasa/cli/project_templates/{calm → default}/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml +1 -1
  6. rasa/cli/project_templates/{calm → default}/e2e_tests/corrections/user_corrects_contact_handle.yml +1 -1
  7. rasa/cli/project_templates/{calm → default}/e2e_tests/corrections/user_corrects_contact_name.yml +1 -1
  8. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml +1 -1
  9. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_lists_contacts.yml +1 -1
  10. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_removes_contact.yml +1 -1
  11. rasa/cli/project_templates/{calm → default}/e2e_tests/happy_paths/user_removes_contact_from_list.yml +1 -1
  12. rasa/cli/project_templates/default/endpoints.yml +18 -2
  13. rasa/cli/scaffold.py +3 -4
  14. rasa/cli/studio/download.py +1 -1
  15. rasa/cli/studio/upload.py +0 -6
  16. rasa/core/channels/channel.py +68 -5
  17. rasa/core/channels/inspector/dist/assets/{arc-c7691751.js → arc-9f75cc3b.js} +1 -1
  18. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-ab99dff7.js → blockDiagram-38ab4fdb-7f34db23.js} +1 -1
  19. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-08c35a6b.js → c4Diagram-3d4e48cf-948bab2c.js} +1 -1
  20. rasa/core/channels/inspector/dist/assets/channel-dfa68278.js +1 -0
  21. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-9e9c71c9.js → classDiagram-70f12bd4-53b0dd0e.js} +1 -1
  22. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-15e7e2bf.js → classDiagram-v2-f2320105-fdf789e7.js} +1 -1
  23. rasa/core/channels/inspector/dist/assets/clone-edb7f119.js +1 -0
  24. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-9c105cb1.js → createText-2e5e7dd3-87c4ece5.js} +1 -1
  25. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-77e89e48.js → edges-e0da2a9e-5a8b0749.js} +1 -1
  26. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-7a011646.js → erDiagram-9861fffd-66da90e2.js} +1 -1
  27. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-b6f105ac.js → flowDb-956e92f1-10044f05.js} +1 -1
  28. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-ce4f18c2.js → flowDiagram-66a62f08-f338f66a.js} +1 -1
  29. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-65e7c670.js +1 -0
  30. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-cb5f6da4.js → flowchart-elk-definition-4a651766-b13140aa.js} +1 -1
  31. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-e4d19e28.js → ganttDiagram-c361ad54-f2b4a55a.js} +1 -1
  32. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-727b1c33.js → gitGraphDiagram-72cf32ee-dedc298d.js} +1 -1
  33. rasa/core/channels/inspector/dist/assets/{graph-6e2ab9a7.js → graph-4ede11ff.js} +1 -1
  34. rasa/core/channels/inspector/dist/assets/{index-3862675e-84ec700f.js → index-3862675e-65549d37.js} +1 -1
  35. rasa/core/channels/inspector/dist/assets/{index-098a1a24.js → index-3a23e736.js} +142 -129
  36. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-78dda442.js → infoDiagram-f8f76790-65439671.js} +1 -1
  37. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-f1cc6dd1.js → journeyDiagram-49397b02-56d03d98.js} +1 -1
  38. rasa/core/channels/inspector/dist/assets/{layout-d98dcd0c.js → layout-dd48f7f4.js} +1 -1
  39. rasa/core/channels/inspector/dist/assets/{line-838e3d82.js → line-1569ad2c.js} +1 -1
  40. rasa/core/channels/inspector/dist/assets/{linear-eae72406.js → linear-48bf4935.js} +1 -1
  41. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-c96fd84b.js → mindmap-definition-fc14e90a-688504c1.js} +1 -1
  42. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-c936d4e2.js → pieDiagram-8a3498a8-78b6d7e6.js} +1 -1
  43. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-b338eb8f.js → quadrantDiagram-120e2f19-048b84b3.js} +1 -1
  44. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-c6b6c0d5.js → requirementDiagram-deff3bca-dd67f107.js} +1 -1
  45. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-b9372e19.js → sankeyDiagram-04a897e0-8128436e.js} +1 -1
  46. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-479e0a3f.js → sequenceDiagram-704730f1-1a0d1461.js} +1 -1
  47. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-fd26eebc.js → stateDiagram-587899a1-46d388ed.js} +1 -1
  48. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-3233e0ae.js → stateDiagram-v2-d93cdb3a-ea42951a.js} +1 -1
  49. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-1fdd392b.js → styles-6aaf32cf-7427ed0c.js} +1 -1
  50. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-6d7bfa1b.js → styles-9a916d00-ff5e5a16.js} +1 -1
  51. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-f86aab11.js → styles-c10674c1-7b3680cf.js} +1 -1
  52. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-e3e49d7a.js → svgDrawCommon-08f97a94-f860f2ad.js} +1 -1
  53. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-6fe08b4d.js → timeline-definition-85554ec2-2eebf0c8.js} +1 -1
  54. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-c2e06fd6.js → xychartDiagram-e933f94c-5d7f4e96.js} +1 -1
  55. rasa/core/channels/inspector/dist/index.html +1 -1
  56. rasa/core/channels/inspector/src/App.tsx +3 -2
  57. rasa/core/channels/inspector/src/components/Chat.tsx +23 -2
  58. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +2 -5
  59. rasa/core/channels/inspector/src/helpers/conversation.ts +16 -0
  60. rasa/core/channels/inspector/src/types.ts +1 -1
  61. rasa/core/channels/voice_ready/audiocodes.py +41 -15
  62. rasa/core/channels/voice_ready/twilio_voice.py +48 -1
  63. rasa/core/channels/voice_stream/tts/azure.py +11 -2
  64. rasa/core/channels/voice_stream/twilio_media_streams.py +101 -26
  65. rasa/core/channels/voice_stream/voice_channel.py +28 -2
  66. rasa/core/concurrent_lock_store.py +24 -10
  67. rasa/core/lock_store.py +151 -60
  68. rasa/dialogue_understanding_test/du_test_case.py +16 -8
  69. rasa/plugin.py +0 -3
  70. rasa/shared/constants.py +1 -0
  71. rasa/shared/core/domain.py +165 -11
  72. rasa/shared/core/flows/flow.py +155 -131
  73. rasa/shared/core/flows/flow_step.py +19 -3
  74. rasa/shared/core/flows/flow_step_links.py +15 -0
  75. rasa/shared/core/flows/flow_step_sequence.py +6 -0
  76. rasa/shared/core/flows/nlu_trigger.py +13 -0
  77. rasa/shared/core/flows/steps/action.py +7 -4
  78. rasa/shared/core/flows/steps/call.py +11 -4
  79. rasa/shared/core/flows/steps/collect.py +27 -6
  80. rasa/shared/core/flows/steps/internal.py +6 -1
  81. rasa/shared/core/flows/steps/link.py +7 -4
  82. rasa/shared/core/flows/steps/no_operation.py +7 -4
  83. rasa/shared/core/flows/steps/set_slots.py +8 -4
  84. rasa/shared/core/flows/yaml_flows_io.py +106 -5
  85. rasa/shared/importers/importer.py +8 -0
  86. rasa/shared/providers/_utils.py +83 -0
  87. rasa/shared/providers/llm/_base_litellm_client.py +6 -3
  88. rasa/shared/providers/llm/azure_openai_llm_client.py +6 -68
  89. rasa/shared/providers/router/_base_litellm_router_client.py +53 -1
  90. rasa/shared/utils/common.py +42 -0
  91. rasa/studio/download/domains.py +49 -0
  92. rasa/studio/download/download.py +439 -0
  93. rasa/studio/download/flows.py +359 -0
  94. rasa/studio/results_logger.py +6 -1
  95. rasa/studio/upload.py +69 -5
  96. rasa/utils/common.py +36 -0
  97. rasa/utils/endpoints.py +22 -1
  98. rasa/utils/licensing.py +1 -1
  99. rasa/validator.py +1 -2
  100. rasa/version.py +1 -1
  101. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/METADATA +8 -8
  102. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/RECORD +119 -125
  103. rasa/cli/project_templates/calm/config.yml +0 -10
  104. rasa/cli/project_templates/calm/credentials.yml +0 -33
  105. rasa/cli/project_templates/calm/endpoints.yml +0 -58
  106. rasa/cli/project_templates/default/actions/actions.py +0 -27
  107. rasa/cli/project_templates/default/data/nlu.yml +0 -91
  108. rasa/cli/project_templates/default/data/rules.yml +0 -13
  109. rasa/cli/project_templates/default/data/stories.yml +0 -30
  110. rasa/cli/project_templates/default/domain.yml +0 -34
  111. rasa/cli/project_templates/default/tests/test_stories.yml +0 -91
  112. rasa/core/channels/inspector/dist/assets/channel-11268142.js +0 -1
  113. rasa/core/channels/inspector/dist/assets/clone-ff7f2ce7.js +0 -1
  114. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-cba7ae20.js +0 -1
  115. rasa/studio/download.py +0 -489
  116. /rasa/cli/project_templates/{calm → default}/actions/action_template.py +0 -0
  117. /rasa/cli/project_templates/{calm → default}/actions/add_contact.py +0 -0
  118. /rasa/cli/project_templates/{calm → default}/actions/db.py +0 -0
  119. /rasa/cli/project_templates/{calm → default}/actions/list_contacts.py +0 -0
  120. /rasa/cli/project_templates/{calm → default}/actions/remove_contact.py +0 -0
  121. /rasa/cli/project_templates/{calm → default}/data/flows/add_contact.yml +0 -0
  122. /rasa/cli/project_templates/{calm → default}/data/flows/list_contacts.yml +0 -0
  123. /rasa/cli/project_templates/{calm → default}/data/flows/remove_contact.yml +0 -0
  124. /rasa/cli/project_templates/{calm → default}/db/contacts.json +0 -0
  125. /rasa/cli/project_templates/{calm → default}/domain/add_contact.yml +0 -0
  126. /rasa/cli/project_templates/{calm → default}/domain/list_contacts.yml +0 -0
  127. /rasa/cli/project_templates/{calm → default}/domain/remove_contact.yml +0 -0
  128. /rasa/cli/project_templates/{calm → default}/domain/shared.yml +0 -0
  129. /rasa/{cli/project_templates/calm/actions → studio/download}/__init__.py +0 -0
  130. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/NOTICE +0 -0
  131. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/WHEEL +0 -0
  132. {rasa_pro-3.13.0.dev3.dist-info → rasa_pro-3.13.0.dev5.dist-info}/entry_points.txt +0 -0
@@ -34,17 +34,20 @@ class NoOperationFlowStep(FlowStep):
34
34
  **base.__dict__,
35
35
  )
36
36
 
37
- def as_json(self) -> Dict[Text, Any]:
37
+ def as_json(self) -> Dict[Text, Any]: # type: ignore[override]
38
38
  """Serialize the NoOperationFlowStep object
39
39
 
40
40
  Returns:
41
41
  the NoOperationFlowStep object as serialized data.
42
42
  """
43
- data = super().as_json()
44
- data["noop"] = self.noop
45
- return data
43
+ return super().as_json(step_properties={"noop": self.noop})
46
44
 
47
45
  @property
48
46
  def default_id_postfix(self) -> str:
49
47
  """Returns the default id postfix of the flow step."""
50
48
  return "noop"
49
+
50
+ def __eq__(self, other: object) -> bool:
51
+ if isinstance(other, type(self)):
52
+ return self.noop == other.noop and super().__eq__(other)
53
+ return False
@@ -35,17 +35,21 @@ class SetSlotsFlowStep(FlowStep):
35
35
  **base.__dict__,
36
36
  )
37
37
 
38
- def as_json(self) -> Dict[Text, Any]:
38
+ def as_json(self) -> Dict[Text, Any]: # type: ignore[override]
39
39
  """Serialize the SetSlotsFlowStep object
40
40
 
41
41
  Returns:
42
42
  the SetSlotsFlowStep object as serialized data
43
43
  """
44
- data = super().as_json()
45
- data["set_slots"] = [{slot["key"]: slot["value"]} for slot in self.slots]
46
- return data
44
+ set_slots = [{slot["key"]: slot["value"]} for slot in self.slots]
45
+ return super().as_json(step_properties={"set_slots": set_slots})
47
46
 
48
47
  @property
49
48
  def default_id_postfix(self) -> str:
50
49
  """Returns the default id postfix of the flow step."""
51
50
  return "set_slots"
51
+
52
+ def __eq__(self, other: object) -> bool:
53
+ if isinstance(other, type(self)):
54
+ return self.slots == other.slots and super().__eq__(other)
55
+ return False
@@ -8,8 +8,15 @@ from ruamel import yaml as yaml
8
8
  import rasa.shared
9
9
  import rasa.shared.data
10
10
  import rasa.shared.utils.io
11
- from rasa.shared.core.flows.flow import Flow
11
+ from rasa.shared.core.flows.flow import DEFAULT_RUN_PATTERN_COMPLETED, Flow
12
+ from rasa.shared.core.flows.flow_step import FlowStep
12
13
  from rasa.shared.core.flows.flows_list import FlowsList
14
+ from rasa.shared.core.flows.steps import CollectInformationFlowStep
15
+ from rasa.shared.core.flows.steps.collect import (
16
+ DEFAULT_ASK_BEFORE_FILLING,
17
+ DEFAULT_FORCE_SLOT_FILLING,
18
+ DEFAULT_RESET_AFTER_FLOW_ENDS,
19
+ )
13
20
  from rasa.shared.exceptions import RasaException, YamlException
14
21
  from rasa.shared.utils.yaml import (
15
22
  dump_obj_as_yaml_to_string,
@@ -242,31 +249,43 @@ class YamlFlowsWriter:
242
249
  """Class that writes flows information in YAML format."""
243
250
 
244
251
  @staticmethod
245
- def dumps(flows: List[Flow]) -> Text:
252
+ def dumps(
253
+ flows: List[Flow],
254
+ should_clean_json: bool = False,
255
+ ) -> Text:
246
256
  """Dump `Flow`s to YAML.
247
257
 
248
258
  Args:
249
259
  flows: The `Flow`s to dump.
260
+ should_clean_json: Flag whether to clean the flow JSON.
250
261
 
251
262
  Returns:
252
263
  The dumped YAML.
253
264
  """
254
265
  dump = {}
255
266
  for flow in flows:
256
- dumped_flow = flow.as_json()
267
+ dumped_flow = get_flow_as_json(flow, should_clean_json)
257
268
  del dumped_flow["id"]
258
269
  dump[flow.id] = dumped_flow
259
270
  return dump_obj_as_yaml_to_string({KEY_FLOWS: dump})
260
271
 
261
272
  @staticmethod
262
- def dump(flows: List[Flow], filename: Union[Text, Path]) -> None:
273
+ def dump(
274
+ flows: List[Flow],
275
+ filename: Union[Text, Path],
276
+ should_clean_json: bool = False,
277
+ ) -> None:
263
278
  """Dump `Flow`s to YAML file.
264
279
 
265
280
  Args:
266
281
  flows: The `Flow`s to dump.
267
282
  filename: The path to the file to write to.
283
+ should_clean_json: Flag whether to clean the flow JSON.
268
284
  """
269
- rasa.shared.utils.io.write_text_file(YamlFlowsWriter.dumps(flows), filename)
285
+ rasa.shared.utils.io.write_text_file(
286
+ YamlFlowsWriter.dumps(flows, should_clean_json),
287
+ filename,
288
+ )
270
289
 
271
290
 
272
291
  def is_flows_file(file_path: Union[Text, Path]) -> bool:
@@ -403,3 +422,85 @@ def process_yaml_content(yaml_content: Dict[str, Any]) -> Dict[str, Any]:
403
422
  )
404
423
 
405
424
  return yaml_content
425
+
426
+
427
+ 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.
430
+
431
+ Args:
432
+ flow: The Flow object to clean.
433
+ should_clean_json: Flag indicating whether to clean the JSON.
434
+
435
+ Returns:
436
+ The cleaned Flow JSON as a dictionary.
437
+ """
438
+ step_id_to_default_id = {}
439
+ flow_data = flow.as_json()
440
+ if not should_clean_json:
441
+ return flow_data
442
+
443
+ def gather_step_ids(step: FlowStep) -> None:
444
+ """Create a map of step IDs to their default IDs."""
445
+ step_id_to_default_id[step.id] = step.default_id
446
+ for link in step.next.links:
447
+ for child in link.child_steps():
448
+ gather_step_ids(child)
449
+
450
+ def clean_flow_data(data: Dict[str, Any]) -> None:
451
+ """Remove or omit flow-level fields if they match defaults."""
452
+ for top_level_step in flow.step_sequence.steps:
453
+ gather_step_ids(top_level_step)
454
+
455
+ if data.get("run_pattern_completed") == DEFAULT_RUN_PATTERN_COMPLETED:
456
+ data.pop("run_pattern_completed", None)
457
+
458
+ if not data.get("persisted_slots"):
459
+ data.pop("persisted_slots", None)
460
+
461
+ _remove_keys_recursively(data, ["metadata"])
462
+
463
+ data.pop("file_path", None)
464
+
465
+ steps = data.get("steps", [])
466
+ for i, current_step in enumerate(steps):
467
+ # Look ahead to the next step if we should remove the 'next' field
468
+ next_step_data = steps[i + 1] if i + 1 < len(steps) else None
469
+ clean_step(current_step, next_step_data)
470
+
471
+ def clean_step(current_step: Dict[str, Any], next_step: Dict[str, Any]) -> None:
472
+ """Remove default fields from a step."""
473
+ # Remove 'next' if it exactly matches the next step's default ID
474
+ if next_step:
475
+ next_id = next_step.get("id")
476
+ default_id = step_id_to_default_id.get(next_id)
477
+ if next_id and current_step.get("next") == default_id:
478
+ current_step.pop("next", None)
479
+
480
+ # Remove 'id' if it equals its own default
481
+ step_id = current_step.get("id")
482
+ if step_id and step_id == step_id_to_default_id.get(step_id):
483
+ current_step.pop("id", None)
484
+
485
+ if "collect" in current_step:
486
+ clean_collect_step(current_step)
487
+
488
+ def clean_collect_step(step_data: Dict[str, Any]) -> None:
489
+ """Remove default fields from a collect step."""
490
+ slot_name = step_data["collect"]
491
+ default_utter = CollectInformationFlowStep._default_utter(slot_name)
492
+
493
+ if step_data.get("utter") == default_utter:
494
+ step_data.pop("utter", None)
495
+ if step_data.get("ask_before_filling") is DEFAULT_ASK_BEFORE_FILLING:
496
+ step_data.pop("ask_before_filling", None)
497
+ if step_data.get("reset_after_flow_ends") is DEFAULT_RESET_AFTER_FLOW_ENDS:
498
+ step_data.pop("reset_after_flow_ends", None)
499
+ if step_data.get("force_slot_filling") is DEFAULT_FORCE_SLOT_FILLING:
500
+ step_data.pop("force_slot_filling", None)
501
+ if not step_data.get("rejections"):
502
+ step_data.pop("rejections", None)
503
+
504
+ clean_flow_data(flow_data)
505
+
506
+ return flow_data
@@ -18,6 +18,7 @@ import importlib_resources
18
18
 
19
19
  import rasa.shared.constants
20
20
  import rasa.shared.core.constants
21
+ import rasa.shared.data
21
22
  import rasa.shared.utils.common
22
23
  import rasa.shared.utils.io
23
24
  from rasa.shared.constants import CONFIG_ADDITIONAL_LANGUAGES_KEY, CONFIG_LANGUAGE_KEY
@@ -279,6 +280,13 @@ class TrainingDataImporter(ABC):
279
280
  """
280
281
  raise NotImplementedError
281
282
 
283
+ @staticmethod
284
+ def get_domain_files(
285
+ data_paths: Optional[Union[List[Text], Text]] = None,
286
+ ) -> List[Text]:
287
+ """Returns the domain file path (see parent class for full docstring)."""
288
+ return rasa.shared.data.get_data_files(data_paths, Domain.is_domain_file)
289
+
282
290
 
283
291
  class NluDataImporter(TrainingDataImporter):
284
292
  """Importer that skips any Core-related file reading."""
@@ -1,7 +1,11 @@
1
+ from typing import Any, Dict, Optional
2
+
1
3
  import structlog
2
4
  from litellm import validate_environment
3
5
 
4
6
  from rasa.shared.constants import (
7
+ API_BASE_CONFIG_KEY,
8
+ API_VERSION_CONFIG_KEY,
5
9
  AWS_ACCESS_KEY_ID_CONFIG_KEY,
6
10
  AWS_ACCESS_KEY_ID_ENV_VAR,
7
11
  AWS_REGION_NAME_CONFIG_KEY,
@@ -10,6 +14,9 @@ from rasa.shared.constants import (
10
14
  AWS_SECRET_ACCESS_KEY_ENV_VAR,
11
15
  AWS_SESSION_TOKEN_CONFIG_KEY,
12
16
  AWS_SESSION_TOKEN_ENV_VAR,
17
+ AZURE_API_BASE_ENV_VAR,
18
+ AZURE_API_VERSION_ENV_VAR,
19
+ DEPLOYMENT_CONFIG_KEY,
13
20
  )
14
21
  from rasa.shared.exceptions import ProviderClientValidationError
15
22
  from rasa.shared.providers.embedding._base_litellm_embedding_client import (
@@ -77,3 +84,79 @@ def validate_aws_setup_for_litellm_clients(
77
84
  missing_environment_variables=missing_environment_variables,
78
85
  )
79
86
  raise ProviderClientValidationError(event_info)
87
+
88
+
89
+ def validate_azure_client_setup(
90
+ api_base: Optional[str],
91
+ api_version: Optional[str],
92
+ deployment: Optional[str],
93
+ ) -> None:
94
+ """Validates the Azure setup for LiteLLM Router clients to ensure
95
+ that all required configuration parameters are set.
96
+ Raises:
97
+ ProviderClientValidationError: If any required Azure configurations
98
+ is missing.
99
+ """
100
+
101
+ def generate_event_info_for_missing_setting(
102
+ setting: str,
103
+ setting_env_var: Optional[str] = None,
104
+ setting_config_key: Optional[str] = None,
105
+ ) -> str:
106
+ """Generate a part of the message with instructions on what to set
107
+ for the missing client setting.
108
+ """
109
+ info = "Set {setting} with {options}. "
110
+ options = ""
111
+ if setting_env_var is not None:
112
+ options += f"environment variable '{setting_env_var}'"
113
+ if setting_config_key is not None and setting_env_var is not None:
114
+ options += " or "
115
+ if setting_config_key is not None:
116
+ options += f"config key '{setting_config_key}'"
117
+
118
+ return info.format(setting=setting, options=options)
119
+
120
+ # All required settings for Azure OpenAI client
121
+ settings: Dict[str, Dict[str, Any]] = {
122
+ "API Base": {
123
+ "current_value": api_base,
124
+ "env_var": AZURE_API_BASE_ENV_VAR,
125
+ "config_key": API_BASE_CONFIG_KEY,
126
+ },
127
+ "API Version": {
128
+ "current_value": api_version,
129
+ "env_var": AZURE_API_VERSION_ENV_VAR,
130
+ "config_key": API_VERSION_CONFIG_KEY,
131
+ },
132
+ "Deployment Name": {
133
+ "current_value": deployment,
134
+ "env_var": None,
135
+ "config_key": DEPLOYMENT_CONFIG_KEY,
136
+ },
137
+ }
138
+
139
+ missing_settings = [
140
+ setting_name
141
+ for setting_name, setting_info in settings.items()
142
+ if setting_info["current_value"] is None
143
+ ]
144
+
145
+ if missing_settings:
146
+ event_info = f"Client settings not set: {', '.join(missing_settings)}. "
147
+
148
+ for missing_setting in missing_settings:
149
+ if settings[missing_setting]["current_value"] is not None:
150
+ continue
151
+ event_info += generate_event_info_for_missing_setting(
152
+ missing_setting,
153
+ settings[missing_setting]["env_var"],
154
+ settings[missing_setting]["config_key"],
155
+ )
156
+
157
+ structlogger.error(
158
+ "azure_openai_llm_client.not_configured",
159
+ event_info=event_info,
160
+ missing_settings=missing_settings,
161
+ )
162
+ raise ProviderClientValidationError(event_info)
@@ -7,7 +7,12 @@ from typing import Any, Dict, List, Union, cast
7
7
  import structlog
8
8
  from litellm import acompletion, completion, validate_environment
9
9
 
10
- from rasa.shared.constants import API_BASE_CONFIG_KEY, API_KEY, ROLE_USER
10
+ from rasa.shared.constants import (
11
+ _VALIDATE_ENVIRONMENT_MISSING_KEYS_KEY,
12
+ API_BASE_CONFIG_KEY,
13
+ API_KEY,
14
+ ROLE_USER,
15
+ )
11
16
  from rasa.shared.exceptions import (
12
17
  ProviderClientAPIException,
13
18
  ProviderClientValidationError,
@@ -21,8 +26,6 @@ from rasa.shared.utils.io import resolve_environment_variables, suppress_logs
21
26
 
22
27
  structlogger = structlog.get_logger()
23
28
 
24
- _VALIDATE_ENVIRONMENT_MISSING_KEYS_KEY = "missing_keys"
25
-
26
29
  # Suppress LiteLLM info and debug logs - Global level.
27
30
  logging.getLogger("LiteLLM").setLevel(logging.WARNING)
28
31
 
@@ -7,15 +7,12 @@ from typing import Any, Dict, Optional
7
7
  import structlog
8
8
 
9
9
  from rasa.shared.constants import (
10
- API_BASE_CONFIG_KEY,
11
10
  API_KEY,
12
- API_VERSION_CONFIG_KEY,
13
11
  AZURE_API_BASE_ENV_VAR,
14
12
  AZURE_API_KEY_ENV_VAR,
15
13
  AZURE_API_TYPE_ENV_VAR,
16
14
  AZURE_API_VERSION_ENV_VAR,
17
15
  AZURE_OPENAI_PROVIDER,
18
- DEPLOYMENT_CONFIG_KEY,
19
16
  OPENAI_API_BASE_ENV_VAR,
20
17
  OPENAI_API_KEY_ENV_VAR,
21
18
  OPENAI_API_TYPE_ENV_VAR,
@@ -26,6 +23,7 @@ from rasa.shared.providers._configs.azure_openai_client_config import (
26
23
  AzureEntraIDOAuthConfig,
27
24
  AzureOpenAIClientConfig,
28
25
  )
26
+ from rasa.shared.providers._utils import validate_azure_client_setup
29
27
  from rasa.shared.providers.constants import (
30
28
  DEFAULT_AZURE_API_KEY_NAME,
31
29
  LITE_LLM_API_BASE_FIELD,
@@ -348,68 +346,8 @@ class AzureOpenAILLMClient(_BaseLiteLLMClient):
348
346
  def validate_client_setup(self) -> None:
349
347
  """Validates that all required configuration parameters are set."""
350
348
 
351
- def generate_event_info_for_missing_setting(
352
- setting: str,
353
- setting_env_var: Optional[str] = None,
354
- setting_config_key: Optional[str] = None,
355
- ) -> str:
356
- """Generate a part of the message with instructions on what to set
357
- for the missing client setting.
358
- """
359
- info = "Set {setting} with {options}. "
360
- options = ""
361
- if setting_env_var is not None:
362
- options += f"environment variable '{setting_env_var}'"
363
- if setting_config_key is not None and setting_env_var is not None:
364
- options += " or "
365
- if setting_config_key is not None:
366
- options += f"config key '{setting_config_key}'"
367
-
368
- return info.format(setting=setting, options=options)
369
-
370
- env_var_field = "env_var"
371
- config_key_field = "config_key"
372
- current_value_field = "current_value"
373
- # All required settings for Azure OpenAI client
374
- settings: Dict[str, Dict[str, Any]] = {
375
- "API Base": {
376
- current_value_field: self.api_base,
377
- env_var_field: AZURE_API_BASE_ENV_VAR,
378
- config_key_field: API_BASE_CONFIG_KEY,
379
- },
380
- "API Version": {
381
- current_value_field: self.api_version,
382
- env_var_field: AZURE_API_VERSION_ENV_VAR,
383
- config_key_field: API_VERSION_CONFIG_KEY,
384
- },
385
- "Deployment Name": {
386
- current_value_field: self.deployment,
387
- env_var_field: None,
388
- config_key_field: DEPLOYMENT_CONFIG_KEY,
389
- },
390
- }
391
-
392
- missing_settings = [
393
- setting_name
394
- for setting_name, setting_info in settings.items()
395
- if setting_info[current_value_field] is None
396
- ]
397
-
398
- if missing_settings:
399
- event_info = f"Client settings not set: " f"{', '.join(missing_settings)}. "
400
-
401
- for missing_setting in missing_settings:
402
- if settings[missing_setting][current_value_field] is not None:
403
- continue
404
- event_info += generate_event_info_for_missing_setting(
405
- missing_setting,
406
- settings[missing_setting][env_var_field],
407
- settings[missing_setting][config_key_field],
408
- )
409
-
410
- structlogger.error(
411
- "azure_openai_llm_client.not_configured",
412
- event_info=event_info,
413
- missing_settings=missing_settings,
414
- )
415
- raise ProviderClientValidationError(event_info)
349
+ return validate_azure_client_setup(
350
+ api_base=self.api_base,
351
+ api_version=self.api_version,
352
+ deployment=self.deployment,
353
+ )
@@ -5,10 +5,16 @@ from copy import deepcopy
5
5
  from typing import Any, Dict, List
6
6
 
7
7
  import structlog
8
- from litellm import Router
8
+ from litellm import Router, validate_environment
9
9
 
10
10
  from rasa.shared.constants import (
11
+ _VALIDATE_ENVIRONMENT_MISSING_KEYS_KEY,
12
+ API_BASE_CONFIG_KEY,
11
13
  API_KEY,
14
+ API_VERSION_CONFIG_KEY,
15
+ AZURE_API_BASE_ENV_VAR,
16
+ AZURE_API_VERSION_ENV_VAR,
17
+ AZURE_OPENAI_PROVIDER,
12
18
  LITELLM_PARAMS_KEY,
13
19
  MODEL_CONFIG_KEY,
14
20
  MODEL_GROUP_ID_CONFIG_KEY,
@@ -23,6 +29,7 @@ from rasa.shared.providers._configs.azure_entra_id_config import AzureEntraIDOAu
23
29
  from rasa.shared.providers._configs.litellm_router_client_config import (
24
30
  LiteLLMRouterClientConfig,
25
31
  )
32
+ from rasa.shared.providers._utils import validate_azure_client_setup
26
33
  from rasa.shared.utils.io import resolve_environment_variables
27
34
 
28
35
  structlogger = structlog.get_logger()
@@ -183,6 +190,7 @@ class _BaseLiteLLMRouterClient:
183
190
 
184
191
  def _create_router_client(self) -> Router:
185
192
  resolved_model_configurations = self._resolve_env_vars_in_model_configurations()
193
+ self._validate_model_configurations(resolved_model_configurations)
186
194
  return Router(model_list=resolved_model_configurations, **self.router_settings)
187
195
 
188
196
  def _has_oauth(self) -> bool:
@@ -214,3 +222,47 @@ class _BaseLiteLLMRouterClient:
214
222
  )
215
223
  model_configuration_with_resolved_keys.append(resolved_model_configuration)
216
224
  return model_configuration_with_resolved_keys
225
+
226
+ def _validate_model_configurations(
227
+ self, resolved_model_configurations: List[Dict[str, Any]]
228
+ ) -> None:
229
+ """Validates the model configurations.
230
+ Args:
231
+ resolved_model_configurations: (List[Dict[str, Any]]) The list of model
232
+ configurations with resolved environment variables.
233
+ Raises:
234
+ ProviderClientValidationError: If the model configurations are invalid.
235
+ """
236
+ for model_configuration in resolved_model_configurations:
237
+ litellm_params = model_configuration.get(LITELLM_PARAMS_KEY, {})
238
+
239
+ model = litellm_params.get(MODEL_CONFIG_KEY)
240
+ provider, deployment = model.split("/", 1)
241
+ api_base = litellm_params.get(API_BASE_CONFIG_KEY)
242
+
243
+ if provider.lower() == AZURE_OPENAI_PROVIDER:
244
+ validate_azure_client_setup(
245
+ api_base=api_base or os.getenv(AZURE_API_BASE_ENV_VAR),
246
+ api_version=litellm_params.get(API_VERSION_CONFIG_KEY)
247
+ or os.getenv(AZURE_API_VERSION_ENV_VAR),
248
+ deployment=deployment,
249
+ )
250
+ else:
251
+ validation_info = validate_environment(
252
+ model=model,
253
+ api_key=litellm_params.get(API_KEY),
254
+ api_base=api_base,
255
+ )
256
+ if missing_environment_variables := validation_info.get(
257
+ _VALIDATE_ENVIRONMENT_MISSING_KEYS_KEY
258
+ ):
259
+ event_info = (
260
+ f"Environment variables: {missing_environment_variables} "
261
+ f"not set. Required for API calls."
262
+ )
263
+ structlogger.error(
264
+ "base_litellm_router_client.validate_environment_variables",
265
+ event_info=event_info,
266
+ missing_environment_variables=missing_environment_variables,
267
+ )
268
+ raise ProviderClientValidationError(event_info)
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import copy
2
3
  import functools
3
4
  import importlib
4
5
  import inspect
@@ -288,6 +289,47 @@ def merge_lists_of_dicts(
288
289
  return list(merged_dicts.values())
289
290
 
290
291
 
292
+ def partial_merge_list(
293
+ self_list: List[Any],
294
+ other_list: List[Any],
295
+ is_same_item_fn: Callable[[Any, Any], bool],
296
+ ) -> List[Any]:
297
+ """Merges two lists based on a custom intersection logic."""
298
+ matched_other_indices = set()
299
+ result = []
300
+
301
+ for s_item in self_list:
302
+ match = next(
303
+ (
304
+ (i, o_item)
305
+ for i, o_item in enumerate(other_list)
306
+ if i not in matched_other_indices and is_same_item_fn(s_item, o_item)
307
+ ),
308
+ None,
309
+ )
310
+ if match:
311
+ i, o_item = match
312
+ result.append(copy.deepcopy(o_item))
313
+ matched_other_indices.add(i)
314
+ else:
315
+ result.append(copy.deepcopy(s_item))
316
+
317
+ return result
318
+
319
+
320
+ def partial_merge_dict(
321
+ self_d: Dict[Text, Any], other_d: Dict[Text, Any]
322
+ ) -> Dict[Text, Any]:
323
+ """Merges two dictionaries based on a custom intersection logic."""
324
+ merged = {}
325
+ for k, v in self_d.items():
326
+ if k in other_d:
327
+ merged[k] = copy.deepcopy(other_d[k])
328
+ else:
329
+ merged[k] = copy.deepcopy(v)
330
+ return merged
331
+
332
+
291
333
  def warn_and_exit_if_module_path_contains_rasa_plus(
292
334
  module_path: Text, lookup_path: Optional[str] = None
293
335
  ) -> None:
@@ -0,0 +1,49 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from rasa.shared.core.domain import (
5
+ Domain,
6
+ )
7
+ from rasa.shared.importers.importer import TrainingDataImporter
8
+ from rasa.studio.constants import STUDIO_DOMAIN_FILENAME
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def merge_domain_with_overwrite(
14
+ data_from_studio: TrainingDataImporter,
15
+ data_local: TrainingDataImporter,
16
+ domain_path: Path,
17
+ ) -> None:
18
+ """
19
+ Merges the domain from Rasa Studio with the local domain.
20
+
21
+ Args:
22
+ data_from_studio: The training data importer for the Rasa Studio domain.
23
+ data_local: The training data importer for the local domain.
24
+ domain_path: The path to the domain file or directory.
25
+ """
26
+ if domain_path.is_file():
27
+ all_local_domain_files = [str(domain_path)]
28
+ domain_path = domain_path.parent
29
+ else:
30
+ all_local_domain_files = data_local.get_domain_files([str(domain_path)])
31
+
32
+ # leftover_domain represents the items in the studio
33
+ # domain that are not in the local domain
34
+ leftover_domain = data_from_studio.get_user_domain()
35
+ for file_path in all_local_domain_files:
36
+ # For each local domain file, we do a partial merge
37
+ local_domain = Domain.from_file(str(file_path))
38
+ updated_local_domain = local_domain.partial_merge(leftover_domain)
39
+
40
+ # If this partial merge made changes, persist them
41
+ if local_domain != updated_local_domain:
42
+ updated_local_domain.persist(file_path)
43
+
44
+ # Remove every item now present in updated_local_domain from leftover_domain
45
+ leftover_domain = leftover_domain.difference(updated_local_domain)
46
+
47
+ # If there are still items in leftover_domain, persist them
48
+ if not leftover_domain.is_empty():
49
+ leftover_domain.persist(domain_path / STUDIO_DOMAIN_FILENAME)