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
rasa/core/lock_store.py CHANGED
@@ -2,18 +2,27 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import json
5
- import logging
6
5
  import os
7
6
  from contextlib import asynccontextmanager
8
- from typing import AsyncGenerator, Dict, Optional, Text, Union
7
+ from typing import Any, AsyncGenerator, Dict, Literal, Optional, Text, Union
8
+
9
+ import structlog
10
+ from pydantic import (
11
+ AnyUrl,
12
+ BaseModel,
13
+ Field,
14
+ NonNegativeInt,
15
+ model_validator,
16
+ )
9
17
 
10
18
  import rasa.shared.utils.common
11
19
  from rasa.core.constants import DEFAULT_LOCK_LIFETIME
12
20
  from rasa.core.lock import TicketLock
13
21
  from rasa.shared.exceptions import ConnectionException, RasaException
22
+ from rasa.shared.utils.io import raise_deprecation_warning
14
23
  from rasa.utils.endpoints import EndpointConfig
15
24
 
16
- logger = logging.getLogger(__name__)
25
+ structlogger = structlog.getLogger(__name__)
17
26
 
18
27
 
19
28
  def _get_lock_lifetime() -> int:
@@ -76,7 +85,10 @@ class LockStore:
76
85
 
77
86
  Creates a new lock if none is found.
78
87
  """
79
- logger.debug(f"Issuing ticket for conversation '{conversation_id}'.")
88
+ structlogger.debug(
89
+ "lock_store.issue_ticket",
90
+ event_info=f"Issuing ticket for conversation '{conversation_id}'.",
91
+ )
80
92
  try:
81
93
  lock = self.get_or_create_lock(conversation_id)
82
94
  ticket = lock.issue_ticket(lock_lifetime)
@@ -109,7 +121,10 @@ class LockStore:
109
121
  async def _acquire_lock(
110
122
  self, conversation_id: Text, ticket: int, wait_time_in_seconds: float
111
123
  ) -> TicketLock:
112
- logger.debug(f"Acquiring lock for conversation '{conversation_id}'.")
124
+ structlogger.debug(
125
+ "lock_store._acquiring_lock_for_conversation",
126
+ event_info=f"Acquiring lock for conversation '{conversation_id}'.",
127
+ )
113
128
  while True:
114
129
  # fetch lock in every iteration because lock might no longer exist
115
130
  lock = self.get_lock(conversation_id)
@@ -120,16 +135,22 @@ class LockStore:
120
135
 
121
136
  # acquire lock if it isn't locked
122
137
  if not lock.is_locked(ticket):
123
- logger.debug(f"Acquired lock for conversation '{conversation_id}'.")
138
+ structlogger.debug(
139
+ "lock_store._acquired_lock_for_conversation",
140
+ event_info=f"Acquired lock for conversation '{conversation_id}'.",
141
+ )
124
142
  return lock
125
143
 
126
144
  items_before_this = ticket - (lock.now_serving or 0)
127
145
 
128
- logger.debug(
129
- f"Failed to acquire lock for conversation ID '{conversation_id}' "
130
- f"because {items_before_this} other item(s) for this "
131
- f"conversation ID have to be finished processing first. "
132
- f"Retrying in {wait_time_in_seconds} seconds ..."
146
+ structlogger.debug(
147
+ "lock_store._retrying_lock_acquisition",
148
+ event_info=(
149
+ f"Failed to acquire lock for conversation ID '{conversation_id}' "
150
+ f"because {items_before_this} other item(s) for this "
151
+ f"conversation ID have to be finished processing first. "
152
+ f"Retrying in {wait_time_in_seconds} seconds ..."
153
+ ),
133
154
  )
134
155
 
135
156
  # sleep and update lock
@@ -186,9 +207,99 @@ class LockStore:
186
207
  @staticmethod
187
208
  def _log_deletion(conversation_id: Text, deletion_successful: bool) -> None:
188
209
  if deletion_successful:
189
- logger.debug(f"Deleted lock for conversation '{conversation_id}'.")
210
+ structlogger.debug(
211
+ "lock_store._deleted_lock_for_conversation",
212
+ event_info=f"Deleted lock for conversation '{conversation_id}'.",
213
+ )
190
214
  else:
191
- logger.debug(f"Could not delete lock for conversation '{conversation_id}'.")
215
+ structlogger.debug(
216
+ "lock_store._failed_to_delete_lock_for_conversation",
217
+ event_info=(
218
+ f"Could not delete lock for conversation '{conversation_id}'."
219
+ ),
220
+ )
221
+
222
+
223
+ class RedisLockStoreConfig(BaseModel):
224
+ host: Union[AnyUrl, Literal["localhost"]] = Field(
225
+ default="localhost", description="The host of the redis server."
226
+ )
227
+ port: NonNegativeInt = Field(
228
+ default=6379, ge=0, le=65535, description="The port of the redis server."
229
+ )
230
+ db: NonNegativeInt = Field(
231
+ default=0,
232
+ ge=0,
233
+ description="The name of the database within Redis "
234
+ "which should be used by Rasa",
235
+ )
236
+ username: Optional[str] = Field(
237
+ default=None,
238
+ description="The username which should be used for "
239
+ "authentication with the Redis database.",
240
+ )
241
+ password: Optional[str] = Field(
242
+ default=None,
243
+ description="The username which should be used for "
244
+ "authentication with the Redis database.",
245
+ )
246
+ use_ssl: bool = Field(
247
+ default=False,
248
+ serialization_alias="ssl",
249
+ description="True if SSL should be used for the connection to Redis.",
250
+ )
251
+ ssl_certfile: Optional[str] = Field(
252
+ default=None,
253
+ description="Path to the SSL certificate file.",
254
+ )
255
+ ssl_keyfile: Optional[str] = Field(
256
+ default=None, description="Path to the SSL private key file."
257
+ )
258
+ ssl_ca_certs: Optional[str] = Field(
259
+ default=None, description="Path to the SSL CA certificate file."
260
+ )
261
+ key_prefix: Optional[str] = Field(
262
+ default=None,
263
+ description="Prefix to prepend to all keys "
264
+ "used by the lock store. Must be alphanumeric.",
265
+ )
266
+ socket_timeout: float = Field(
267
+ default=DEFAULT_SOCKET_TIMEOUT_IN_SECONDS,
268
+ description="Timeout in seconds after which an exception "
269
+ "will be raised in case Redis doesn't respond "
270
+ "within `socket_timeout` seconds.",
271
+ )
272
+
273
+ @model_validator(mode="before")
274
+ @classmethod
275
+ def validate_url_and_host_properties(cls, data: Any) -> Any:
276
+ if isinstance(data, dict):
277
+ if bool(data.get("url", None)) and bool(data.get("host", None)):
278
+ raise RasaException(
279
+ "You cannot specify both 'url' and 'host' in the Redis lock store "
280
+ "configuration. Please use only one of them."
281
+ )
282
+
283
+ if data.get("url", None):
284
+ raise_deprecation_warning(
285
+ "The 'url' property in the redis lock store "
286
+ "configuration is deprecated. Please use 'host' instead."
287
+ )
288
+ data["host"] = data.pop("url")
289
+ return data
290
+
291
+ @model_validator(mode="after")
292
+ def verify_username_password(self) -> RedisLockStoreConfig:
293
+ if bool(self.username) ^ bool(self.password):
294
+ raise ValueError(
295
+ f"Expected username and password. "
296
+ f"Found: username: {'<has value>' if self.username else '<N/A>'}, "
297
+ f"password: {'<has value>' if self.password else '<N/A>'}"
298
+ )
299
+ return self
300
+
301
+ def to_strict_redis(self) -> Dict[str, Any]:
302
+ return self.model_dump(by_alias=True, exclude={"key_prefix"})
192
303
 
193
304
 
194
305
  class RedisLockStore(LockStore):
@@ -196,57 +307,28 @@ class RedisLockStore(LockStore):
196
307
 
197
308
  def __init__(
198
309
  self,
199
- host: Text = "localhost",
200
- port: int = 6379,
201
- db: int = 1,
202
- username: Optional[Text] = None,
203
- password: Optional[Text] = None,
204
- use_ssl: bool = False,
205
- ssl_certfile: Optional[Text] = None,
206
- ssl_keyfile: Optional[Text] = None,
207
- ssl_ca_certs: Optional[Text] = None,
208
- key_prefix: Optional[Text] = None,
209
- socket_timeout: float = DEFAULT_SOCKET_TIMEOUT_IN_SECONDS,
310
+ config: RedisLockStoreConfig = RedisLockStoreConfig(),
210
311
  ) -> None:
211
312
  """Create a lock store which uses Redis for persistence.
