rasa-pro 3.11.0rc2__py3-none-any.whl → 3.11.1__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 (65) hide show
  1. rasa/__main__.py +9 -3
  2. rasa/cli/studio/upload.py +0 -15
  3. rasa/cli/utils.py +1 -1
  4. rasa/core/channels/development_inspector.py +8 -2
  5. rasa/core/channels/voice_ready/audiocodes.py +3 -4
  6. rasa/core/channels/voice_stream/asr/asr_engine.py +19 -1
  7. rasa/core/channels/voice_stream/asr/asr_event.py +1 -1
  8. rasa/core/channels/voice_stream/asr/azure.py +16 -9
  9. rasa/core/channels/voice_stream/asr/deepgram.py +17 -14
  10. rasa/core/channels/voice_stream/tts/azure.py +3 -1
  11. rasa/core/channels/voice_stream/tts/cartesia.py +3 -3
  12. rasa/core/channels/voice_stream/tts/tts_engine.py +10 -1
  13. rasa/core/channels/voice_stream/voice_channel.py +48 -18
  14. rasa/core/information_retrieval/qdrant.py +1 -0
  15. rasa/core/nlg/contextual_response_rephraser.py +2 -2
  16. rasa/core/persistor.py +93 -49
  17. rasa/core/policies/enterprise_search_policy.py +5 -5
  18. rasa/core/policies/flows/flow_executor.py +18 -8
  19. rasa/core/policies/intentless_policy.py +9 -5
  20. rasa/core/processor.py +7 -5
  21. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +2 -1
  22. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +9 -0
  23. rasa/e2e_test/aggregate_test_stats_calculator.py +11 -1
  24. rasa/e2e_test/assertions.py +133 -16
  25. rasa/e2e_test/assertions_schema.yml +23 -0
  26. rasa/e2e_test/e2e_test_runner.py +2 -2
  27. rasa/engine/loader.py +12 -0
  28. rasa/engine/validation.py +310 -86
  29. rasa/model_manager/config.py +8 -0
  30. rasa/model_manager/model_api.py +166 -61
  31. rasa/model_manager/runner_service.py +31 -26
  32. rasa/model_manager/trainer_service.py +14 -23
  33. rasa/model_manager/warm_rasa_process.py +187 -0
  34. rasa/model_service.py +3 -5
  35. rasa/model_training.py +3 -1
  36. rasa/shared/constants.py +27 -5
  37. rasa/shared/core/constants.py +1 -1
  38. rasa/shared/core/domain.py +8 -31
  39. rasa/shared/core/flows/yaml_flows_io.py +13 -4
  40. rasa/shared/importers/importer.py +19 -2
  41. rasa/shared/importers/rasa.py +5 -1
  42. rasa/shared/nlu/training_data/formats/rasa_yaml.py +18 -3
  43. rasa/shared/providers/_configs/litellm_router_client_config.py +29 -9
  44. rasa/shared/providers/_utils.py +79 -0
  45. rasa/shared/providers/embedding/default_litellm_embedding_client.py +24 -0
  46. rasa/shared/providers/embedding/litellm_router_embedding_client.py +1 -1
  47. rasa/shared/providers/llm/_base_litellm_client.py +26 -0
  48. rasa/shared/providers/llm/default_litellm_llm_client.py +24 -0
  49. rasa/shared/providers/llm/litellm_router_llm_client.py +56 -1
  50. rasa/shared/providers/llm/self_hosted_llm_client.py +4 -28
  51. rasa/shared/providers/router/_base_litellm_router_client.py +35 -1
  52. rasa/shared/utils/common.py +30 -3
  53. rasa/shared/utils/health_check/health_check.py +26 -24
  54. rasa/shared/utils/yaml.py +116 -31
  55. rasa/studio/data_handler.py +3 -1
  56. rasa/studio/upload.py +119 -57
  57. rasa/telemetry.py +3 -1
  58. rasa/tracing/config.py +1 -1
  59. rasa/validator.py +40 -4
  60. rasa/version.py +1 -1
  61. {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/METADATA +2 -2
  62. {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/RECORD +65 -63
  63. {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/NOTICE +0 -0
  64. {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/WHEEL +0 -0
  65. {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/entry_points.txt +0 -0
rasa/core/persistor.py CHANGED
@@ -4,6 +4,7 @@ import abc
4
4
  import os
5
5
  import shutil
6
6
  from enum import Enum
7
+ from pathlib import Path
7
8
  from typing import TYPE_CHECKING, List, Optional, Text, Tuple, Union
8
9
 
9
10
  import structlog
@@ -122,7 +123,8 @@ class Persistor(abc.ABC):
122
123
 
123
124
  def persist(self, trained_model: str) -> None:
124
125
  """Uploads a trained model persisted in the `target_dir` to cloud storage."""
125
- file_key = self._create_file_key(trained_model)
126
+ absolute_file_key = self._create_file_key(trained_model)
127
+ file_key = Path(absolute_file_key).name
126
128
  self._persist_tar(file_key, trained_model)
127
129
 
128
130
  def retrieve(self, model_name: Text, target_path: Text) -> Text:
@@ -141,7 +143,8 @@ class Persistor(abc.ABC):
141
143
  # ensure backward compatibility
142
144
  tar_name = self._tar_name(model_name)
143
145
  tar_name = self._create_file_key(tar_name)
144
- self._retrieve_tar(tar_name)
146
+ target_filename = os.path.basename(tar_name)
147
+ self._retrieve_tar(target_filename)
145
148
  self._copy(os.path.basename(tar_name), target_path)
146
149
 
147
150
  if os.path.isdir(target_path):
@@ -149,6 +152,36 @@ class Persistor(abc.ABC):
149
152
 
150
153
  return target_path
151
154
 
155
+ def size_of_persisted_model(self, model_name: Text) -> int:
156
+ """Returns the size of the model that has been persisted to cloud storage.
157
+
158
+ Args:
159
+ model_name: The name of the model to retrieve.
160
+ """
161
+ tar_name = model_name
162
+ if not model_name.endswith(MODEL_ARCHIVE_EXTENSION):
163
+ # ensure backward compatibility
164
+ tar_name = self._tar_name(model_name)
165
+ tar_name = self._create_file_key(tar_name)
166
+ target_filename = os.path.basename(tar_name)
167
+ return self._retrieve_tar_size(target_filename)
168
+
169
+ def _retrieve_tar_size(self, filename: Text) -> int:
170
+ """Returns the size of the model that has been persisted to cloud storage."""
171
+ structlogger.warning(
172
+ "persistor.retrieve_tar_size.not_implemented",
173
+ filename=filename,
174
+ event_info=(
175
+ "This method should be implemented in the persistor. "
176
+ "The default implementation will download the model "
177
+ "to calculate the size. Most persistors should override "
178
+ "this method to avoid downloading the model and get the "
179
+ "size directly from the cloud storage."
180
+ ),
181
+ )
182
+ self._retrieve_tar(filename)
183
+ return os.path.getsize(os.path.basename(filename))
184
+
152
185
  @abc.abstractmethod
153
186
  def _retrieve_tar(self, filename: Text) -> None:
154
187
  """Downloads a model previously persisted to cloud storage."""
@@ -197,10 +230,7 @@ class Persistor(abc.ABC):
197
230
  f"{REMOTE_STORAGE_PATH_ENV} is deprecated and will be "
198
231
  "removed in future versions. "
199
232
  "Please use the -m path/to/model.tar.gz option to "
200
- "specify the model path when loading a model."
201
- "Or use --output and --fixed-model-name to specify the "
202
- "output directory and the model name when saving a "
203
- "trained model to remote storage.",
233
+ "specify the model path when loading a model.",
204
234
  )
205
235
 
206
236
  file_key = os.path.basename(model_path)
@@ -272,50 +302,48 @@ class AWSPersistor(Persistor):
272
302
  with open(tar_path, "rb") as f:
273
303
  self.s3.Object(self.bucket_name, file_key).put(Body=f)
274
304
 
275
- def _retrieve_tar(self, model_path: Text) -> None:
305
+ def _retrieve_tar_size(self, model_path: Text) -> int:
306
+ """Returns the size of the model that has been persisted to s3."""
307
+ try:
308
+ obj = self.s3.Object(self.bucket_name, model_path)
309
+ return obj.content_length
310
+ except Exception:
311
+ raise ModelNotFound()
312
+
313
+ def _retrieve_tar(self, target_filename: str) -> None:
276
314
  """Downloads a model that has previously been persisted to s3."""
277
315
  from botocore import exceptions
278
316
 
279
- target_filename = os.path.basename(model_path)
280
- bucket_objects = list(self.bucket.objects.all())
281
-
282
- model_found = False
283
-
284
317
  log = (
285
318
  f"Model '{target_filename}' not found in the specified bucket "
286
319
  f"'{self.bucket_name}'. Please make sure the model exists "
287
320
  f"in the bucket."
288
321
  )
289
322
 
290
- for obj in bucket_objects:
291
- if model_path not in obj.key:
292
- continue
323
+ try:
324
+ with open(target_filename, "wb") as f:
325
+ self.bucket.download_fileobj(target_filename, f)
326
+
293
327
  structlogger.debug(
294
- "aws_persistor.retrieve_tar.object_found", object_key=obj.key
328
+ "aws_persistor.retrieve_tar.object_found", object_key=target_filename
295
329
  )
296
-
297
- try:
298
- with open(target_filename, "wb") as f:
299
- self.bucket.download_fileobj(obj.key, f)
300
- model_found = True
301
- break
302
- except exceptions.ClientError as exc:
303
- if self._error_code(exc) == HTTP_STATUS_NOT_FOUND:
304
- structlogger.error(
305
- "aws_persistor.retrieve_tar.model_not_found",
306
- bucket_name=self.bucket_name,
307
- target_filename=target_filename,
308
- event_info=log,
309
- )
310
- raise ModelNotFound() from exc
311
- if not model_found:
330
+ except exceptions.ClientError as exc:
331
+ if self._error_code(exc) == HTTP_STATUS_NOT_FOUND:
332
+ structlogger.error(
333
+ "aws_persistor.retrieve_tar.model_not_found",
334
+ bucket_name=self.bucket_name,
335
+ target_filename=target_filename,
336
+ event_info=log,
337
+ )
338
+ raise ModelNotFound() from exc
339
+ except exceptions.BotoCoreError as exc:
312
340
  structlogger.error(
313
- "aws_persistor.retrieve_tar.model_not_found",
341
+ "aws_persistor.retrieve_tar.model_download_error",
314
342
  bucket_name=self.bucket_name,
315
343
  target_filename=target_filename,
316
344
  event_info=log,
317
345
  )
318
- raise ModelNotFound()
346
+ raise ModelNotFound() from exc
319
347
 
320
348
 
321
349
  class GCSPersistor(Persistor):
@@ -397,6 +425,14 @@ class GCSPersistor(Persistor):
397
425
  blob = self.bucket.blob(file_key)
398
426
  blob.upload_from_filename(tar_path)
399
427
 
428
+ def _retrieve_tar_size(self, target_filename: Text) -> int:
429
+ """Returns the size of the model that has been persisted to GCS."""
430
+ try:
431
+ blob = self.bucket.blob(target_filename)
432
+ return blob.size
433
+ except Exception:
434
+ raise ModelNotFound()
435
+
400
436
  def _retrieve_tar(self, target_filename: Text) -> None:
401
437
  """Downloads a model that has previously been persisted to GCS."""
402
438
  from google.api_core import exceptions
@@ -404,6 +440,10 @@ class GCSPersistor(Persistor):
404
440
  blob = self.bucket.blob(target_filename)
405
441
  try:
406
442
  blob.download_to_filename(target_filename)
443
+
444
+ structlogger.debug(
445
+ "gcs_persistor.retrieve_tar.object_found", object_key=target_filename
446
+ )
407
447
  except exceptions.NotFound as exc:
408
448
  log = (
409
449
  f"Model '{target_filename}' not found in the specified bucket "
@@ -460,24 +500,28 @@ class AzurePersistor(Persistor):
460
500
  with open(tar_path, "rb") as data:
461
501
  self._container_client().upload_blob(name=file_key, data=data)
462
502
 
463
- def _retrieve_tar(self, target_filename: Text) -> None:
464
- """Downloads a model that has previously been persisted to Azure."""
503
+ def _retrieve_tar_size(self, target_filename: Text) -> int:
504
+ """Returns the size of the model that has been persisted to Azure."""
465
505
  try:
466
- blob_list = self._container_client().list_blobs()
467
-
468
- for blob in blob_list:
469
- if target_filename not in blob.name:
470
- continue
506
+ blob_client = self._container_client().get_blob_client(target_filename)
507
+ properties = blob_client.get_blob_properties()
508
+ return properties.size
509
+ except Exception:
510
+ raise ModelNotFound()
471
511
 
472
- structlogger.debug(
473
- "azure_persistor.retrieve_tar.blob_found", blob_name=blob.name
474
- )
512
+ def _retrieve_tar(self, target_filename: Text) -> None:
513
+ """Downloads a model that has previously been persisted to Azure."""
514
+ from azure.core.exceptions import AzureError
475
515
 
476
- with open(target_filename, "wb") as model_file:
477
- blob_client = self._container_client().get_blob_client(blob.name)
478
- download_stream = blob_client.download_blob()
479
- model_file.write(download_stream.readall())
480
- except Exception as exc:
516
+ try:
517
+ with open(target_filename, "wb") as model_file:
518
+ blob_client = self._container_client().get_blob_client(target_filename)
519
+ download_stream = blob_client.download_blob()
520
+ model_file.write(download_stream.readall())
521
+ structlogger.debug(
522
+ "azure_persistor.retrieve_tar.blob_found", blob_name=target_filename
523
+ )
524
+ except AzureError as exc:
481
525
  log = (
482
526
  f"An exception occurred while trying to download "
483
527
  f"the model '{target_filename}' in the specified container "
@@ -51,7 +51,7 @@ from rasa.shared.constants import (
51
51
  OPENAI_PROVIDER,
52
52
  TIMEOUT_CONFIG_KEY,
53
53
  MODEL_NAME_CONFIG_KEY,
54
- MODEL_GROUP_CONFIG_KEY,
54
+ MODEL_GROUP_ID_CONFIG_KEY,
55
55
  )
56
56
  from rasa.shared.core.constants import (
57
57
  ACTION_CANCEL_FLOW,
@@ -337,12 +337,12 @@ class EnterpriseSearchPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Po
337
337
  embeddings_model=self.embeddings_config.get(MODEL_CONFIG_KEY)
338
338
  or self.embeddings_config.get(MODEL_NAME_CONFIG_KEY),
339
339
  embeddings_model_group_id=self.embeddings_config.get(
340
- MODEL_GROUP_CONFIG_KEY
340
+ MODEL_GROUP_ID_CONFIG_KEY
341
341
  ),
342
342
  llm_type=self.llm_config.get(PROVIDER_CONFIG_KEY),
343
343
  llm_model=self.llm_config.get(MODEL_CONFIG_KEY)
344
344
  or self.llm_config.get(MODEL_NAME_CONFIG_KEY),
345
- llm_model_group_id=self.llm_config.get(MODEL_GROUP_CONFIG_KEY),
345
+ llm_model_group_id=self.llm_config.get(MODEL_GROUP_ID_CONFIG_KEY),
346
346
  citation_enabled=self.citation_enabled,
347
347
  )
348
348
  self.persist()
@@ -538,12 +538,12 @@ class EnterpriseSearchPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Po
538
538
  embeddings_model=self.embeddings_config.get(MODEL_CONFIG_KEY)
539
539
  or self.embeddings_config.get(MODEL_NAME_CONFIG_KEY),
540
540
  embeddings_model_group_id=self.embeddings_config.get(
541
- MODEL_GROUP_CONFIG_KEY
541
+ MODEL_GROUP_ID_CONFIG_KEY
542
542
  ),
543
543
  llm_type=self.llm_config.get(PROVIDER_CONFIG_KEY),
544
544
  llm_model=self.llm_config.get(MODEL_CONFIG_KEY)
545
545
  or self.llm_config.get(MODEL_NAME_CONFIG_KEY),
546
- llm_model_group_id=self.llm_config.get(MODEL_GROUP_CONFIG_KEY),
546
+ llm_model_group_id=self.llm_config.get(MODEL_GROUP_ID_CONFIG_KEY),
547
547
  citation_enabled=self.citation_enabled,
548
548
  )
549
549
  return self._create_prediction(
@@ -487,7 +487,8 @@ def validate_collect_step(
487
487
  step: CollectInformationFlowStep,
488
488
  stack: DialogueStack,
489
489
  available_actions: List[str],
490
- slots: Dict[Text, Slot],
490
+ slots: Dict[str, Slot],
491
+ flow_name: str,
491
492
  ) -> bool:
492
493
  """Validate that a collect step can be executed.
493
494
 
@@ -510,12 +511,12 @@ def validate_collect_step(
510
511
  slot_name=step.collect,
511
512
  )
512
513
 
513
- cancel_flow_and_push_internal_error(stack)
514
+ cancel_flow_and_push_internal_error(stack, flow_name)
514
515
 
515
516
  return False
516
517
 
517
518
 
518
- def cancel_flow_and_push_internal_error(stack: DialogueStack) -> None:
519
+ def cancel_flow_and_push_internal_error(stack: DialogueStack, flow_name: str) -> None:
519
520
  """Cancel the top user flow and push the internal error pattern."""
520
521
  top_frame = stack.top()
521
522
 
@@ -527,7 +528,7 @@ def cancel_flow_and_push_internal_error(stack: DialogueStack) -> None:
527
528
  canceled_frames = CancelFlowCommand.select_canceled_frames(stack)
528
529
  stack.push(
529
530
  CancelPatternFlowStackFrame(
530
- canceled_name=top_frame.flow_id,
531
+ canceled_name=flow_name,
531
532
  canceled_frames=canceled_frames,
532
533
  )
533
534
  )
@@ -539,6 +540,7 @@ def validate_custom_slot_mappings(
539
540
  stack: DialogueStack,
540
541
  tracker: DialogueStateTracker,
541
542
  available_actions: List[str],
543
+ flow_name: str,
542
544
  ) -> bool:
543
545
  """Validate a slot with custom mappings.
544
546
 
@@ -559,7 +561,7 @@ def validate_custom_slot_mappings(
559
561
  action=step.collect_action,
560
562
  collect=step.collect,
561
563
  )
562
- cancel_flow_and_push_internal_error(stack)
564
+ cancel_flow_and_push_internal_error(stack, flow_name)
563
565
  return False
564
566
 
565
567
  return True
@@ -599,7 +601,12 @@ def run_step(
599
601
 
600
602
  if isinstance(step, CollectInformationFlowStep):
601
603
  return _run_collect_information_step(
602
- available_actions, initial_events, stack, step, tracker
604
+ available_actions,
605
+ initial_events,
606
+ stack,
607
+ step,
608
+ tracker,
609
+ flow.readable_name(),
603
610
  )
604
611
 
605
612
  elif isinstance(step, ActionFlowStep):
@@ -719,15 +726,18 @@ def _run_collect_information_step(
719
726
  stack: DialogueStack,
720
727
  step: CollectInformationFlowStep,
721
728
  tracker: DialogueStateTracker,
729
+ flow_name: str,
722
730
  ) -> FlowStepResult:
723
- is_step_valid = validate_collect_step(step, stack, available_actions, tracker.slots)
731
+ is_step_valid = validate_collect_step(
732
+ step, stack, available_actions, tracker.slots, flow_name
733
+ )
724
734
 
725
735
  if not is_step_valid:
726
736
  # if we return any other FlowStepResult, the assistant will stay silent
727
737
  # instead of triggering the internal error pattern
728
738
  return ContinueFlowWithNextStep(events=initial_events)
729
739
  is_mapping_valid = validate_custom_slot_mappings(
730
- step, stack, tracker, available_actions
740
+ step, stack, tracker, available_actions, flow_name
731
741
  )
732
742
 
733
743
  if not is_mapping_valid:
@@ -39,7 +39,7 @@ from rasa.shared.constants import (
39
39
  PROVIDER_CONFIG_KEY,
40
40
  OPENAI_PROVIDER,
41
41
  TIMEOUT_CONFIG_KEY,
42
- MODEL_GROUP_CONFIG_KEY,
42
+ MODEL_GROUP_ID_CONFIG_KEY,
43
43
  )
44
44
  from rasa.shared.core.constants import ACTION_LISTEN_NAME
45
45
  from rasa.shared.core.constants import ACTION_TRIGGER_CHITCHAT
@@ -558,11 +558,13 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
558
558
  embeddings_type=self.embeddings_property(PROVIDER_CONFIG_KEY),
559
559
  embeddings_model=self.embeddings_property(MODEL_CONFIG_KEY)
560
560
  or self.embeddings_property(MODEL_NAME_CONFIG_KEY),
561
- embeddings_model_group_id=self.embeddings_property(MODEL_GROUP_CONFIG_KEY),
561
+ embeddings_model_group_id=self.embeddings_property(
562
+ MODEL_GROUP_ID_CONFIG_KEY
563
+ ),
562
564
  llm_type=self.llm_property(PROVIDER_CONFIG_KEY),
563
565
  llm_model=self.llm_property(MODEL_CONFIG_KEY)
564
566
  or self.llm_property(MODEL_NAME_CONFIG_KEY),
565
- llm_model_group_id=self.llm_property(MODEL_GROUP_CONFIG_KEY),
567
+ llm_model_group_id=self.llm_property(MODEL_GROUP_ID_CONFIG_KEY),
566
568
  )
567
569
 
568
570
  self.persist()
@@ -642,11 +644,13 @@ class IntentlessPolicy(LLMHealthCheckMixin, EmbeddingsHealthCheckMixin, Policy):
642
644
  embeddings_type=self.embeddings_property(PROVIDER_CONFIG_KEY),
643
645
  embeddings_model=self.embeddings_property(MODEL_CONFIG_KEY)
644
646
  or self.embeddings_property(MODEL_NAME_CONFIG_KEY),
645
- embeddings_model_group_id=self.embeddings_property(MODEL_GROUP_CONFIG_KEY),
647
+ embeddings_model_group_id=self.embeddings_property(
648
+ MODEL_GROUP_ID_CONFIG_KEY
649
+ ),
646
650
  llm_type=self.llm_property(PROVIDER_CONFIG_KEY),
647
651
  llm_model=self.llm_property(MODEL_CONFIG_KEY)
648
652
  or self.llm_property(MODEL_NAME_CONFIG_KEY),
649
- llm_model_group_id=self.llm_property(MODEL_GROUP_CONFIG_KEY),
653
+ llm_model_group_id=self.llm_property(MODEL_GROUP_ID_CONFIG_KEY),
650
654
  score=score,
651
655
  )
652
656
 
rasa/core/processor.py CHANGED
@@ -1279,11 +1279,13 @@ class MessageProcessor:
1279
1279
  tracker.update(events[0])
1280
1280
  return self.should_predict_another_action(action.name())
1281
1281
  except Exception:
1282
- logger.exception(
1283
- f"Encountered an exception while running action '{action.name()}'."
1284
- "Bot will continue, but the actions events are lost. "
1285
- "Please check the logs of your action server for "
1286
- "more information."
1282
+ structlogger.exception(
1283
+ "rasa.core.processor.run_action.exception",
1284
+ event_info=f"Encountered an exception while "
1285
+ f"running action '{action.name()}'."
1286
+ f"Bot will continue, but the actions events are lost. "
1287
+ f"Please check the logs of your action server for "
1288
+ f"more information.",
1287
1289
  )
1288
1290
  events = []
1289
1291
 
@@ -113,6 +113,7 @@ class SingleStepLLMCommandGenerator(LLMBasedCommandGenerator):
113
113
  )
114
114
 
115
115
  self.trace_prompt_tokens = self.config.get("trace_prompt_tokens", False)
116
+ self.repeat_command_enabled = self.is_repeat_command_enabled()
116
117
 
117
118
  ### Implementations of LLMBasedCommandGenerator parent
118
119
  @staticmethod
@@ -458,7 +459,7 @@ class SingleStepLLMCommandGenerator(LLMBasedCommandGenerator):
458
459
  "current_slot": current_slot,
459
460
  "current_slot_description": current_slot_description,
460
461
  "user_message": latest_user_message,
461
- "is_repeat_command_enabled": self.is_repeat_command_enabled(),
462
+ "is_repeat_command_enabled": self.repeat_command_enabled,
462
463
  }
463
464
 
464
465
  return self.compile_template(self.prompt_template).render(**inputs)
@@ -111,6 +111,15 @@ slots:
111
111
  type: bool
112
112
  mappings:
113
113
  - type: from_llm
114
+ silence_timeout:
115
+ type: float
116
+ initial_value: 6.0
117
+ max_value: 1000000
118
+ consecutive_silence_timeouts:
119
+ type: float
120
+ initial_value: 0.0
121
+ max_value: 1000000
122
+
114
123
 
115
124
  flows:
116
125
  pattern_cancel_flow:
@@ -35,6 +35,7 @@ class AggregateTestStatsCalculator:
35
35
  self.test_cases = test_cases
36
36
 
37
37
  self.failed_assertion_set: Set["Assertion"] = set()
38
+ self.failed_test_cases_without_assertion_failure: Set[str] = set()
38
39
  self.passed_count_mapping = {
39
40
  subclass_type: 0
40
41
  for subclass_type in _get_all_assertion_subclasses().keys()
@@ -89,8 +90,14 @@ class AggregateTestStatsCalculator:
89
90
  passed_test_case_names = [
90
91
  passed.test_case.name for passed in self.passed_results
91
92
  ]
93
+ # We filter out test cases that failed without an assertion failure
94
+ filtered_test_cases = [
95
+ test_case
96
+ for test_case in self.test_cases
97
+ if test_case.name not in self.failed_test_cases_without_assertion_failure
98
+ ]
92
99
 
93
- for test_case in self.test_cases:
100
+ for test_case in filtered_test_cases:
94
101
  if test_case.name in passed_test_case_names:
95
102
  for step in test_case.steps:
96
103
  if step.assertions is None:
@@ -118,6 +125,9 @@ class AggregateTestStatsCalculator:
118
125
  "no_assertion_failure_in_failed_result",
119
126
  test_case=failed.test_case.name,
120
127
  )
128
+ self.failed_test_cases_without_assertion_failure.add(
129
+ failed.test_case.name
130
+ )
121
131
  continue
122
132
 
123
133
  self.failed_assertion_set.add(failed.assertion_failure.assertion)
@@ -71,6 +71,7 @@ class AssertionType(Enum):
71
71
  SLOT_WAS_SET = "slot_was_set"
72
72
  SLOT_WAS_NOT_SET = "slot_was_not_set"
73
73
  BOT_UTTERED = "bot_uttered"
74
+ BOT_DID_NOT_UTTER = "bot_did_not_utter"
74
75
  GENERATIVE_RESPONSE_IS_RELEVANT = "generative_response_is_relevant"
75
76
  GENERATIVE_RESPONSE_IS_GROUNDED = "generative_response_is_grounded"
76
77
 
@@ -722,6 +723,7 @@ class BotUtteredAssertion(Assertion):
722
723
  ) -> Tuple[Optional[AssertionFailure], Optional[Event]]:
723
724
  """Run the bot_uttered assertion on the given events for that user turn."""
724
725
  matching_event = None
726
+ error_messages = []
725
727
 
726
728
  if self.utter_name is not None:
727
729
  try:
@@ -732,11 +734,8 @@ class BotUtteredAssertion(Assertion):
732
734
  and event.metadata.get("utter_action") == self.utter_name
733
735
  )
734
736
  except StopIteration:
735
- error_message = f"Bot did not utter '{self.utter_name}' response."
736
- error_message += assertion_order_error_message
737
-
738
- return self._generate_assertion_failure(
739
- error_message, prior_events, turn_events, self.line
737
+ error_messages.append(
738
+ f"Bot did not utter '{self.utter_name}' response."
740
739
  )
741
740
 
742
741
  if self.text_matches is not None:
@@ -748,16 +747,11 @@ class BotUtteredAssertion(Assertion):
748
747
  if isinstance(event, BotUttered) and pattern.search(event.text)
749
748
  )
750
749
  except StopIteration:
751
- error_message = (
750
+ error_messages.append(
752
751
  f"Bot did not utter any response which "
753
752
  f"matches the provided text pattern "
754
753
  f"'{self.text_matches}'."
755
754
  )
756
- error_message += assertion_order_error_message
757
-
758
- return self._generate_assertion_failure(
759
- error_message, prior_events, turn_events, self.line
760
- )
761
755
 
762
756
  if self.buttons:
763
757
  try:
@@ -767,13 +761,16 @@ class BotUtteredAssertion(Assertion):
767
761
  if isinstance(event, BotUttered) and self._buttons_match(event)
768
762
  )
769
763
  except StopIteration:
770
- error_message = (
764
+ error_messages.append(
771
765
  "Bot did not utter any response with the expected buttons."
772
766
  )
773
- error_message += assertion_order_error_message
774
- return self._generate_assertion_failure(
775
- error_message, prior_events, turn_events, self.line
776
- )
767
+
768
+ if error_messages:
769
+ error_message = " ".join(error_messages)
770
+ error_message += assertion_order_error_message
771
+ return self._generate_assertion_failure(
772
+ error_message, prior_events, turn_events, self.line
773
+ )
777
774
 
778
775
  return None, matching_event
779
776
 
@@ -803,6 +800,126 @@ class BotUtteredAssertion(Assertion):
803
800
  return hash(json.dumps(self.as_dict()))
804
801
 
805
802
 
803
+ @dataclass
804
+ class BotDidNotUtterAssertion(Assertion):
805
+ """Class for the 'bot_did_not_utter' assertion."""
806
+
807
+ utter_name: Optional[str] = None
808
+ text_matches: Optional[str] = None
809
+ buttons: Optional[List[AssertedButton]] = None
810
+ line: Optional[int] = None
811
+
812
+ @classmethod
813
+ def type(cls) -> str:
814
+ return AssertionType.BOT_DID_NOT_UTTER.value
815
+
816
+ @staticmethod
817
+ def from_dict(assertion_dict: Dict[Text, Any]) -> BotDidNotUtterAssertion:
818
+ """Creates a BotDidNotUtterAssertion from a dictionary."""
819
+ assertion_dict = assertion_dict.get(AssertionType.BOT_DID_NOT_UTTER.value, {})
820
+ utter_name = assertion_dict.get("utter_name")
821
+ text_matches = assertion_dict.get("text_matches")
822
+ buttons = [
823
+ AssertedButton.from_dict(button)
824
+ for button in assertion_dict.get("buttons", [])
825
+ ]
826
+
827
+ if not utter_name and not text_matches and not buttons:
828
+ raise RasaException(
829
+ "A 'bot_did_not_utter' assertion is empty. "
830
+ "It should contain at least one of the allowed properties: "
831
+ "'utter_name', 'text_matches', or 'buttons'."
832
+ )
833
+
834
+ return BotDidNotUtterAssertion(
835
+ utter_name=utter_name,
836
+ text_matches=text_matches,
837
+ buttons=buttons,
838
+ line=assertion_dict.lc.line + 1 if hasattr(assertion_dict, "lc") else None,
839
+ )
840
+
841
+ def run(
842
+ self,
843
+ turn_events: List[Event],
844
+ prior_events: List[Event],
845
+ assertion_order_error_message: str = "",
846
+ **kwargs: Any,
847
+ ) -> Tuple[Optional[AssertionFailure], Optional[Event]]:
848
+ """Checks that the bot did not utter the specified messages or buttons."""
849
+ for event in turn_events:
850
+ if isinstance(event, BotUttered):
851
+ error_messages = []
852
+ if self._utter_name_matches(event):
853
+ error_messages.append(
854
+ f"Bot uttered a forbidden utterance '{self.utter_name}'."
855
+ )
856
+ if self._text_matches(event):
857
+ error_messages.append(
858
+ f"Bot uttered a forbidden message matching "
859
+ f"the pattern '{self.text_matches}'."
860
+ )
861
+ if self._buttons_match(event):
862
+ error_messages.append(
863
+ "Bot uttered a forbidden response with specified buttons."
864
+ )
865
+
866
+ if error_messages:
867
+ error_message = " ".join(error_messages)
868
+ error_message += assertion_order_error_message
869
+ return self._generate_assertion_failure(
870
+ error_message, prior_events, turn_events, self.line
871
+ )
872
+ return None, None
873
+
874
+ def _utter_name_matches(self, event: BotUttered) -> bool:
875
+ if self.utter_name is not None:
876
+ if event.metadata.get("utter_action") == self.utter_name:
877
+ return True
878
+ return False
879
+
880
+ def _text_matches(self, event: BotUttered) -> bool:
881
+ if self.text_matches is not None:
882
+ pattern = re.compile(self.text_matches)
883
+ if pattern.search(event.text):
884
+ return True
885
+ return False
886
+
887
+ def _buttons_match(self, event: BotUttered) -> bool:
888
+ """Check if the bot response contains any of the forbidden buttons."""
889
+ if self.buttons is None:
890
+ return False
891
+
892
+ actual_buttons = event.data.get("buttons", [])
893
+ if not actual_buttons:
894
+ return False
895
+
896
+ for actual_button in actual_buttons:
897
+ if any(
898
+ self._is_forbidden_button(actual_button, forbidden_button)
899
+ for forbidden_button in self.buttons
900
+ ):
901
+ return True
902
+ return False
903
+
904
+ @staticmethod
905
+ def _is_forbidden_button(
906
+ actual_button: Dict[str, Any], forbidden_button: AssertedButton
907
+ ) -> bool:
908
+ """Check if the button matches any of the forbidden buttons."""
909
+ actual_title = actual_button.get("title")
910
+ actual_payload = actual_button.get("payload")
911
+
912
+ title_matches = forbidden_button.title == actual_title
913
+ payload_matches = forbidden_button.payload == actual_payload
914
+ if title_matches and payload_matches:
915
+ return True
916
+ return False
917
+
918
+ def __hash__(self) -> int:
919
+ """Hash method to ensure the assertion is hashable."""
920
+ return hash(json.dumps(self.as_dict()))
921
+
922
+
806
923
  @dataclass
807
924
  class GenerativeResponseMixin(Assertion):
808
925
  """Mixin class for storing generative response assertions."""