diff --git a/kairon/shared/data/processor.py b/kairon/shared/data/processor.py index d430021c4..5cb85d0ca 100644 --- a/kairon/shared/data/processor.py +++ b/kairon/shared/data/processor.py @@ -1136,7 +1136,20 @@ def fetch_actions(self, bot: Text, status=True): :return: list of actions """ actions = Actions.objects(bot=bot, status=status).values_list("name") - return list(actions) + actions_list = list(actions) + + for story in Stories.objects(bot=bot, status=status): + for event in story.events: + if event.name == 'action_listen': + if 'action_listen' not in actions_list: + actions_list.append('action_listen') + + for story in MultiflowStories.objects(bot=bot, status=status): + for event in story.events: + if event.step.name == 'stop_flow_action' and 'stop_flow_action' not in actions_list: + actions_list.append('stop_flow_action') + + return actions_list def __prepare_training_actions(self, bot: Text): actions = self.fetch_actions(bot) @@ -1698,6 +1711,10 @@ def __prepare_training_multiflow_story_events(self, events, metadata, timestamp) story_events.append( SlotSet(key=event.name, value=event.value, timestamp=timestamp) ) + elif event.step_type == StoryStepType.stop_flow_action.value: + story_events.append( + ActionExecuted(action_name=ACTION_LISTEN_NAME, timestamp=timestamp) + ) else: story_events.append( ActionExecuted(action_name=event.name, timestamp=timestamp) @@ -3348,6 +3365,9 @@ def get_stories(self, bot: Text): step["type"] = StoryStepType.web_search_action.value elif event['name'] == 'live_agent_action': step["type"] = StoryStepType.live_agent_action.value + elif event['name'] == 'action_listen': + step["type"] = StoryStepType.stop_flow_action.value + step["name"] = 'stop_flow_action' elif str(event["name"]).startswith("utter_"): step["type"] = StoryStepType.bot.value else: diff --git a/kairon/shared/kairon_yaml_story_writer.py b/kairon/shared/kairon_yaml_story_writer.py new file mode 100644 index 000000000..0729c1e6f --- /dev/null +++ b/kairon/shared/kairon_yaml_story_writer.py @@ -0,0 +1,49 @@ +from typing import List, Text, Any, Union +from collections import OrderedDict + +from rasa.shared.core.training_data.story_writer.yaml_story_writer import YAMLStoryWriter +from rasa.shared.core.training_data.structures import StoryStep +from rasa.shared.core.events import Event + + +from rasa.shared.core.training_data.story_reader.yaml_story_reader import KEY_STORY_NAME, KEY_STEPS + + +class kaironYAMLStoryWriter(YAMLStoryWriter): + """Custom YAML Story Writer with overridden _filter_event function.""" + + def process_story_step(self, story_step: StoryStep) -> OrderedDict: + """Converts a single story step into an ordered dict with a custom filter.""" + result: OrderedDict[Text, Any] = OrderedDict() + result[KEY_STORY_NAME] = story_step.block_name + steps = self.process_checkpoints(story_step.start_checkpoints) + for event in story_step.events: + if not self._filter_event(event): + continue + processed = self.process_event(event) + if processed: + steps.append(processed) + + steps.extend(self.process_checkpoints(story_step.end_checkpoints)) + + result[KEY_STEPS] = steps + + return result + + @staticmethod + def _filter_event(event: Union["Event", List["Event"]]) -> bool: + """Identifies if the event should be converted/written. + + Args: + event: target event to check. + + Returns: + `True` if the event should be converted/written, `False` otherwise. + """ + if isinstance(event, list): + return True + + return ( + not StoryStep.is_action_unlikely_intent(event) + and not StoryStep.is_action_session_start(event) + ) \ No newline at end of file diff --git a/kairon/shared/utils.py b/kairon/shared/utils.py index 93fe213c6..0d9fd41a9 100644 --- a/kairon/shared/utils.py +++ b/kairon/shared/utils.py @@ -79,6 +79,7 @@ KAIRON_TWO_STAGE_FALLBACK, SLOT_TYPE, ) +from kairon.shared.kairon_yaml_story_writer import kaironYAMLStoryWriter from .data.dto import KaironStoryStep from .models import StoryStepType, LlmPromptType, LlmPromptSource from ..exceptions import AppException @@ -1166,7 +1167,8 @@ def write_training_data( yaml.safe_dump(domain, open(domain_path, "w")) Utility.write_to_file(nlu_path, nlu_as_str) Utility.write_to_file(config_path, config_as_str) - YAMLStoryWriter().dump(stories_path, stories.story_steps) + story_writer = kaironYAMLStoryWriter() + story_writer.dump(stories_path, stories.story_steps) if rules: YAMLStoryWriter().dump(rules_path, rules.story_steps) if actions: @@ -2271,6 +2273,12 @@ def validate_steps(steps: List, flow_metadata: List): raise AppException( "Intent can only have one connection of action type or slot type" ) + if [ + successor + for successor in story_graph.successors(story_node) + if successor.step_type == "STOP_FLOW_ACTION" + ]: + raise AppException("STOP_FLOW_ACTION cannot be a successor of an intent!") if story_node.step_type == "SLOT" and story_node.value: if story_node.value is not None and not isinstance( story_node.value, (str, int, bool) @@ -2286,6 +2294,8 @@ def validate_steps(steps: List, flow_metadata: List): raise AppException("Slots cannot be leaf nodes!") if story_node.step_type == "INTENT" and story_node.node_id in leaf_node_ids: raise AppException("Leaf nodes cannot be intent") + if story_node.step_type == "STOP_FLOW_ACTION" and story_node.node_id not in leaf_node_ids: + raise AppException("STOP_FLOW_ACTION should be a leaf node!") if flow_metadata: for value in flow_metadata: if value.get("flow_type") == "RULE": diff --git a/tests/integration_test/services_test.py b/tests/integration_test/services_test.py index 08d9bbc43..663c7c8c3 100644 --- a/tests/integration_test/services_test.py +++ b/tests/integration_test/services_test.py @@ -6,6 +6,7 @@ import tempfile from datetime import datetime, timedelta from io import BytesIO +import yaml from mock import patch from urllib.parse import urljoin from zipfile import ZipFile @@ -6149,59 +6150,6 @@ def test_add_story_empty_event(): } ] - -def test_add_story_stop_flow_action_not_at_end(): - response = client.post( - f"/api/bot/{pytest.bot}/stories", - json={ - "name": "test_add_story_stop_flow_action_not_at_end", - "type": "STORY", - "steps": [ - {"name": "greet", "type": "INTENT"}, - {"name": "utter_goodbye", "type": "BOT"}, - {"name": "stop", "type": "STOP_FLOW_ACTION"}, - {"name": "utter_goodbye", "type": "BOT"}, - ], - }, - headers={"Authorization": pytest.token_type + " " + pytest.access_token}, - ) - actual = response.json() - assert not actual["success"] - assert actual["error_code"] == 422 - assert actual["message"] == [ - { - "loc": ["body", "steps"], - "msg": "Stop Flow Action should only be at the end of the flow", - "type": "value_error", - } - ] - - -def test_add_story_stop_flow_action_after_intent(): - response = client.post( - f"/api/bot/{pytest.bot}/stories", - json={ - "name": "test_add_story_stop_flow_action_after_intent", - "type": "STORY", - "steps": [ - {"name": "greet", "type": "INTENT"}, - {"name": "stop", "type": "STOP_FLOW_ACTION"}, - ], - }, - headers={"Authorization": pytest.token_type + " " + pytest.access_token}, - ) - actual = response.json() - assert not actual["success"] - assert actual["error_code"] == 422 - assert actual["message"] == [ - { - "loc": ["body", "steps"], - "msg": "Stop Flow Action should not be after intent", - "type": "value_error", - } - ] - - def test_add_story_lone_intent(): response = client.post( f"/api/bot/{pytest.bot}/stories", @@ -7745,174 +7693,815 @@ def test_get_utterance_from_not_exist_intent(): assert Utility.check_empty_string(actual["message"]) -@responses.activate -def test_train_on_updated_data(monkeypatch): - def _mock_training_limit(*arge, **kwargs): - return False - - monkeypatch.setattr( - ModelProcessor, "is_daily_training_limit_exceeded", _mock_training_limit - ) - - event_url = urljoin( - Utility.environment["events"]["server_url"], - f"/api/events/execute/{EventClass.model_training}", - ) - responses.add( - "POST", - event_url, - json={"success": True, "message": "Event triggered successfully!"}, - ) - +def test_add_story_stop_flow_action(): response = client.post( - f"/api/bot/{pytest.bot}/slots", + f"/api/bot/{pytest.bot}/stories", json={ - "name": "frontend", - "type": "text", - "influence_conversation": True, + "name": "test_add_story_stop_flow_action", + "type": "STORY", + "steps": [ + {"name": "greet", "type": "INTENT"}, + {"name": "utter_goodbye", "type": "BOT"}, + {"name": "utter_goodbye", "type": "BOT"}, + {"name": "stop", "type": "STOP_FLOW_ACTION"}, + ], }, headers={"Authorization": pytest.token_type + " " + pytest.access_token}, ) - actual = response.json() - assert "data" in actual - assert actual["message"] == "Slot added successfully!" + assert actual["message"] == "Flow added successfully" assert actual["data"]["_id"] assert actual["success"] assert actual["error_code"] == 0 - response = client.post( - f"/api/bot/{pytest.bot}/slots", - json={ - "name": "more_queries", - "type": "text", - "influence_conversation": True, - }, - headers={"Authorization": pytest.token_type + " " + pytest.access_token}, - ) - - response = client.post( - f"/api/bot/{pytest.bot}/train", - headers={"Authorization": pytest.token_type + " " + pytest.access_token}, - ) - actual = response.json() - assert actual["success"] - assert actual["error_code"] == 0 - assert actual["data"] is None - assert actual["message"] == "Model training started." - complete_end_to_end_event_execution( - pytest.bot, "integration@demo.ai", EventClass.model_training - ) - - -def test_download_model_training_logs(monkeypatch): - start_date = datetime.utcnow() - timedelta(days=1) - end_date = datetime.utcnow() + timedelta(days=1) response = client.get( - f"/api/bot/{pytest.bot}/logs/download/model_training?start_date={start_date}&end_date={end_date}", - headers={"Authorization": pytest.token_type + " " + pytest.access_token}, - ) - assert response.content - - -@pytest.fixture -def mock_is_training_inprogress_exception(monkeypatch): - def _inprogress_execption_response(*args, **kwargs): - raise AppException("Previous model training in progress.") - - monkeypatch.setattr( - ModelProcessor, "is_training_inprogress", _inprogress_execption_response - ) - - -def test_train_inprogress(mock_is_training_inprogress_exception): - response = client.post( - f"/api/bot/{pytest.bot}/train", + f"/api/bot/{pytest.bot}/stories", headers={"Authorization": pytest.token_type + " " + pytest.access_token}, ) actual = response.json() - assert actual["success"] is False - assert actual["error_code"] == 422 - assert actual["data"] is None - assert actual["message"] == "Previous model training in progress." - - -@pytest.fixture -def mock_is_training_inprogress(monkeypatch): - def _inprogress_response(*args, **kwargs): - return False - - monkeypatch.setattr(ModelProcessor, "is_training_inprogress", _inprogress_response) + assert actual["success"] + story = next((item for item in actual['data'] if item['name'] == 'test_add_story_stop_flow_action'), None) + assert story is not None + assert story['steps'][-1]['name'] == 'stop_flow_action' + assert story['steps'][-1]['type'] == 'STOP_FLOW_ACTION' -def test_train_daily_limit_exceed(mock_is_training_inprogress, monkeypatch): - bot_settings = BotSettings.objects(bot=pytest.bot).get() - bot_settings.training_limit_per_day = 2 - bot_settings.save() +def test_add_story_stop_flow_action_not_at_end(): response = client.post( - f"/api/bot/{pytest.bot}/train", + f"/api/bot/{pytest.bot}/stories", + json={ + "name": "test_add_story_stop_flow_action_not_at_end", + "type": "STORY", + "steps": [ + {"name": "greet", "type": "INTENT"}, + {"name": "utter_goodbye", "type": "BOT"}, + {"name": "stop", "type": "STOP_FLOW_ACTION"}, + {"name": "utter_goodbye", "type": "BOT"}, + ], + }, headers={"Authorization": pytest.token_type + " " + pytest.access_token}, ) actual = response.json() assert not actual["success"] assert actual["error_code"] == 422 - assert actual["data"] is None - assert actual["message"] == "Daily model training limit exceeded." - - -def test_get_model_training_history(): - response = client.get( - f"/api/bot/{pytest.bot}/train/history", - headers={"Authorization": pytest.token_type + " " + pytest.access_token}, - ) - actual = response.json() - assert actual["success"] is True - assert actual["error_code"] == 0 - assert actual["data"] - assert "training_history" in actual["data"] + assert actual["message"] == [ + { + "loc": ["body", "steps"], + "msg": "Stop Flow Action should only be at the end of the flow", + "type": "value_error", + } + ] -def test_model_testing_limit_exceeded(monkeypatch): - monkeypatch.setitem(Utility.environment["model"]["test"], "limit_per_day", 0) +def test_add_story_stop_flow_action_after_intent(): response = client.post( - url=f"/api/bot/{pytest.bot}/test", + f"/api/bot/{pytest.bot}/stories", + json={ + "name": "test_add_story_stop_flow_action_after_intent", + "type": "STORY", + "steps": [ + {"name": "greet", "type": "INTENT"}, + {"name": "stop", "type": "STOP_FLOW_ACTION"}, + ], + }, headers={"Authorization": pytest.token_type + " " + pytest.access_token}, ) actual = response.json() - assert actual["error_code"] == 422 - assert actual["message"] == "Daily limit exceeded." assert not actual["success"] - + assert actual["error_code"] == 422 + assert actual["message"] == [ + { + "loc": ["body", "steps"], + "msg": "Stop Flow Action should not be after intent", + "type": "value_error", + } + ] @responses.activate -def test_model_testing_event(monkeypatch): - bot_settings = BotSettings.objects(bot=pytest.bot).get() - bot_settings.test_limit_per_day = 5 - bot_settings.save() +def test_upload_stop_flow_action(): event_url = urljoin( Utility.environment["events"]["server_url"], - f"/api/events/execute/{EventClass.model_testing}", + f"/api/events/execute/{EventClass.data_importer}", ) responses.add( "POST", event_url, json={"success": True, "message": "Event triggered successfully!"}, ) + + files = ( + ( + "training_files", + ("nlu.yml", open("tests/testing_data/stop_flow_action/upload_story/data/nlu.yml", "rb")), + ), + ( + "training_files", + ("domain.yml", open("tests/testing_data/stop_flow_action/upload_story/domain.yml", "rb")), + ), + ( + "training_files", + ("stories.yml", open("tests/testing_data/stop_flow_action/upload_story/data/stories.yml", "rb")), + ), + ( + "training_files", + ("config.yml", open("tests/testing_data/stop_flow_action/upload_story/config.yml", "rb")), + ), + ( + "training_files", + ( + "chat_client_config.yml", + open("tests/testing_data/stop_flow_action/upload_story/chat_client_config.yml", "rb"), + ), + ), + ) response = client.post( - url=f"/api/bot/{pytest.bot}/test", + f"/api/bot/{pytest.bot}/upload?import_data=true&overwrite=true", headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + files=files, ) actual = response.json() + assert actual["message"] == "Upload in progress! Check logs." assert actual["error_code"] == 0 - assert actual["message"] == "Testing in progress! Check logs." + assert actual["data"] is None assert actual["success"] + complete_end_to_end_event_execution( + pytest.bot, "integration@demo.ai", EventClass.data_importer + ) - -@responses.activate -def test_model_testing_in_progress(): - event_url = urljoin( - Utility.environment["events"]["server_url"], - f"/api/events/execute/{EventClass.model_testing}", + response = client.get( + f"/api/bot/{pytest.bot}/stories", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + + story = next((story_item for story_item in actual["data"] if story_item["name"] == "story_with_stop_flow_action"), None) + + assert story is not None + assert story["steps"][-1]["name"] == "stop_flow_action" + assert story["steps"][-1]["type"] == "STOP_FLOW_ACTION" + + +def test_download_story_with_stop_flow_action(): + response = client.post( + f"/api/bot/{pytest.bot}/stories", + json={ + "name": "test_download_story_with_stop_flow_action", + "type": "STORY", + "steps": [ + {"name": "greet", "type": "INTENT"}, + {"name": "utter_goodbye", "type": "BOT"}, + {"name": "stop_flow_action", "type": "STOP_FLOW_ACTION"}, + ], + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["message"] == "Flow added successfully" + assert actual["data"]["_id"] + assert actual["success"] + assert actual["error_code"] == 0 + + response = client.get( + f"/api/bot/{pytest.bot}/download/data", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + file_bytes = BytesIO(response.content) + + zip_file = ZipFile(file_bytes, mode="r") + with zip_file.open("data/stories.yml") as file: + stories_yaml = yaml.safe_load(file.read().decode("utf-8")) + with zip_file.open("domain.yml") as file: + domain_yaml = yaml.safe_load(file.read().decode("utf-8")) + + story = next((story_item for story_item in stories_yaml["stories"] if story_item["story"] == "test_download_story_with_stop_flow_action"), None) + assert story is not None + assert story["steps"][-1]["action"] == "action_listen" + assert domain_yaml["actions"][-1] == "action_listen" + + +@responses.activate +def test_add_multiflow_story_stop_flow_action(): + response = client.post( + f"/api/bot/{pytest.bot}/v2/stories", + json={ + "name": "test_add_multiflow_story_stop_flow_action", + "steps": [ + { + "step": { + "name": "greet", + "type": "INTENT", + "node_id": "1", + "component_id": "Mnvehd", + }, + "connections": [ + { + "name": "utter_greet", + "type": "BOT", + "node_id": "2", + "component_id": "PLhfhs", + } + ], + }, + { + "step": { + "name": "utter_greet", + "type": "BOT", + "node_id": "2", + "component_id": "PLhfhs", + }, + "connections": [ + { + "name": "stop_flow", + "type": "STOP_FLOW_ACTION", + "node_id": "3", + "component_id": "NNXX", + }, + { + "name": "utter_bye", + "type": "BOT", + "node_id": "4", + "component_id": "NNXXY", + }, + ], + }, + { + "step": { + "name": "stop_flow", + "type": "STOP_FLOW_ACTION", + "node_id": "3", + "component_id": "NNXX", + }, + "connections": None, + }, + { + "step": { + "name": "utter_bye", + "type": "BOT", + "node_id": "4", + "component_id": "NNXXY", + }, + "connections": [ + { + "name": "utter_bye", + "type": "BOT", + "node_id": "5", + "component_id": "NNXXY", + } + ], + }, + { + "step": { + "name": "utter_bye", + "type": "BOT", + "node_id": "5", + "component_id": "NNXXY", + }, + "connections": None, + } + ], + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["message"] == "Story flow added successfully" + assert actual["data"]["_id"] + assert actual["success"] + assert actual["error_code"] == 0 + + response = client.get( + f"/api/bot/{pytest.bot}/stories", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + + story = next((item for item in actual['data'] if item['name'] == 'test_add_multiflow_story_stop_flow_action'), None) + assert story is not None + + stop_flow_step = next((step for step in story['steps'] if + step['step']['name'] == 'stop_flow' and step['step']['type'] == 'STOP_FLOW_ACTION'), None) + + assert stop_flow_step is not None + assert stop_flow_step['connections'] == [] + + +def test_add_multiflow_story_stop_flow_action_not_at_end(): + response = client.post( + f"/api/bot/{pytest.bot}/v2/stories", + json={ + "name": "test_stop_path", + "steps": [ + { + "step": { + "name": "greet", + "type": "INTENT", + "node_id": "1", + "component_id": "Mnvehd", + }, + "connections": [ + { + "name": "utter_greet", + "type": "BOT", + "node_id": "2", + "component_id": "PLhfhs", + } + ], + }, + { + "step": { + "name": "utter_greet", + "type": "BOT", + "node_id": "2", + "component_id": "PLhfhs", + }, + "connections": [ + { + "name": "more_queries", + "type": "INTENT", + "node_id": "3", + "component_id": "MNbcg", + }, + { + "name": "stop_flow", + "type": "STOP_FLOW_ACTION", + "node_id": "4", + "component_id": "QQAA", + }, + ], + }, + { + "step": { + "name": "stop_flow", + "type": "STOP_FLOW_ACTION", + "node_id": "4", + "component_id": "QQAA", + }, + "connections": [ + { + "name": "utter_goodbye", + "type": "BOT", + "node_id": "5", + "component_id": "NNXX", + } + ], + }, + { + "step": { + "name": "utter_goodbye", + "type": "BOT", + "node_id": "5", + "component_id": "NNXX", + }, + "connections": None, + }, + { + "step": { + "name": "utter_more_queries", + "type": "BOT", + "node_id": "6", + "component_id": "MnveRRhd", + }, + "connections": None, + }, + { + "step": { + "name": "more_queries", + "type": "INTENT", + "node_id": "3", + "component_id": "MNbcg", + }, + "connections": [ + { + "name": "utter_more_queries", + "type": "BOT", + "node_id": "6", + "component_id": "MnveRRhd", + } + ], + }, + ], + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert not actual['success'] + assert actual['message'] == 'STOP_FLOW_ACTION should be a leaf node!' + assert actual["error_code"] == 422 + + +def test_add_multiflow_story_stop_flow_action_after_intent(): + response = client.post( + f"/api/bot/{pytest.bot}/v2/stories", + json={ + "name": "test_stop_path", + "steps": [ + { + "step": { + "name": "greet", + "type": "INTENT", + "node_id": "1", + "component_id": "Mnvehd", + }, + "connections": [ + { + "name": "stop_flow", + "type": "STOP_FLOW_ACTION", + "node_id": "2", + "component_id": "PLhfhs", + } + ], + }, + { + "step": { + "name": "stop_flow", + "type": "STOP_FLOW_ACTION", + "node_id": "2", + "component_id": "PLhfhs", + }, + "connections": None, + }, + ], + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert not actual['success'] + assert actual['message'] == 'STOP_FLOW_ACTION cannot be a successor of an intent!' + assert actual["error_code"] == 422 + +@responses.activate +@patch.object(ModelProcessor, "is_daily_training_limit_exceeded") +def test_upload_stop_flow_action_multiflow(mock_training_limit): + event_url = urljoin( + Utility.environment["events"]["server_url"], + f"/api/events/execute/{EventClass.data_importer}", + ) + responses.add( + "POST", + event_url, + json={"success": True, "message": "Event triggered successfully!"}, + ) + + files = ( + ( + "training_files", + ("nlu.yml", open("tests/testing_data/stop_flow_action/upload_multiflow_story/data/nlu.yml", "rb")), + ), + ( + "training_files", + ("rules.yml", open("tests/testing_data/stop_flow_action/upload_multiflow_story/data/rules.yml", "rb")), + ), + ( + "training_files", + ("domain.yml", open("tests/testing_data/stop_flow_action/upload_multiflow_story/domain.yml", "rb")), + ), + ( + "training_files", + ("stories.yml", open("tests/testing_data/stop_flow_action/upload_multiflow_story/data/stories.yml", "rb")), + ), + ( + "training_files", + ("config.yml", open("tests/testing_data/stop_flow_action/upload_multiflow_story/config.yml", "rb")), + ), + ( + "training_files", + ( + "chat_client_config.yml", + open("tests/testing_data/stop_flow_action/upload_multiflow_story/chat_client_config.yml", "rb"), + ), + ), + ( + "training_files", + ( + "actions.yml", + open("tests/testing_data/stop_flow_action/upload_multiflow_story/actions.yml", "rb"), + ), + ), + ( + "training_files", + ( + "multiflow_stories.yml", + open("tests/testing_data/stop_flow_action/upload_multiflow_story/multiflow_stories.yml", "rb"), + ), + ), + ) + response = client.post( + f"/api/bot/{pytest.bot}/upload?import_data=true&overwrite=true", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + files=files, + ) + actual = response.json() + assert actual["message"] == "Upload in progress! Check logs." + assert actual["error_code"] == 0 + assert actual["data"] is None + assert actual["success"] + complete_end_to_end_event_execution( + pytest.bot, "integration@demo.ai", EventClass.data_importer + ) + + response = client.get( + f"/api/bot/{pytest.bot}/stories", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + + for story in actual["data"]: + if story.get("type") == "MULTIFLOW": + for substep in story["steps"]: + if substep.get("step", {}).get("name") == "stop_flow_action": + assert substep["connections"] == [] + + + mock_training_limit.return_value = False + event_url = urljoin( + Utility.environment["events"]["server_url"], + f"/api/events/execute/{EventClass.model_training}", + ) + responses.add( + "POST", + event_url, + json={"success": True, "message": "Event triggered successfully!"}, + ) + + response = client.post( + f"/api/bot/{pytest.bot}/train", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["success"] + assert actual["error_code"] == 0 + assert actual["data"] is None + assert actual["message"] == "Model training started." + complete_end_to_end_event_execution( + pytest.bot, "integration@demo.ai", EventClass.model_training + ) + + +def test_download_multiflow_story_with_stop_flow_action(): + response = client.post( + f"/api/bot/{pytest.bot}/v2/stories", + json={ + "name": "test_download_multiflow_story_with_stop_flow_action", + "steps": [ + { + "step": { + "name": "greet", + "type": "INTENT", + "node_id": "1", + "component_id": "Mnvehd", + }, + "connections": [ + { + "name": "utter_greet", + "type": "BOT", + "node_id": "2", + "component_id": "PLhfhs", + } + ], + }, + { + "step": { + "name": "utter_greet", + "type": "BOT", + "node_id": "2", + "component_id": "PLhfhs", + }, + "connections": [ + { + "name": "stop_flow_action", + "type": "STOP_FLOW_ACTION", + "node_id": "3", + "component_id": "NNXX", + }, + { + "name": "utter_bye", + "type": "BOT", + "node_id": "4", + "component_id": "NNXXY", + }, + ], + }, + { + "step": { + "name": "stop_flow_action", + "type": "STOP_FLOW_ACTION", + "node_id": "3", + "component_id": "NNXX", + }, + "connections": None, + }, + { + "step": { + "name": "utter_bye", + "type": "BOT", + "node_id": "4", + "component_id": "NNXXY", + }, + "connections": None, + }, + ], + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["message"] == "Story flow added successfully" + assert actual["data"]["_id"] + assert actual["success"] + assert actual["error_code"] == 0 + + response = client.get( + f"/api/bot/{pytest.bot}/download/data", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + file_bytes = BytesIO(response.content) + zip_file = ZipFile(file_bytes, mode="r") + + with zip_file.open("multiflow_stories.yml") as file: + multi_stories_content = yaml.safe_load(file.read().decode("utf-8")) + with zip_file.open("domain.yml") as file: + domain_content = yaml.safe_load(file.read().decode("utf-8")) + + events = next(block['events'] for block in multi_stories_content['multiflow_story'] if block['block_name'] == 'test_download_multiflow_story_with_stop_flow_action') + + for event in events: + if event["step"]["name"] == "stop_flow_action": + assert event["connections"] == [] + + for event in events: + if event["step"]["name"] == "utter_greet": + assert any(conn["name"] == "stop_flow_action" for conn in event["connections"]) + + assert domain_content['actions'][-1] == 'stop_flow_action' + + + +@responses.activate +def test_train_on_updated_data(monkeypatch): + def _mock_training_limit(*arge, **kwargs): + return False + + monkeypatch.setattr( + ModelProcessor, "is_daily_training_limit_exceeded", _mock_training_limit + ) + + event_url = urljoin( + Utility.environment["events"]["server_url"], + f"/api/events/execute/{EventClass.model_training}", + ) + responses.add( + "POST", + event_url, + json={"success": True, "message": "Event triggered successfully!"}, + ) + + response = client.post( + f"/api/bot/{pytest.bot}/slots", + json={ + "name": "frontend", + "type": "text", + "influence_conversation": True, + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + actual = response.json() + assert "data" in actual + assert actual["message"] == "Slot added successfully!" + assert actual["data"]["_id"] + assert actual["success"] + assert actual["error_code"] == 0 + + response = client.post( + f"/api/bot/{pytest.bot}/slots", + json={ + "name": "more_queries", + "type": "text", + "influence_conversation": True, + }, + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + + response = client.post( + f"/api/bot/{pytest.bot}/train", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["success"] + assert actual["error_code"] == 0 + assert actual["data"] is None + assert actual["message"] == "Model training started." + complete_end_to_end_event_execution( + pytest.bot, "integration@demo.ai", EventClass.model_training + ) + + +def test_download_model_training_logs(monkeypatch): + start_date = datetime.utcnow() - timedelta(days=1) + end_date = datetime.utcnow() + timedelta(days=1) + response = client.get( + f"/api/bot/{pytest.bot}/logs/download/model_training?start_date={start_date}&end_date={end_date}", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + assert response.content + + +@pytest.fixture +def mock_is_training_inprogress_exception(monkeypatch): + def _inprogress_execption_response(*args, **kwargs): + raise AppException("Previous model training in progress.") + + monkeypatch.setattr( + ModelProcessor, "is_training_inprogress", _inprogress_execption_response + ) + + +def test_train_inprogress(mock_is_training_inprogress_exception): + response = client.post( + f"/api/bot/{pytest.bot}/train", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["success"] is False + assert actual["error_code"] == 422 + assert actual["data"] is None + assert actual["message"] == "Previous model training in progress." + + +@pytest.fixture +def mock_is_training_inprogress(monkeypatch): + def _inprogress_response(*args, **kwargs): + return False + + monkeypatch.setattr(ModelProcessor, "is_training_inprogress", _inprogress_response) + + +def test_train_daily_limit_exceed(mock_is_training_inprogress, monkeypatch): + bot_settings = BotSettings.objects(bot=pytest.bot).get() + bot_settings.training_limit_per_day = 2 + bot_settings.save() + response = client.post( + f"/api/bot/{pytest.bot}/train", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert not actual["success"] + assert actual["error_code"] == 422 + assert actual["data"] is None + assert actual["message"] == "Daily model training limit exceeded." + + +def test_get_model_training_history(): + response = client.get( + f"/api/bot/{pytest.bot}/train/history", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["success"] is True + assert actual["error_code"] == 0 + assert actual["data"] + assert "training_history" in actual["data"] + + +def test_model_testing_limit_exceeded(monkeypatch): + monkeypatch.setitem(Utility.environment["model"]["test"], "limit_per_day", 0) + response = client.post( + url=f"/api/bot/{pytest.bot}/test", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["error_code"] == 422 + assert actual["message"] == "Daily limit exceeded." + assert not actual["success"] + + +@responses.activate +def test_model_testing_event(monkeypatch): + bot_settings = BotSettings.objects(bot=pytest.bot).get() + bot_settings.test_limit_per_day = 5 + bot_settings.save() + event_url = urljoin( + Utility.environment["events"]["server_url"], + f"/api/events/execute/{EventClass.model_testing}", + ) + responses.add( + "POST", + event_url, + json={"success": True, "message": "Event triggered successfully!"}, + ) + response = client.post( + url=f"/api/bot/{pytest.bot}/test", + headers={"Authorization": pytest.token_type + " " + pytest.access_token}, + ) + actual = response.json() + assert actual["error_code"] == 0 + assert actual["message"] == "Testing in progress! Check logs." + assert actual["success"] + + +@responses.activate +def test_model_testing_in_progress(): + event_url = urljoin( + Utility.environment["events"]["server_url"], + f"/api/events/execute/{EventClass.model_testing}", ) responses.add( "POST", @@ -8189,7 +8778,7 @@ def test_integration_token(): ) actual = response.json() assert "data" in actual - assert len(actual["data"]) == 20 + assert len(actual["data"]) == 8 assert actual["success"] assert actual["error_code"] == 0 assert Utility.check_empty_string(actual["message"]) diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/actions.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/actions.yml new file mode 100644 index 000000000..55a0e98fd --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/actions.yml @@ -0,0 +1,31 @@ +database_action: [] +email_action: [] +form_validation_action: [] +google_search_action: [] +http_action: +- action_name: api + content_type: json + http_url: https://reqres.in/api/users?page=2 + request_method: GET + response: + dispatch: true + dispatch_type: text + evaluation_type: expression + value: ${data.data.0.first_name} + set_slots: + - evaluation_type: expression + name: apiresponse + value: ${data.data.0.first_name} +jira_action: [] +pipedrive_leads_action: [] +prompt_action: [] +pyscript_action: [] +razorpay_action: [] +slot_set_action: +- name: reset + set_slots: + - name: apiresponse + type: reset_slot + value: '' +two_stage_fallback: [] +zendesk_action: [] diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/bot_content.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/bot_content.yml new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/bot_content.yml @@ -0,0 +1 @@ +[] diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/chat_client_config.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/chat_client_config.yml new file mode 100644 index 000000000..a1f31f259 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/chat_client_config.yml @@ -0,0 +1,46 @@ +config: + api_server_host_url: https://api.kairon.com + botClassName: '' + buttonType: button + chatContainerClassName: '' + chat_server_base_url: https://chat.kairon.com/ + container: '#root' + containerClassName: '' + formClassName: '' + headerClassName: '' + live_agent_socket_url: wss://liveagent.kairon.com/ws/client + name: kairon + nudge_server_url: https://nudge.kairon.com + openButtonClassName: '' + styles: + botStyle: + backgroundColor: '#e0e0e0' + color: '#000000' + fontFamily: '''Roboto'', sans-serif' + fontSize: 14px + iconSrc: '' + showIcon: 'false' + buttonStyle: + backgroundColor: '#2b3595' + color: '#ffffff' + containerStyles: + background: '#ffffff' + height: 500px + width: 350px + headerStyle: + backgroundColor: '#2b3595' + color: '#ffffff' + height: 60px + userStyle: + backgroundColor: '#2b3595' + color: '#ffffff' + fontFamily: '''Roboto'', sans-serif' + fontSize: 14px + iconSrc: '' + showIcon: 'false' + userClassName: '' + userStorage: ls + userType: custom + welcomeMessage: Hello! How are you? + whitelist: + - '*' diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/config.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/config.yml new file mode 100644 index 000000000..2d09311f4 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/config.yml @@ -0,0 +1,29 @@ +language: en +pipeline: +- name: WhitespaceTokenizer +- name: RegexEntityExtractor +- from_pt: true + model_name: bert + model_weights: google/bert_uncased_L-2_H-128_A-2 + name: kairon.shared.nlu.featurizer.lm_featurizer.LanguageModelFeaturizer +- name: LexicalSyntacticFeaturizer +- name: CountVectorsFeaturizer +- epochs: 50 + name: DIETClassifier +- name: EntitySynonymMapper +- name: FallbackClassifier + threshold: 0.7 +- epochs: 100 + name: ResponseSelector +policies: +- max_history: 10 + name: MemoizationPolicy +- epochs: 100 + max_history: 10 + name: TEDPolicy +- core_fallback_action_name: action_default_fallback + core_fallback_threshold: 0.5 + enable_fallback_prediction: false + max_history: 10 + name: RulePolicy +recipe: default.v1 diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/data/nlu.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/data/nlu.yml new file mode 100644 index 000000000..49072cb15 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/data/nlu.yml @@ -0,0 +1,42 @@ +version: "3.1" +nlu: +- intent: bye + examples: | + - good bye + - bye + - See you later! + - Goodbye! +- intent: get_user + examples: | + - get user list + - user list is what I need + - User list is what I need. + - I need the user list. + - The user list is what you should get. + - The user list is something to get. + - Get the user list. + - The user list should be obtained. + - You can get a user list. + - I need a user list. + - The user list is what I need. + - A user list is what I need. + - Get the list of users. + - Get a user list. + - The user list is what I need the most. + - User list can be obtained. + - The user list is what I want. + - I need user list. + - Get user list. + - What I need is a user list. + - Get a list of users. +- intent: greet + examples: | + - Hi + - Hello + - Greetings + - Hi there + - Good day + - Howdy + - Hey there + - What's up? + - Hullo diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/data/rules.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/data/rules.yml new file mode 100644 index 000000000..8521e4f6c --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/data/rules.yml @@ -0,0 +1,14 @@ +version: "3.1" +rules: +- rule: ask the user to rephrase whenever they send a message with low nlu confidence + steps: + - intent: nlu_fallback + - action: utter_please_rephrase +- rule: bye + steps: + - intent: bye + - action: utter_bye +- rule: greet + steps: + - intent: greet + - action: utter_greet diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/data/stories.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/data/stories.yml new file mode 100644 index 000000000..f9744d6f4 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/data/stories.yml @@ -0,0 +1 @@ +version: "3.1" diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/domain.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/domain.yml new file mode 100644 index 000000000..839202260 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/domain.yml @@ -0,0 +1,164 @@ +version: "3.1" +intents: +- bye: + use_entities: [] +- get_user: + use_entities: [] +- greet: + use_entities: [] +actions: +- api +- reset +- stop_flow_action +slots: + apiresponse: + initial_value: null + value_reset_delay: null + values: + - Michael + - Lindsay + type: categorical + influence_conversation: true + mappings: + - type: from_entity + entity: apiresponse + quick_reply: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: quick_reply + latitude: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: latitude + longitude: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: longitude + doc_url: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: doc_url + document: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: document + video: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: video + audio: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: audio + image: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: image + http_status_code: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: http_status_code + flow_reply: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: flow_reply + order: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: order + kairon_action_response: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: kairon_action_response + bot: + initial_value: 667548f8f740fc8dc6791e0c + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: bot +session_config: + session_expiration_time: 60 + carry_over_slots: true +responses: + utter_test: + - text: Testing is going on + utter_greet: + - text: I'm your AI Assistant, ready to assist + - text: Let me be your AI Assistant and provide you with service + utter_lindsay: + - text: This is lindsay + utter_default: + - text: Sorry I didn't get that. Can you rephrase? + utter_bye: + - text: Take care, I'm here for you if you need anything. + - text: Adieu, always here for you. + - text: See you later, I'm here to help. + utter_please_rephrase: + - text: I'm sorry, I didn't quite understand that. Could you rephrase? + utter_michael: + - text: This is michael +entities: +- bot +- kairon_action_response +- order +- flow_reply +- http_status_code +- image +- audio +- video +- document +- doc_url +- longitude +- latitude +- quick_reply +- apiresponse diff --git a/tests/testing_data/stop_flow_action/upload_multiflow_story/multiflow_stories.yml b/tests/testing_data/stop_flow_action/upload_multiflow_story/multiflow_stories.yml new file mode 100644 index 000000000..a505923a8 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_multiflow_story/multiflow_stories.yml @@ -0,0 +1,112 @@ +multiflow_story: +- block_name: multiflow + end_checkpoints: [] + events: + - connections: + - component_id: 667549adf740fc8dc6791e89 + name: api + node_id: b065e255-33bf-421e-8fe9-55a2ca03381f + type: HTTP_ACTION + step: + component_id: 667549d17f5299a48f5706a6 + name: get_user + node_id: f8f0a842-dac6-4754-bc2f-bc6ffd58de73 + type: INTENT + - connections: + - component_id: 65fea24f-4be1-4369-8e8d-9a41cbb6da25 + name: apiresponse + node_id: 65fea24f-4be1-4369-8e8d-9a41cbb6da25 + type: SLOT + value: Michael + - component_id: 814c5348-54f9-4e4a-a1da-942c7d53b887 + name: apiresponse + node_id: 814c5348-54f9-4e4a-a1da-942c7d53b887 + type: SLOT + value: Lindsay + step: + component_id: 667549adf740fc8dc6791e89 + name: api + node_id: b065e255-33bf-421e-8fe9-55a2ca03381f + type: HTTP_ACTION + - connections: + - component_id: 66754a8ea838995a2102e618 + name: utter_michael + node_id: 0d51ee48-2bd4-43db-9802-d091b0790c80 + type: BOT + step: + component_id: '' + name: apiresponse + node_id: 65fea24f-4be1-4369-8e8d-9a41cbb6da25 + type: SLOT + value: Michael + - connections: + - component_id: 66754a9357c7b9f0ecd227f6 + name: utter_lindsay + node_id: c69bb7d2-1fa7-4f47-824b-553d16ac25fb + type: BOT + step: + component_id: '' + name: apiresponse + node_id: 814c5348-54f9-4e4a-a1da-942c7d53b887 + type: SLOT + value: Lindsay + - connections: + - component_id: 66754ac47f5299a48f5706ab + name: reset + node_id: 6ea2eb12-99c3-4bb4-892f-d8bda800992d + type: SLOT_SET_ACTION + step: + component_id: 66754a8ea838995a2102e618 + name: utter_michael + node_id: 0d51ee48-2bd4-43db-9802-d091b0790c80 + type: BOT + - connections: + - component_id: 66756220a838995a2102e63e + name: utter_test + node_id: 93ae8e47-a423-4997-b042-8c745caf7310 + type: BOT + step: + component_id: 66754a9357c7b9f0ecd227f6 + name: utter_lindsay + node_id: c69bb7d2-1fa7-4f47-824b-553d16ac25fb + type: BOT + - connections: + - component_id: 66754ac47f5299a48f570116 + name: stop_flow_action + node_id: 6ea2eb12-99c3-4bb4-892f-d8bda800116d + type: STOP_FLOW_ACTION + step: + component_id: 66754ac47f5299a48f5706ab + name: reset + node_id: 6ea2eb12-99c3-4bb4-892f-d8bda800992d + type: SLOT_SET_ACTION + - connections: + - component_id: 66754ac47f5299a48f5706ab + name: reset + node_id: b4a352e5-da75-4fa3-8707-9f299a5161a1 + type: SLOT_SET_ACTION + step: + component_id: 66756220a838995a2102e63e + name: utter_test + node_id: 93ae8e47-a423-4997-b042-8c745caf7310 + type: BOT + - connections: [] + step: + component_id: 66754ac47f5299a48f5706ab + name: reset + node_id: b4a352e5-da75-4fa3-8707-9f299a5161a1 + type: SLOT_SET_ACTION + - connections: [] + step: + component_id: 66754ac47f5299a48f570116 + name: stop_flow_action + node_id: 6ea2eb12-99c3-4bb4-892f-d8bda800116d + type: STOP_FLOW_ACTION + metadata: + - flow_type: RULE + node_id: 6ea2eb12-99c3-4bb4-892f-d8bda800116d + - flow_type: STORY + node_id: b4a352e5-da75-4fa3-8707-9f299a5161a1 + start_checkpoints: + - STORY_START + template_type: CUSTOM diff --git a/tests/testing_data/stop_flow_action/upload_story/actions.yml b/tests/testing_data/stop_flow_action/upload_story/actions.yml new file mode 100644 index 000000000..a0656865f --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/actions.yml @@ -0,0 +1,13 @@ +database_action: [] +email_action: [] +form_validation_action: [] +google_search_action: [] +http_action: [] +jira_action: [] +pipedrive_leads_action: [] +prompt_action: [] +pyscript_action: [] +razorpay_action: [] +slot_set_action: [] +two_stage_fallback: [] +zendesk_action: [] diff --git a/tests/testing_data/stop_flow_action/upload_story/bot_content.yml b/tests/testing_data/stop_flow_action/upload_story/bot_content.yml new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/bot_content.yml @@ -0,0 +1 @@ +[] diff --git a/tests/testing_data/stop_flow_action/upload_story/chat_client_config.yml b/tests/testing_data/stop_flow_action/upload_story/chat_client_config.yml new file mode 100644 index 000000000..a1f31f259 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/chat_client_config.yml @@ -0,0 +1,46 @@ +config: + api_server_host_url: https://api.kairon.com + botClassName: '' + buttonType: button + chatContainerClassName: '' + chat_server_base_url: https://chat.kairon.com/ + container: '#root' + containerClassName: '' + formClassName: '' + headerClassName: '' + live_agent_socket_url: wss://liveagent.kairon.com/ws/client + name: kairon + nudge_server_url: https://nudge.kairon.com + openButtonClassName: '' + styles: + botStyle: + backgroundColor: '#e0e0e0' + color: '#000000' + fontFamily: '''Roboto'', sans-serif' + fontSize: 14px + iconSrc: '' + showIcon: 'false' + buttonStyle: + backgroundColor: '#2b3595' + color: '#ffffff' + containerStyles: + background: '#ffffff' + height: 500px + width: 350px + headerStyle: + backgroundColor: '#2b3595' + color: '#ffffff' + height: 60px + userStyle: + backgroundColor: '#2b3595' + color: '#ffffff' + fontFamily: '''Roboto'', sans-serif' + fontSize: 14px + iconSrc: '' + showIcon: 'false' + userClassName: '' + userStorage: ls + userType: custom + welcomeMessage: Hello! How are you? + whitelist: + - '*' diff --git a/tests/testing_data/stop_flow_action/upload_story/config.yml b/tests/testing_data/stop_flow_action/upload_story/config.yml new file mode 100644 index 000000000..2d09311f4 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/config.yml @@ -0,0 +1,29 @@ +language: en +pipeline: +- name: WhitespaceTokenizer +- name: RegexEntityExtractor +- from_pt: true + model_name: bert + model_weights: google/bert_uncased_L-2_H-128_A-2 + name: kairon.shared.nlu.featurizer.lm_featurizer.LanguageModelFeaturizer +- name: LexicalSyntacticFeaturizer +- name: CountVectorsFeaturizer +- epochs: 50 + name: DIETClassifier +- name: EntitySynonymMapper +- name: FallbackClassifier + threshold: 0.7 +- epochs: 100 + name: ResponseSelector +policies: +- max_history: 10 + name: MemoizationPolicy +- epochs: 100 + max_history: 10 + name: TEDPolicy +- core_fallback_action_name: action_default_fallback + core_fallback_threshold: 0.5 + enable_fallback_prediction: false + max_history: 10 + name: RulePolicy +recipe: default.v1 diff --git a/tests/testing_data/stop_flow_action/upload_story/data/nlu.yml b/tests/testing_data/stop_flow_action/upload_story/data/nlu.yml new file mode 100644 index 000000000..c353523b7 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/data/nlu.yml @@ -0,0 +1,13 @@ +version: "3.1" +nlu: +- intent: greet + examples: | + - Hi + - Hello + - Greetings + - Hi there + - Good day + - Howdy + - Hey there + - What's up? + - Hullo diff --git a/tests/testing_data/stop_flow_action/upload_story/data/rules.yml b/tests/testing_data/stop_flow_action/upload_story/data/rules.yml new file mode 100644 index 000000000..f9744d6f4 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/data/rules.yml @@ -0,0 +1 @@ +version: "3.1" diff --git a/tests/testing_data/stop_flow_action/upload_story/data/stories.yml b/tests/testing_data/stop_flow_action/upload_story/data/stories.yml new file mode 100644 index 000000000..84f232de6 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/data/stories.yml @@ -0,0 +1,7 @@ +version: "3.1" +stories: +- story: story_with_stop_flow_action + steps: + - intent: greet + - action: utter_goodbye + - action: action_listen diff --git a/tests/testing_data/stop_flow_action/upload_story/domain.yml b/tests/testing_data/stop_flow_action/upload_story/domain.yml new file mode 100644 index 000000000..e9bce6968 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/domain.yml @@ -0,0 +1,135 @@ +version: "3.1" +intents: +- greet: + use_entities: [] +actions: +- action_listen +slots: + quick_reply: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: quick_reply + latitude: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: latitude + longitude: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: longitude + doc_url: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: doc_url + document: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: document + video: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: video + audio: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: audio + image: + initial_value: null + value_reset_delay: null + type: text + influence_conversation: true + mappings: + - type: from_entity + entity: image + http_status_code: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: http_status_code + flow_reply: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: flow_reply + order: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: order + kairon_action_response: + initial_value: null + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: kairon_action_response + bot: + initial_value: 667cd22413292148a54baa89 + value_reset_delay: null + type: any + influence_conversation: false + mappings: + - type: from_entity + entity: bot +session_config: + session_expiration_time: 60 + carry_over_slots: true +responses: + utter_goodbye: + - text: Bye Bye + utter_default: + - text: Sorry I didn't get that. Can you rephrase? + utter_please_rephrase: + - text: I'm sorry, I didn't quite understand that. Could you rephrase? +entities: +- bot +- kairon_action_response +- order +- flow_reply +- http_status_code +- image +- audio +- video +- document +- doc_url +- longitude +- latitude +- quick_reply diff --git a/tests/testing_data/stop_flow_action/upload_story/multiflow_stories.yml b/tests/testing_data/stop_flow_action/upload_story/multiflow_stories.yml new file mode 100644 index 000000000..76b006463 --- /dev/null +++ b/tests/testing_data/stop_flow_action/upload_story/multiflow_stories.yml @@ -0,0 +1 @@ +multiflow_story: [] diff --git a/tests/unit_test/data_processor/data_processor_test.py b/tests/unit_test/data_processor/data_processor_test.py index a1db7c0a8..99920fe9d 100644 --- a/tests/unit_test/data_processor/data_processor_test.py +++ b/tests/unit_test/data_processor/data_processor_test.py @@ -11067,6 +11067,50 @@ def test_add_complex_story_with_stop_flow_action(self): assert stop_action_event.name == "action_listen" assert stop_action_event.type == "action" + def test_add_multiflow_story_with_stop_flow_action(self): + processor = MongoProcessor() + steps = [ + {"step": {"name": "greet", "type": "INTENT", "node_id": "1", "component_id": "637d0j9GD059jEwt2jPnlZ7I"}, + "connections": [ + {"name": "utter_greet", "type": "BOT", "node_id": "2", "component_id": "63uNJw1QvpQZvIpP07dxnmFU"}] + }, + {"step": {"name": "utter_greet", "type": "BOT", "node_id": "2", "component_id": "63uNJw1QvpQZvIpP07dxnmFU"}, + "connections": [ + {"name": "more_queries", "type": "INTENT", "node_id": "3", "component_id": "633w6kSXuz3qqnPU571jZyCv"}, + {"name": "goodbye", "type": "INTENT", "node_id": "4", "component_id": "63WKbWs5K0ilkujWJQpXEXGD"}] + }, + {"step": {"name": "more_queries", "type": "INTENT", "node_id": "3", + "component_id": "633w6kSXuz3qqnPU571jZyCv"}, + "connections": [{"name": "utter_more_queries", "type": "BOT", "node_id": "6", + "component_id": "634a9bwPPj2y3zF5HOVgLiXx"}] + }, + {"step": {"name": "goodbye", "type": "INTENT", "node_id": "4", "component_id": "63WKbWs5K0ilkujWJQpXEXGD"}, + "connections": [ + {"name": "utter_goodbye", "type": "BOT", "node_id": "5", "component_id": "63gm5BzYuhC1bc6yzysEnN4E"}] + }, + {"step": {"name": "utter_more_queries", "type": "BOT", "node_id": "6", + "component_id": "634a9bwPPj2y3zF5HOVgLiXx"}, + "connections": None + }, + {"step": {"name": "utter_goodbye", "type": "BOT", "node_id": "5", + "component_id": "63gm5BzYuhC1bc6yzysEnN4E"}, + "connections": [ + {"name": "stop_flow", "type": "STOP_FLOW_ACTION", "node_id": "7", + "component_id": "63gm5BzYuhC1bc6yzysEnN65"}] + }, + {"step": {"name": "stop_flow", "type": "STOP_FLOW_ACTION", "node_id": "7", + "component_id": "63gm5BzYuhC1bc6yzysEnN65"}, + "connections": None + }, + ] + story_dict = {'name': "multiflow story with stop flow action", 'steps': steps, 'type': 'MULTIFLOW', + 'template_type': 'CUSTOM'} + processor.add_multiflow_story(story_dict, "test", "TestUser") + story = MultiflowStories.objects(block_name="multiflow story with stop flow action", bot="test").get() + assert len(story.events) == 7 + stop_flow_step = story.events[6]['step'] + assert stop_flow_step['name'] == "stop_flow" + assert stop_flow_step['type'] == "STOP_FLOW_ACTION" def test_add_duplicate_complex_story(self): processor = MongoProcessor()