212
313
 
213
314
  Args:
214
- host: The host of the redis server.
215
- port: The port of the redis server.
216
- db: The name of the database within Redis which should be used by Rasa
217
- Open Source.
218
- username: The username which should be used for authentication with the
219
- Redis database.
220
- password: The password which should be used for authentication with the
221
- Redis database.
222
- use_ssl: `True` if SSL should be used for the connection to Redis.
223
- ssl_certfile: Path to the SSL certificate file.
224
- ssl_keyfile: Path to the SSL private key file.
225
- ssl_ca_certs: Path to the SSL CA certificate file.
226
- key_prefix: prefix to prepend to all keys used by the lock store. Must be
227
- alphanumeric.
228
- socket_timeout: Timeout in seconds after which an exception will be raised
229
- in case Redis doesn't respond within `socket_timeout` seconds.
315
+ config: Redis lock store configuration.
230
316
  """
231
317
  import redis
232
318
 
233
- self.red = redis.StrictRedis(
234
- host=host,
235
- port=int(port),
236
- db=int(db),
237
- username=username,
238
- password=password,
239
- ssl=use_ssl,
240
- ssl_certfile=ssl_certfile,
241
- ssl_keyfile=ssl_keyfile,
242
- ssl_ca_certs=ssl_ca_certs,
243
- socket_timeout=socket_timeout,
244
- )
319
+ self.config = config
320
+ self.red = redis.StrictRedis(**self.config.to_strict_redis())
245
321
 
246
322
  self.key_prefix = DEFAULT_REDIS_LOCK_STORE_KEY_PREFIX
247
- if key_prefix:
248
- logger.debug(f"Setting non-default redis key prefix: '{key_prefix}'.")
249
- self._set_key_prefix(key_prefix)
323
+ if self.config.key_prefix:
324
+ structlogger.debug(
325
+ "redis_lock_store._set_key_prefix.non_default_key_prefix",
326
+ event_info=(
327
+ f"Setting non-default "
328
+ f"redis key prefix: '{self.config.key_prefix}'.",
329
+ ),
330
+ )
331
+ self._set_key_prefix(self.config.key_prefix)
250
332
 
251
333
  super().__init__()
252
334
 
@@ -254,9 +336,13 @@ class RedisLockStore(LockStore):
254
336
  if isinstance(key_prefix, str) and key_prefix.isalnum():
255
337
  self.key_prefix = key_prefix + ":" + DEFAULT_REDIS_LOCK_STORE_KEY_PREFIX
256
338
  else:
257
- logger.warning(
258
- f"Omitting provided non-alphanumeric redis key prefix: '{key_prefix}'. "
259
- f"Using default '{self.key_prefix}' instead."
339
+ structlogger.warning(
340
+ "redis_lock_store._set_key_prefix.default_instead_of_invalid_key_prefix",
341
+ event_info=(
342
+ f"Omitting provided non-alphanumeric "
343
+ f"redis key prefix: '{key_prefix}'. "
344
+ f"Using default '{self.key_prefix}' instead."
345
+ ),
260
346
  )
261
347
 
262
348
  def get_lock(self, conversation_id: Text) -> Optional[TicketLock]:
@@ -313,7 +399,9 @@ def _create_from_endpoint_config(
313
399
 
314
400
  lock_store: LockStore = InMemoryLockStore()
315
401
  elif endpoint_config.type == "redis":
316
- lock_store = RedisLockStore(host=endpoint_config.url, **endpoint_config.kwargs)
402
+ config = RedisLockStoreConfig.model_validate(endpoint_config.to_dict())
403
+
404
+ lock_store = RedisLockStore(config)
317
405
  elif endpoint_config.type == "concurrent_redis":
318
406
  from rasa.core.concurrent_lock_store import ConcurrentRedisLockStore
319
407
 
@@ -321,7 +409,10 @@ def _create_from_endpoint_config(
321
409
  else:
322
410
  lock_store = _load_from_module_name_in_endpoint_config(endpoint_config)
323
411
 
324
- logger.debug(f"Connected to lock store '{lock_store.__class__.__name__}'.")
412
+ structlogger.debug(
413
+ "lock_store._create_from_endpoint_config.lock_store_connected",
414
+ event_info=f"Connected to lock store '{lock_store.__class__.__name__}'.",
415
+ )
325
416
 
326
417
  return lock_store
327
418
 
@@ -261,19 +261,27 @@ class DialogueUnderstandingTestStep(BaseModel):
261
261
  # Safely extract commands from the step.
262
262
  commands = []
263
263
  for command in step.get(KEY_COMMANDS, []):
264
+ parsed_commands = None
264
265
  try:
265
- commands.extend(
266
- parse_commands(
267
- command,
268
- flows,
269
- clarify_options_optional=True,
270
- additional_commands=custom_command_classes,
271
- default_commands_to_remove=remove_default_commands,
272
- )
266
+ parsed_commands = parse_commands(
267
+ command,
268
+ flows,
269
+ clarify_options_optional=True,
270
+ additional_commands=custom_command_classes,
271
+ default_commands_to_remove=remove_default_commands,
273
272
  )
274
273
  except (IndexError, ValueError) as e:
275
274
  raise ValueError(f"Failed to parse command '{command}': {e}") from e
276
275
 
276
+ if not parsed_commands:
277
+ raise ValueError(
278
+ f"Failed to parse command '{command}': command parser returned "
279
+ f"None. Please make sure that you are using the correct command "
280
+ f"syntax and the command arguments are valid."
281
+ )
282
+
283
+ commands.extend(parsed_commands)
284
+
277
285
  # Construct the DialogueUnderstandingTestStep
278
286
  return DialogueUnderstandingTestStep(
279
287
  actor=ACTOR_USER if ACTOR_USER in step else ACTOR_BOT,
rasa/plugin.py CHANGED
@@ -32,11 +32,8 @@ def plugin_manager() -> pluggy.PluginManager:
32
32
 
33
33
  def init_hooks(manager: pluggy.PluginManager) -> None:
34
34
  """Initialise hooks into rasa."""
35
- import rasa.utils.licensing
36
35
  from rasa import hooks
37
36
 
38
- rasa.utils.licensing.validate_license_from_env()
39
-
40
37
  manager.register(hooks)
41
38
 
42
39
 
rasa/shared/constants.py CHANGED
@@ -238,6 +238,7 @@ EXTRA_PARAMETERS_KEY = "extra_parameters"
238
238
  MODEL_GROUP_ID_KEY = "model_group_id"
239
239
  MODEL_LIST_KEY = "model_list"
240
240
  LITELLM_PARAMS_KEY = "litellm_params"
241
+ _VALIDATE_ENVIRONMENT_MISSING_KEYS_KEY = "missing_keys"
241
242
 
242
243
  LLM_API_HEALTH_CHECK_ENV_VAR = "LLM_API_HEALTH_CHECK"
243
244
  LLM_API_HEALTH_CHECK_DEFAULT_VALUE = "false"
@@ -134,6 +134,22 @@ ALL_DOMAIN_KEYS = [
134
134
 
135
135
  PREV_PREFIX = "prev_"
136
136
 
137
+ MERGE_FUNC_MAPPING: Dict[Text, Callable[..., Any]] = {
138
+ KEY_ACTIONS: rasa.shared.utils.common.merge_lists_of_dicts,
139
+ KEY_RESPONSES: rasa.shared.utils.common.merge_dicts,
140
+ KEY_SLOTS: rasa.shared.utils.common.merge_dicts,
141
+ KEY_INTENTS: rasa.shared.utils.common.merge_lists_of_dicts,
142
+ KEY_ENTITIES: rasa.shared.utils.common.merge_lists_of_dicts,
143
+ KEY_E2E_ACTIONS: rasa.shared.utils.common.merge_lists,
144
+ KEY_FORMS: rasa.shared.utils.common.merge_dicts,
145
+ }
146
+
147
+ DICT_DATA_KEYS = [
148
+ key
149
+ for key, value in MERGE_FUNC_MAPPING.items()
150
+ if value == rasa.shared.utils.common.merge_dicts
151
+ ]
152
+
137
153
  # State is a dictionary with keys (USER, PREVIOUS_ACTION, SLOTS, ACTIVE_LOOP)
138
154
  # representing the origin of a SubState;
139
155
  # the values are SubStates, that contain the information needed for featurization
@@ -466,17 +482,7 @@ class Domain:
466
482
 
467
483
  duplicates: Dict[Text, List[Text]] = {}
468
484
 
469
- merge_func_mappings: Dict[Text, Callable[..., Any]] = {
470
- KEY_INTENTS: rasa.shared.utils.common.merge_lists_of_dicts,
471
- KEY_ENTITIES: rasa.shared.utils.common.merge_lists_of_dicts,
472
- KEY_ACTIONS: rasa.shared.utils.common.merge_lists_of_dicts,
473
- KEY_E2E_ACTIONS: rasa.shared.utils.common.merge_lists,
474
- KEY_FORMS: rasa.shared.utils.common.merge_dicts,
475
- KEY_RESPONSES: rasa.shared.utils.common.merge_dicts,
476
- KEY_SLOTS: rasa.shared.utils.common.merge_dicts,
477
- }
478
-
479
- for key, merge_func in merge_func_mappings.items():
485
+ for key, merge_func in MERGE_FUNC_MAPPING.items():
480
486
  duplicates[key] = rasa.shared.utils.common.extract_duplicates(
481
487
  combined.get(key, []), domain_dict.get(key, [])
482
488
  )
@@ -494,6 +500,74 @@ class Domain:
494
500
 
495
501
  return combined
496
502
 
503
+ def partial_merge(self, other: Domain) -> Domain:
504
+ """
505
+ Returns a new Domain with intersection-based merging:
506
+ - For each domain section only overwrite items that already exist in self.
507
+ - Brand-new items in `other` are ignored.
508
+
509
+ Args:
510
+ other: The domain to merge with.
511
+
512
+ Returns:
513
+ A new Domain object with the merged content.
514
+ """
515
+ updated_self = copy.deepcopy(self.as_dict())
516
+ other_dict = other.as_dict()
517
+
518
+ keys_to_merge = MERGE_FUNC_MAPPING.keys()
519
+ for key in keys_to_merge:
520
+ if key in DICT_DATA_KEYS:
521
+ # Merge dictionaries
522
+ self_val = updated_self.get(key, {})
523
+ other_val = other_dict.get(key, {})
524
+ updated_self[key] = rasa.shared.utils.common.partial_merge_dict(
525
+ self_val, other_val
526
+ )
527
+ else:
528
+ # Merge lists
529
+ self_val = updated_self.get(key, [])
530
+ other_val = other_dict.get(key, [])
531
+ is_same_item_fn = SAME_ITEM_FUNCTIONS.get(key, default_is_same_item)
532
+ updated_self[key] = rasa.shared.utils.common.partial_merge_list(
533
+ self_val, other_val, is_same_item_fn
534
+ )
535
+
536
+ return Domain.from_dict(updated_self)
537
+
538
+ def difference(self, other: Domain) -> Domain:
539
+ """
540
+ Returns a new Domain containing items in `self` that are NOT in `other`,
541
+ using simple equality checks for dict/list items.
542
+
543
+ Args:
544
+ other: The domain to compare with.
545
+
546
+ Returns:
547
+ A new Domain object with the difference content.
548
+ """
549
+ self_dict = self.as_dict()
550
+ other_dict = other.as_dict()
551
+
552
+ difference_dict = {}
553
+ for key in MERGE_FUNC_MAPPING.keys():
554
+ is_dict = key in DICT_DATA_KEYS
555
+ self_val = self_dict.get(key, {} if is_dict else [])
556
+ other_val = other_dict.get(key, {} if is_dict else [])
557
+
558
+ if is_dict and isinstance(self_val, dict) and isinstance(other_val, dict):
559
+ difference_dict[key] = {
560
+ k: v
561
+ for k, v in self_val.items()
562
+ if k not in other_val or v != other_val[k]
563
+ }
564
+ else:
565
+ difference_dict[key] = [
566
+ item for item in self_val if item not in other_val
567
+ ] # type: ignore[assignment]
568
+
569
+ return Domain.from_dict(difference_dict)
570
+
497
571
  def _preprocess_domain_dict(
498
572
  self,
499
573
  data: Dict,
@@ -2120,6 +2194,11 @@ class Domain:
2120
2194
  """Remove all builtin slots from the domain."""
2121
2195
  self.slots = [slot for slot in self.slots if not slot.is_builtin]
2122
2196
 
2197
+ def __eq__(self, other: object) -> bool:
2198
+ if isinstance(other, Domain):
2199
+ return self.as_dict() == other.as_dict()
2200
+ return False
2201
+
2123
2202
 
2124
2203
  def warn_about_duplicates_found_during_domain_merging(
2125
2204
  duplicates: Dict[Text, List[Text]],
@@ -2178,3 +2257,78 @@ def _validate_forms(forms: Union[Dict, List]) -> None:
2178
2257
  f"the keyword `{REQUIRED_SLOTS_KEY}` is required. "
2179
2258
  f"Please see {DOCS_URL_FORMS} for more information."
2180
2259
  )
2260
+
2261
+
2262
+ def is_same_entity(e1: Any, e2: Any) -> bool:
2263
+ """Check if two entities are the 'same' (string or dict).
2264
+
2265
+ Args:
2266
+ e1: First entity to compare.
2267
+ e2: Second entity to compare.
2268
+
2269
+ Returns:
2270
+ True if the entities are the same, False otherwise.
2271
+ """
2272
+ if isinstance(e1, str) and isinstance(e2, str):
2273
+ return e1 == e2
2274
+
2275
+ if isinstance(e1, dict) and isinstance(e2, dict):
2276
+ return (
2277
+ e1.get(ENTITY_ATTRIBUTE_TYPE) == e2.get(ENTITY_ATTRIBUTE_TYPE)
2278
+ and e1.get(ENTITY_ATTRIBUTE_ROLE) == e2.get(ENTITY_ATTRIBUTE_ROLE)
2279
+ and e1.get(ENTITY_ATTRIBUTE_GROUP) == e2.get(ENTITY_ATTRIBUTE_GROUP)
2280
+ )
2281
+
2282
+ return False
2283
+
2284
+
2285
+ def is_same_intent(i1: Any, i2: Any) -> bool:
2286
+ """Check if two intents are the 'same' (string or dict).
2287
+
2288
+ Args:
2289
+ i1: First intent to compare.
2290
+ i2: Second intent to compare.
2291
+
2292
+ Returns:
2293
+ True if the intents are the same, False otherwise.
2294
+ """
2295
+ if isinstance(i1, str) and isinstance(i2, str):
2296
+ return i1 == i2
2297
+
2298
+ if isinstance(i1, dict) and isinstance(i2, dict):
2299
+ key1, key2 = next(iter(i1.keys())), next(iter((i2.keys())))
2300
+ return key1 == key2
2301
+
2302
+ return False
2303
+
2304
+
2305
+ def is_same_action(a1: Any, a2: Any) -> bool:
2306
+ """Check if two actions are the 'same' (string or dict).
2307
+
2308
+ Args:
2309
+ a1: First action to compare.
2310
+ a2: Second action to compare.
2311
+
2312
+ Returns:
2313
+ True if the actions are the same, False otherwise.
2314
+ """
2315
+ if isinstance(a1, str) and isinstance(a2, str):
2316
+ return a1 == a2
2317
+
2318
+ if isinstance(a1, dict) and isinstance(a2, dict):
2319
+ key1, key2 = next(iter((a1.keys()))), next(iter((a2.keys())))
2320
+ return key1 == key2
2321
+
2322
+ return False
2323
+
2324
+
2325
+ def default_is_same_item(a: Any, b: Any) -> bool:
2326
+ """Fallback exact equality check if a key doesn't need special handling."""
2327
+ return a == b
2328
+
2329
+
2330
+ SAME_ITEM_FUNCTIONS: Dict[Text, Callable[[Any, Any], bool]] = {
2331
+ KEY_ENTITIES: is_same_entity,
2332
+ KEY_INTENTS: is_same_intent,
2333
+ KEY_ACTIONS: is_same_action,
2334
+ }