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.
- rasa/__main__.py +9 -3
- rasa/cli/studio/upload.py +0 -15
- rasa/cli/utils.py +1 -1
- rasa/core/channels/development_inspector.py +8 -2
- rasa/core/channels/voice_ready/audiocodes.py +3 -4
- rasa/core/channels/voice_stream/asr/asr_engine.py +19 -1
- rasa/core/channels/voice_stream/asr/asr_event.py +1 -1
- rasa/core/channels/voice_stream/asr/azure.py +16 -9
- rasa/core/channels/voice_stream/asr/deepgram.py +17 -14
- rasa/core/channels/voice_stream/tts/azure.py +3 -1
- rasa/core/channels/voice_stream/tts/cartesia.py +3 -3
- rasa/core/channels/voice_stream/tts/tts_engine.py +10 -1
- rasa/core/channels/voice_stream/voice_channel.py +48 -18
- rasa/core/information_retrieval/qdrant.py +1 -0
- rasa/core/nlg/contextual_response_rephraser.py +2 -2
- rasa/core/persistor.py +93 -49
- rasa/core/policies/enterprise_search_policy.py +5 -5
- rasa/core/policies/flows/flow_executor.py +18 -8
- rasa/core/policies/intentless_policy.py +9 -5
- rasa/core/processor.py +7 -5
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +2 -1
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +9 -0
- rasa/e2e_test/aggregate_test_stats_calculator.py +11 -1
- rasa/e2e_test/assertions.py +133 -16
- rasa/e2e_test/assertions_schema.yml +23 -0
- rasa/e2e_test/e2e_test_runner.py +2 -2
- rasa/engine/loader.py +12 -0
- rasa/engine/validation.py +310 -86
- rasa/model_manager/config.py +8 -0
- rasa/model_manager/model_api.py +166 -61
- rasa/model_manager/runner_service.py +31 -26
- rasa/model_manager/trainer_service.py +14 -23
- rasa/model_manager/warm_rasa_process.py +187 -0
- rasa/model_service.py +3 -5
- rasa/model_training.py +3 -1
- rasa/shared/constants.py +27 -5
- rasa/shared/core/constants.py +1 -1
- rasa/shared/core/domain.py +8 -31
- rasa/shared/core/flows/yaml_flows_io.py +13 -4
- rasa/shared/importers/importer.py +19 -2
- rasa/shared/importers/rasa.py +5 -1
- rasa/shared/nlu/training_data/formats/rasa_yaml.py +18 -3
- rasa/shared/providers/_configs/litellm_router_client_config.py +29 -9
- rasa/shared/providers/_utils.py +79 -0
- rasa/shared/providers/embedding/default_litellm_embedding_client.py +24 -0
- rasa/shared/providers/embedding/litellm_router_embedding_client.py +1 -1
- rasa/shared/providers/llm/_base_litellm_client.py +26 -0
- rasa/shared/providers/llm/default_litellm_llm_client.py +24 -0
- rasa/shared/providers/llm/litellm_router_llm_client.py +56 -1
- rasa/shared/providers/llm/self_hosted_llm_client.py +4 -28
- rasa/shared/providers/router/_base_litellm_router_client.py +35 -1
- rasa/shared/utils/common.py +30 -3
- rasa/shared/utils/health_check/health_check.py +26 -24
- rasa/shared/utils/yaml.py +116 -31
- rasa/studio/data_handler.py +3 -1
- rasa/studio/upload.py +119 -57
- rasa/telemetry.py +3 -1
- rasa/tracing/config.py +1 -1
- rasa/validator.py +40 -4
- rasa/version.py +1 -1
- {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/METADATA +2 -2
- {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/RECORD +65 -63
- {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/NOTICE +0 -0
- {rasa_pro-3.11.0rc2.dist-info → rasa_pro-3.11.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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=
|
|
328
|
+
"aws_persistor.retrieve_tar.object_found", object_key=target_filename
|
|
295
329
|
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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
|
|
464
|
-
"""
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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[
|
|
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=
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
"
|
|
1285
|
-
"
|
|
1286
|
-
"
|
|
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.
|
|
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
|
|
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)
|
rasa/e2e_test/assertions.py
CHANGED
|
@@ -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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
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
|
-
|
|
764
|
+
error_messages.append(
|
|
771
765
|
"Bot did not utter any response with the expected buttons."
|
|
772
766
|
)
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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."""
|