From 321e937be895bf77b8784bc7cdff482f04b7d8d9 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 26 Nov 2020 19:18:45 +0100 Subject: [PATCH 01/34] remove unused imports --- .../core/training_data/story_writer/yaml_story_writer.py | 2 +- .../core/training_data/story_writer/test_yaml_story_writer.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index 07d1867ef305..6175932b687c 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -4,7 +4,7 @@ from ruamel import yaml from ruamel.yaml.comments import CommentedMap -from ruamel.yaml.scalarstring import DoubleQuotedScalarString, LiteralScalarString +from ruamel.yaml.scalarstring import DoubleQuotedScalarString import rasa.shared.utils.io import rasa.shared.core.constants diff --git a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py index 4e48ea67d793..7c344e7d06f5 100644 --- a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py +++ b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py @@ -18,9 +18,6 @@ YAMLStoryWriter, ) from rasa.shared.core.training_data.structures import STORY_START -from rasa.utils.endpoints import EndpointConfig - -import rasa.shared.utils.io @pytest.mark.parametrize( From a8d8e041d29364bc04373aa243d513dc387eb784 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 26 Nov 2020 19:19:09 +0100 Subject: [PATCH 02/34] test and fix writing YAML stories --- .../story_writer/yaml_story_writer.py | 16 ++- .../story_writer/test_yaml_story_writer.py | 121 +++++++++++++++++- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index 6175932b687c..67c78e5a984c 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -4,11 +4,12 @@ from ruamel import yaml from ruamel.yaml.comments import CommentedMap -from ruamel.yaml.scalarstring import DoubleQuotedScalarString +from ruamel.yaml.scalarstring import DoubleQuotedScalarString, LiteralScalarString import rasa.shared.utils.io import rasa.shared.core.constants from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION +import rasa.shared.core.events from rasa.shared.core.events import ( UserUttered, ActionExecuted, @@ -216,12 +217,21 @@ def process_user_utterance( ) if user_utterance.text and ( + # We only print the utterance text if it was an end-to-end prediction user_utterance.use_text_for_featurization or user_utterance.use_text_for_featurization is None + # or if we want to print a conversation test story. + or is_test_story ): - result[KEY_USER_MESSAGE] = user_utterance.as_story_string() + result[KEY_USER_MESSAGE] = LiteralScalarString( + rasa.shared.core.events.md_format_message( + user_utterance.text, + user_utterance.intent_name, + user_utterance.entities, + ) + ) - if len(user_utterance.entities): + if len(user_utterance.entities) and not is_test_story: entities = [] for entity in user_utterance.entities: if entity["value"]: diff --git a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py index 7c344e7d06f5..88fff828a159 100644 --- a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py +++ b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py @@ -6,7 +6,11 @@ from rasa.shared.core.constants import ACTION_SESSION_START_NAME, ACTION_LISTEN_NAME from rasa.shared.core.domain import Domain -from rasa.shared.core.events import ActionExecuted, UserUttered +from rasa.shared.core.events import ( + ActionExecuted, + UserUttered, + DefinePrevUserUtteredFeaturization, +) from rasa.shared.core.trackers import DialogueStateTracker from rasa.shared.core.training_data.story_reader.markdown_story_reader import ( MarkdownStoryReader, @@ -105,6 +109,8 @@ def test_yaml_writer_dumps_user_messages(): - story: default steps: - intent: greet + user: |- + Hello - action: utter_greet """ @@ -177,3 +183,116 @@ def test_yaml_writer_stories_to_yaml(default_domain: Domain): assert isinstance(result, OrderedDict) assert "stories" in result assert len(result["stories"]) == 1 + + +def test_writing_end_to_end_stories(default_domain: Domain): + story_name = "test_writing_end_to_end_stories" + events = [ + # Training story story with intent and action labels + ActionExecuted(ACTION_LISTEN_NAME), + UserUttered(intent={"name": "greet"}), + ActionExecuted("utter_greet"), + ActionExecuted(ACTION_LISTEN_NAME), + # Prediction story story with intent and action labels + ActionExecuted(ACTION_LISTEN_NAME), + UserUttered(text="Hi", intent={"name": "greet"}), + ActionExecuted("utter_greet"), + ActionExecuted(ACTION_LISTEN_NAME), + # End-To-End Training Story + UserUttered(text="Hi"), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), + ActionExecuted(action_text="Hi, I'm a bot."), + ActionExecuted(ACTION_LISTEN_NAME), + # End-To-End Prediction Story + UserUttered("Hi", intent={"name": "greet"}), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), + ActionExecuted(action_text="Hi, I'm a bot."), + ActionExecuted(ACTION_LISTEN_NAME), + ] + + tracker = DialogueStateTracker.from_events(story_name, events) + dump = YAMLStoryWriter().dumps(tracker.as_story().story_steps) + + assert ( + dump.strip() + == textwrap.dedent( + f""" + version: "2.0" + stories: + - story: {story_name} + steps: + - intent: greet + - action: utter_greet + - intent: greet + - action: utter_greet + - user: |- + Hi + - bot: Hi, I'm a bot. + - user: |- + Hi + - bot: Hi, I'm a bot. + """ + ).strip() + ) + + +def test_writing_end_to_end_stories_in_test_mode(default_domain: Domain): + story_name = "test_writing_end_to_end_stories_in_test_mode" + events = [ + # Conversation tests (end-to-end _testing_) + ActionExecuted(ACTION_LISTEN_NAME), + UserUttered(text="Hi", intent={"name": "greet"}), + ActionExecuted("utter_greet"), + ActionExecuted(ACTION_LISTEN_NAME), + # Conversation tests (end-to-end _testing_) and entities + UserUttered( + text="Hi", + intent={"name": "greet"}, + entities=[{"value": "Hi", "entity": "test", "start": 0, "end": 2}], + ), + ActionExecuted("utter_greet"), + ActionExecuted(ACTION_LISTEN_NAME), + # Conversation test with actual end-to-end utterances + UserUttered(text="Hi", intent={"name": "greet"}), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), + ActionExecuted(action_text="Hi, I'm a bot."), + ActionExecuted(ACTION_LISTEN_NAME), + # Conversation test with actual end-to-end utterances + UserUttered( + text="Hi", + intent={"name": "greet"}, + entities=[{"value": "Hi", "entity": "test", "start": 0, "end": 2}], + ), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), + ActionExecuted(action_text="Hi, I'm a bot."), + ActionExecuted(ACTION_LISTEN_NAME), + ] + + tracker = DialogueStateTracker.from_events(story_name, events) + dump = YAMLStoryWriter().dumps(tracker.as_story().story_steps, is_test_story=True) + + assert ( + dump.strip() + == textwrap.dedent( + f""" + version: "2.0" + stories: + - story: {story_name} + steps: + - intent: greet + user: |- + Hi + - action: utter_greet + - intent: greet + user: |- + [Hi](test) + - action: utter_greet + - user: |- + Hi + - bot: Hi, I'm a bot. + - user: |- + [Hi](test) + - bot: Hi, I'm a bot. + """ + ).strip() + ) From 4d1ba87a022052311f974af20c5577afda0a0f8a Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Tue, 1 Dec 2020 15:47:17 +0100 Subject: [PATCH 03/34] move `MarkdownStoryWriter` tests to separate file --- tests/shared/core/test_trackers.py | 19 ------------------ .../test_markdown_story_writer.py | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 tests/shared/core/training_data/story_writer/test_markdown_story_writer.py diff --git a/tests/shared/core/test_trackers.py b/tests/shared/core/test_trackers.py index 9ab196669ea3..63f0cb2f462a 100644 --- a/tests/shared/core/test_trackers.py +++ b/tests/shared/core/test_trackers.py @@ -65,9 +65,6 @@ get_tracker, ) -from rasa.shared.core.training_data.story_writer.markdown_story_writer import ( - MarkdownStoryWriter, -) from rasa.shared.nlu.constants import ACTION_NAME, PREDICTED_CONFIDENCE_KEY domain = Domain.load("examples/moodbot/domain.yml") @@ -635,22 +632,6 @@ def test_session_started_not_part_of_applied_events(default_agent: Agent): assert tracker.applied_events() == list(tracker.events)[6:] -async def test_tracker_dump_e2e_story(default_agent: Agent): - sender_id = "test_tracker_dump_e2e_story" - - await default_agent.handle_text("/greet", sender_id=sender_id) - await default_agent.handle_text("/goodbye", sender_id=sender_id) - tracker = default_agent.tracker_store.get_or_create_tracker(sender_id) - - story = tracker.export_stories(MarkdownStoryWriter(), e2e=True) - assert story.strip().split("\n") == [ - "## test_tracker_dump_e2e_story", - "* greet: /greet", - " - utter_greet", - "* goodbye: /goodbye", - ] - - def test_get_last_event_for(): events = [ActionExecuted("one"), user_uttered("two", 1)] diff --git a/tests/shared/core/training_data/story_writer/test_markdown_story_writer.py b/tests/shared/core/training_data/story_writer/test_markdown_story_writer.py new file mode 100644 index 000000000000..e3399f9fdf3c --- /dev/null +++ b/tests/shared/core/training_data/story_writer/test_markdown_story_writer.py @@ -0,0 +1,20 @@ +from rasa.core.agent import Agent +from rasa.shared.core.training_data.story_writer.markdown_story_writer import ( + MarkdownStoryWriter, +) + + +async def test_tracker_dump_e2e_story(default_agent: Agent): + sender_id = "test_tracker_dump_e2e_story" + + await default_agent.handle_text("/greet", sender_id=sender_id) + await default_agent.handle_text("/goodbye", sender_id=sender_id) + tracker = default_agent.tracker_store.get_or_create_tracker(sender_id) + + story = tracker.export_stories(MarkdownStoryWriter(), e2e=True) + assert story.strip().split("\n") == [ + "## test_tracker_dump_e2e_story", + "* greet: /greet", + " - utter_greet", + "* goodbye: /goodbye", + ] From dae731d9fb3122cc625269e45882791ba1002884 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Tue, 1 Dec 2020 15:54:25 +0100 Subject: [PATCH 04/34] use `tmp_path` --- .../story_writer/yaml_story_writer.py | 1 + .../test_story_markdown_to_yaml_converter.py | 62 ++++++++----------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index 67c78e5a984c..1cfe405b885b 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -104,6 +104,7 @@ def stories_to_yaml( Args: story_steps: Original story steps to be converted to the YAML. + is_test_story: `True` if the story is an end-to-end conversation test story. """ from rasa.shared.utils.validation import KEY_TRAINING_DATA_FORMAT_VERSION diff --git a/tests/core/training/converters/test_story_markdown_to_yaml_converter.py b/tests/core/training/converters/test_story_markdown_to_yaml_converter.py index 04f9a30dd2a0..16b1ca0091b7 100644 --- a/tests/core/training/converters/test_story_markdown_to_yaml_converter.py +++ b/tests/core/training/converters/test_story_markdown_to_yaml_converter.py @@ -7,14 +7,6 @@ StoryMarkdownToYamlConverter, ) -from rasa.shared.core.training_data.story_reader.markdown_story_reader import ( - MarkdownStoryReader, -) - -from rasa.shared.core.training_data.story_reader.yaml_story_reader import ( - YAMLStoryReader, -) - from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION @@ -32,12 +24,12 @@ def test_converter_filters_correct_files(training_data_file: Text, should_filter ) -async def test_stories_are_converted(tmpdir: Path): - converted_data_folder = tmpdir / "converted_data" - os.mkdir(converted_data_folder) +async def test_stories_are_converted(tmp_path: Path): + converted_data_folder = tmp_path / "converted_data" + converted_data_folder.mkdir() - training_data_folder = tmpdir / "data/core" - os.makedirs(training_data_folder, exist_ok=True) + training_data_folder = tmp_path / "data" / "core" + training_data_folder.mkdir(parents=True) training_data_file = Path(training_data_folder / "stories.md") simple_story_md = """ @@ -48,8 +40,7 @@ async def test_stories_are_converted(tmpdir: Path): - slot{"name": ["value1", "value2"]} """ - with open(training_data_file, "w") as f: - f.write(simple_story_md) + training_data_file.write_text(simple_story_md) await StoryMarkdownToYamlConverter().convert_and_write( training_data_file, converted_data_folder @@ -76,12 +67,12 @@ async def test_stories_are_converted(tmpdir: Path): ) -async def test_test_stories(tmpdir: Path): - converted_data_folder = tmpdir / "converted_data" - os.mkdir(converted_data_folder) +async def test_test_stories(tmp_path: Path): + converted_data_folder = tmp_path / "converted_data" + converted_data_folder.mkdir() - test_data_folder = tmpdir / "tests" - os.makedirs(test_data_folder, exist_ok=True) + test_data_folder = tmp_path / "tests" + test_data_folder.mkdir(exist_ok=True) test_data_file = Path(test_data_folder / "test_stories.md") simple_story_md = """ @@ -92,14 +83,13 @@ async def test_test_stories(tmpdir: Path): - action_set_faq_slot """ - with open(test_data_file, "w") as f: - f.write(simple_story_md) + test_data_file.write_text(simple_story_md) await StoryMarkdownToYamlConverter().convert_and_write( test_data_file, converted_data_folder ) - assert len(os.listdir(converted_data_folder)) == 1 + assert len(list(converted_data_folder.glob("*"))) == 1 with open(f"{converted_data_folder}/test_stories_converted.yml", "r") as f: content = f.read() @@ -118,12 +108,12 @@ async def test_test_stories(tmpdir: Path): ) -async def test_test_stories_conversion_response_key(tmpdir: Path): - converted_data_folder = tmpdir / "converted_data" - os.mkdir(converted_data_folder) +async def test_test_stories_conversion_response_key(tmp_path: Path): + converted_data_folder = tmp_path / "converted_data" + converted_data_folder.mkdir() - test_data_folder = tmpdir / "tests" - os.makedirs(test_data_folder, exist_ok=True) + test_data_folder = tmp_path / "tests" + test_data_folder.mkdir(exist_ok=True) test_data_file = Path(test_data_folder / "test_stories.md") simple_story_md = """ @@ -133,8 +123,7 @@ async def test_test_stories_conversion_response_key(tmpdir: Path): - utter_out_of_scope/other """ - with open(test_data_file, "w") as f: - f.write(simple_story_md) + test_data_file.write_text(simple_story_md) await StoryMarkdownToYamlConverter().convert_and_write( test_data_file, converted_data_folder @@ -155,12 +144,12 @@ async def test_test_stories_conversion_response_key(tmpdir: Path): ) -async def test_stories_conversion_response_key(tmpdir: Path): - converted_data_folder = tmpdir / "converted_data" - os.mkdir(converted_data_folder) +async def test_stories_conversion_response_key(tmp_path: Path): + converted_data_folder = tmp_path / "converted_data" + converted_data_folder.mkdir() - training_data_folder = tmpdir / "data/core" - os.makedirs(training_data_folder, exist_ok=True) + training_data_folder = tmp_path / "data" / "core" + training_data_folder.mkdir(parents=True) training_data_file = Path(training_data_folder / "stories.md") simple_story_md = """ @@ -169,8 +158,7 @@ async def test_stories_conversion_response_key(tmpdir: Path): - utter_out_of_scope/other """ - with open(training_data_file, "w") as f: - f.write(simple_story_md) + training_data_file.write_text(simple_story_md) await StoryMarkdownToYamlConverter().convert_and_write( training_data_file, converted_data_folder From 810bc3eefd0db9e7bb72e60dd53d3c113a18b458 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Tue, 1 Dec 2020 19:42:11 +0100 Subject: [PATCH 05/34] consider end-to-end stories correctly --- .../training_data/story_reader/markdown_story_reader.py | 7 ++++--- .../story_reader/test_markdown_story_reader.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rasa/shared/core/training_data/story_reader/markdown_story_reader.py b/rasa/shared/core/training_data/story_reader/markdown_story_reader.py index 4be37a33b61e..ff7cfc466f58 100644 --- a/rasa/shared/core/training_data/story_reader/markdown_story_reader.py +++ b/rasa/shared/core/training_data/story_reader/markdown_story_reader.py @@ -203,8 +203,9 @@ def _add_e2e_messages(self, e2e_messages: List[Text], line_num: int) -> None: parsed_messages.append(parsed) self.current_step_builder.add_user_messages(parsed_messages) - @staticmethod - def parse_e2e_message(line: Text, is_used_for_training: bool = True) -> Message: + def parse_e2e_message( + self, line: Text, is_used_for_training: bool = True + ) -> Message: """Parses an md list item line based on the current section type. Matches expressions of the form `:`. For the @@ -231,7 +232,7 @@ def parse_e2e_message(line: Text, is_used_for_training: bool = True) -> Message: intent = match.group(2) message = match.group(4) example = entities_parser.parse_training_example(message, intent) - if not is_used_for_training: + if not is_used_for_training and not self.use_e2e: # In case this is a simple conversion from Markdown we should copy over # the original text and not parse the entities example.data[rasa.shared.nlu.constants.TEXT] = message diff --git a/tests/shared/core/training_data/story_reader/test_markdown_story_reader.py b/tests/shared/core/training_data/story_reader/test_markdown_story_reader.py index 2252c72b8c52..b7aebd49cd3e 100644 --- a/tests/shared/core/training_data/story_reader/test_markdown_story_reader.py +++ b/tests/shared/core/training_data/story_reader/test_markdown_story_reader.py @@ -360,7 +360,7 @@ async def test_read_rules_without_stories(default_domain: Domain): ], ) def test_e2e_parsing(line: Text, expected: Dict): - actual = MarkdownStoryReader.parse_e2e_message(line) + actual = MarkdownStoryReader().parse_e2e_message(line) assert actual.as_dict() == expected From 4eb2a434905ad77177364522fdb601cb7c08cdce Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Tue, 1 Dec 2020 20:46:57 +0100 Subject: [PATCH 06/34] fix story reading for retrieval intents --- .../story_reader/yaml_story_reader.py | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py index ab8b70e652ca..2bfd391e3aba 100644 --- a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py +++ b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py @@ -275,10 +275,8 @@ def _parse_step(self, step: Union[Text, Dict[Text, Any]]) -> None: f"'{RULE_SNIPPET_ACTION_NAME}'. It will be skipped.", docs=self._get_docs_link(), ) - elif KEY_USER_MESSAGE in step.keys(): - self._parse_user_message(step) elif KEY_USER_INTENT in step.keys() or KEY_USER_MESSAGE in step.keys(): - self._parse_labeled_user_utterance(step) + self._parse_user_utterance(step) elif KEY_OR in step.keys(): self._parse_or_statement(step) elif KEY_ACTION in step.keys(): @@ -312,33 +310,19 @@ def _get_plural_item_title(self) -> Text: def _get_docs_link(self) -> Text: raise NotImplementedError() - def _parse_labeled_user_utterance(self, step: Dict[Text, Any]) -> None: + def _parse_user_utterance(self, step: Dict[Text, Any]) -> None: utterance = self._parse_raw_user_utterance(step) - if utterance: - self._validate_that_utterance_is_in_domain(utterance) - self.current_step_builder.add_user_messages([utterance]) - def _parse_user_message(self, step: Dict[Text, Any]) -> None: - is_end_to_end_utterance = KEY_USER_INTENT not in step + if not utterance: + return + is_end_to_end_utterance = KEY_USER_INTENT not in step if is_end_to_end_utterance: - intent = {"name": None} + utterance.intent = {"name": None} else: - intent_name = self._user_intent_from_step(step) - intent = {"name": intent_name, "confidence": 1.0} - - user_message = step[KEY_USER_MESSAGE].strip() - entities = entities_parser.find_entities_in_training_example(user_message) - plain_text = entities_parser.replace_entities(user_message) - - if plain_text.startswith(INTENT_MESSAGE_PREFIX): - entities = ( - RegexInterpreter().synchronous_parse(plain_text).get(ENTITIES, []) - ) + self._validate_that_utterance_is_in_domain(utterance) - self.current_step_builder.add_user_messages( - [UserUttered(plain_text, intent, entities=entities)] - ) + self.current_step_builder.add_user_messages([utterance]) def _validate_that_utterance_is_in_domain(self, utterance: UserUttered) -> None: From 32537d7effdef4647181b3094950326ee5ee40ba Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Tue, 1 Dec 2020 20:58:56 +0100 Subject: [PATCH 07/34] fix missing renames for `prepare_from_domain` --- tests/core/featurizers/test_single_state_featurizers.py | 6 +++--- tests/core/featurizers/test_tracker_featurizer.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/core/featurizers/test_single_state_featurizers.py b/tests/core/featurizers/test_single_state_featurizers.py index 725cf499cd3e..f00c493c2fb2 100644 --- a/tests/core/featurizers/test_single_state_featurizers.py +++ b/tests/core/featurizers/test_single_state_featurizers.py @@ -133,7 +133,7 @@ def test_single_state_featurizer_correctly_encodes_non_existing_value(): assert (encoded[INTENT][0].features != scipy.sparse.coo_matrix([[0, 0]])).nnz == 0 -def test_single_state_featurizer_prepare_from_domain(): +def test_single_state_featurizer_prepare_for_training(): domain = Domain( intents=["greet"], entities=["name"], @@ -144,7 +144,7 @@ def test_single_state_featurizer_prepare_from_domain(): ) f = SingleStateFeaturizer() - f.prepare_from_domain(domain) + f.prepare_for_training(domain, RegexInterpreter()) assert len(f._default_feature_states[INTENT]) > 1 assert "greet" in f._default_feature_states[INTENT] @@ -197,7 +197,7 @@ def test_single_state_featurizer_with_entity_roles_and_groups( action_names=[], ) f = SingleStateFeaturizer() - f.prepare_from_domain(domain) + f.prepare_for_training(domain, RegexInterpreter()) encoded = f.encode_entities( { TEXT: "I am flying from London to Paris", diff --git a/tests/core/featurizers/test_tracker_featurizer.py b/tests/core/featurizers/test_tracker_featurizer.py index f6b904d8397b..7660174abe58 100644 --- a/tests/core/featurizers/test_tracker_featurizer.py +++ b/tests/core/featurizers/test_tracker_featurizer.py @@ -21,7 +21,7 @@ def test_fail_to_load_non_existent_featurizer(): def test_persist_and_load_tracker_featurizer(tmp_path: Text, moodbot_domain: Domain): state_featurizer = SingleStateFeaturizer() - state_featurizer.prepare_from_domain(moodbot_domain) + state_featurizer.prepare_for_training(moodbot_domain, RegexInterpreter()) tracker_featurizer = MaxHistoryTrackerFeaturizer(state_featurizer) tracker_featurizer.persist(tmp_path) From ecb335568638dbe803a0078cd82459e10476529c Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 08:57:15 +0100 Subject: [PATCH 08/34] fixup for last merge in from `master` --- tests/core/test_processor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/core/test_processor.py b/tests/core/test_processor.py index 4eadda7cd840..b4f093702a11 100644 --- a/tests/core/test_processor.py +++ b/tests/core/test_processor.py @@ -903,10 +903,12 @@ async def test_restart_triggers_session_start( [{"entity": entity, "start": 6, "end": 23, "value": "name1"}], ), SlotSet(entity, slot_1[entity]), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=False), ActionExecuted("utter_greet"), BotUttered("hey there name1!", metadata={"template_name": "utter_greet"}), ActionExecuted(ACTION_LISTEN_NAME), UserUttered("/restart", {INTENT_NAME_KEY: "restart", "confidence": 1.0}), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=False), ActionExecuted(ACTION_RESTART_NAME), Restarted(), ActionExecuted(ACTION_SESSION_START_NAME), @@ -914,7 +916,8 @@ async def test_restart_triggers_session_start( # No previous slot is set due to restart. ActionExecuted(ACTION_LISTEN_NAME), ] - assert list(tracker.events) == expected + for actual, expected in zip(tracker.events, expected): + assert actual == expected async def test_handle_message_if_action_manually_rejects( From 1493ad7d8e773866d79744704ffb11cd460e8c40 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 08:58:19 +0100 Subject: [PATCH 09/34] dump story not as test story --- tests/test_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_test.py b/tests/test_test.py index 8bbc45bececb..aea5aed8b157 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -187,7 +187,7 @@ def test_write_classification_errors(): WronglyPredictedAction("utter_greet", "", "utter_goodbye"), ] tracker = DialogueStateTracker.from_events("default", events) - dump = YAMLStoryWriter().dumps(tracker.as_story().story_steps, is_test_story=True) + dump = YAMLStoryWriter().dumps(tracker.as_story().story_steps) assert ( dump.strip() == textwrap.dedent( From 12912fb60bc8f5549290c91a8591338f79df69a7 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 08:59:03 +0100 Subject: [PATCH 10/34] fix docstring errors --- .../core/training_data/story_reader/markdown_story_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rasa/shared/core/training_data/story_reader/markdown_story_reader.py b/rasa/shared/core/training_data/story_reader/markdown_story_reader.py index ff7cfc466f58..2b89238b996c 100644 --- a/rasa/shared/core/training_data/story_reader/markdown_story_reader.py +++ b/rasa/shared/core/training_data/story_reader/markdown_story_reader.py @@ -209,8 +209,8 @@ def parse_e2e_message( """Parses an md list item line based on the current section type. Matches expressions of the form `:`. For the - syntax of `` see the Rasa docs on NLU training data.""" - + syntax of `` see the Rasa docs on NLU training data. + """ # Match three groups: # 1) Potential "form" annotation # 2) The correct intent From cbce93de73121f3d9e4bc34040e31bf4d37d93c9 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 09:31:48 +0100 Subject: [PATCH 11/34] remove unused method (not used in Rasa X either) --- .../core/training_data/story_writer/yaml_story_writer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index 1cfe405b885b..d04f114af038 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -187,13 +187,6 @@ def stories_contain_loops(stories: List[StoryStep]) -> bool: ] ) - @staticmethod - def _text_is_real_message(user_utterance: UserUttered) -> bool: - return ( - not user_utterance.intent - or user_utterance.text != user_utterance.as_story_string() - ) - @staticmethod def process_user_utterance( user_utterance: UserUttered, is_test_story: bool = False From 1109185a3960cd98915cfc04aff565944446f2f2 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 09:32:25 +0100 Subject: [PATCH 12/34] raise if printing end-to-end things in Markdown --- rasa/exceptions.py | 5 ++++ rasa/shared/core/events.py | 43 ++++++++++++++++++++++++-------- tests/shared/core/test_events.py | 19 ++++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/rasa/exceptions.py b/rasa/exceptions.py index 6c07487d32a7..38c4068b80a9 100644 --- a/rasa/exceptions.py +++ b/rasa/exceptions.py @@ -35,4 +35,9 @@ def __init__(self, timestamp: float) -> None: super(PublishingError, self).__init__() def __str__(self) -> Text: + """Returns string representation of exception.""" return str(self.timestamp) + + +class UnsupportedFeatureException(RasaException): + """Raised if a certain feature is not supported.""" diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index d03510c54d3b..5ae937db96f9 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -12,6 +12,7 @@ import rasa.shared.utils.common from typing import Union +from rasa.exceptions import UnsupportedFeatureException from rasa.shared.core.constants import ( LOOP_NAME, EXTERNAL_MESSAGE_PREFIX, @@ -323,6 +324,7 @@ def __init__( input_channel: Optional[Text] = None, message_id: Optional[Text] = None, metadata: Optional[Dict] = None, + use_text_for_featurization: Optional[bool] = None, ) -> None: self.text = text self.intent = intent if intent else {} @@ -332,6 +334,9 @@ def __init__( super().__init__(timestamp, metadata) + # The featurization is set by the policies during prediction time using a + # `DefinePrevUserUtteredFeaturization` event. + self.use_text_for_featurization = use_text_for_featurization # define how this user utterance should be featurized if self.text and not self.intent_name: # happens during training @@ -339,11 +344,6 @@ def __init__( elif self.intent_name and not self.text: # happens during training self.use_text_for_featurization = False - else: - # happens during prediction - # featurization should be defined by the policy - # and set in the applied events - self.use_text_for_featurization = None self.parse_data = { "intent": self.intent, @@ -489,17 +489,32 @@ def _entity_string(self): return "" def as_story_string(self, e2e: bool = False) -> Text: + """Return event as string for Markdown training format. + + Args: + e2e: `True` if the the event should be printed in the format for + end-to-end conversation tests. + + Returns: + Event as string. + """ + if self.use_text_for_featurization: + raise UnsupportedFeatureException( + "Printing end-to-end user utterances is not supported in the " + "Markdown training format. Please use the YAML training data instead." + ) + text_with_entities = md_format_message( self.text or "", self.intent_name, self.entities ) - if e2e or self.use_text_for_featurization is None: + if e2e: intent_prefix = f"{self.intent_name}: " if self.intent_name else "" return f"{intent_prefix}{text_with_entities}" - if self.intent_name and not self.use_text_for_featurization: + if self.intent_name: return f"{self.intent_name or ''}{self._entity_string()}" - if self.text and self.use_text_for_featurization: + if self.text: return text_with_entities # UserUttered is empty @@ -1217,9 +1232,11 @@ def __str__(self) -> Text: ) def __hash__(self) -> int: - return hash(self.action_name) + """Returns unique hash for action event.""" + return hash(self.action_name or self.action_text) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: + """Checks if object is equal to another.""" if not isinstance(other, ActionExecuted): return False else: @@ -1230,6 +1247,12 @@ def __eq__(self, other) -> bool: return equal def as_story_string(self) -> Text: + if self.action_text: + raise UnsupportedFeatureException( + "Printing end-to-end bot utterances is not supported in the " + "Markdown training format. Please use the YAML training data instead." + ) + return self.action_name @classmethod diff --git a/tests/shared/core/test_events.py b/tests/shared/core/test_events.py index cbbd0e71c5df..e96430a93012 100644 --- a/tests/shared/core/test_events.py +++ b/tests/shared/core/test_events.py @@ -9,6 +9,7 @@ import rasa.shared.utils.common import rasa.shared.core.events +from rasa.exceptions import UnsupportedFeatureException from rasa.shared.core.constants import ACTION_LISTEN_NAME, ACTION_SESSION_START_NAME from rasa.shared.core.events import ( Event, @@ -30,6 +31,7 @@ SessionStarted, md_format_message, ) +from rasa.shared.nlu.constants import INTENT_NAME_KEY from tests.core.policies.test_rule_policy import GREET_INTENT_NAME, UTTER_GREET_ACTION @@ -507,3 +509,20 @@ def test_events_begin_with_session_start( rasa.shared.core.events.do_events_begin_with_session_start(test_events) == begin_with_session_start ) + + +@pytest.mark.parametrize( + "end_to_end_event", + [ + ActionExecuted(action_text="I insist on using Markdown"), + UserUttered(text="Markdown is much more readable"), + UserUttered( + text="but YAML ❤️", + intent={INTENT_NAME_KEY: "use_yaml"}, + use_text_for_featurization=True, + ), + ], +) +def test_print_end_to_end_events_in_markdown(end_to_end_event: Event): + with pytest.raises(UnsupportedFeatureException): + end_to_end_event.as_story_string() From 60b2dca89fdb5fc3c3ef9b4a8a13a860c9d7b546 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 11:50:05 +0100 Subject: [PATCH 13/34] add todos --- rasa/core/training/story_conflict.py | 1 + rasa/shared/core/training_data/structures.py | 1 + 2 files changed, 2 insertions(+) diff --git a/rasa/core/training/story_conflict.py b/rasa/core/training/story_conflict.py index b1e76bcd8e3f..dd78c0c0076a 100644 --- a/rasa/core/training/story_conflict.py +++ b/rasa/core/training/story_conflict.py @@ -196,6 +196,7 @@ def _find_conflicting_states( # represented by its hash state_action_mapping = defaultdict(list) for element in _sliced_states_iterator(trackers, domain, max_history): + # TODO: Hash or just store as dict hashed_state = element.sliced_states_hash if element.event.as_story_string() not in state_action_mapping[hashed_state]: state_action_mapping[hashed_state] += [element.event.as_story_string()] diff --git a/rasa/shared/core/training_data/structures.py b/rasa/shared/core/training_data/structures.py index ed07148ca1a1..a6c72299dfba 100644 --- a/rasa/shared/core/training_data/structures.py +++ b/rasa/shared/core/training_data/structures.py @@ -407,6 +407,7 @@ def fingerprint(self) -> Text: Returns: fingerprint of the stories """ + # TODO: Use yaml story writer self_as_string = self.as_story_string() return rasa.shared.utils.io.get_text_hash(self_as_string) From d7f2a89adedcd9cd16882aa4518b3fd925bb9890 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 17:04:11 +0100 Subject: [PATCH 14/34] fix error with entity formatting --- rasa/shared/core/events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 5ae937db96f9..e3db587bba34 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -504,6 +504,9 @@ def as_story_string(self, e2e: bool = False) -> Text: "Markdown training format. Please use the YAML training data instead." ) + if self.intent_name: + return f"{self.intent_name or ''}{self._entity_string()}" + text_with_entities = md_format_message( self.text or "", self.intent_name, self.entities ) @@ -511,9 +514,6 @@ def as_story_string(self, e2e: bool = False) -> Text: intent_prefix = f"{self.intent_name}: " if self.intent_name else "" return f"{intent_prefix}{text_with_entities}" - if self.intent_name: - return f"{self.intent_name or ''}{self._entity_string()}" - if self.text: return text_with_entities From 1bfaafdb78745baa9b23926eb7f8f8767ab0ead4 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 18:26:15 +0100 Subject: [PATCH 15/34] move to `rasa.shared` --- rasa/exceptions.py | 4 ---- rasa/shared/core/events.py | 2 +- rasa/shared/exceptions.py | 4 ++++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rasa/exceptions.py b/rasa/exceptions.py index 38c4068b80a9..93794bbc8284 100644 --- a/rasa/exceptions.py +++ b/rasa/exceptions.py @@ -37,7 +37,3 @@ def __init__(self, timestamp: float) -> None: def __str__(self) -> Text: """Returns string representation of exception.""" return str(self.timestamp) - - -class UnsupportedFeatureException(RasaException): - """Raised if a certain feature is not supported.""" diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index e3db587bba34..832921b84cce 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -12,7 +12,6 @@ import rasa.shared.utils.common from typing import Union -from rasa.exceptions import UnsupportedFeatureException from rasa.shared.core.constants import ( LOOP_NAME, EXTERNAL_MESSAGE_PREFIX, @@ -23,6 +22,7 @@ ENTITY_LABEL_SEPARATOR, ACTION_SESSION_START_NAME, ) +from rasa.shared.exceptions import UnsupportedFeatureException from rasa.shared.nlu.constants import ( ENTITY_ATTRIBUTE_TYPE, INTENT, diff --git a/rasa/shared/exceptions.py b/rasa/shared/exceptions.py index 156c30313b89..15ffefb9499f 100644 --- a/rasa/shared/exceptions.py +++ b/rasa/shared/exceptions.py @@ -73,3 +73,7 @@ class FileIOException(RasaException): class InvalidConfigException(ValueError, RasaException): """Raised if an invalid configuration is encountered.""" + + +class UnsupportedFeatureException(RasaCoreException): + """Raised if a certain feature is not supported.""" From 76424f5570c46b0e59644bcbd18dfac16cb9db9e Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 19:09:06 +0100 Subject: [PATCH 16/34] remove CoreDataImporter --- rasa/shared/importers/importer.py | 26 -------------------- tests/shared/importers/test_importer.py | 32 +------------------------ 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/rasa/shared/importers/importer.py b/rasa/shared/importers/importer.py index 8501278a8fa1..6ba3f05c8e6e 100644 --- a/rasa/shared/importers/importer.py +++ b/rasa/shared/importers/importer.py @@ -214,32 +214,6 @@ async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: return await self._importer.get_nlu_data(language) -class CoreDataImporter(TrainingDataImporter): - """Importer that skips any NLU related file reading.""" - - def __init__(self, actual_importer: TrainingDataImporter): - self._importer = actual_importer - - async def get_domain(self) -> Domain: - return await self._importer.get_domain() - - async def get_stories( - self, - template_variables: Optional[Dict] = None, - use_e2e: bool = False, - exclusion_percentage: Optional[int] = None, - ) -> StoryGraph: - return await self._importer.get_stories( - template_variables, use_e2e, exclusion_percentage - ) - - async def get_config(self) -> Dict: - return await self._importer.get_config() - - async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: - return TrainingData() - - class CombinedDataImporter(TrainingDataImporter): """A `TrainingDataImporter` that combines multiple importers. Uses multiple `TrainingDataImporter` instances diff --git a/tests/shared/importers/test_importer.py b/tests/shared/importers/test_importer.py index 08ab28a43688..b753c4eb623c 100644 --- a/tests/shared/importers/test_importer.py +++ b/tests/shared/importers/test_importer.py @@ -17,7 +17,6 @@ CombinedDataImporter, TrainingDataImporter, NluDataImporter, - CoreDataImporter, E2EImporter, RetrievalModelsDataImporter, ) @@ -155,29 +154,6 @@ async def test_nlu_only(project: Text): assert not nlu_data.is_empty() -async def test_core_only(project: Text): - config_path = os.path.join(project, DEFAULT_CONFIG_PATH) - domain_path = os.path.join(project, DEFAULT_DOMAIN_PATH) - default_data_path = os.path.join(project, DEFAULT_DATA_PATH) - actual = TrainingDataImporter.load_core_importer_from_config( - config_path, domain_path, training_data_paths=[default_data_path] - ) - - assert isinstance(actual, CoreDataImporter) - - stories = await actual.get_stories() - assert not stories.is_empty() - - domain = await actual.get_domain() - assert not domain.is_empty() - - config = await actual.get_config() - assert config - - nlu_data = await actual.get_nlu_data() - assert nlu_data.is_empty() - - async def test_import_nlu_training_data_from_e2e_stories( default_importer: TrainingDataImporter, ): @@ -348,16 +324,10 @@ async def test_nlu_data_domain_sync_with_retrieval_intents(project: Text): "data/test_nlu/default_retrieval_intents.md", "data/test_responses/default.md", ] - base_data_importer = TrainingDataImporter.load_from_dict( + importer = TrainingDataImporter.load_from_dict( {}, config_path, domain_path, data_paths ) - nlu_importer = NluDataImporter(base_data_importer) - core_importer = CoreDataImporter(base_data_importer) - - importer = RetrievalModelsDataImporter( - CombinedDataImporter([nlu_importer, core_importer]) - ) domain = await importer.get_domain() nlu_data = await importer.get_nlu_data() From 4f967b3a9288b0b4b2a47a7ba4ccbbf5e7db6f84 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 19:09:49 +0100 Subject: [PATCH 17/34] change fingerprinting to use yaml writer --- rasa/shared/core/training_data/structures.py | 7 +++++-- tests/shared/importers/test_importer.py | 6 ++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/rasa/shared/core/training_data/structures.py b/rasa/shared/core/training_data/structures.py index a6c72299dfba..8ad0d7b62af1 100644 --- a/rasa/shared/core/training_data/structures.py +++ b/rasa/shared/core/training_data/structures.py @@ -407,8 +407,11 @@ def fingerprint(self) -> Text: Returns: fingerprint of the stories """ - # TODO: Use yaml story writer - self_as_string = self.as_story_string() + from rasa.shared.core.training_data.story_writer.yaml_story_writer import ( + YAMLStoryWriter, + ) + + self_as_string = YAMLStoryWriter().dumps(self.story_steps) return rasa.shared.utils.io.get_text_hash(self_as_string) def ordered_steps(self) -> List[StoryStep]: diff --git a/tests/shared/importers/test_importer.py b/tests/shared/importers/test_importer.py index b753c4eb623c..90d49e96c589 100644 --- a/tests/shared/importers/test_importer.py +++ b/tests/shared/importers/test_importer.py @@ -186,9 +186,9 @@ async def mocked_stories(*_: Any, **__: Any) -> StoryGraph: importer_without_e2e.get_stories = mocked_stories # The wrapping `E2EImporter` simply forwards these method calls - assert (await importer_without_e2e.get_stories()).as_story_string() == ( + assert (await importer_without_e2e.get_stories()).fingerprint() == ( await default_importer.get_stories() - ).as_story_string() + ).fingerprint() assert (await importer_without_e2e.get_config()) == ( await default_importer.get_config() ) @@ -209,8 +209,6 @@ async def mocked_stories(*_: Any, **__: Any) -> StoryGraph: Message(data={ACTION_TEXT: "Hi Joey."}), ] - print([t.data for t in nlu_data.training_examples]) - assert all(m in nlu_data.training_examples for m in expected_additional_messages) From 040dad5fa485e57af1d713a84b8d17a1bde12f47 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 19:10:37 +0100 Subject: [PATCH 18/34] fix tests failing due to new default story file --- tests/shared/test_data.py | 16 +++++-------- tests/test_server.py | 50 ++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/tests/shared/test_data.py b/tests/shared/test_data.py index a06baf4d2c69..0b21c73d5301 100644 --- a/tests/shared/test_data.py +++ b/tests/shared/test_data.py @@ -213,21 +213,17 @@ def test_get_core_nlu_directories_with_none(): assert all(not os.listdir(directory) for directory in directories) -def test_same_file_names_get_resolved(tmp_path): +def test_same_file_names_get_resolved(tmp_path: Path): # makes sure the resolution properly handles if there are two files with # with the same name in different directories (tmp_path / "one").mkdir() (tmp_path / "two").mkdir() - data_dir_one = str(tmp_path / "one" / "stories.md") - data_dir_two = str(tmp_path / "two" / "stories.md") - shutil.copy2(DEFAULT_STORIES_FILE, data_dir_one) - shutil.copy2(DEFAULT_STORIES_FILE, data_dir_two) + shutil.copy2(DEFAULT_STORIES_FILE, tmp_path / "one" / "stories.yml") + shutil.copy2(DEFAULT_STORIES_FILE, tmp_path / "two" / "stories.yml") - nlu_dir_one = str(tmp_path / "one" / "nlu.yml") - nlu_dir_two = str(tmp_path / "two" / "nlu.yml") - shutil.copy2(DEFAULT_NLU_DATA, nlu_dir_one) - shutil.copy2(DEFAULT_NLU_DATA, nlu_dir_two) + shutil.copy2(DEFAULT_NLU_DATA, tmp_path / "one" / "nlu.yml") + shutil.copy2(DEFAULT_NLU_DATA, tmp_path / "two" / "nlu.yml") core_directory, nlu_directory = rasa.shared.data.get_core_nlu_directories( [str(tmp_path)] @@ -241,7 +237,7 @@ def test_same_file_names_get_resolved(tmp_path): stories = os.listdir(core_directory) assert len(stories) == 2 - assert all(f.endswith("stories.md") for f in stories) + assert all(f.endswith("stories.yml") for f in stories) @pytest.mark.parametrize( diff --git a/tests/test_server.py b/tests/test_server.py index d8414b7bef73..5adb6731775a 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -407,26 +407,19 @@ async def test_parse_on_invalid_emulation_mode(rasa_app_nlu: SanicASGITestClient assert response.status == 400 -async def test_train_stack_success( +async def test_train_stack_success_with_md( rasa_app: SanicASGITestClient, default_domain_path: Text, - default_stories_file: Text, default_stack_config: Text, default_nlu_data: Text, tmp_path: Path, ): - with ExitStack() as stack: - domain_file = stack.enter_context(open(default_domain_path)) - config_file = stack.enter_context(open(default_stack_config)) - stories_file = stack.enter_context(open(default_stories_file)) - nlu_file = stack.enter_context(open(default_nlu_data)) - - payload = dict( - domain=domain_file.read(), - config=config_file.read(), - stories=stories_file.read(), - nlu=nlu_file.read(), - ) + payload = dict( + domain=Path(default_domain_path).read_text(), + config=Path(default_stack_config).read_text(), + stories=Path("data/test_stories/stories_defaultdomain.md").read_text(), + nlu=Path(default_nlu_data).read_text(), + ) _, response = await rasa_app.post("/model/train", json=payload) assert response.status == 200 @@ -479,25 +472,24 @@ async def test_train_nlu_success( assert os.path.exists(os.path.join(model_path, "fingerprint.json")) -async def test_train_core_success( +async def test_train_core_success_with( rasa_app: SanicASGITestClient, default_stack_config: Text, default_stories_file: Text, default_domain_path: Text, tmp_path: Path, ): - with ExitStack() as stack: - domain_file = stack.enter_context(open(default_domain_path)) - config_file = stack.enter_context(open(default_stack_config)) - core_file = stack.enter_context(open(default_stories_file)) - - payload = dict( - domain=domain_file.read(), - config=config_file.read(), - stories=core_file.read(), - ) + payload = f""" +{Path(default_domain_path).read_text()} +{Path(default_stack_config).read_text()} +{Path(default_stories_file).read_text()} + """ - _, response = await rasa_app.post("/model/train", json=payload) + _, response = await rasa_app.post( + "/model/train", + data=payload, + headers={"Content-type": rasa.server.YAML_CONTENT_TYPE}, + ) assert response.status == 200 # save model to temporary file @@ -697,7 +689,11 @@ async def test_evaluate_stories( ): stories = rasa.shared.utils.io.read_file(default_stories_file) - _, response = await rasa_app.post("/model/test/stories", data=stories) + _, response = await rasa_app.post( + "/model/test/stories", + data=stories, + headers={"Content-type": rasa.server.YAML_CONTENT_TYPE}, + ) assert response.status == 200 From 8fe73062d16b0d29c1e2c52e6d69372bac387e03 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 19:12:07 +0100 Subject: [PATCH 19/34] adapt remaining parts to `as_story_string` failing if end-to-end event --- rasa/shared/core/events.py | 18 +++------ .../story_writer/yaml_story_writer.py | 1 - tests/core/test_policies.py | 10 +---- .../core/training_data/test_structures.py | 40 +++++++++++++++---- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 832921b84cce..2aae1ab45530 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -498,27 +498,21 @@ def as_story_string(self, e2e: bool = False) -> Text: Returns: Event as string. """ - if self.use_text_for_featurization: + if self.use_text_for_featurization and not e2e: raise UnsupportedFeatureException( "Printing end-to-end user utterances is not supported in the " "Markdown training format. Please use the YAML training data instead." ) - if self.intent_name: - return f"{self.intent_name or ''}{self._entity_string()}" - - text_with_entities = md_format_message( - self.text or "", self.intent_name, self.entities - ) if e2e: + text_with_entities = md_format_message( + self.text or "", self.intent_name, self.entities + ) + intent_prefix = f"{self.intent_name}: " if self.intent_name else "" return f"{intent_prefix}{text_with_entities}" - if self.text: - return text_with_entities - - # UserUttered is empty - return "" + return f"{self.intent_name or ''}{self._entity_string()}" def apply_to(self, tracker: "DialogueStateTracker") -> None: tracker.latest_message = self diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index d04f114af038..93f79e2edc08 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -213,7 +213,6 @@ def process_user_utterance( if user_utterance.text and ( # We only print the utterance text if it was an end-to-end prediction user_utterance.use_text_for_featurization - or user_utterance.use_text_for_featurization is None # or if we want to print a conversation test story. or is_test_story ): diff --git a/tests/core/test_policies.py b/tests/core/test_policies.py index 2ef8e94d4342..3dcd1538f7e5 100644 --- a/tests/core/test_policies.py +++ b/tests/core/test_policies.py @@ -1110,10 +1110,7 @@ async def test_successful_rephrasing( ) assert "bye" == tracker.latest_message.parse_data["intent"][INTENT_NAME_KEY] - assert ( - tracker.export_stories(MarkdownStoryWriter()) - == "## sender\n* bye: Random\n" - ) + assert tracker.export_stories(MarkdownStoryWriter()) == "## sender\n* bye\n" def test_affirm_rephrased_intent( self, trained_policy: Policy, default_domain: Domain @@ -1158,10 +1155,7 @@ async def test_affirmed_rephrasing( ) assert "bye" == tracker.latest_message.parse_data["intent"][INTENT_NAME_KEY] - assert ( - tracker.export_stories(MarkdownStoryWriter()) - == "## sender\n* bye: Random\n" - ) + assert tracker.export_stories(MarkdownStoryWriter()) == "## sender\n* bye\n" def test_denied_rephrasing_affirmation( self, trained_policy: Policy, default_domain: Domain diff --git a/tests/shared/core/training_data/test_structures.py b/tests/shared/core/training_data/test_structures.py index 5794aac4eb9f..c6e2236ac815 100644 --- a/tests/shared/core/training_data/test_structures.py +++ b/tests/shared/core/training_data/test_structures.py @@ -1,9 +1,22 @@ import rasa.core from rasa.shared.core.constants import ACTION_SESSION_START_NAME from rasa.shared.core.domain import Domain -from rasa.shared.core.events import SessionStarted, SlotSet, UserUttered, ActionExecuted +from rasa.shared.core.events import ( + SessionStarted, + SlotSet, + UserUttered, + ActionExecuted, + DefinePrevUserUtteredFeaturization, +) from rasa.shared.core.trackers import DialogueStateTracker +from rasa.shared.core.training_data.story_reader.yaml_story_reader import ( + YAMLStoryReader, +) +from rasa.shared.core.training_data.story_writer.yaml_story_writer import ( + YAMLStoryWriter, +) from rasa.shared.core.training_data.structures import Story +from rasa.shared.nlu.constants import INTENT_NAME_KEY domain = Domain.load("examples/moodbot/domain.yml") @@ -19,17 +32,28 @@ def test_session_start_is_not_serialised(default_domain: Domain): # add the two SessionStarted events and a user event tracker.update(ActionExecuted(ACTION_SESSION_START_NAME)) tracker.update(SessionStarted()) - tracker.update(UserUttered("say something")) + tracker.update( + UserUttered("say something", intent={INTENT_NAME_KEY: "some_intent"}) + ) + tracker.update(DefinePrevUserUtteredFeaturization(False)) - # make sure session start is not serialised - story = Story.from_events(tracker.events, "some-story01") + YAMLStoryWriter().dumps( + Story.from_events(tracker.events, "some-story01").story_steps + ) - expected = """## some-story01 - - slot{"slot": "value"} -* say something + expected = """version: "2.0" +stories: +- story: some-story01 + steps: + - slot_was_set: + - slot: value + - intent: some_intent """ - assert story.as_story_string(flat=True) == expected + actual = YAMLStoryWriter().dumps( + Story.from_events(tracker.events, "some-story01").story_steps + ) + assert actual == expected def test_as_story_string_or_statement(): From cdc5ff60869caa420dc8ef943922b2e96fa96835 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 19:47:22 +0100 Subject: [PATCH 20/34] remove `as_story_string` from story validator --- rasa/core/training/story_conflict.py | 9 ++++----- rasa/shared/core/events.py | 9 ++++++++- rasa/shared/importers/importer.py | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/rasa/core/training/story_conflict.py b/rasa/core/training/story_conflict.py index dd78c0c0076a..a6adfd54f10b 100644 --- a/rasa/core/training/story_conflict.py +++ b/rasa/core/training/story_conflict.py @@ -196,10 +196,10 @@ def _find_conflicting_states( # represented by its hash state_action_mapping = defaultdict(list) for element in _sliced_states_iterator(trackers, domain, max_history): - # TODO: Hash or just store as dict hashed_state = element.sliced_states_hash - if element.event.as_story_string() not in state_action_mapping[hashed_state]: - state_action_mapping[hashed_state] += [element.event.as_story_string()] + current_hash = hash(element.event) + if current_hash not in state_action_mapping[hashed_state]: + state_action_mapping[hashed_state] += [current_hash] # Keep only conflicting `state_action_mapping`s return { @@ -239,8 +239,7 @@ def _build_conflicts_from_states( conflicts[hashed_state] = StoryConflict(element.sliced_states) conflicts[hashed_state].add_conflicting_action( - action=element.event.as_story_string(), - story_name=element.tracker.sender_id, + action=str(element.event), story_name=element.tracker.sender_id, ) # Return list of conflicts that arise from unpredictable actions diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 2aae1ab45530..8eb89a44396b 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -515,6 +515,7 @@ def as_story_string(self, e2e: bool = False) -> Text: return f"{self.intent_name or ''}{self._entity_string()}" def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to tracker. See docstring of `Event`.""" tracker.latest_message = self tracker.clear_followup_action() @@ -1220,11 +1221,16 @@ def __init__( super().__init__(timestamp, metadata) - def __str__(self) -> Text: + def __repr__(self) -> Text: + """Returns event as string for debugging.""" return "ActionExecuted(action: {}, policy: {}, confidence: {})".format( self.action_name, self.policy, self.confidence ) + def __str__(self) -> Text: + """Returns event as human readable string.""" + return self.action_name or self.action_text + def __hash__(self) -> int: """Returns unique hash for action event.""" return hash(self.action_name or self.action_text) @@ -1241,6 +1247,7 @@ def __eq__(self, other: Any) -> bool: return equal def as_story_string(self) -> Text: + """Returns event in Markdown format.""" if self.action_text: raise UnsupportedFeatureException( "Printing end-to-end bot utterances is not supported in the " diff --git a/rasa/shared/importers/importer.py b/rasa/shared/importers/importer.py index 6ba3f05c8e6e..c318b78d118b 100644 --- a/rasa/shared/importers/importer.py +++ b/rasa/shared/importers/importer.py @@ -216,6 +216,7 @@ async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: class CombinedDataImporter(TrainingDataImporter): """A `TrainingDataImporter` that combines multiple importers. + Uses multiple `TrainingDataImporter` instances to load the data as if they were a single instance. """ From a382eb6e220575c5e9f7a625dd1735d50c586aef Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Wed, 2 Dec 2020 20:04:52 +0100 Subject: [PATCH 21/34] only train NLU model if data or end to end --- rasa/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/train.py b/rasa/train.py index 90d8b190279c..20d629dda2bf 100644 --- a/rasa/train.py +++ b/rasa/train.py @@ -175,7 +175,7 @@ async def _train_async_internal( ) # We will train nlu if there are any nlu example, including from e2e stories. - if nlu_data.is_empty(): + if nlu_data.contains_no_pure_nlu_data() and not nlu_data.has_e2e_examples(): print_warning("No NLU data present. Just a Rasa Core model will be trained.") return await _train_core_with_validated_data( file_importer, From bec8926994aee758211b9580df24a90c478c63f8 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 3 Dec 2020 17:20:24 +0100 Subject: [PATCH 22/34] fix import --- tests/shared/core/test_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/core/test_events.py b/tests/shared/core/test_events.py index e96430a93012..ec4ba363a173 100644 --- a/tests/shared/core/test_events.py +++ b/tests/shared/core/test_events.py @@ -9,7 +9,7 @@ import rasa.shared.utils.common import rasa.shared.core.events -from rasa.exceptions import UnsupportedFeatureException +from rasa.shared.exceptions import UnsupportedFeatureException from rasa.shared.core.constants import ACTION_LISTEN_NAME, ACTION_SESSION_START_NAME from rasa.shared.core.events import ( Event, From d4152186645c911f83c0f40303e245065d862b3b Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 3 Dec 2020 17:40:47 +0100 Subject: [PATCH 23/34] read and write in test --- .../story_reader/yaml_story_reader.py | 3 +- rasa/shared/exceptions.py | 2 +- .../story_writer/test_yaml_story_writer.py | 55 ++++++++----------- 3 files changed, 24 insertions(+), 36 deletions(-) diff --git a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py index 2bfd391e3aba..9afd2a0d2835 100644 --- a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py +++ b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py @@ -318,14 +318,13 @@ def _parse_user_utterance(self, step: Dict[Text, Any]) -> None: is_end_to_end_utterance = KEY_USER_INTENT not in step if is_end_to_end_utterance: - utterance.intent = {"name": None} + utterance.intent = {INTENT_NAME_KEY: None} else: self._validate_that_utterance_is_in_domain(utterance) self.current_step_builder.add_user_messages([utterance]) def _validate_that_utterance_is_in_domain(self, utterance: UserUttered) -> None: - intent_name = utterance.intent.get(INTENT_NAME_KEY) # check if this is a retrieval intent diff --git a/rasa/shared/exceptions.py b/rasa/shared/exceptions.py index 15ffefb9499f..8a2eb43e3c48 100644 --- a/rasa/shared/exceptions.py +++ b/rasa/shared/exceptions.py @@ -76,4 +76,4 @@ class InvalidConfigException(ValueError, RasaException): class UnsupportedFeatureException(RasaCoreException): - """Raised if a certain feature is not supported.""" + """Raised if a requested feature is not supported.""" diff --git a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py index 88fff828a159..6c68921be1ca 100644 --- a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py +++ b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py @@ -196,11 +196,11 @@ def test_writing_end_to_end_stories(default_domain: Domain): # Prediction story story with intent and action labels ActionExecuted(ACTION_LISTEN_NAME), UserUttered(text="Hi", intent={"name": "greet"}), + DefinePrevUserUtteredFeaturization(use_text_for_featurization=False), ActionExecuted("utter_greet"), ActionExecuted(ACTION_LISTEN_NAME), # End-To-End Training Story UserUttered(text="Hi"), - DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), ActionExecuted(action_text="Hi, I'm a bot."), ActionExecuted(ACTION_LISTEN_NAME), # End-To-End Prediction Story @@ -236,40 +236,29 @@ def test_writing_end_to_end_stories(default_domain: Domain): ) -def test_writing_end_to_end_stories_in_test_mode(default_domain: Domain): +def test_reading_and_writing_end_to_end_stories_in_test_mode(default_domain: Domain): story_name = "test_writing_end_to_end_stories_in_test_mode" - events = [ - # Conversation tests (end-to-end _testing_) - ActionExecuted(ACTION_LISTEN_NAME), - UserUttered(text="Hi", intent={"name": "greet"}), - ActionExecuted("utter_greet"), - ActionExecuted(ACTION_LISTEN_NAME), - # Conversation tests (end-to-end _testing_) and entities - UserUttered( - text="Hi", - intent={"name": "greet"}, - entities=[{"value": "Hi", "entity": "test", "start": 0, "end": 2}], - ), - ActionExecuted("utter_greet"), - ActionExecuted(ACTION_LISTEN_NAME), - # Conversation test with actual end-to-end utterances - UserUttered(text="Hi", intent={"name": "greet"}), - DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), - ActionExecuted(action_text="Hi, I'm a bot."), - ActionExecuted(ACTION_LISTEN_NAME), - # Conversation test with actual end-to-end utterances - UserUttered( - text="Hi", - intent={"name": "greet"}, - entities=[{"value": "Hi", "entity": "test", "start": 0, "end": 2}], - ), - DefinePrevUserUtteredFeaturization(use_text_for_featurization=True), - ActionExecuted(action_text="Hi, I'm a bot."), - ActionExecuted(ACTION_LISTEN_NAME), - ] - tracker = DialogueStateTracker.from_events(story_name, events) - dump = YAMLStoryWriter().dumps(tracker.as_story().story_steps, is_test_story=True) + conversation_tests = f""" +stories: +- story: {story_name} + steps: + - intent: greet + user: Hi + - action: utter_greet + - intent: greet + user: | + [Hi](test) + - action: utter_greet + - user: Hi + - bot: Hi, I'm a bot. + - user: | + [Hi](test) + - bot: Hi, I'm a bot. + """ + + end_to_end_tests = YAMLStoryReader().read_from_string(conversation_tests) + dump = YAMLStoryWriter().dumps(end_to_end_tests, is_test_story=True) assert ( dump.strip() From c6a245cb48cbe6a2bf7082696e6f31be790996a6 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 3 Dec 2020 18:00:34 +0100 Subject: [PATCH 24/34] fix displaying of end-to-end actions in rasa interactive --- rasa/core/training/interactive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py index a025062cdd4e..7b3677895389 100644 --- a/rasa/core/training/interactive.py +++ b/rasa/core/training/interactive.py @@ -534,7 +534,7 @@ def add_user_cell(data, cell): for idx, event in enumerate(applied_events): if isinstance(event, ActionExecuted): - bot_column.append(colored(event.action_name, "autocyan")) + bot_column.append(colored(str(event), "autocyan")) if event.confidence is not None: bot_column[-1] += colored(f" {event.confidence:03.2f}", "autowhite") From c8971c9a7e42d69a93ebe7830ecb4400a39c4a89 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 3 Dec 2020 18:00:57 +0100 Subject: [PATCH 25/34] skip warning for end-to-end user messages in training data --- .../shared/core/training_data/story_reader/yaml_story_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py index 9afd2a0d2835..8382d3cc3c4e 100644 --- a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py +++ b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py @@ -372,7 +372,7 @@ def _user_intent_from_step( ) -> Tuple[Text, Optional[Text]]: user_intent = step.get(KEY_USER_INTENT, "").strip() - if not user_intent: + if not user_intent and KEY_USER_MESSAGE not in step: rasa.shared.utils.io.raise_warning( f"Issue found in '{self.source_name}':\n" f"User utterance cannot be empty. " From 311684b4cbe711fec2d25251fbfbe64bcdb30c5e Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 3 Dec 2020 18:08:05 +0100 Subject: [PATCH 26/34] add docs link --- rasa/shared/core/events.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 8eb89a44396b..524b5349b60e 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -12,6 +12,7 @@ import rasa.shared.utils.common from typing import Union +from rasa.shared.constants import DOCS_URL_TRAINING_DATA from rasa.shared.core.constants import ( LOOP_NAME, EXTERNAL_MESSAGE_PREFIX, @@ -500,8 +501,9 @@ def as_story_string(self, e2e: bool = False) -> Text: """ if self.use_text_for_featurization and not e2e: raise UnsupportedFeatureException( - "Printing end-to-end user utterances is not supported in the " - "Markdown training format. Please use the YAML training data instead." + f"Printing end-to-end user utterances is not supported in the " + f"Markdown training format. Please use the YAML training data format " + f"instead. Please see {DOCS_URL_TRAINING_DATA} for more information." ) if e2e: @@ -1250,8 +1252,9 @@ def as_story_string(self) -> Text: """Returns event in Markdown format.""" if self.action_text: raise UnsupportedFeatureException( - "Printing end-to-end bot utterances is not supported in the " - "Markdown training format. Please use the YAML training data instead." + f"Printing end-to-end bot utterances is not supported in the " + f"Markdown training format. Please use the YAML training data format " + f"instead. Please see {DOCS_URL_TRAINING_DATA} for more information." ) return self.action_name From d01f4ab8328595a184654997aa156e360c5e3338 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Thu, 3 Dec 2020 19:24:18 +0100 Subject: [PATCH 27/34] remove trailing whitespace --- .../core/training_data/story_writer/test_yaml_story_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py index 6c68921be1ca..38830a9ccb66 100644 --- a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py +++ b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py @@ -247,12 +247,12 @@ def test_reading_and_writing_end_to_end_stories_in_test_mode(default_domain: Dom user: Hi - action: utter_greet - intent: greet - user: | + user: | [Hi](test) - action: utter_greet - user: Hi - bot: Hi, I'm a bot. - - user: | + - user: | [Hi](test) - bot: Hi, I'm a bot. """ From 6d239d8964bb9349a3580536c53a7767f17d3751 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Fri, 4 Dec 2020 17:59:20 +0100 Subject: [PATCH 28/34] return `NotImplemented` if other class --- rasa/shared/core/events.py | 129 ++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 75 deletions(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 524b5349b60e..2ffea0e3b531 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -306,6 +306,12 @@ def resolve_by_type( def apply_to(self, tracker: "DialogueStateTracker") -> None: pass + def __eq__(self, other: Any) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + + return True + # noinspection PyProtectedMember class UserUttered(Event): @@ -386,17 +392,17 @@ def intent_name(self) -> Optional[Text]: def __eq__(self, other: Any) -> bool: if not isinstance(other, UserUttered): - return False - else: - return ( - self.text, - self.intent_name, - [jsonpickle.encode(ent) for ent in self.entities], - ) == ( - other.text, - other.intent_name, - [jsonpickle.encode(ent) for ent in other.entities], - ) + return NotImplemented + + return ( + self.text, + self.intent_name, + [jsonpickle.encode(ent) for ent in self.entities], + ) == ( + other.text, + other.intent_name, + [jsonpickle.encode(ent) for ent in other.entities], + ) def __str__(self) -> Text: return ( @@ -556,9 +562,6 @@ def __str__(self) -> Text: def __hash__(self) -> int: return hash(self.use_text_for_featurization) - def __eq__(self, other) -> bool: - return isinstance(other, DefinePrevUserUtteredFeaturization) - def as_story_string(self) -> None: return None @@ -605,9 +608,9 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, BotUttered): - return False - else: - return self.__members() == other.__members() + return NotImplemented + + return self.__members() == other.__members() def __str__(self) -> Text: return "BotUttered(text: {}, data: {}, metadata: {})".format( @@ -696,9 +699,9 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, SlotSet): - return False - else: - return (self.key, self.value) == (other.key, other.value) + return NotImplemented + + return (self.key, self.value) == (other.key, other.value) def as_story_string(self) -> Text: props = json.dumps({self.key: self.value}, ensure_ascii=False) @@ -750,9 +753,6 @@ class Restarted(Event): def __hash__(self) -> int: return hash(32143124312) - def __eq__(self, other) -> bool: - return isinstance(other, Restarted) - def __str__(self) -> Text: return "Restarted()" @@ -778,9 +778,6 @@ class UserUtteranceReverted(Event): def __hash__(self) -> int: return hash(32143124315) - def __eq__(self, other) -> bool: - return isinstance(other, UserUtteranceReverted) - def __str__(self) -> Text: return "UserUtteranceReverted()" @@ -805,9 +802,6 @@ class AllSlotsReset(Event): def __hash__(self) -> int: return hash(32143124316) - def __eq__(self, other) -> bool: - return isinstance(other, AllSlotsReset) - def __str__(self) -> Text: return "AllSlotsReset()" @@ -870,9 +864,9 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, ReminderScheduled): - return False - else: - return self.name == other.name + return NotImplemented + + return self.name == other.name def __str__(self) -> Text: return ( @@ -960,9 +954,9 @@ def __hash__(self) -> int: def __eq__(self, other: Any) -> bool: if not isinstance(other, ReminderCancelled): - return False - else: - return hash(self) == hash(other) + return NotImplemented + + return hash(self) == hash(other) def __str__(self) -> Text: return f"ReminderCancelled(name: {self.name}, intent: {self.intent}, entities: {self.entities})" @@ -1039,9 +1033,6 @@ class ActionReverted(Event): def __hash__(self) -> int: return hash(32143124318) - def __eq__(self, other) -> bool: - return isinstance(other, ActionReverted) - def __str__(self) -> Text: return "ActionReverted()" @@ -1071,9 +1062,6 @@ def __init__( def __hash__(self) -> int: return hash(32143124319) - def __eq__(self, other) -> bool: - return isinstance(other, StoryExported) - def __str__(self) -> Text: return "StoryExported()" @@ -1115,9 +1103,9 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, FollowupAction): - return False - else: - return self.action_name == other.action_name + return NotImplemented + + return self.action_name == other.action_name def __str__(self) -> Text: return f"FollowupAction(action: {self.action_name})" @@ -1158,9 +1146,6 @@ class ConversationPaused(Event): def __hash__(self) -> int: return hash(32143124313) - def __eq__(self, other) -> bool: - return isinstance(other, ConversationPaused) - def __str__(self) -> Text: return "ConversationPaused()" @@ -1183,9 +1168,6 @@ class ConversationResumed(Event): def __hash__(self) -> int: return hash(32143124314) - def __eq__(self, other) -> bool: - return isinstance(other, ConversationResumed) - def __str__(self) -> Text: return "ConversationResumed()" @@ -1240,13 +1222,13 @@ def __hash__(self) -> int: def __eq__(self, other: Any) -> bool: """Checks if object is equal to another.""" if not isinstance(other, ActionExecuted): - return False - else: - equal = self.action_name == other.action_name - if hasattr(self, "action_text") and hasattr(other, "action_text"): - equal = equal and self.action_text == other.action_text + return NotImplemented + + equal = self.action_name == other.action_name + if hasattr(self, "action_text") and hasattr(other, "action_text"): + equal = equal and self.action_text == other.action_text - return equal + return equal def as_story_string(self) -> Text: """Returns event in Markdown format.""" @@ -1332,12 +1314,12 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, AgentUttered): - return False - else: - return (self.text, jsonpickle.encode(self.data)) == ( - other.text, - jsonpickle.encode(other.data), - ) + return NotImplemented + + return (self.text, jsonpickle.encode(self.data)) == ( + other.text, + jsonpickle.encode(other.data), + ) def __str__(self) -> Text: return "AgentUttered(text: {}, data: {})".format( @@ -1395,9 +1377,9 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, ActiveLoop): - return False - else: - return self.name == other.name + return NotImplemented + + return self.name == other.name def as_story_string(self) -> Text: props = json.dumps({LOOP_NAME: self.name}) @@ -1462,10 +1444,10 @@ def __hash__(self) -> int: return hash(self.is_interrupted) def __eq__(self, other) -> bool: - return ( - isinstance(other, LoopInterrupted) - and self.is_interrupted == other.is_interrupted - ) + if not isinstance(other, LoopInterrupted): + return NotImplemented + + return self.is_interrupted == other.is_interrupted def as_story_string(self) -> None: return None @@ -1553,9 +1535,9 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: if not isinstance(other, ActionExecutionRejected): - return False - else: - return self.action_name == other.action_name + return NotImplemented + + return self.action_name == other.action_name @classmethod def _from_parameters(cls, parameters) -> "ActionExecutionRejected": @@ -1593,9 +1575,6 @@ class SessionStarted(Event): def __hash__(self) -> int: return hash(32143124320) - def __eq__(self, other: Any) -> bool: - return isinstance(other, SessionStarted) - def __str__(self) -> Text: return "SessionStarted()" From 7a340573b7f8566c14f699d5f3ff127a45d9f582 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Fri, 4 Dec 2020 18:08:37 +0100 Subject: [PATCH 29/34] remove `md_` as it's not related to md --- rasa/core/test.py | 8 ++++---- rasa/shared/core/events.py | 4 ++-- .../training_data/story_writer/yaml_story_writer.py | 2 +- tests/shared/core/test_events.py | 12 +++++------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/rasa/core/test.py b/rasa/core/test.py index ce6946848219..994f33fa13ff 100644 --- a/rasa/core/test.py +++ b/rasa/core/test.py @@ -304,17 +304,17 @@ def __init__(self, event: UserUttered, eval_store: EvaluationStore) -> None: def inline_comment(self) -> Text: """A comment attached to this event. Used during dumping.""" - from rasa.shared.core.events import md_format_message + from rasa.shared.core.events import format_message - predicted_message = md_format_message( + predicted_message = format_message( self.text, self.predicted_intent, self.predicted_entities ) return f"predicted: {self.predicted_intent}: {predicted_message}" def as_story_string(self, e2e: bool = True) -> Text: - from rasa.shared.core.events import md_format_message + from rasa.shared.core.events import format_message - correct_message = md_format_message( + correct_message = format_message( self.text, self.intent.get("name"), self.entities ) return ( diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 2ffea0e3b531..3b176e26781a 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -73,7 +73,7 @@ def deserialise_entities(entities: Union[Text, List[Any]]) -> List[Dict[Text, An return [e for e in entities if isinstance(e, dict)] -def md_format_message( +def format_message( text: Text, intent: Optional[Text], entities: Union[Text, List[Any]] ) -> Text: """Uses NLU parser information to generate a message with inline entity annotations. @@ -513,7 +513,7 @@ def as_story_string(self, e2e: bool = False) -> Text: ) if e2e: - text_with_entities = md_format_message( + text_with_entities = format_message( self.text or "", self.intent_name, self.entities ) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index 93f79e2edc08..23d415ef594e 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -217,7 +217,7 @@ def process_user_utterance( or is_test_story ): result[KEY_USER_MESSAGE] = LiteralScalarString( - rasa.shared.core.events.md_format_message( + rasa.shared.core.events.format_message( user_utterance.text, user_utterance.intent_name, user_utterance.entities, diff --git a/tests/shared/core/test_events.py b/tests/shared/core/test_events.py index ec4ba363a173..f49063c09344 100644 --- a/tests/shared/core/test_events.py +++ b/tests/shared/core/test_events.py @@ -29,7 +29,7 @@ UserUtteranceReverted, AgentUttered, SessionStarted, - md_format_message, + format_message, ) from rasa.shared.nlu.constants import INTENT_NAME_KEY from tests.core.policies.test_rule_policy import GREET_INTENT_NAME, UTTER_GREET_ACTION @@ -321,17 +321,15 @@ def test_user_uttered_intent_name(event: UserUttered, intent_name: Optional[Text def test_md_format_message(): - assert ( - md_format_message("Hello there!", intent="greet", entities=[]) == "Hello there!" - ) + assert format_message("Hello there!", intent="greet", entities=[]) == "Hello there!" def test_md_format_message_empty(): - assert md_format_message("", intent=None, entities=[]) == "" + assert format_message("", intent=None, entities=[]) == "" def test_md_format_message_using_short_entity_syntax(): - formatted = md_format_message( + formatted = format_message( "I am from Berlin.", intent="location", entities=[{"start": 10, "end": 16, "entity": "city", "value": "Berlin"}], @@ -340,7 +338,7 @@ def test_md_format_message_using_short_entity_syntax(): def test_md_format_message_using_long_entity_syntax(): - formatted = md_format_message( + formatted = format_message( "I am from Berlin in Germany.", intent="location", entities=[ From dcbdac1db736134afeecd821e2d0b116361b477f Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Fri, 4 Dec 2020 19:10:26 +0100 Subject: [PATCH 30/34] add docstrings to entire module --- rasa/core/test.py | 1 + rasa/shared/core/events.py | 248 +++++++++++++++++++++++++++++-------- 2 files changed, 198 insertions(+), 51 deletions(-) diff --git a/rasa/core/test.py b/rasa/core/test.py index 994f33fa13ff..8398c86e11be 100644 --- a/rasa/core/test.py +++ b/rasa/core/test.py @@ -312,6 +312,7 @@ def inline_comment(self) -> Text: return f"predicted: {self.predicted_intent}: {predicted_message}" def as_story_string(self, e2e: bool = True) -> Text: + """Returns text representation of event.""" from rasa.shared.core.events import format_message correct_message = format_message( diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 3b176e26781a..f30e01a290c4 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -304,14 +304,24 @@ def resolve_by_type( raise ValueError(f"Unknown event name '{type_name}'.") def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state. + + Args: + tracker: The current conversation state. + """ pass def __eq__(self, other: Any) -> bool: + """Compares object with other object.""" if not isinstance(other, self.__class__): return NotImplemented return True + def __str__(self) -> Text: + """Returns text representation of event.""" + return f"{self.__class__.__name__}()" + # noinspection PyProtectedMember class UserUttered(Event): @@ -333,6 +343,21 @@ def __init__( metadata: Optional[Dict] = None, use_text_for_featurization: Optional[bool] = None, ) -> None: + """Creates event for incoming user message. + + Args: + text: Text of user message. + intent: Intent prediction of user message. + entities: Extracted entities. + parse_data: Detailed NLU parsing result for message. + timestamp: When the event was created. + metadata: Additional event metadata. + input_channel: Which channel the user used to send message. + message_id: Unique ID for message. + use_text_for_featurization: `True` if the message's text was used to predict + next action. `False` if the message's intent was used. + + """ self.text = text self.intent = intent if intent else {} self.entities = entities if entities else [] @@ -384,13 +409,16 @@ def _from_parse_data( ) def __hash__(self) -> int: + """Returns unique hash of object.""" return hash((self.text, self.intent_name, jsonpickle.encode(self.entities))) @property def intent_name(self) -> Optional[Text]: + """Returns intent name or `None` if no intent.""" return self.intent.get(INTENT_NAME_KEY) def __eq__(self, other: Any) -> bool: + """Compares object with other object.""" if not isinstance(other, UserUttered): return NotImplemented @@ -544,6 +572,7 @@ def create_external( # noinspection PyProtectedMember class DefinePrevUserUtteredFeaturization(Event): + """Stores information whether action was predicted based on text or intent.""" type_name = "user_featurization" @@ -553,16 +582,27 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Creates event. + + Args: + use_text_for_featurization: `True` if message text was used to predict + action. `False` if intent was used. + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.use_text_for_featurization = use_text_for_featurization super().__init__(timestamp, metadata) def __str__(self) -> Text: + """Returns text representation of event.""" return f"DefinePrevUserUtteredFeaturization({self.use_text_for_featurization})" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(self.use_text_for_featurization) def as_story_string(self) -> None: + """Skips representing the event in stories.""" return None @classmethod @@ -574,6 +614,7 @@ def _from_parameters(cls, parameters) -> "DefinePrevUserUtteredFeaturization": ) def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({USE_TEXT_FOR_FEATURIZATION: self.use_text_for_featurization}) return d @@ -590,6 +631,14 @@ class BotUttered(Event): type_name = "bot" def __init__(self, text=None, data=None, metadata=None, timestamp=None) -> None: + """Creates event for a bot response. + + Args: + text: Plain text which bot responded with. + data: Additional data for more complex utterances (e.g. buttons). + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.text = text self.data = data or {} super().__init__(timestamp, metadata) @@ -651,6 +700,7 @@ def empty() -> "BotUttered": return BotUttered() def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({"text": self.text, "data": self.data, "metadata": self.metadata}) return d @@ -670,13 +720,14 @@ def _from_parameters(cls, parameters) -> "BotUttered": # noinspection PyProtectedMember class SlotSet(Event): - """The user has specified their preference for the value of a ``slot``. + """The user has specified their preference for the value of a `slot`. Every slot has a name and a value. This event can be used to set a value for a slot on a conversation. - As a side effect the ``Tracker``'s slots will be updated so - that ``tracker.slots[key]=value``.""" + As a side effect the `Tracker`'s slots will be updated so + that `tracker.slots[key]=value`. + """ type_name = "slot" @@ -687,17 +738,28 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Creates event to set slot. + + Args: + key: Name of the slot which is set. + value: Value to which slot is set. + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.key = key self.value = value super().__init__(timestamp, metadata) def __str__(self) -> Text: + """Returns text representation of event.""" return f"SlotSet(key: {self.key}, value: {self.value})" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash((self.key, jsonpickle.encode(self.value))) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, SlotSet): return NotImplemented @@ -720,6 +782,7 @@ def _from_story_string(cls, parameters: Dict[Text, Any]) -> Optional[List[Event] return None def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({"name": self.key, "value": self.value}) return d @@ -737,6 +800,7 @@ def _from_parameters(cls, parameters) -> "SlotSet": raise ValueError(f"Failed to parse set slot event. {e}") def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker._set_slot(self.key, self.value) @@ -746,17 +810,17 @@ class Restarted(Event): Instead of deleting all events, this event can be used to reset the trackers state (e.g. ignoring any past user messages & resetting all - the slots).""" + the slots). + """ type_name = "restart" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124312) - def __str__(self) -> Text: - return "Restarted()" - def as_story_string(self) -> Text: + """Returns text representation of event.""" return self.type_name def apply_to(self, tracker: "DialogueStateTracker") -> None: @@ -771,20 +835,21 @@ class UserUtteranceReverted(Event): The bot will revert all events after the latest `UserUttered`, this also means that the last event on the tracker is usually `action_listen` - and the bot is waiting for a new user message.""" + and the bot is waiting for a new user message. + """ type_name = "rewind" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124315) - def __str__(self) -> Text: - return "UserUtteranceReverted()" - def as_story_string(self) -> Text: + """Returns text representation of event.""" return self.type_name def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker._reset() tracker.replay_events() @@ -795,20 +860,21 @@ class AllSlotsReset(Event): If you want to keep the dialogue history and only want to reset the slots, you can use this event to set all the slots to their initial - values.""" + values. + """ type_name = "reset_slots" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124316) - def __str__(self) -> Text: - return "AllSlotsReset()" - def as_story_string(self) -> Text: + """Returns text representation of event.""" return self.type_name def apply_to(self, tracker) -> None: + """Applies event to current conversation state.""" tracker._reset_slots() @@ -829,7 +895,7 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: - """Creates the reminder + """Creates the reminder. Args: intent: Name of the intent to be triggered. @@ -852,6 +918,7 @@ def __init__( super().__init__(timestamp, metadata) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash( ( self.intent, @@ -891,10 +958,12 @@ def _properties(self) -> Dict[Text, Any]: } def as_story_string(self) -> Text: + """Returns text representation of event.""" props = json.dumps(self._properties()) return f"{self.type_name}{props}" def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update(self._properties()) return d @@ -943,7 +1012,6 @@ def __init__( timestamp: Optional timestamp. metadata: Optional event metadata. """ - self.name = name self.intent = intent self.entities = entities @@ -1000,6 +1068,7 @@ def _matches_entities_hash(self, entities_hash: Text) -> bool: return str(hash(str(self.entities))) == entities_hash def as_story_string(self) -> Text: + """Returns text representation of event.""" props = json.dumps( {"name": self.name, "intent": self.intent, "entities": self.entities} ) @@ -1026,20 +1095,21 @@ class ActionReverted(Event): This includes the action itself, as well as any events that action created, like set slot events - the bot will now predict a new action using the state before the most recent - action.""" + action. + """ type_name = "undo" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124318) - def __str__(self) -> Text: - return "ActionReverted()" - def as_story_string(self) -> Text: + """Returns text representation of event.""" return self.type_name def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker._reset() tracker.replay_events() @@ -1056,15 +1126,20 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Creates event about story exporting. + + Args: + path: Path to which story was exported to. + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.path = path super().__init__(timestamp, metadata) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124319) - def __str__(self) -> Text: - return "StoryExported()" - @classmethod def _from_story_string(cls, parameters: Dict[Text, Any]) -> Optional[List[Event]]: return [ @@ -1076,9 +1151,11 @@ def _from_story_string(cls, parameters: Dict[Text, Any]) -> Optional[List[Event] ] def as_story_string(self) -> Text: + """Returns text representation of event.""" return self.type_name def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" if self.path: tracker.export_stories_to_file(self.path) @@ -1095,22 +1172,33 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Creates an event which forces the model to run a certain action next. + + Args: + name: Name of the action to run. + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.action_name = name super().__init__(timestamp, metadata) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(self.action_name) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, FollowupAction): return NotImplemented return self.action_name == other.action_name def __str__(self) -> Text: + """Returns text representation of event.""" return f"FollowupAction(action: {self.action_name})" def as_story_string(self) -> Text: + """Returns text representation of event.""" props = json.dumps({"name": self.action_name}) return f"{self.type_name}{props}" @@ -1126,11 +1214,13 @@ def _from_story_string(cls, parameters: Dict[Text, Any]) -> Optional[List[Event] ] def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({"name": self.action_name}) return d def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker.trigger_followup_action(self.action_name) @@ -1138,21 +1228,22 @@ def apply_to(self, tracker: "DialogueStateTracker") -> None: class ConversationPaused(Event): """Ignore messages from the user to let a human take over. - As a side effect the ``Tracker``'s ``paused`` attribute will - be set to ``True``.""" + As a side effect the `Tracker`'s `paused` attribute will + be set to `True`. + """ type_name = "pause" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124313) - def __str__(self) -> Text: - return "ConversationPaused()" - def as_story_string(self) -> Text: - return self.type_name + """Returns text representation of event.""" + return str(self) def apply_to(self, tracker) -> None: + """Applies event to current conversation state.""" tracker._paused = True @@ -1160,21 +1251,22 @@ def apply_to(self, tracker) -> None: class ConversationResumed(Event): """Bot takes over conversation. - Inverse of ``PauseConversation``. As a side effect the ``Tracker``'s - ``paused`` attribute will be set to ``False``.""" + Inverse of `PauseConversation`. As a side effect the `Tracker`'s + `paused` attribute will be set to `False`. + """ type_name = "resume" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124314) - def __str__(self) -> Text: - return "ConversationResumed()" - def as_story_string(self) -> Text: + """Returns text representation of event.""" return self.type_name def apply_to(self, tracker) -> None: + """Applies event to current conversation state.""" tracker._paused = False @@ -1197,6 +1289,18 @@ def __init__( metadata: Optional[Dict] = None, action_text: Optional[Text] = None, ) -> None: + """Creates event for a successful event execution. + + Args: + action_name: Name of the action which was executed. `None` if it was an + end-to-end prediction. + policy: Policy which predicted action. + confidence: Confidence with which policy predicted action. + timestamp: When the event was created. + metadata: Additional event metadata. + action_text: In case it's an end-to-end action prediction, the text which + was predicted. + """ self.action_name = action_name self.policy = policy self.confidence = confidence @@ -1216,7 +1320,7 @@ def __str__(self) -> Text: return self.action_name or self.action_text def __hash__(self) -> int: - """Returns unique hash for action event.""" + """Returns unique hash for event.""" return hash(self.action_name or self.action_text) def __eq__(self, other: Any) -> bool: @@ -1256,6 +1360,7 @@ def _from_story_string(cls, parameters: Dict[Text, Any]) -> Optional[List[Event] ] def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() policy = None # for backwards compatibility (persisted events) if hasattr(self, "policy"): @@ -1276,9 +1381,12 @@ def as_dict(self) -> Dict[Text, Any]: def as_sub_state(self) -> Dict[Text, Text]: """Turns ActionExecuted into a dictionary containing action name or action text. + One action cannot have both set at the same time + Returns: - a dictionary containing action name or action text with the corresponding key + a dictionary containing action name or action text with the corresponding + key. """ if self.action_name: return {ACTION_NAME: self.action_name} @@ -1286,6 +1394,7 @@ def as_sub_state(self) -> Dict[Text, Text]: return {ACTION_TEXT: self.action_text} def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker.set_latest_action(self.as_sub_state()) tracker.clear_followup_action() @@ -1294,7 +1403,8 @@ class AgentUttered(Event): """The agent has said something to the user. This class is not used in the story training as it is contained in the - ``ActionExecuted`` class. An entry is made in the ``Tracker``.""" + ``ActionExecuted`` class. An entry is made in the ``Tracker``. + """ type_name = "agent" @@ -1305,14 +1415,17 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """See docstring of `BotUttered`.""" self.text = text self.data = data super().__init__(timestamp, metadata) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash((self.text, jsonpickle.encode(self.data))) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, AgentUttered): return NotImplemented @@ -1322,26 +1435,21 @@ def __eq__(self, other) -> bool: ) def __str__(self) -> Text: + """Returns text representation of event.""" return "AgentUttered(text: {}, data: {})".format( self.text, json.dumps(self.data) ) - def apply_to(self, tracker: "DialogueStateTracker") -> None: - - pass - def as_story_string(self) -> None: + """Skips representing the event in stories.""" return None def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({"text": self.text, "data": self.data}) return d - @staticmethod - def empty() -> "AgentUttered": - return AgentUttered() - @classmethod def _from_parameters(cls, parameters) -> "AgentUttered": try: @@ -1366,22 +1474,33 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Creates event for active loop. + + Args: + name: Name of activated loop or `None` if current loop is deactivated. + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.name = name super().__init__(timestamp, metadata) def __str__(self) -> Text: + """Returns text representation of event.""" return f"Loop({self.name})" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(self.name) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, ActiveLoop): return NotImplemented return self.name == other.name def as_story_string(self) -> Text: + """Returns text representation of event.""" props = json.dumps({LOOP_NAME: self.name}) return f"{ActiveLoop.type_name}{props}" @@ -1397,11 +1516,13 @@ def _from_story_string(cls, parameters: Dict[Text, Any]) -> List["ActiveLoop"]: ] def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({LOOP_NAME: self.name}) return d def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker.change_loop_to(self.name) @@ -1415,6 +1536,7 @@ class LegacyForm(ActiveLoop): type_name = "form" def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() # Dump old `Form` events as `ActiveLoop` events instead of keeping the old # event type. @@ -1423,8 +1545,10 @@ def as_dict(self) -> Dict[Text, Any]: class LoopInterrupted(Event): - """Event added by FormPolicy and RulePolicy to notify form action - whether or not to validate the user input.""" + """Event added by FormPolicy and RulePolicy. + + Notifies form action whether or not to validate the user input. + """ type_name = "loop_interrupted" @@ -1438,18 +1562,22 @@ def __init__( self.is_interrupted = is_interrupted def __str__(self) -> Text: + """Returns text representation of event.""" return f"{LoopInterrupted.__name__}({self.is_interrupted})" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(self.is_interrupted) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, LoopInterrupted): return NotImplemented return self.is_interrupted == other.is_interrupted def as_story_string(self) -> None: + """Skips representing event in stories.""" return None @classmethod @@ -1461,11 +1589,13 @@ def _from_parameters(cls, parameters) -> "LoopInterrupted": ) def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update({LOOP_INTERRUPTED: self.is_interrupted}) return d def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker.interrupt_loop(self.is_interrupted) @@ -1485,6 +1615,7 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """See parent class docstring.""" # `validate = True` is the same as `interrupted = False` super().__init__(not validate, timestamp, metadata) @@ -1498,6 +1629,7 @@ def _from_parameters(cls, parameters: Dict) -> "LoopInterrupted": ) def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() # Dump old `Form` events as `ActiveLoop` events instead of keeping the old # event type. @@ -1518,12 +1650,22 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Creates event. + + Args: + action_name: Action which was rejected. + policy: Policy which predicted the rejected action. + confidence: Confidence with which the reject action was predicted. + timestamp: When the event was created. + metadata: Additional event metadata. + """ self.action_name = action_name self.policy = policy self.confidence = confidence super().__init__(timestamp, metadata) def __str__(self) -> Text: + """Returns text representation of event.""" return ( "ActionExecutionRejected(" "action: {}, policy: {}, confidence: {})" @@ -1531,9 +1673,11 @@ def __str__(self) -> Text: ) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(self.action_name) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, ActionExecutionRejected): return NotImplemented @@ -1550,9 +1694,11 @@ def _from_parameters(cls, parameters) -> "ActionExecutionRejected": ) def as_story_string(self) -> None: + """Skips representing this event in stories.""" return None def as_dict(self) -> Dict[Text, Any]: + """Returns serialized event.""" d = super().as_dict() d.update( { @@ -1573,16 +1719,16 @@ class SessionStarted(Event): type_name = "session_started" def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(32143124320) - def __str__(self) -> Text: - return "SessionStarted()" - def as_story_string(self) -> None: + """Skips representing event in stories.""" logger.warning( f"'{self.type_name}' events cannot be serialised as story strings." ) def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" # noinspection PyProtectedMember tracker._reset() From 59c7cc01ae92c2aa7e68cdefe2b97179f4586f17 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Mon, 7 Dec 2020 11:13:02 +0100 Subject: [PATCH 31/34] add more docstrings --- rasa/shared/core/events.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index f30e01a290c4..8c550386edfc 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -433,6 +433,7 @@ def __eq__(self, other: Any) -> bool: ) def __str__(self) -> Text: + """Returns text representation of event.""" return ( f"UserUttered(text: {self.text}, intent: {self.intent}, " f"entities: {self.entities})" @@ -653,29 +654,34 @@ def __members(self): ) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash(self.__members()) def __eq__(self, other) -> bool: + """Compares object with other object.""" if not isinstance(other, BotUttered): return NotImplemented return self.__members() == other.__members() def __str__(self) -> Text: + """Returns text representation of event.""" return "BotUttered(text: {}, data: {}, metadata: {})".format( self.text, json.dumps(self.data), json.dumps(self.metadata) ) def __repr__(self) -> Text: + """Returns text representation of event for debugging.""" return "BotUttered('{}', {}, {}, {})".format( self.text, json.dumps(self.data), json.dumps(self.metadata), self.timestamp ) def apply_to(self, tracker: "DialogueStateTracker") -> None: - + """Applies event to current conversation state.""" tracker.latest_bot_utterance = self def as_story_string(self) -> None: + """Skips representing the event in stories.""" return None def message(self) -> Dict[Text, Any]: @@ -697,6 +703,7 @@ def message(self) -> Dict[Text, Any]: @staticmethod def empty() -> "BotUttered": + """Creates an empty bot utterance.""" return BotUttered() def as_dict(self) -> Dict[Text, Any]: @@ -766,6 +773,7 @@ def __eq__(self, other) -> bool: return (self.key, self.value) == (other.key, other.value) def as_story_string(self) -> Text: + """Returns text representation of event.""" props = json.dumps({self.key: self.value}, ensure_ascii=False) return f"{self.type_name}{props}" @@ -929,13 +937,15 @@ def __hash__(self) -> int: ) ) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: + """Compares object with other object.""" if not isinstance(other, ReminderScheduled): return NotImplemented return self.name == other.name def __str__(self) -> Text: + """Returns text representation of event.""" return ( f"ReminderScheduled(intent: {self.intent}, trigger_date: {self.trigger_date_time}, " f"entities: {self.entities}, name: {self.name})" @@ -1018,15 +1028,18 @@ def __init__( super().__init__(timestamp, metadata) def __hash__(self) -> int: + """Returns unique hash for event.""" return hash((self.name, self.intent, str(self.entities))) def __eq__(self, other: Any) -> bool: + """Compares object with other object.""" if not isinstance(other, ReminderCancelled): return NotImplemented return hash(self) == hash(other) def __str__(self) -> Text: + """Returns text representation of event.""" return f"ReminderCancelled(name: {self.name}, intent: {self.intent}, entities: {self.entities})" def cancels_job_with_name(self, job_name: Text, sender_id: Text) -> bool: @@ -1558,6 +1571,17 @@ def __init__( timestamp: Optional[float] = None, metadata: Optional[Dict[Text, Any]] = None, ) -> None: + """Event to notify that loop was interrupted. + + This e.g. happens when a user is within a form, and is de-railing the + form-filling by asking FAQs. + + Args: + is_interrupted: `True` if the loop execution was interrupted, and ML + policies had to take over the last prediction. + timestamp: When the event was created. + metadata: Additional event metadata. + """ super().__init__(timestamp, metadata) self.is_interrupted = is_interrupted @@ -1638,7 +1662,7 @@ def as_dict(self) -> Dict[Text, Any]: class ActionExecutionRejected(Event): - """Notify Core that the execution of the action has been rejected""" + """Notify Core that the execution of the action has been rejected.""" type_name = "action_execution_rejected" @@ -1710,6 +1734,7 @@ def as_dict(self) -> Dict[Text, Any]: return d def apply_to(self, tracker: "DialogueStateTracker") -> None: + """Applies event to current conversation state.""" tracker.reject_action(self.action_name) From 97e1860881e424b9629299e59d6b7ecded9b024a Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Mon, 7 Dec 2020 13:21:47 +0100 Subject: [PATCH 32/34] increase timeout due to failing windows tests --- tests/core/featurizers/test_single_state_featurizers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/core/featurizers/test_single_state_featurizers.py b/tests/core/featurizers/test_single_state_featurizers.py index f00c493c2fb2..f76a84bde98a 100644 --- a/tests/core/featurizers/test_single_state_featurizers.py +++ b/tests/core/featurizers/test_single_state_featurizers.py @@ -181,6 +181,7 @@ def test_single_state_featurizer_creates_encoded_all_actions(): ) +@pytest.mark.timeout(300) # these can take a longer time than the default timeout def test_single_state_featurizer_with_entity_roles_and_groups( unpacked_trained_moodbot_path: Text, ): @@ -241,6 +242,7 @@ def test_single_state_featurizer_uses_dtype_float(): assert encoded[ACTION_NAME][0].features.dtype == np.float32 +@pytest.mark.timeout(300) # these can take a longer time than the default timeout def test_single_state_featurizer_with_interpreter_state_with_action_listen( unpacked_trained_moodbot_path: Text, ): @@ -304,6 +306,7 @@ def test_single_state_featurizer_with_interpreter_state_with_action_listen( ).nnz == 0 +@pytest.mark.timeout(300) # these can take a longer time than the default timeout def test_single_state_featurizer_with_interpreter_state_not_with_action_listen( unpacked_trained_moodbot_path: Text, ): @@ -340,6 +343,7 @@ def test_single_state_featurizer_with_interpreter_state_not_with_action_listen( ).nnz == 0 +@pytest.mark.timeout(300) # these can take a longer time than the default timeout def test_single_state_featurizer_with_interpreter_state_with_no_action_name( unpacked_trained_moodbot_path: Text, ): @@ -402,6 +406,7 @@ def test_to_sparse_sentence_features(): assert sentence_features[0].features.shape == (1, 10) +@pytest.mark.timeout(300) # these can take a longer time than the default timeout def test_single_state_featurizer_uses_regex_interpreter( unpacked_trained_moodbot_path: Text, ): From 8ba989c3d13ca91925a868a8eeae12c7f4e3888a Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Mon, 7 Dec 2020 15:36:59 +0100 Subject: [PATCH 33/34] improve string representation of `UserUttered` --- rasa/shared/core/events.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 8c550386edfc..f07a01baeb5d 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -434,9 +434,19 @@ def __eq__(self, other: Any) -> bool: def __str__(self) -> Text: """Returns text representation of event.""" + entities = "" + if self.entities: + entities = [ + f"{entity[ENTITY_ATTRIBUTE_VALUE]} " + f"(Type: {entity[ENTITY_ATTRIBUTE_TYPE]}, " + f"Role: {entity.get(ENTITY_ATTRIBUTE_ROLE)}, " + f"Group: {entity.get(ENTITY_ATTRIBUTE_GROUP)}" + for entity in self.entities + ] + entities = f", entities: {', '.join(entities)}" + return ( - f"UserUttered(text: {self.text}, intent: {self.intent}, " - f"entities: {self.entities})" + f"UserUttered(text: {self.text}, intent: {self.intent_name}" f"{entities}))" ) @staticmethod @@ -513,7 +523,7 @@ def _from_story_string(cls, parameters: Dict[Text, Any]) -> Optional[List[Event] except KeyError as e: raise ValueError(f"Failed to parse bot uttered event. {e}") - def _entity_string(self): + def _entity_string(self) -> Text: if self.entities: return json.dumps( { From a1e2beee9b1d9cd62138520db84f9284c1e3c433 Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Mon, 7 Dec 2020 15:39:57 +0100 Subject: [PATCH 34/34] fix hashing of `UserUttered` --- rasa/shared/core/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index f07a01baeb5d..5b0123bde6ce 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -410,7 +410,7 @@ def _from_parse_data( def __hash__(self) -> int: """Returns unique hash of object.""" - return hash((self.text, self.intent_name, jsonpickle.encode(self.entities))) + return hash(json.dumps(self.as_sub_state())) @property def intent_name(self) -> Optional[Text]: