From 559320198751ddfe2a500d62ccda15e8da6f5695 Mon Sep 17 00:00:00 2001 From: Daksh Date: Mon, 7 Sep 2020 18:20:43 +0200 Subject: [PATCH 01/25] first implementation works --- .../responseselectorbot/data/responses.yml | 4 +-- examples/responseselectorbot/data/rules.yml | 2 +- examples/responseselectorbot/domain.yml | 3 -- rasa/core/actions/action.py | 25 +++++++++++++---- rasa/core/constants.py | 1 - rasa/core/domain.py | 25 +++++++++++++++-- rasa/importers/importer.py | 28 ++++++++++++++++++- rasa/nlu/selectors/response_selector.py | 19 ++++++++++--- rasa/nlu/training_data/training_data.py | 7 +++-- 9 files changed, 92 insertions(+), 22 deletions(-) diff --git a/examples/responseselectorbot/data/responses.yml b/examples/responseselectorbot/data/responses.yml index cef24554ff62..93aa74cc95d9 100644 --- a/examples/responseselectorbot/data/responses.yml +++ b/examples/responseselectorbot/data/responses.yml @@ -1,12 +1,12 @@ version: "2.0" responses: - chitchat/ask_name: + utter_chitchat/ask_name: - image: "https://i.imgur.com/zTvA58i.jpeg" text: hello, my name is retrieval bot. - text: Oh yeah, I am called the retrieval bot. - chitchat/ask_weather: + utter_chitchat/ask_weather: - text: Oh, it does look sunny right now in Berlin. image: "https://i.imgur.com/vwv7aHN.png" - text: I am not sure of the whole week but I can see the sun is out today. diff --git a/examples/responseselectorbot/data/rules.yml b/examples/responseselectorbot/data/rules.yml index d95269f3ee74..4bbb69938c35 100644 --- a/examples/responseselectorbot/data/rules.yml +++ b/examples/responseselectorbot/data/rules.yml @@ -15,4 +15,4 @@ rules: - rule: Response with a chitchat utterance whenever user indulges in some chitchat steps: - intent: chitchat - - action: respond_chitchat + - action: utter_chitchat diff --git a/examples/responseselectorbot/domain.yml b/examples/responseselectorbot/domain.yml index 5b845f9ea0c1..35cfeb27093d 100644 --- a/examples/responseselectorbot/domain.yml +++ b/examples/responseselectorbot/domain.yml @@ -10,9 +10,6 @@ intents: - bot_challenge - chitchat -actions: - - respond_chitchat - responses: utter_greet: - text: "Hey! How are you?" diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 45f38061b31b..deb0fab7e1ac 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -15,7 +15,6 @@ REQUESTED_SLOT, USER_INTENT_OUT_OF_SCOPE, UTTER_PREFIX, - RESPOND_PREFIX, ) from rasa.nlu.constants import ( RESPONSE_SELECTOR_DEFAULT_INTENT, @@ -92,6 +91,11 @@ def default_action_names() -> List[Text]: return [a.name() for a in default_actions()] + [RULE_SNIPPET_ACTION_NAME] +def construct_retrieval_action_names(retrieval_intents) -> List[Text]: + + return [f"{UTTER_PREFIX}{intent}" for intent in retrieval_intents] + + def combine_user_with_default_actions(user_actions: List[Text]) -> List[Text]: # remove all user actions that overwrite default actions # this logic is a bit reversed, you'd think that we should remove @@ -115,11 +119,21 @@ def combine_with_templates( return actions + unique_template_names +def is_retrieval_action(action_name: Text, retrieval_intents: List[Text]) -> bool: + + return ( + True + if retrieval_intents and action_name.split(UTTER_PREFIX)[1] in retrieval_intents + else False + ) + + def action_from_name( name: Text, action_endpoint: Optional[EndpointConfig], user_actions: List[Text], should_use_form_action: bool = False, + retrieval_intents: List[Text] = [], ) -> "Action": """Return an action instance for the name.""" @@ -128,9 +142,10 @@ def action_from_name( if name in defaults and name not in user_actions: return defaults[name] elif name.startswith(UTTER_PREFIX): - return ActionUtterTemplate(name) - elif name.startswith(RESPOND_PREFIX): - return ActionRetrieveResponse(name) + if is_retrieval_action(name, retrieval_intents): + return ActionRetrieveResponse(name) + else: + return ActionUtterTemplate(name) elif should_use_form_action: from rasa.core.actions.forms import FormAction @@ -220,7 +235,7 @@ def __init__(self, name: Text, silent_fail: Optional[bool] = False): self.silent_fail = silent_fail def intent_name_from_action(self) -> Text: - return self.action_name.split(RESPOND_PREFIX)[1] + return self.action_name.split(UTTER_PREFIX)[1] async def run( self, diff --git a/rasa/core/constants.py b/rasa/core/constants.py index 645f85fff63d..f6f76ce7118a 100644 --- a/rasa/core/constants.py +++ b/rasa/core/constants.py @@ -66,7 +66,6 @@ # it is the highest to prioritize form to the rest of the policies FORM_POLICY_PRIORITY = 5 UTTER_PREFIX = "utter_" -RESPOND_PREFIX = "respond_" DIALOGUE = "dialogue" diff --git a/rasa/core/domain.py b/rasa/core/domain.py index 3e1a373e4113..06b0f3a87a04 100644 --- a/rasa/core/domain.py +++ b/rasa/core/domain.py @@ -30,11 +30,13 @@ SLOT_LAST_OBJECT_TYPE, SLOT_LISTED_ITEMS, DEFAULT_INTENTS, + UTTER_PREFIX, ) from rasa.core.events import SlotSet, UserUttered from rasa.shared.core.slots import Slot, UnfeaturizedSlot, CategoricalSlot from rasa.utils.endpoints import EndpointConfig from rasa.utils.validation import InvalidYamlFileError, validate_yaml_schema +from rasa.nlu.training_data import TrainingData logger = logging.getLogger(__name__) @@ -311,6 +313,7 @@ def _transform_intent_properties_for_internal_use( properties.setdefault(USE_ENTITIES_KEY, True) properties.setdefault(IGNORE_ENTITIES_KEY, []) + properties.setdefault("is_retrieval_intent", False) if not properties[USE_ENTITIES_KEY]: # this covers False, None and [] properties[USE_ENTITIES_KEY] = [] @@ -343,6 +346,18 @@ def _transform_intent_properties_for_internal_use( return intent + def _update_retrieval_intent_properties(self, retrieval_intents: List[Text]): + for retrieval_intent in retrieval_intents: + self.intent_properties[retrieval_intent]["is_retrieval_intent"] = True + + @lazy_property + def retrieval_intents(self) -> List[Text]: + return [ + intent + for intent in self.intent_properties + if self.intent_properties[intent]["is_retrieval_intent"] + ] + @classmethod def collect_intent_properties( cls, intents: List[Union[Text, Dict[Text, Any]]], entities: List[Text] @@ -385,7 +400,13 @@ def _intent_properties( ) -> Tuple[Text, Dict[Text, Any]]: if not isinstance(intent, dict): intent_name = intent - intent = {intent_name: {USE_ENTITIES_KEY: True, IGNORE_ENTITIES_KEY: []}} + intent = { + intent_name: { + USE_ENTITIES_KEY: True, + IGNORE_ENTITIES_KEY: [], + "is_retrieval_intent": False, + } + } else: intent_name = list(intent.keys())[0] @@ -537,6 +558,7 @@ def action_for_name( action_endpoint, self.user_actions_and_forms, should_use_form_action, + self.retrieval_intents, ) def action_for_index( @@ -635,7 +657,6 @@ def input_state_map(self) -> Dict[Text, int]: @lazy_property def input_states(self) -> List[Text]: """Returns all available states.""" - return ( self.intent_states + self.entity_states diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index abf29d3016c9..724d747159c0 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -10,6 +10,7 @@ from rasa.importers.autoconfig import TrainingType import rasa.utils.io as io_utils import rasa.utils.common as common_utils +from rasa.core.actions import action logger = logging.getLogger(__name__) @@ -251,10 +252,35 @@ async def get_domain(self) -> Domain: domains = [importer.get_domain() for importer in self._importers] domains = await asyncio.gather(*domains) - return reduce( + # Check if NLU data has any retrieval intents, + # if yes add corresponding retrieval actions with `utter_` prefix automatically to an empty domain. + nlu_data = await self.get_nlu_data() + if nlu_data.retrieval_intents: + domains.append( + self._get_domain_with_retrieval_actions(nlu_data.retrieval_intents) + ) + + combined_domain = reduce( lambda merged, other: merged.merge(other), domains, Domain.empty() ) + # Make the domain known which intents are retrieval intents + combined_domain._update_retrieval_intent_properties(nlu_data.retrieval_intents) + + return combined_domain + + @staticmethod + def _get_domain_with_retrieval_actions(retrieval_intents): + + return Domain( + [], + [], + [], + {}, + action.construct_retrieval_action_names(retrieval_intents), + [], + ) + async def get_stories( self, template_variables: Optional[Dict] = None, diff --git a/rasa/nlu/selectors/response_selector.py b/rasa/nlu/selectors/response_selector.py index 0dbdd8fb0c6e..f700ededa41b 100644 --- a/rasa/nlu/selectors/response_selector.py +++ b/rasa/nlu/selectors/response_selector.py @@ -333,6 +333,14 @@ def preprocess_train_data(self, training_data: TrainingData) -> RasaModelData: return model_data + @staticmethod + def _intent_response_key_to_template_key(intent_response_key: Text) -> Text: + return f"utter_{intent_response_key}" + + @staticmethod + def _template_key_to_intent_response_key(template_key: Text) -> Text: + return template_key.split("utter_")[1] + def _resolve_intent_response_key( self, label: Dict[Text, Optional[Text]] ) -> Optional[Text]: @@ -350,13 +358,14 @@ def _resolve_intent_response_key( for key, responses in self.responses.items(): # First check if the predicted label was the key itself - if hash(key) == label.get("id"): - return key + search_key = self._template_key_to_intent_response_key(key) + if hash(search_key) == label.get("id"): + return search_key # Otherwise loop over the responses to check if the text has a direct match for response in responses: if hash(response.get(TEXT, "")) == label.get("id"): - return key + return search_key return None def process(self, message: Message, **kwargs: Any) -> None: @@ -370,7 +379,9 @@ def process(self, message: Message, **kwargs: Any) -> None: label_intent_response_key = ( self._resolve_intent_response_key(top_label) or top_label[INTENT_NAME_KEY] ) - label_response_templates = self.responses.get(label_intent_response_key) + label_response_templates = self.responses.get( + self._intent_response_key_to_template_key(label_intent_response_key) + ) if label_intent_response_key and not label_response_templates: # response templates seem to be unavailable, diff --git a/rasa/nlu/training_data/training_data.py b/rasa/nlu/training_data/training_data.py index 78a2a4c34c5a..67f92e96dac1 100644 --- a/rasa/nlu/training_data/training_data.py +++ b/rasa/nlu/training_data/training_data.py @@ -22,6 +22,7 @@ ENTITIES, TEXT, ) +from rasa.core.constants import UTTER_PREFIX from rasa.nlu.training_data.message import Message from rasa.nlu.training_data.util import check_duplicate_synonym from rasa.nlu.utils import list_to_str @@ -240,8 +241,8 @@ def _fill_response_phrases(self) -> None: continue # look for corresponding bot utterance - story_lookup_intent = example.get_full_intent() - assistant_utterances = self.responses.get(story_lookup_intent, []) + story_lookup_key = f"{UTTER_PREFIX}{example.get_full_intent()}" + assistant_utterances = self.responses.get(story_lookup_key, []) if assistant_utterances: # Use the first response text as training label if needed downstream @@ -251,7 +252,7 @@ def _fill_response_phrases(self) -> None: # If no text attribute was found use the key for training if not example.get(RESPONSE): - example.set(RESPONSE, story_lookup_intent) + example.set(RESPONSE, story_lookup_key) def nlu_as_json(self, **kwargs: Any) -> Text: """Represent this set of training examples as json.""" From c4df077d2e5149c7ef3b9ba93f0f6d09de1b6d61 Mon Sep 17 00:00:00 2001 From: Daksh Date: Mon, 7 Sep 2020 18:41:33 +0200 Subject: [PATCH 02/25] more reuse --- rasa/core/domain.py | 9 +++++---- rasa/nlu/selectors/response_selector.py | 13 +++---------- rasa/nlu/training_data/training_data.py | 12 +++++++++++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/rasa/core/domain.py b/rasa/core/domain.py index 06b0f3a87a04..f27b2fd4555c 100644 --- a/rasa/core/domain.py +++ b/rasa/core/domain.py @@ -49,6 +49,7 @@ USED_ENTITIES_KEY = "used_entities" USE_ENTITIES_KEY = "use_entities" IGNORE_ENTITIES_KEY = "ignore_entities" +IS_RETRIEVAL_INTENT_KEY = "is_retrieval_intent" KEY_SLOTS = "slots" KEY_INTENTS = "intents" @@ -313,7 +314,7 @@ def _transform_intent_properties_for_internal_use( properties.setdefault(USE_ENTITIES_KEY, True) properties.setdefault(IGNORE_ENTITIES_KEY, []) - properties.setdefault("is_retrieval_intent", False) + properties.setdefault(IS_RETRIEVAL_INTENT_KEY, False) if not properties[USE_ENTITIES_KEY]: # this covers False, None and [] properties[USE_ENTITIES_KEY] = [] @@ -348,14 +349,14 @@ def _transform_intent_properties_for_internal_use( def _update_retrieval_intent_properties(self, retrieval_intents: List[Text]): for retrieval_intent in retrieval_intents: - self.intent_properties[retrieval_intent]["is_retrieval_intent"] = True + self.intent_properties[retrieval_intent][IS_RETRIEVAL_INTENT_KEY] = True @lazy_property def retrieval_intents(self) -> List[Text]: return [ intent for intent in self.intent_properties - if self.intent_properties[intent]["is_retrieval_intent"] + if self.intent_properties[intent][IS_RETRIEVAL_INTENT_KEY] ] @classmethod @@ -404,7 +405,7 @@ def _intent_properties( intent_name: { USE_ENTITIES_KEY: True, IGNORE_ENTITIES_KEY: [], - "is_retrieval_intent": False, + IS_RETRIEVAL_INTENT_KEY: False, } } else: diff --git a/rasa/nlu/selectors/response_selector.py b/rasa/nlu/selectors/response_selector.py index f700ededa41b..ed49280033d4 100644 --- a/rasa/nlu/selectors/response_selector.py +++ b/rasa/nlu/selectors/response_selector.py @@ -87,6 +87,7 @@ TEXT, INTENT_NAME_KEY, ) +from rasa.nlu.training_data import training_data from rasa.utils.tensorflow.model_data import RasaModelData from rasa.utils.tensorflow.models import RasaModel @@ -333,14 +334,6 @@ def preprocess_train_data(self, training_data: TrainingData) -> RasaModelData: return model_data - @staticmethod - def _intent_response_key_to_template_key(intent_response_key: Text) -> Text: - return f"utter_{intent_response_key}" - - @staticmethod - def _template_key_to_intent_response_key(template_key: Text) -> Text: - return template_key.split("utter_")[1] - def _resolve_intent_response_key( self, label: Dict[Text, Optional[Text]] ) -> Optional[Text]: @@ -358,7 +351,7 @@ def _resolve_intent_response_key( for key, responses in self.responses.items(): # First check if the predicted label was the key itself - search_key = self._template_key_to_intent_response_key(key) + search_key = training_data.template_key_to_intent_response_key(key) if hash(search_key) == label.get("id"): return search_key @@ -380,7 +373,7 @@ def process(self, message: Message, **kwargs: Any) -> None: self._resolve_intent_response_key(top_label) or top_label[INTENT_NAME_KEY] ) label_response_templates = self.responses.get( - self._intent_response_key_to_template_key(label_intent_response_key) + training_data.intent_response_key_to_template_key(label_intent_response_key) ) if label_intent_response_key and not label_response_templates: diff --git a/rasa/nlu/training_data/training_data.py b/rasa/nlu/training_data/training_data.py index 67f92e96dac1..6ce8439f3e44 100644 --- a/rasa/nlu/training_data/training_data.py +++ b/rasa/nlu/training_data/training_data.py @@ -32,6 +32,14 @@ logger = logging.getLogger(__name__) +def intent_response_key_to_template_key(intent_response_key: Text) -> Text: + return f"{UTTER_PREFIX}{intent_response_key}" + + +def template_key_to_intent_response_key(template_key: Text) -> Text: + return template_key.split(UTTER_PREFIX)[1] + + class TrainingData: """Holds loaded intent and entity training data.""" @@ -241,7 +249,9 @@ def _fill_response_phrases(self) -> None: continue # look for corresponding bot utterance - story_lookup_key = f"{UTTER_PREFIX}{example.get_full_intent()}" + story_lookup_key = intent_response_key_to_template_key( + example.get_full_intent() + ) assistant_utterances = self.responses.get(story_lookup_key, []) if assistant_utterances: From f9d499658d291be74bfc00e8f11061f055370964 Mon Sep 17 00:00:00 2001 From: Daksh Varshneya Date: Tue, 8 Sep 2020 08:42:42 +0200 Subject: [PATCH 03/25] Update rasa/core/actions/action.py --- rasa/core/actions/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index deb0fab7e1ac..15453d692081 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -91,7 +91,7 @@ def default_action_names() -> List[Text]: return [a.name() for a in default_actions()] + [RULE_SNIPPET_ACTION_NAME] -def construct_retrieval_action_names(retrieval_intents) -> List[Text]: +def construct_retrieval_action_names(retrieval_intents: List[Text]) -> List[Text]: return [f"{UTTER_PREFIX}{intent}" for intent in retrieval_intents] From 172245ed92fbec8caea397ca4e94e759d49edb09 Mon Sep 17 00:00:00 2001 From: Daksh Date: Tue, 8 Sep 2020 08:45:31 +0200 Subject: [PATCH 04/25] change if condition style --- rasa/core/actions/action.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index deb0fab7e1ac..3468c51fd95f 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -142,10 +142,11 @@ def action_from_name( if name in defaults and name not in user_actions: return defaults[name] elif name.startswith(UTTER_PREFIX): - if is_retrieval_action(name, retrieval_intents): - return ActionRetrieveResponse(name) - else: - return ActionUtterTemplate(name) + return ( + ActionRetrieveResponse(name) + if is_retrieval_action(name, retrieval_intents) + else ActionUtterTemplate(name) + ) elif should_use_form_action: from rasa.core.actions.forms import FormAction From 1af6cbe6a0e2d31d7b346a8fe8a92a365b2b18fc Mon Sep 17 00:00:00 2001 From: Daksh Date: Tue, 8 Sep 2020 13:24:46 +0200 Subject: [PATCH 05/25] refactor call to update retrieval intent props --- rasa/core/actions/action.py | 12 +++++------- rasa/core/domain.py | 4 ---- rasa/importers/importer.py | 36 ++++++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index eb8d3c0d21c9..864da40e15a7 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -2,7 +2,7 @@ import json import logging import typing -from typing import List, Text, Optional, Dict, Any +from typing import List, Text, Optional, Dict, Any, Set import random import aiohttp @@ -91,7 +91,7 @@ def default_action_names() -> List[Text]: return [a.name() for a in default_actions()] + [RULE_SNIPPET_ACTION_NAME] -def construct_retrieval_action_names(retrieval_intents: List[Text]) -> List[Text]: +def construct_retrieval_action_names(retrieval_intents: Set[Text]) -> List[Text]: return [f"{UTTER_PREFIX}{intent}" for intent in retrieval_intents] @@ -141,12 +141,10 @@ def action_from_name( if name in defaults and name not in user_actions: return defaults[name] + elif name.startswith(UTTER_PREFIX) and is_retrieval_action(name, retrieval_intents): + return ActionRetrieveResponse(name) elif name.startswith(UTTER_PREFIX): - return ( - ActionRetrieveResponse(name) - if is_retrieval_action(name, retrieval_intents) - else ActionUtterTemplate(name) - ) + return ActionUtterTemplate(name) elif should_use_form_action: from rasa.core.actions.forms import FormAction diff --git a/rasa/core/domain.py b/rasa/core/domain.py index f27b2fd4555c..a1d6b208b126 100644 --- a/rasa/core/domain.py +++ b/rasa/core/domain.py @@ -347,10 +347,6 @@ def _transform_intent_properties_for_internal_use( return intent - def _update_retrieval_intent_properties(self, retrieval_intents: List[Text]): - for retrieval_intent in retrieval_intents: - self.intent_properties[retrieval_intent][IS_RETRIEVAL_INTENT_KEY] = True - @lazy_property def retrieval_intents(self) -> List[Text]: return [ diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index 724d747159c0..db6dfca9bf95 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -1,6 +1,6 @@ import asyncio from functools import reduce -from typing import Text, Optional, List, Dict +from typing import Text, Optional, List, Dict, Set import logging import rasa.shared.utils.common @@ -11,6 +11,7 @@ import rasa.utils.io as io_utils import rasa.utils.common as common_utils from rasa.core.actions import action +from rasa.core.domain import IS_RETRIEVAL_INTENT_KEY logger = logging.getLogger(__name__) @@ -252,28 +253,39 @@ async def get_domain(self) -> Domain: domains = [importer.get_domain() for importer in self._importers] domains = await asyncio.gather(*domains) + combined_domain = reduce( + lambda merged, other: merged.merge(other), domains, Domain.empty() + ) + # Check if NLU data has any retrieval intents, - # if yes add corresponding retrieval actions with `utter_` prefix automatically to an empty domain. + # if yes add corresponding retrieval actions with `utter_` prefix + # automatically to an empty domain and update the properties of existing retrieval intents. nlu_data = await self.get_nlu_data() if nlu_data.retrieval_intents: - domains.append( - self._get_domain_with_retrieval_actions(nlu_data.retrieval_intents) - ) - combined_domain = reduce( - lambda merged, other: merged.merge(other), domains, Domain.empty() - ) + domain_with_retrieval_intents = self._get_domain_with_retrieval_intents( + nlu_data.retrieval_intents, combined_domain + ) - # Make the domain known which intents are retrieval intents - combined_domain._update_retrieval_intent_properties(nlu_data.retrieval_intents) + combined_domain = combined_domain.merge( + domain_with_retrieval_intents, override=True + ) return combined_domain @staticmethod - def _get_domain_with_retrieval_actions(retrieval_intents): + def _get_domain_with_retrieval_intents( + retrieval_intents: Set[Text], existing_domain: Domain + ) -> Domain: + + modified_properties = [] + for intent in retrieval_intents: + intent_property = existing_domain.intent_properties[intent] + intent_property[IS_RETRIEVAL_INTENT_KEY] = True + modified_properties.append({intent: intent_property}) return Domain( - [], + modified_properties, [], [], {}, From 34ac79734a9c6d5782d55c0250266bb2a8026524 Mon Sep 17 00:00:00 2001 From: Daksh Date: Tue, 8 Sep 2020 16:57:40 +0200 Subject: [PATCH 06/25] fix existing tests. TODO: add more tests --- data/examples/rasa/demo-rasa-responses.md | 4 +- .../default_retrieval_intents.yml | 4 +- data/test_responses/default.md | 4 +- rasa/core/actions/action.py | 38 +++++++++++++------ rasa/core/domain.py | 11 +----- rasa/importers/importer.py | 6 ++- rasa/nlu/constants.py | 1 + rasa/nlu/selectors/response_selector.py | 9 ++++- rasa/nlu/training_data/training_data.py | 16 +++----- rasa/nlu/training_data/util.py | 9 +++++ tests/core/test_actions.py | 29 ++++++++------ tests/core/test_domain.py | 21 +++++++++- tests/nlu/selectors/test_selectors.py | 26 ++++++++++--- .../training_data/formats/test_rasa_yaml.py | 16 ++++---- tests/nlu/training_data/test_training_data.py | 32 ++++++++++++++-- 15 files changed, 155 insertions(+), 71 deletions(-) diff --git a/data/examples/rasa/demo-rasa-responses.md b/data/examples/rasa/demo-rasa-responses.md index db03efbff623..8438e2db37c1 100644 --- a/data/examples/rasa/demo-rasa-responses.md +++ b/data/examples/rasa/demo-rasa-responses.md @@ -1,7 +1,7 @@ ## -* chitchat/ask_weather +* utter_chitchat/ask_weather - It's sunny where I live ## -* chitchat/ask_name +* utter_chitchat/ask_name - I am Mr. Bot \ No newline at end of file diff --git a/data/test_domains/default_retrieval_intents.yml b/data/test_domains/default_retrieval_intents.yml index 4d41d889716c..4a23002adecc 100644 --- a/data/test_domains/default_retrieval_intents.yml +++ b/data/test_domains/default_retrieval_intents.yml @@ -7,8 +7,6 @@ intents: - mood_unhappy - bot_challenge - chitchat - - chitchat/ask_name - - chitchat/ask_weather responses: utter_greet: @@ -26,7 +24,7 @@ responses: - text: I am a bot, powered by Rasa. actions: - - respond_chitchat + - utter_chitchat - utter_greet - utter_cheer_up - utter_did_that_help diff --git a/data/test_responses/default.md b/data/test_responses/default.md index aeae1f9c9ac1..bbfcb8579aa2 100644 --- a/data/test_responses/default.md +++ b/data/test_responses/default.md @@ -1,7 +1,7 @@ ## ask name -* chitchat/ask_name +* utter_chitchat/ask_name - my name is Sara, Rasa's documentation bot! ## ask weather -* chitchat/ask_weather +* utter_chitchat/ask_weather - it's always sunny where I live diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 864da40e15a7..8cc40b666ff4 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -21,6 +21,7 @@ RESPONSE_SELECTOR_PROPERTY_NAME, RESPONSE_SELECTOR_RESPONSES_KEY, RESPONSE_SELECTOR_PREDICTION_KEY, + RESPONSE_SELECTOR_TEMPLATE_KEY, INTENT_RANKING_KEY, INTENT_NAME_KEY, INTENT_RESPONSE_KEY, @@ -93,7 +94,10 @@ def default_action_names() -> List[Text]: def construct_retrieval_action_names(retrieval_intents: Set[Text]) -> List[Text]: - return [f"{UTTER_PREFIX}{intent}" for intent in retrieval_intents] + return [ + ActionRetrieveResponse.action_name_from_intent(intent) + for intent in retrieval_intents + ] def combine_user_with_default_actions(user_actions: List[Text]) -> List[Text]: @@ -122,9 +126,7 @@ def combine_with_templates( def is_retrieval_action(action_name: Text, retrieval_intents: List[Text]) -> bool: return ( - True - if retrieval_intents and action_name.split(UTTER_PREFIX)[1] in retrieval_intents - else False + ActionRetrieveResponse.intent_name_from_action(action_name) in retrieval_intents ) @@ -157,11 +159,15 @@ def actions_from_names( action_names: List[Text], action_endpoint: Optional[EndpointConfig], user_actions: List[Text], + retrieval_intents: List[Text] = [], ) -> List["Action"]: """Converts the names of actions into class instances.""" return [ - action_from_name(name, action_endpoint, user_actions) for name in action_names + action_from_name( + name, action_endpoint, user_actions, retrieval_intents=retrieval_intents + ) + for name in action_names ] @@ -233,8 +239,13 @@ def __init__(self, name: Text, silent_fail: Optional[bool] = False): self.action_name = name self.silent_fail = silent_fail - def intent_name_from_action(self) -> Text: - return self.action_name.split(UTTER_PREFIX)[1] + @staticmethod + def intent_name_from_action(action_name) -> Text: + return action_name.split(UTTER_PREFIX)[1] + + @staticmethod + def action_name_from_intent(intent_name) -> Text: + return f"{UTTER_PREFIX}{intent_name}" async def run( self, @@ -249,8 +260,11 @@ async def run( RESPONSE_SELECTOR_PROPERTY_NAME ] - if self.intent_name_from_action() in response_selector_properties: - query_key = self.intent_name_from_action() + if ( + self.intent_name_from_action(self.action_name) + in response_selector_properties + ): + query_key = self.intent_name_from_action(self.action_name) elif RESPONSE_SELECTOR_DEFAULT_INTENT in response_selector_properties: query_key = RESPONSE_SELECTOR_DEFAULT_INTENT else: @@ -273,9 +287,9 @@ async def run( picked_message_idx = random.randint(0, len(possible_messages) - 1) picked_message = copy.deepcopy(possible_messages[picked_message_idx]) - picked_message["template_name"] = selected[RESPONSE_SELECTOR_PREDICTION_KEY][ - INTENT_RESPONSE_KEY - ] + picked_message[RESPONSE_SELECTOR_TEMPLATE_KEY] = selected[ + RESPONSE_SELECTOR_PREDICTION_KEY + ][RESPONSE_SELECTOR_TEMPLATE_KEY] return [create_bot_utterance(picked_message)] diff --git a/rasa/core/domain.py b/rasa/core/domain.py index aad7d80860ba..1e1a36f2debb 100644 --- a/rasa/core/domain.py +++ b/rasa/core/domain.py @@ -326,7 +326,6 @@ def _transform_intent_properties_for_internal_use( properties.setdefault(USE_ENTITIES_KEY, True) properties.setdefault(IGNORE_ENTITIES_KEY, []) - properties.setdefault(IS_RETRIEVAL_INTENT_KEY, False) if not properties[USE_ENTITIES_KEY]: # this covers False, None and [] properties[USE_ENTITIES_KEY] = [] @@ -364,7 +363,7 @@ def retrieval_intents(self) -> List[Text]: return [ intent for intent in self.intent_properties - if self.intent_properties[intent][IS_RETRIEVAL_INTENT_KEY] + if self.intent_properties[intent].get(IS_RETRIEVAL_INTENT_KEY) ] @classmethod @@ -409,13 +408,7 @@ def _intent_properties( ) -> Tuple[Text, Dict[Text, Any]]: if not isinstance(intent, dict): intent_name = intent - intent = { - intent_name: { - USE_ENTITIES_KEY: True, - IGNORE_ENTITIES_KEY: [], - IS_RETRIEVAL_INTENT_KEY: False, - } - } + intent = {intent_name: {USE_ENTITIES_KEY: True, IGNORE_ENTITIES_KEY: []}} else: intent_name = list(intent.keys())[0] diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index 9f38ce28754e..dc5139c5ec50 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -288,7 +288,11 @@ def _get_domain_with_retrieval_intents( modified_properties = [] for intent in retrieval_intents: - intent_property = existing_domain.intent_properties[intent] + intent_property = ( + existing_domain.intent_properties[intent] + if intent in existing_domain.intent_properties + else {} + ) intent_property[IS_RETRIEVAL_INTENT_KEY] = True modified_properties.append({intent: intent_property}) diff --git a/rasa/nlu/constants.py b/rasa/nlu/constants.py index c35a3c2cd87d..8f8058744802 100644 --- a/rasa/nlu/constants.py +++ b/rasa/nlu/constants.py @@ -82,6 +82,7 @@ RESPONSE_SELECTOR_PREDICTION_KEY = "response" RESPONSE_SELECTOR_RANKING_KEY = "ranking" RESPONSE_SELECTOR_RESPONSES_KEY = "response_templates" +RESPONSE_SELECTOR_TEMPLATE_KEY = "template_name" RESPONSE_IDENTIFIER_DELIMITER = "/" INTENT_RANKING_KEY = "intent_ranking" diff --git a/rasa/nlu/selectors/response_selector.py b/rasa/nlu/selectors/response_selector.py index 6bf17082d088..413d9374f9dc 100644 --- a/rasa/nlu/selectors/response_selector.py +++ b/rasa/nlu/selectors/response_selector.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Optional, Text, Tuple, Union, List, Type +from rasa.nlu.training_data import util import rasa.shared.utils.io from rasa.nlu.config import InvalidConfigError from rasa.nlu.training_data import TrainingData, Message @@ -78,6 +79,7 @@ RESPONSE_SELECTOR_RESPONSES_KEY, RESPONSE_SELECTOR_PREDICTION_KEY, RESPONSE_SELECTOR_RANKING_KEY, + RESPONSE_SELECTOR_TEMPLATE_KEY, PREDICTED_CONFIDENCE_KEY, INTENT_RESPONSE_KEY, INTENT, @@ -353,7 +355,7 @@ def _resolve_intent_response_key( for key, responses in self.responses.items(): # First check if the predicted label was the key itself - search_key = training_data.template_key_to_intent_response_key(key) + search_key = util.template_key_to_intent_response_key(key) if hash(search_key) == label.get("id"): return search_key @@ -375,7 +377,7 @@ def process(self, message: Message, **kwargs: Any) -> None: self._resolve_intent_response_key(top_label) or top_label[INTENT_NAME_KEY] ) label_response_templates = self.responses.get( - training_data.intent_response_key_to_template_key(label_intent_response_key) + util.intent_response_key_to_template_key(label_intent_response_key) ) if label_intent_response_key and not label_response_templates: @@ -414,6 +416,9 @@ def process(self, message: Message, **kwargs: Any) -> None: RESPONSE_SELECTOR_RESPONSES_KEY: label_response_templates, PREDICTED_CONFIDENCE_KEY: top_label[PREDICTED_CONFIDENCE_KEY], INTENT_RESPONSE_KEY: label_intent_response_key, + RESPONSE_SELECTOR_TEMPLATE_KEY: util.intent_response_key_to_template_key( + label_intent_response_key + ), }, RESPONSE_SELECTOR_RANKING_KEY: label_ranking, } diff --git a/rasa/nlu/training_data/training_data.py b/rasa/nlu/training_data/training_data.py index eaf1f7c095ee..09206359e9d1 100644 --- a/rasa/nlu/training_data/training_data.py +++ b/rasa/nlu/training_data/training_data.py @@ -24,9 +24,11 @@ INTENT_NAME, TEXT, ) -from rasa.core.constants import UTTER_PREFIX from rasa.nlu.training_data.message import Message -from rasa.nlu.training_data.util import check_duplicate_synonym +from rasa.nlu.training_data.util import ( + check_duplicate_synonym, + intent_response_key_to_template_key, +) from rasa.nlu.utils import list_to_str DEFAULT_TRAINING_DATA_OUTPUT_PATH = "training_data.json" @@ -34,14 +36,6 @@ logger = logging.getLogger(__name__) -def intent_response_key_to_template_key(intent_response_key: Text) -> Text: - return f"{UTTER_PREFIX}{intent_response_key}" - - -def template_key_to_intent_response_key(template_key: Text) -> Text: - return template_key.split(UTTER_PREFIX)[1] - - class TrainingData: """Holds loaded intent and entity training data.""" @@ -491,7 +485,7 @@ def _needed_responses_for_examples( responses = {} for ex in examples: if ex.get(INTENT_RESPONSE_KEY) and ex.get(RESPONSE): - key = ex.get_full_intent() + key = intent_response_key_to_template_key(ex.get_full_intent()) responses[key] = self.responses[key] return responses diff --git a/rasa/nlu/training_data/util.py b/rasa/nlu/training_data/util.py index 0d7007448688..d489b05b59ea 100644 --- a/rasa/nlu/training_data/util.py +++ b/rasa/nlu/training_data/util.py @@ -4,6 +4,7 @@ from typing import Any, Dict, Optional, Text import rasa.utils.io as io_utils +from rasa.core.constants import UTTER_PREFIX from rasa.nlu.constants import ENTITIES, EXTRACTOR, PRETRAINED_EXTRACTORS import rasa.shared.utils.io @@ -85,3 +86,11 @@ def remove_untrainable_entities_from(example: Dict[Text, Any]) -> None: trainable_entities.append(entity) example[ENTITIES] = trainable_entities + + +def intent_response_key_to_template_key(intent_response_key: Text) -> Text: + return f"{UTTER_PREFIX}{intent_response_key}" + + +def template_key_to_intent_response_key(template_key: Text) -> Text: + return template_key.split(UTTER_PREFIX)[1] diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index edea83394bc3..d2181cc7b3b4 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -82,16 +82,17 @@ def test_text_format(): == "ActionUtterTemplate('my_action_name')" ) assert ( - "{}".format(ActionRetrieveResponse("respond_test")) - == "ActionRetrieveResponse('respond_test')" + "{}".format(ActionRetrieveResponse("utter_test")) + == "ActionRetrieveResponse('utter_test')" ) def test_action_instantiation_from_names(): instantiated_actions = action.actions_from_names( - ["random_name", "utter_test", "respond_test"], + ["random_name", "utter_test", "utter_faq"], None, ["random_name", "utter_test"], + retrieval_intents=["faq"], ) assert len(instantiated_actions) == 3 assert isinstance(instantiated_actions[0], RemoteAction) @@ -101,16 +102,16 @@ def test_action_instantiation_from_names(): assert instantiated_actions[1].name() == "utter_test" assert isinstance(instantiated_actions[2], ActionRetrieveResponse) - assert instantiated_actions[2].name() == "respond_test" + assert instantiated_actions[2].name() == "utter_faq" def test_domain_action_instantiation(): domain = Domain( - intents={}, + intents=[{"chitchat": {"is_retrieval_intent": True}}], entities=[], slots=[], templates={}, - action_names=["my_module.ActionTest", "utter_test", "respond_test"], + action_names=["my_module.ActionTest", "utter_test", "utter_chitchat"], forms=[], ) @@ -130,7 +131,7 @@ def test_domain_action_instantiation(): assert instantiated_actions[10].name() == RULE_SNIPPET_ACTION_NAME assert instantiated_actions[11].name() == "my_module.ActionTest" assert instantiated_actions[12].name() == "utter_test" - assert instantiated_actions[13].name() == "respond_test" + assert instantiated_actions[13].name() == "utter_chitchat" async def test_remote_action_runs( @@ -361,7 +362,7 @@ async def test_action_utter_retrieved_response( ): from rasa.core.channels.channel import UserMessage - action_name = "respond_chitchat" + action_name = "utter_chitchat" default_tracker.latest_message = UserMessage( "Who are you?", parse_data={ @@ -370,6 +371,7 @@ async def test_action_utter_retrieved_response( "response": { "intent_response_key": "chitchat/ask_name", "response_templates": [{"text": "I am a bot."}], + "template_name": "utter_chitchat/ask_name", } } } @@ -383,7 +385,8 @@ async def test_action_utter_retrieved_response( "text" ) assert ( - events[0].as_dict().get("metadata").get("template_name") == "chitchat/ask_name" + events[0].as_dict().get("metadata").get("template_name") + == "utter_chitchat/ask_name" ) @@ -392,7 +395,7 @@ async def test_action_utter_default_retrieved_response( ): from rasa.core.channels.channel import UserMessage - action_name = "respond_chitchat" + action_name = "utter_chitchat" default_tracker.latest_message = UserMessage( "Who are you?", parse_data={ @@ -401,6 +404,7 @@ async def test_action_utter_default_retrieved_response( "response": { "intent_response_key": "chitchat/ask_name", "response_templates": [{"text": "I am a bot."}], + "template_name": "utter_chitchat/ask_name", } } } @@ -415,7 +419,8 @@ async def test_action_utter_default_retrieved_response( ) assert ( - events[0].as_dict().get("metadata").get("template_name") == "chitchat/ask_name" + events[0].as_dict().get("metadata").get("template_name") + == "utter_chitchat/ask_name" ) @@ -424,7 +429,7 @@ async def test_action_utter_retrieved_empty_response( ): from rasa.core.channels.channel import UserMessage - action_name = "respond_chitchat" + action_name = "utter_chitchat" default_tracker.latest_message = UserMessage( "Who are you?", parse_data={ diff --git a/tests/core/test_domain.py b/tests/core/test_domain.py index d54c6e975a37..5bd2c65d476f 100644 --- a/tests/core/test_domain.py +++ b/tests/core/test_domain.py @@ -1,7 +1,7 @@ import copy import json from pathlib import Path -from typing import Dict +from typing import Dict, List, Text, Any, Union, Set import pytest @@ -484,9 +484,26 @@ def test_merge_domain_with_forms(): "goodbye": {USED_ENTITIES_KEY: []}, }, ), + ( + [ + "greet", + "goodbye", + {"chitchat": {"is_retrieval_intent": True, "use_entities": None}}, + ], + ["entity", "other", "third"], + { + "greet": {USED_ENTITIES_KEY: ["entity", "other", "third"]}, + "goodbye": {USED_ENTITIES_KEY: ["entity", "other", "third"]}, + "chitchat": {USED_ENTITIES_KEY: [], "is_retrieval_intent": True}, + }, + ), ], ) -def test_collect_intent_properties(intents, entities, intent_properties): +def test_collect_intent_properties( + intents: Union[Set[Text], List[Union[Text, Dict[Text, Any]]]], + entities: List[Text], + intent_properties: Dict[Text, Dict[Text, Union[bool, List]]], +): Domain._add_default_intents(intent_properties, entities) assert Domain.collect_intent_properties(intents, entities) == intent_properties diff --git a/tests/nlu/selectors/test_selectors.py b/tests/nlu/selectors/test_selectors.py index 28c0bc25d775..decf93926fc4 100644 --- a/tests/nlu/selectors/test_selectors.py +++ b/tests/nlu/selectors/test_selectors.py @@ -1,5 +1,6 @@ import pytest +import rasa.nlu.training_data.util from rasa.nlu.config import RasaNLUModelConfig from rasa.nlu.training_data import load_data from rasa.nlu.train import Trainer, Interpreter @@ -10,6 +11,7 @@ TRANSFORMER_SIZE, ) from rasa.nlu.selectors.response_selector import ResponseSelector +from rasa.nlu.training_data import training_data @pytest.mark.parametrize( @@ -62,6 +64,12 @@ def test_train_selector(pipeline, component_builder, tmpdir): .get("response") .get("intent_response_key") ) is not None + assert ( + parsed.get("response_selector") + .get("default") + .get("response") + .get("template_name") + ) is not None assert ( parsed.get("response_selector") .get("default") @@ -114,20 +122,28 @@ def test_resolve_intent_response_key_from_label( ): # use data that include some responses - training_data = load_data("data/examples/rasa/demo-rasa.md") + preset_training_data = load_data("data/examples/rasa/demo-rasa.md") training_data_responses = load_data("data/examples/rasa/demo-rasa-responses.md") - training_data = training_data.merge(training_data_responses) + preset_training_data = preset_training_data.merge(training_data_responses) response_selector = ResponseSelector( component_config={"use_text_as_label": train_on_text} ) - response_selector.preprocess_train_data(training_data) + response_selector.preprocess_train_data(preset_training_data) label_intent_response_key = response_selector._resolve_intent_response_key( {"id": hash(predicted_label), "name": predicted_label} ) assert resolved_intent_response_key == label_intent_response_key assert ( - response_selector.responses[label_intent_response_key] - == training_data.responses[resolved_intent_response_key] + response_selector.responses[ + rasa.nlu.training_data.util.intent_response_key_to_template_key( + label_intent_response_key + ) + ] + == preset_training_data.responses[ + rasa.nlu.training_data.util.intent_response_key_to_template_key( + resolved_intent_response_key + ) + ] ) diff --git a/tests/nlu/training_data/formats/test_rasa_yaml.py b/tests/nlu/training_data/formats/test_rasa_yaml.py index 7c415518c8ca..8fe1d383e2d1 100644 --- a/tests/nlu/training_data/formats/test_rasa_yaml.py +++ b/tests/nlu/training_data/formats/test_rasa_yaml.py @@ -268,7 +268,7 @@ def test_nlg_reads_text(): responses_yml = textwrap.dedent( """ responses: - chitchat/ask_weather: + utter_chitchat/ask_weather: - text: Where do you want to check the weather? """ ) @@ -277,7 +277,9 @@ def test_nlg_reads_text(): result = reader.reads(responses_yml) assert result.responses == { - "chitchat/ask_weather": [{"text": "Where do you want to check the weather?"}] + "utter_chitchat/ask_weather": [ + {"text": "Where do you want to check the weather?"} + ] } @@ -285,7 +287,7 @@ def test_nlg_reads_any_multimedia(): responses_yml = textwrap.dedent( """ responses: - chitchat/ask_weather: + utter_chitchat/ask_weather: - text: Where do you want to check the weather? image: https://example.com/weather.jpg """ @@ -295,7 +297,7 @@ def test_nlg_reads_any_multimedia(): result = reader.reads(responses_yml) assert result.responses == { - "chitchat/ask_weather": [ + "utter_chitchat/ask_weather": [ { "text": "Where do you want to check the weather?", "image": "https://example.com/weather.jpg", @@ -321,7 +323,7 @@ def test_nlg_fails_on_empty_response(): responses_yml = textwrap.dedent( """ responses: - chitchat/ask_weather: + utter_chitchat/ask_weather: """ ) @@ -335,11 +337,11 @@ def test_nlg_multimedia_load_dump_roundtrip(): responses_yml = textwrap.dedent( """ responses: - chitchat/ask_weather: + utter_chitchat/ask_weather: - text: Where do you want to check the weather? image: https://example.com/weather.jpg - chitchat/ask_name: + utter_chitchat/ask_name: - text: My name is Sara. """ ) diff --git a/tests/nlu/training_data/test_training_data.py b/tests/nlu/training_data/test_training_data.py index f56676264c3a..896657a5323a 100644 --- a/tests/nlu/training_data/test_training_data.py +++ b/tests/nlu/training_data/test_training_data.py @@ -16,7 +16,11 @@ from rasa.nlu.tokenizers.whitespace_tokenizer import WhitespaceTokenizer from rasa.nlu.training_data import TrainingData from rasa.nlu.training_data.loading import guess_format, UNK, RASA_YAML, JSON, MARKDOWN -from rasa.nlu.training_data.util import get_file_format +from rasa.nlu.training_data.util import ( + get_file_format, + template_key_to_intent_response_key, + intent_response_key_to_template_key, +) def test_luis_data(): @@ -109,6 +113,22 @@ def test_composite_entities_data(): assert td.number_of_examples_per_entity["role 'from'"] == 3 +@pytest.mark.parametrize( + "intent_response_key, template_key", + [["chitchat/ask_name", "utter_chitchat/ask_name"]], +) +def test_intent_response_key_to_template_key(intent_response_key, template_key): + assert intent_response_key_to_template_key(intent_response_key) == template_key + + +@pytest.mark.parametrize( + "intent_response_key, template_key", + [["chitchat/ask_name", "utter_chitchat/ask_name"]], +) +def test_template_key_to_intent_response_key(intent_response_key, template_key): + assert template_key_to_intent_response_key(template_key) == intent_response_key + + @pytest.mark.parametrize( "files", [ @@ -128,7 +148,10 @@ def test_demo_data(files): td = training_data_from_paths(files, language="en") assert td.intents == {"affirm", "greet", "restaurant_search", "goodbye", "chitchat"} assert td.entities == {"location", "cuisine"} - assert set(td.responses.keys()) == {"chitchat/ask_name", "chitchat/ask_weather"} + assert set(td.responses.keys()) == { + "utter_chitchat/ask_name", + "utter_chitchat/ask_weather", + } assert len(td.training_examples) == 46 assert len(td.intent_examples) == 46 assert len(td.response_examples) == 4 @@ -193,7 +216,10 @@ def test_train_test_split(filepaths): assert td.intents == {"affirm", "greet", "restaurant_search", "goodbye", "chitchat"} assert td.entities == {"location", "cuisine"} - assert set(td.responses.keys()) == {"chitchat/ask_name", "chitchat/ask_weather"} + assert set(td.responses.keys()) == { + "utter_chitchat/ask_name", + "utter_chitchat/ask_weather", + } assert len(td.training_examples) == 46 assert len(td.intent_examples) == 46 From 9b7d25ada38cf5b65efeffd9aeb6959161ec4d5f Mon Sep 17 00:00:00 2001 From: Daksh Date: Tue, 8 Sep 2020 23:57:40 +0200 Subject: [PATCH 07/25] more tests --- rasa/core/actions/action.py | 1 - rasa/importers/importer.py | 10 +++++----- tests/importers/test_importer.py | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 8cc40b666ff4..4d8b647e9c3c 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -24,7 +24,6 @@ RESPONSE_SELECTOR_TEMPLATE_KEY, INTENT_RANKING_KEY, INTENT_NAME_KEY, - INTENT_RESPONSE_KEY, ) from rasa.core.events import ( diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index dc5139c5ec50..a3110f68e11c 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -286,18 +286,18 @@ def _get_domain_with_retrieval_intents( retrieval_intents: Set[Text], existing_domain: Domain ) -> Domain: - modified_properties = [] + intent_modified_properties = [] for intent in retrieval_intents: - intent_property = ( + intent_properties = ( existing_domain.intent_properties[intent] if intent in existing_domain.intent_properties else {} ) - intent_property[IS_RETRIEVAL_INTENT_KEY] = True - modified_properties.append({intent: intent_property}) + intent_properties[IS_RETRIEVAL_INTENT_KEY] = True + intent_modified_properties.append({intent: intent_properties}) return Domain( - modified_properties, + intent_modified_properties, [], [], {}, diff --git a/tests/importers/test_importer.py b/tests/importers/test_importer.py index bb9781b17eeb..d3e68ec238d2 100644 --- a/tests/importers/test_importer.py +++ b/tests/importers/test_importer.py @@ -291,3 +291,24 @@ async def test_adding_e2e_actions_to_domain(project: Text): domain = await importer.get_domain() assert all(action_name in domain.action_names for action_name in additional_actions) + + +async def test_adding_retrieval_intents_to_domain(project: Text): + config_path = os.path.join(project, DEFAULT_CONFIG_PATH) + domain_path = "data/test_domains/default_retrieval_intents.yml" + data_paths = [ + "data/test_nlu/default_retrieval_intents.md", + "data/test_responses/default.md", + ] + existing = TrainingDataImporter.load_from_dict( + {}, config_path, domain_path, data_paths + ) + + nlu_importer = NluDataImporter(existing) + core_importer = CoreDataImporter(existing) + + importer = CombinedDataImporter([nlu_importer, core_importer]) + domain = await importer.get_domain() + + assert domain.retrieval_intents == ["chitchat"] + assert domain.intent_properties["chitchat"].get("is_retrieval_intent") From ffe598e068abf25f9cfc05e29b730c7d476b95d4 Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 10:54:46 +0200 Subject: [PATCH 08/25] add docsstrings, tests, changelog --- changelog/6591.improvement.md | 5 ++ rasa/core/actions/action.py | 24 +++++- rasa/core/domain.py | 2 + rasa/importers/importer.py | 21 +++-- rasa/nlu/constants.py | 2 +- rasa/nlu/selectors/response_selector.py | 5 +- tests/importers/test_importer.py | 6 +- tests/nlu/training_data/test_training_data.py | 84 +++++++++++-------- 8 files changed, 100 insertions(+), 49 deletions(-) create mode 100644 changelog/6591.improvement.md diff --git a/changelog/6591.improvement.md b/changelog/6591.improvement.md new file mode 100644 index 000000000000..76970a18eb4a --- /dev/null +++ b/changelog/6591.improvement.md @@ -0,0 +1,5 @@ +Retrieval actions with `respond_` prefix are now replaced with usual utterance actions with `utter_` prefix. + +If you were using retrieval actions before, rename all of them to start with `utter_` prefix. For example, `respond_chitchat` becomes `utter_chitchat`. +Also, in order to keep the response templates more consistent, you should now add the `utter_` prefix to all response templates defined for retrieval intents. For example, a response template `chitchat/ask_name` becomes `utter_chitchat/ask_name`. Note that the NLU examples for this will still be under `chitchat/ask_name` intent. +The example `responseselectorbot` should help clarify these changes further. \ No newline at end of file diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 4d8b647e9c3c..3ff9a7ebbd56 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -21,7 +21,7 @@ RESPONSE_SELECTOR_PROPERTY_NAME, RESPONSE_SELECTOR_RESPONSES_KEY, RESPONSE_SELECTOR_PREDICTION_KEY, - RESPONSE_SELECTOR_TEMPLATE_KEY, + RESPONSE_SELECTOR_TEMPLATE_NAME_KEY, INTENT_RANKING_KEY, INTENT_NAME_KEY, ) @@ -92,6 +92,13 @@ def default_action_names() -> List[Text]: def construct_retrieval_action_names(retrieval_intents: Set[Text]) -> List[Text]: + """List names of all retrieval actions corresponding to passed retrieval intents. + + Args: + retrieval_intents: List of retrieval intents defined in the NLU training data. + + Returns: Names of corresponding retrieval actions + """ return [ ActionRetrieveResponse.action_name_from_intent(intent) @@ -123,6 +130,15 @@ def combine_with_templates( def is_retrieval_action(action_name: Text, retrieval_intents: List[Text]) -> bool: + """Check if an action name is a retrieval action. + + The name for a retrieval action has an extra `utter_` prefix added to the corresponding retrieval intent name. + Args: + action_name: Name of the action. + retrieval_intents: List of retrieval intents defined in the NLU training data. + + Returns: True or False depending on whether the resolved intent name is present in the list of retrieval intents. + """ return ( ActionRetrieveResponse.intent_name_from_action(action_name) in retrieval_intents @@ -240,10 +256,12 @@ def __init__(self, name: Text, silent_fail: Optional[bool] = False): @staticmethod def intent_name_from_action(action_name) -> Text: + """Resolve the name of the intent from the action name.""" return action_name.split(UTTER_PREFIX)[1] @staticmethod def action_name_from_intent(intent_name) -> Text: + """Resolve the action name from the name of the intent.""" return f"{UTTER_PREFIX}{intent_name}" async def run( @@ -286,9 +304,9 @@ async def run( picked_message_idx = random.randint(0, len(possible_messages) - 1) picked_message = copy.deepcopy(possible_messages[picked_message_idx]) - picked_message[RESPONSE_SELECTOR_TEMPLATE_KEY] = selected[ + picked_message[RESPONSE_SELECTOR_TEMPLATE_NAME_KEY] = selected[ RESPONSE_SELECTOR_PREDICTION_KEY - ][RESPONSE_SELECTOR_TEMPLATE_KEY] + ][RESPONSE_SELECTOR_TEMPLATE_NAME_KEY] return [create_bot_utterance(picked_message)] diff --git a/rasa/core/domain.py b/rasa/core/domain.py index 1e1a36f2debb..05c9ff010589 100644 --- a/rasa/core/domain.py +++ b/rasa/core/domain.py @@ -360,6 +360,7 @@ def _transform_intent_properties_for_internal_use( @lazy_property def retrieval_intents(self) -> List[Text]: + """List all retrieval intents present in the domain according to the intent properties.""" return [ intent for intent in self.intent_properties @@ -631,6 +632,7 @@ def input_state_map(self) -> Dict[Text, int]: @lazy_property def input_states(self) -> List[Text]: """Returns all available states.""" + return ( self.intents + self.entities diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index a3110f68e11c..6cc998d04fd6 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -275,9 +275,7 @@ async def get_domain(self) -> Domain: nlu_data.retrieval_intents, combined_domain ) - combined_domain = combined_domain.merge( - domain_with_retrieval_intents, override=True - ) + combined_domain = combined_domain.merge(domain_with_retrieval_intents) return combined_domain @@ -285,8 +283,19 @@ async def get_domain(self) -> Domain: def _get_domain_with_retrieval_intents( retrieval_intents: Set[Text], existing_domain: Domain ) -> Domain: + """Construct a domain which has all the information for retrieval intents listed in the NLU training data. + + Args: + retrieval_intents: Set of retrieval intents defined in the NLU training data. + existing_domain: Domain which is already loaded from the domain file. + + Returns: domain with retrieval actions added to action names and properties for retrieval intents updated. + """ - intent_modified_properties = [] + # Get all the properties already defined + # for each retrieval intent in other domains + # and add the retrieval intent property to them + retrieval_intent_properties = [] for intent in retrieval_intents: intent_properties = ( existing_domain.intent_properties[intent] @@ -294,10 +303,10 @@ def _get_domain_with_retrieval_intents( else {} ) intent_properties[IS_RETRIEVAL_INTENT_KEY] = True - intent_modified_properties.append({intent: intent_properties}) + retrieval_intent_properties.append({intent: intent_properties}) return Domain( - intent_modified_properties, + retrieval_intent_properties, [], [], {}, diff --git a/rasa/nlu/constants.py b/rasa/nlu/constants.py index 8f8058744802..a415c7aac377 100644 --- a/rasa/nlu/constants.py +++ b/rasa/nlu/constants.py @@ -82,7 +82,7 @@ RESPONSE_SELECTOR_PREDICTION_KEY = "response" RESPONSE_SELECTOR_RANKING_KEY = "ranking" RESPONSE_SELECTOR_RESPONSES_KEY = "response_templates" -RESPONSE_SELECTOR_TEMPLATE_KEY = "template_name" +RESPONSE_SELECTOR_TEMPLATE_NAME_KEY = "template_name" RESPONSE_IDENTIFIER_DELIMITER = "/" INTENT_RANKING_KEY = "intent_ranking" diff --git a/rasa/nlu/selectors/response_selector.py b/rasa/nlu/selectors/response_selector.py index 413d9374f9dc..0768b31059e5 100644 --- a/rasa/nlu/selectors/response_selector.py +++ b/rasa/nlu/selectors/response_selector.py @@ -79,7 +79,7 @@ RESPONSE_SELECTOR_RESPONSES_KEY, RESPONSE_SELECTOR_PREDICTION_KEY, RESPONSE_SELECTOR_RANKING_KEY, - RESPONSE_SELECTOR_TEMPLATE_KEY, + RESPONSE_SELECTOR_TEMPLATE_NAME_KEY, PREDICTED_CONFIDENCE_KEY, INTENT_RESPONSE_KEY, INTENT, @@ -87,7 +87,6 @@ TEXT, INTENT_NAME_KEY, ) -from rasa.nlu.training_data import training_data from rasa.utils.tensorflow.model_data import RasaModelData from rasa.utils.tensorflow.models import RasaModel @@ -416,7 +415,7 @@ def process(self, message: Message, **kwargs: Any) -> None: RESPONSE_SELECTOR_RESPONSES_KEY: label_response_templates, PREDICTED_CONFIDENCE_KEY: top_label[PREDICTED_CONFIDENCE_KEY], INTENT_RESPONSE_KEY: label_intent_response_key, - RESPONSE_SELECTOR_TEMPLATE_KEY: util.intent_response_key_to_template_key( + RESPONSE_SELECTOR_TEMPLATE_NAME_KEY: util.intent_response_key_to_template_key( label_intent_response_key ), }, diff --git a/tests/importers/test_importer.py b/tests/importers/test_importer.py index d3e68ec238d2..6229f32205d4 100644 --- a/tests/importers/test_importer.py +++ b/tests/importers/test_importer.py @@ -300,12 +300,12 @@ async def test_adding_retrieval_intents_to_domain(project: Text): "data/test_nlu/default_retrieval_intents.md", "data/test_responses/default.md", ] - existing = TrainingDataImporter.load_from_dict( + base_data_importer = TrainingDataImporter.load_from_dict( {}, config_path, domain_path, data_paths ) - nlu_importer = NluDataImporter(existing) - core_importer = CoreDataImporter(existing) + nlu_importer = NluDataImporter(base_data_importer) + core_importer = CoreDataImporter(base_data_importer) importer = CombinedDataImporter([nlu_importer, core_importer]) domain = await importer.get_domain() diff --git a/tests/nlu/training_data/test_training_data.py b/tests/nlu/training_data/test_training_data.py index 896657a5323a..f0d14105edd5 100644 --- a/tests/nlu/training_data/test_training_data.py +++ b/tests/nlu/training_data/test_training_data.py @@ -1,6 +1,6 @@ import asyncio from pathlib import Path -from typing import Text +from typing import Text, List import pytest @@ -142,23 +142,29 @@ def test_template_key_to_intent_response_key(intent_response_key, template_key): ], ], ) -def test_demo_data(files): +def test_demo_data(files: List[Text]): from rasa.importers.utils import training_data_from_paths - td = training_data_from_paths(files, language="en") - assert td.intents == {"affirm", "greet", "restaurant_search", "goodbye", "chitchat"} - assert td.entities == {"location", "cuisine"} - assert set(td.responses.keys()) == { + trainingdata = training_data_from_paths(files, language="en") + assert trainingdata.intents == { + "affirm", + "greet", + "restaurant_search", + "goodbye", + "chitchat", + } + assert trainingdata.entities == {"location", "cuisine"} + assert set(trainingdata.responses.keys()) == { "utter_chitchat/ask_name", "utter_chitchat/ask_weather", } - assert len(td.training_examples) == 46 - assert len(td.intent_examples) == 46 - assert len(td.response_examples) == 4 - assert len(td.entity_examples) == 11 - assert len(td.responses) == 2 + assert len(trainingdata.training_examples) == 46 + assert len(trainingdata.intent_examples) == 46 + assert len(trainingdata.response_examples) == 4 + assert len(trainingdata.entity_examples) == 11 + assert len(trainingdata.responses) == 2 - assert td.entity_synonyms == { + assert trainingdata.entity_synonyms == { "Chines": "chinese", "Chinese": "chinese", "chines": "chinese", @@ -166,7 +172,7 @@ def test_demo_data(files): "veggie": "vegetarian", } - assert td.regex_features == [ + assert trainingdata.regex_features == [ {"name": "greet", "pattern": r"hey[^\s]*"}, {"name": "zipcode", "pattern": r"[0-9]{5}"}, ] @@ -209,39 +215,51 @@ def test_demo_data_filter_out_retrieval_intents(files): "filepaths", [["data/examples/rasa/demo-rasa.md", "data/examples/rasa/demo-rasa-responses.md"]], ) -def test_train_test_split(filepaths): +def test_train_test_split(filepaths: List[Text]): from rasa.importers.utils import training_data_from_paths - td = training_data_from_paths(filepaths, language="en") + trainingdata = training_data_from_paths(filepaths, language="en") - assert td.intents == {"affirm", "greet", "restaurant_search", "goodbye", "chitchat"} - assert td.entities == {"location", "cuisine"} - assert set(td.responses.keys()) == { + assert trainingdata.intents == { + "affirm", + "greet", + "restaurant_search", + "goodbye", + "chitchat", + } + assert trainingdata.entities == {"location", "cuisine"} + assert set(trainingdata.responses.keys()) == { "utter_chitchat/ask_name", "utter_chitchat/ask_weather", } - assert len(td.training_examples) == 46 - assert len(td.intent_examples) == 46 - assert len(td.response_examples) == 4 + assert len(trainingdata.training_examples) == 46 + assert len(trainingdata.intent_examples) == 46 + assert len(trainingdata.response_examples) == 4 - td_train, td_test = td.train_test_split(train_frac=0.8) + trainingdata_train, trainingdata_test = trainingdata.train_test_split( + train_frac=0.8 + ) - assert len(td_test.training_examples) + len(td_train.training_examples) == 46 - assert len(td_train.training_examples) == 34 - assert len(td_test.training_examples) == 12 + assert ( + len(trainingdata_test.training_examples) + + len(trainingdata_train.training_examples) + == 46 + ) + assert len(trainingdata_train.training_examples) == 34 + assert len(trainingdata_test.training_examples) == 12 - assert len(td.number_of_examples_per_intent.keys()) == len( - td_test.number_of_examples_per_intent.keys() + assert len(trainingdata.number_of_examples_per_intent.keys()) == len( + trainingdata_test.number_of_examples_per_intent.keys() ) - assert len(td.number_of_examples_per_intent.keys()) == len( - td_train.number_of_examples_per_intent.keys() + assert len(trainingdata.number_of_examples_per_intent.keys()) == len( + trainingdata_train.number_of_examples_per_intent.keys() ) - assert len(td.number_of_examples_per_response.keys()) == len( - td_test.number_of_examples_per_response.keys() + assert len(trainingdata.number_of_examples_per_response.keys()) == len( + trainingdata_test.number_of_examples_per_response.keys() ) - assert len(td.number_of_examples_per_response.keys()) == len( - td_train.number_of_examples_per_response.keys() + assert len(trainingdata.number_of_examples_per_response.keys()) == len( + trainingdata_train.number_of_examples_per_response.keys() ) From 7c69bb304a6c35cfab7cd11491829ae2b47dd562 Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 11:18:49 +0200 Subject: [PATCH 09/25] add migration guide --- docs/docs/migration-guide.mdx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/docs/migration-guide.mdx b/docs/docs/migration-guide.mdx index 51fe61a220d0..6e58264ac28c 100644 --- a/docs/docs/migration-guide.mdx +++ b/docs/docs/migration-guide.mdx @@ -377,13 +377,21 @@ This means that the output in these files should look like - } ``` +* Retrieval actions with `respond_` prefix are now replaced with usual utterance actions with `utter_` prefix. +If you were using retrieval actions before, rename all of them to start with `utter_` prefix. For example, +`respond_chitchat` becomes `utter_chitchat`. Also, in order to keep the response templates more consistent, +you should now add the `utter_` prefix to all response templates defined for retrieval intents. For example, +a response template `chitchat/ask_name` becomes `utter_chitchat/ask_name`. Note that the NLU examples for this +will still be under `chitchat/ask_name` intent. The example `responseselectorbot` should help clarify these changes further. + * The output schema of `ResponseSelector` has changed - `full_retrieval_intent` and `name` have been deprecated in favour of `intent_response_key` and `response_templates` respectively. -Additionally a key `all_retrieval_intents` is added to the response selector output which will -hold a list of all retrieval intents(faq, chitchat, etc.) that are present in the training data. +Additionally, two keys are added to the output - +1. `all_retrieval_intents` - Holds a list of all retrieval intents(faq, chitchat, etc.) that are present in the training data. +2. `template_name` - Holds the name of the response template which is predicted by the response selector(`utter_faq/is_legit`) An example output looks like this - -```json {3-4,10,11} +```json {3-4,10,11,20} { "response_selector": { "all_retrieval_intents": [ @@ -403,6 +411,7 @@ An example output looks like this - "text": "I think so." } ] + "template_name": "utter_faq/is_legit" }, "ranking": [ { From 8bfc171ad86a150d440ef771c13c8dda0efea8cd Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 12:39:58 +0200 Subject: [PATCH 10/25] fix deepsource issues --- rasa/core/actions/action.py | 6 ++++-- rasa/core/domain.py | 2 +- rasa/importers/importer.py | 15 ++++++++------- rasa/nlu/training_data/util.py | 2 ++ tests/nlu/selectors/test_selectors.py | 1 - 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 3ff9a7ebbd56..d147efccbca8 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -132,12 +132,14 @@ def combine_with_templates( def is_retrieval_action(action_name: Text, retrieval_intents: List[Text]) -> bool: """Check if an action name is a retrieval action. - The name for a retrieval action has an extra `utter_` prefix added to the corresponding retrieval intent name. + The name for a retrieval action has an extra `utter_` prefix added to + the corresponding retrieval intent name. Args: action_name: Name of the action. retrieval_intents: List of retrieval intents defined in the NLU training data. - Returns: True or False depending on whether the resolved intent name is present in the list of retrieval intents. + Returns: True or False depending on whether the resolved intent name + is present in the list of retrieval intents. """ return ( diff --git a/rasa/core/domain.py b/rasa/core/domain.py index 05c9ff010589..28ed8d23f2de 100644 --- a/rasa/core/domain.py +++ b/rasa/core/domain.py @@ -360,7 +360,7 @@ def _transform_intent_properties_for_internal_use( @lazy_property def retrieval_intents(self) -> List[Text]: - """List all retrieval intents present in the domain according to the intent properties.""" + """List retrieval intents present in the domain.""" return [ intent for intent in self.intent_properties diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index 6cc998d04fd6..e5551ffa2b66 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -13,7 +13,6 @@ from rasa.importers.autoconfig import TrainingType import rasa.utils.io as io_utils import rasa.utils.common as common_utils -from rasa.core.actions import action from rasa.core.domain import IS_RETRIEVAL_INTENT_KEY logger = logging.getLogger(__name__) @@ -265,9 +264,9 @@ async def get_domain(self) -> Domain: lambda merged, other: merged.merge(other), domains, Domain.empty() ) - # Check if NLU data has any retrieval intents, - # if yes add corresponding retrieval actions with `utter_` prefix - # automatically to an empty domain and update the properties of existing retrieval intents. + # Check if NLU data has any retrieval intents, if yes + # add corresponding retrieval actions with `utter_` prefix automatically + # to an empty domain and update the properties of existing retrieval intents. nlu_data = await self.get_nlu_data() if nlu_data.retrieval_intents: @@ -283,14 +282,16 @@ async def get_domain(self) -> Domain: def _get_domain_with_retrieval_intents( retrieval_intents: Set[Text], existing_domain: Domain ) -> Domain: - """Construct a domain which has all the information for retrieval intents listed in the NLU training data. + """Construct a domain consisting of retrieval intents listed in the NLU training data. Args: - retrieval_intents: Set of retrieval intents defined in the NLU training data. + retrieval_intents: Set of retrieval intents defined in NLU training data. existing_domain: Domain which is already loaded from the domain file. - Returns: domain with retrieval actions added to action names and properties for retrieval intents updated. + Returns: Domain with retrieval actions added to action names and properties + for retrieval intents updated. """ + from rasa.core.actions import action # Get all the properties already defined # for each retrieval intent in other domains diff --git a/rasa/nlu/training_data/util.py b/rasa/nlu/training_data/util.py index d489b05b59ea..66402d3ca019 100644 --- a/rasa/nlu/training_data/util.py +++ b/rasa/nlu/training_data/util.py @@ -89,8 +89,10 @@ def remove_untrainable_entities_from(example: Dict[Text, Any]) -> None: def intent_response_key_to_template_key(intent_response_key: Text) -> Text: + """Resolve the response template key for a given intent response key.""" return f"{UTTER_PREFIX}{intent_response_key}" def template_key_to_intent_response_key(template_key: Text) -> Text: + """Resolve the intent response key for a given response template key.""" return template_key.split(UTTER_PREFIX)[1] diff --git a/tests/nlu/selectors/test_selectors.py b/tests/nlu/selectors/test_selectors.py index decf93926fc4..1e6a120c430d 100644 --- a/tests/nlu/selectors/test_selectors.py +++ b/tests/nlu/selectors/test_selectors.py @@ -11,7 +11,6 @@ TRANSFORMER_SIZE, ) from rasa.nlu.selectors.response_selector import ResponseSelector -from rasa.nlu.training_data import training_data @pytest.mark.parametrize( From 4f17e663bd4643ab559da6d968acb9ea3aa25e04 Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 14:33:14 +0200 Subject: [PATCH 11/25] change import to make shared independent --- rasa/shared/nlu/constants.py | 2 ++ rasa/shared/nlu/training_data/util.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rasa/shared/nlu/constants.py b/rasa/shared/nlu/constants.py index f2b6e56b7fa0..a1e3a87d9db6 100644 --- a/rasa/shared/nlu/constants.py +++ b/rasa/shared/nlu/constants.py @@ -24,3 +24,5 @@ ENTITY_ATTRIBUTE_START = "start" ENTITY_ATTRIBUTE_END = "end" NO_ENTITY_TAG = "O" + +UTTER_PREFIX = "utter_" diff --git a/rasa/shared/nlu/training_data/util.py b/rasa/shared/nlu/training_data/util.py index 3d27993a1b19..b6fd55ba3bdd 100644 --- a/rasa/shared/nlu/training_data/util.py +++ b/rasa/shared/nlu/training_data/util.py @@ -15,7 +15,7 @@ ENTITY_ATTRIBUTE_ROLE, ENTITY_ATTRIBUTE_GROUP, ) -from rasa.core.constants import UTTER_PREFIX +from rasa.shared.nlu.constants import UTTER_PREFIX import rasa.shared.utils.io logger = logging.getLogger(__name__) From 02b9898fc3249f640ea7fa8303a7becd2000766f Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 15:50:01 +0200 Subject: [PATCH 12/25] add docs first pass --- docs/docs/components/selectors.mdx | 100 +++++++++- docs/docs/glossary.mdx | 4 + docs/docs/responses.mdx | 28 +++ docs/docs/retrieval-actions.mdx | 285 ----------------------------- docs/docs/training-data-format.mdx | 29 +++ docs/sidebars.js | 1 - 6 files changed, 159 insertions(+), 288 deletions(-) delete mode 100644 docs/docs/retrieval-actions.mdx diff --git a/docs/docs/components/selectors.mdx b/docs/docs/components/selectors.mdx index 8c3fd7b9f830..d46e3f3abb8c 100644 --- a/docs/docs/components/selectors.mdx +++ b/docs/docs/components/selectors.mdx @@ -47,7 +47,8 @@ Selectors predict a bot response from a set of candidate responses. { "text": "I think it's about to rain." } - ] + ], + "template_name": "utter_chitchat/ask_weather" }, "ranking": [ { @@ -71,7 +72,7 @@ Selectors predict a bot response from a set of candidate responses. * **Description** Response Selector component can be used to build a response retrieval model to directly predict a bot response from - a set of candidate responses. The prediction of this model is used by [Retrieval Actions](../retrieval-actions). + a set of candidate responses. The prediction of this model is used by the dialogue manager to utter the predicted responses. It embeds user inputs and response labels into the same space and follows the exact same neural network architecture and optimization as the [DIETClassifier](../components/intent-classifiers.mdx#dietclassifier). @@ -142,6 +143,10 @@ Selectors predict a bot response from a set of candidate responses. template which has a text attribute for training. If none are found, it falls back to using the retrieval intent combined with the response key as the label. + See this [example assistant](https://github.com/RasaHQ/rasa/tree/master/examples/responseselectorbot) to understand + how you can use the `ResponseSelector` component in your assistant. Additionally, you will find this tutorial on + [handling FAQs](./chitchat-faqs.mdx#handling-faqs-using-a-response-selector) using a `ResponseSelector` useful as well. +
The above configuration parameters are the ones you should configure to fit your model to your data. @@ -289,3 +294,94 @@ Selectors predict a bot response from a set of candidate responses. :::
+ +* **Under the hood: Parsing Response Selector Output** + + The parsed output from NLU will have a property named `response_selector` + containing the output for each response selector component. Each response selector is + identified by `retrieval_intent` parameter of that response selector + and stores two properties: + + * `response`: The predicted response key under the corresponding retrieval intent, + prediction's confidence and the associated response templates. + + * `ranking`: Ranking with confidences of top 10 candidate response keys. + + Example result: + + ```json + { + "text": "How's the weather today?", + "response_selector": { + "faq": { + "response": { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather", + "response_templates": [ + { + "text": "It's sunny in Berlin today", + "image": "https://i.imgur.com/nGF1K8f.jpg" + }, + { + "text": "I think it's about to rain." + } + ], + "template_name": "utter_chitchat/ask_weather" + }, + "ranking": [ + { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather" + }, + { + "id": 1388783286124361986, + "confidence": 0.3, + "intent_response_key": "chitchat/ask_name" + } + ] + } + } + } + ``` + + If the `retrieval_intent` parameter of a particular response selector was left to its default value, + the corresponding response selector will be identified as `default` in the returned output. + + ```json {4} + { + "text": "How's the weather today?", + "response_selector": { + "default": { + "response": { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather", + "response_templates": [ + { + "text": "It's sunny in Berlin today", + "image": "https://i.imgur.com/nGF1K8f.jpg" + }, + { + "text": "I think it's about to rain." + } + ], + "template_name": "utter_chitchat/ask_weather" + }, + "ranking": [ + { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather" + }, + { + "id": 1388783286124361986, + "confidence": 0.3, + "intent_response_key": "chitchat/ask_name" + } + ] + } + } + } + ``` diff --git a/docs/docs/glossary.mdx b/docs/docs/glossary.mdx index 01815716a035..c1dee0c45e87 100644 --- a/docs/docs/glossary.mdx +++ b/docs/docs/glossary.mdx @@ -55,6 +55,10 @@ description: Glossary for all Rasa-related terms Something that a user is trying to convey or accomplish (e,g., greeting, specifying a location). +## Retrieval Intent + + A special instance of an intent which can be divided into smaller sub-intents. Each sub-intent has a fixed response and hence the context of the conversation does not matter when user expresses one of these sub-intents. + ## [Interactive Learning](./writing-stories.mdx#using-interactive-learning) A mode of training the bot where the user provides feedback to the bot while talking to it. diff --git a/docs/docs/responses.mdx b/docs/docs/responses.mdx index 3915f0c62e06..d6113997fcc0 100644 --- a/docs/docs/responses.mdx +++ b/docs/docs/responses.mdx @@ -258,3 +258,31 @@ responses: - text: "Hey, {name}. How are you?" - text: "Hey, {name}. How is your day going?" ``` + +## Responses for Retrieval Intents + +If you are using retrieval intents in your assistant, you also need to add response templates +for your assistant's replies to these intents in a separate file: + +```yaml-rasa title="responses.yml" +responses: + utter_chitchat/ask_name: + - image: "https://i.imgur.com/zTvA58i.jpeg" + text: hello, my name is retrieval bot. + - text: Oh yeah, I am called the retrieval bot. + + utter_chitchat/ask_weather: + - text: Oh, it does look sunny right now in Berlin. + image: "https://i.imgur.com/vwv7aHN.png" + - text: I am not sure of the whole week but I can see the sun is out today. +``` +All such response templates start with the `utter_` prefix followed by the retrieval intent name(`chitchat`) +and the associated response key(`ask_name` or `ask_weather`). + +:::info Responses format +The responses for retrieval intents use the same format as the [responses in the domain](responses.mdx). +This means, you can also use buttons, images and any other multimedia elements in +your responses. +::: + + diff --git a/docs/docs/retrieval-actions.mdx b/docs/docs/retrieval-actions.mdx deleted file mode 100644 index bfdd28b024ea..000000000000 --- a/docs/docs/retrieval-actions.mdx +++ /dev/null @@ -1,285 +0,0 @@ ---- -id: retrieval-actions -sidebar_label: Retrieval Actions -title: Retrieval Actions -description: Use a retrieval model to select chatbot responses in open source bot framework Rasa. ---- - -Retrieval actions are designed to make it simpler to work with small talk and simple questions. -For example, if your assistant can handle 100 FAQs and 50 different small talk intents, you can use a single retrieval -action to cover all of these. -From a dialogue perspective, these single-turn exchanges can all be treated equally, so this simplifies your stories. - -Instead of having a lot of stories like: - -```yaml-rasa -stories: -- story: weather - steps: - - intent: ask_weather - - action: utter_ask_weather - -- story: introduction - steps: - - intent: ask_name - - action: utter_introduce_myself - -# ... -``` - -You can cover all of these with a single story where the above intents are grouped -under a common `chitchat` intent: - -```yaml-rasa -stories: -- story: chitchat - steps: - - intent: chitchat - - action: respond_chitchat -``` - -A retrieval action uses the output of a [ResponseSelector](./components/selectors.mdx#responseselector) component from NLU which learns a -retrieval model to predict the correct response from a list of candidate responses given a user message text. - -:::note Retrieval Model Blog Post -There is an in-depth blog post [here](https://blog.rasa.com/response-retrieval-models/) about how to use retrieval -actions for handling single turn interactions. - -::: - - - -## Training Data - -Like the name suggests, retrieval actions learn to select the correct response from a list of candidates. -As with other message data, you need to include examples of what your users will say in -your training data file: - -```yaml-rasa title="data/nlu.yml" -nlu: -- intent: chitchat/ask_name - examples: | - - what's your name - - who are you? - - what are you called? - -- intent: chitchat/ask_weather - examples: | - - how's weather? - - is it sunny where you are? -``` - -First, all of these examples will be combined into a single `chitchat` -retrieval intent that NLU will predict. All retrieval intents have a suffix -added to them which identifies a particular response key for your assistant, in the -above example - `ask_name` and `ask_weather`. The suffix is separated from -the intent name by a `/` delimiter. - -Next, include response texts for all retrieval intents in a training data file: - -```yaml-rasa title="data/responses.yml" -responses: - chitchat/ask_name: - - text: "my name is Sara, Rasa's documentation bot!" - - chitchat/ask_weather: - - text: "it's always sunny where I live" -``` - -:::info Responses format -The responses use the same format as the [responses in the domain](responses.mdx). -This means, you can also use buttons, images and any other multimedia elements in -your responses. -::: - -The retrieval model is trained separately as part of the NLU training pipeline -to select the correct response. The default configuration uses the user message text as input and the retrieval intent combined with the -response key suffix (e.g. `chitchat/ask_name`) as the correct label for that user message. However, the -retrieval model can also be configured to use the text of the response message as the label by setting `use_text_as_label` -to `True` in the component's configuration. In that case, if you change the text of these responses, -you have to retrain your retrieval model! This is a key difference to the responses defined in -your domain file. - -:::note Special meaning of `/` -As shown in the above examples, the `/` symbol is reserved as a delimiter to separate -retrieval intents from response text identifier. Make sure not to use it in the -name of your intents. - -::: - -## Configuration File - -You need to include the [ResponseSelector](./components/selectors.mdx#responseselector) -component in your configuration. The component needs a tokenizer, a featurizer and an -intent classifier to operate on the user message before it can predict a response -and hence these components should be placed before `ResponseSelector` in the -NLU configuration. An example: - -```yaml-rasa title="config.yml" {8} -language: "en" - -pipeline: -- name: "WhitespaceTokenizer" - intent_split_symbol: "_" -- name: "CountVectorsFeaturizer" -- name: "DIETClassifier" -- name: "ResponseSelector" -``` - -## Domain - -Rasa uses a naming convention to match a retrieval intent name, e.g. `chitchat`, -to its corresponding retrieval action. -The correct action name in this case is `respond_chitchat`. -The prefix `respond_` is mandatory to identify it as a retrieval action. -Another example - the correct action name for `faq` would be `respond_faq`. -To include this in your domain, add it to the list of actions: - -```yaml-rasa title="domain.yml" -actions: - # ... - - respond_chitchat - - respond_faq -``` - -A simple way to ensure that the retrieval action is predicted after the chitchat -intent is to use [Rules](./rules.mdx). -However, you can also include this action in your stories. -For example, if you want to repeat a question after handling chitchat: - -```yaml-rasa title="data/stories.yml" {6-8} -stories: -- story: interruption - steps: - - intent: search_restaurant - - action: utter_ask_cuisine - - intent: chitchat - - action: respond_chitchat - - action: utter_ask_cuisine -``` - -## Multiple Retrieval Actions - -If your assistant includes both FAQs **and** chitchat, it is possible to -separate these into separate retrieval actions, for example having sub-intents -like `chitchat/ask_weather` and `faq/returns_policy`. Rasa supports adding multiple `RetrievalActions` -like `respond_chitchat` and `respond_faq`. To train separate retrieval models for each of the retrieval intents, -you need to include a separate `ResponseSelector` component in the config for each retrieval intent: - -```yaml-rasa title="config.yml" {8-12} -language: "en" - -pipeline: -- name: "WhitespaceTokenizer" - intent_split_symbol: "_" -- name: "CountVectorsFeaturizer" -- name: "DIETClassifier" -- name: "ResponseSelector" - retrieval_intent: "faq" -- name: "ResponseSelector" - retrieval_intent: "chitchat" -``` - -Alternatively, if you want the retrieval actions for both the intents to share a single retrieval model, -specifying just one `ResponseSelector` component is enough. - -```yaml-rasa title="config.yml" {8} -language: "en" - -pipeline: -- name: "WhitespaceTokenizer" - intent_split_symbol: "_" -- name: "CountVectorsFeaturizer" -- name: "DIETClassifier" -- name: "ResponseSelector" -``` - - -## Under the hood: Parsing Response Selector Output - -The parsed output from NLU will have a property named `response_selector` -containing the output for each response selector component. Each response selector is -identified by `retrieval_intent` parameter of that response selector -and stores two properties: - -* `response`: The predicted response key under the corresponding retrieval intent, -prediction's confidence and the associated response templates - -* `ranking`: Ranking with confidences of top 10 candidate response keys. - -Example result: - -```json -{ - "text": "How's the weather today?", - "response_selector": { - "faq": { - "response": { - "id": 1388783286124361986, - "confidence": 0.7, - "intent_response_key": "chitchat/ask_weather", - "response_templates": [ - { - "text": "It's sunny in Berlin today", - "image": "https://i.imgur.com/nGF1K8f.jpg" - }, - { - "text": "I think it's about to rain." - } - ] - }, - "ranking": [ - { - "id": 1388783286124361986, - "confidence": 0.7, - "intent_response_key": "chitchat/ask_weather" - }, - { - "id": 1388783286124361986, - "confidence": 0.3, - "intent_response_key": "chitchat/ask_name" - } - ] - } - } -} -``` - -If the `retrieval_intent` parameter of a particular response selector was left to its default value, -the corresponding response selector will be identified as `default` in the returned output. - -```json {4} -{ - "text": "How's the weather today?", - "response_selector": { - "default": { - "response": { - "id": 1388783286124361986, - "confidence": 0.7, - "intent_response_key": "chitchat/ask_weather", - "response_templates": [ - { - "text": "It's sunny in Berlin today", - "image": "https://i.imgur.com/nGF1K8f.jpg" - }, - { - "text": "I think it's about to rain." - } - ] - }, - "ranking": [ - { - "id": 1388783286124361986, - "confidence": 0.7, - "intent_response_key": "chitchat/ask_weather" - }, - { - "id": 1388783286124361986, - "confidence": 0.3, - "intent_response_key": "chitchat/ask_name" - } - ] - } - } -} -``` \ No newline at end of file diff --git a/docs/docs/training-data-format.mdx b/docs/docs/training-data-format.mdx index 0c66a2e0133b..87ecce9231d8 100644 --- a/docs/docs/training-data-format.mdx +++ b/docs/docs/training-data-format.mdx @@ -185,6 +185,35 @@ nlu: The `metadata` key can contain arbitrary key-value data that stays with an example and is accessible by the components in the NLU pipeline. In the example above, the sentiment of the example could be used by a custom component in the pipeline for sentiment analysis. +If you want to specify [retrieval intents](glossary.mdx#retrieval-intent), then your NLU examples will look as follows: +```yaml-rasa +nlu: +- intent: chitchat/ask_name + examples: | + - What is your name? + - May I know your name? + - What do people call you? + - Do you have a name for yourself? + +- intent: chitchat/ask_weather + examples: | + - What's the weather like today? + - Does it look sunny outside today? + - Oh, do you mind checking the weather for me please? + - I like sunny days in Berlin. +``` +All retrieval intents have a suffix +added to them which identifies a particular response key for your assistant, in the +above example - `ask_name` and `ask_weather`. The suffix is separated from +the retrieval intent name by a `/` delimiter. + +:::note Special meaning of `/` +As shown in the above examples, the `/` symbol is reserved as a delimiter to separate +retrieval intents from their associated response keys. Make sure not to use it in the +name of your intents. +::: + + ### Entities [Entities](glossary.mdx#entity) are structured pieces of information that can be extracted from a user's message. For entity extraction to work, you need to either specify training data to train an ML model or you need to define [regular expressions](#regular-expressions-for-entity-extraction) to extract entities using the [`RegexEntityExtractor`](components/entity-extractors.mdx#regexentityextractor) based on a character pattern. diff --git a/docs/sidebars.js b/docs/sidebars.js index 9ed35a38315b..aa99c0b49921 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -125,7 +125,6 @@ module.exports = { }, ], }, - 'retrieval-actions', 'forms', 'reminders-and-external-events', 'default-actions', From bbcc9fc2621a2530f26ae91d200d24fde84a453d Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 16:02:19 +0200 Subject: [PATCH 13/25] bring back retrieval actions page --- docs/docs/retrieval-actions.mdx | 285 ++++++++++++++++++++++++++++++++ docs/sidebars.js | 1 + 2 files changed, 286 insertions(+) create mode 100644 docs/docs/retrieval-actions.mdx diff --git a/docs/docs/retrieval-actions.mdx b/docs/docs/retrieval-actions.mdx new file mode 100644 index 000000000000..bfdd28b024ea --- /dev/null +++ b/docs/docs/retrieval-actions.mdx @@ -0,0 +1,285 @@ +--- +id: retrieval-actions +sidebar_label: Retrieval Actions +title: Retrieval Actions +description: Use a retrieval model to select chatbot responses in open source bot framework Rasa. +--- + +Retrieval actions are designed to make it simpler to work with small talk and simple questions. +For example, if your assistant can handle 100 FAQs and 50 different small talk intents, you can use a single retrieval +action to cover all of these. +From a dialogue perspective, these single-turn exchanges can all be treated equally, so this simplifies your stories. + +Instead of having a lot of stories like: + +```yaml-rasa +stories: +- story: weather + steps: + - intent: ask_weather + - action: utter_ask_weather + +- story: introduction + steps: + - intent: ask_name + - action: utter_introduce_myself + +# ... +``` + +You can cover all of these with a single story where the above intents are grouped +under a common `chitchat` intent: + +```yaml-rasa +stories: +- story: chitchat + steps: + - intent: chitchat + - action: respond_chitchat +``` + +A retrieval action uses the output of a [ResponseSelector](./components/selectors.mdx#responseselector) component from NLU which learns a +retrieval model to predict the correct response from a list of candidate responses given a user message text. + +:::note Retrieval Model Blog Post +There is an in-depth blog post [here](https://blog.rasa.com/response-retrieval-models/) about how to use retrieval +actions for handling single turn interactions. + +::: + + + +## Training Data + +Like the name suggests, retrieval actions learn to select the correct response from a list of candidates. +As with other message data, you need to include examples of what your users will say in +your training data file: + +```yaml-rasa title="data/nlu.yml" +nlu: +- intent: chitchat/ask_name + examples: | + - what's your name + - who are you? + - what are you called? + +- intent: chitchat/ask_weather + examples: | + - how's weather? + - is it sunny where you are? +``` + +First, all of these examples will be combined into a single `chitchat` +retrieval intent that NLU will predict. All retrieval intents have a suffix +added to them which identifies a particular response key for your assistant, in the +above example - `ask_name` and `ask_weather`. The suffix is separated from +the intent name by a `/` delimiter. + +Next, include response texts for all retrieval intents in a training data file: + +```yaml-rasa title="data/responses.yml" +responses: + chitchat/ask_name: + - text: "my name is Sara, Rasa's documentation bot!" + + chitchat/ask_weather: + - text: "it's always sunny where I live" +``` + +:::info Responses format +The responses use the same format as the [responses in the domain](responses.mdx). +This means, you can also use buttons, images and any other multimedia elements in +your responses. +::: + +The retrieval model is trained separately as part of the NLU training pipeline +to select the correct response. The default configuration uses the user message text as input and the retrieval intent combined with the +response key suffix (e.g. `chitchat/ask_name`) as the correct label for that user message. However, the +retrieval model can also be configured to use the text of the response message as the label by setting `use_text_as_label` +to `True` in the component's configuration. In that case, if you change the text of these responses, +you have to retrain your retrieval model! This is a key difference to the responses defined in +your domain file. + +:::note Special meaning of `/` +As shown in the above examples, the `/` symbol is reserved as a delimiter to separate +retrieval intents from response text identifier. Make sure not to use it in the +name of your intents. + +::: + +## Configuration File + +You need to include the [ResponseSelector](./components/selectors.mdx#responseselector) +component in your configuration. The component needs a tokenizer, a featurizer and an +intent classifier to operate on the user message before it can predict a response +and hence these components should be placed before `ResponseSelector` in the +NLU configuration. An example: + +```yaml-rasa title="config.yml" {8} +language: "en" + +pipeline: +- name: "WhitespaceTokenizer" + intent_split_symbol: "_" +- name: "CountVectorsFeaturizer" +- name: "DIETClassifier" +- name: "ResponseSelector" +``` + +## Domain + +Rasa uses a naming convention to match a retrieval intent name, e.g. `chitchat`, +to its corresponding retrieval action. +The correct action name in this case is `respond_chitchat`. +The prefix `respond_` is mandatory to identify it as a retrieval action. +Another example - the correct action name for `faq` would be `respond_faq`. +To include this in your domain, add it to the list of actions: + +```yaml-rasa title="domain.yml" +actions: + # ... + - respond_chitchat + - respond_faq +``` + +A simple way to ensure that the retrieval action is predicted after the chitchat +intent is to use [Rules](./rules.mdx). +However, you can also include this action in your stories. +For example, if you want to repeat a question after handling chitchat: + +```yaml-rasa title="data/stories.yml" {6-8} +stories: +- story: interruption + steps: + - intent: search_restaurant + - action: utter_ask_cuisine + - intent: chitchat + - action: respond_chitchat + - action: utter_ask_cuisine +``` + +## Multiple Retrieval Actions + +If your assistant includes both FAQs **and** chitchat, it is possible to +separate these into separate retrieval actions, for example having sub-intents +like `chitchat/ask_weather` and `faq/returns_policy`. Rasa supports adding multiple `RetrievalActions` +like `respond_chitchat` and `respond_faq`. To train separate retrieval models for each of the retrieval intents, +you need to include a separate `ResponseSelector` component in the config for each retrieval intent: + +```yaml-rasa title="config.yml" {8-12} +language: "en" + +pipeline: +- name: "WhitespaceTokenizer" + intent_split_symbol: "_" +- name: "CountVectorsFeaturizer" +- name: "DIETClassifier" +- name: "ResponseSelector" + retrieval_intent: "faq" +- name: "ResponseSelector" + retrieval_intent: "chitchat" +``` + +Alternatively, if you want the retrieval actions for both the intents to share a single retrieval model, +specifying just one `ResponseSelector` component is enough. + +```yaml-rasa title="config.yml" {8} +language: "en" + +pipeline: +- name: "WhitespaceTokenizer" + intent_split_symbol: "_" +- name: "CountVectorsFeaturizer" +- name: "DIETClassifier" +- name: "ResponseSelector" +``` + + +## Under the hood: Parsing Response Selector Output + +The parsed output from NLU will have a property named `response_selector` +containing the output for each response selector component. Each response selector is +identified by `retrieval_intent` parameter of that response selector +and stores two properties: + +* `response`: The predicted response key under the corresponding retrieval intent, +prediction's confidence and the associated response templates + +* `ranking`: Ranking with confidences of top 10 candidate response keys. + +Example result: + +```json +{ + "text": "How's the weather today?", + "response_selector": { + "faq": { + "response": { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather", + "response_templates": [ + { + "text": "It's sunny in Berlin today", + "image": "https://i.imgur.com/nGF1K8f.jpg" + }, + { + "text": "I think it's about to rain." + } + ] + }, + "ranking": [ + { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather" + }, + { + "id": 1388783286124361986, + "confidence": 0.3, + "intent_response_key": "chitchat/ask_name" + } + ] + } + } +} +``` + +If the `retrieval_intent` parameter of a particular response selector was left to its default value, +the corresponding response selector will be identified as `default` in the returned output. + +```json {4} +{ + "text": "How's the weather today?", + "response_selector": { + "default": { + "response": { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather", + "response_templates": [ + { + "text": "It's sunny in Berlin today", + "image": "https://i.imgur.com/nGF1K8f.jpg" + }, + { + "text": "I think it's about to rain." + } + ] + }, + "ranking": [ + { + "id": 1388783286124361986, + "confidence": 0.7, + "intent_response_key": "chitchat/ask_weather" + }, + { + "id": 1388783286124361986, + "confidence": 0.3, + "intent_response_key": "chitchat/ask_name" + } + ] + } + } +} +``` \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index aa99c0b49921..9ed35a38315b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -125,6 +125,7 @@ module.exports = { }, ], }, + 'retrieval-actions', 'forms', 'reminders-and-external-events', 'default-actions', From 5a56afccf94687e405e6dd6f807dd07b8c26683a Mon Sep 17 00:00:00 2001 From: Daksh Varshneya Date: Wed, 9 Sep 2020 17:52:01 +0200 Subject: [PATCH 14/25] Apply suggestions from code review Partial Co-authored-by: Tom Bocklisch --- docs/docs/responses.mdx | 5 ++--- docs/docs/training-data-format.mdx | 2 +- rasa/core/actions/action.py | 16 +++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/docs/responses.mdx b/docs/docs/responses.mdx index d6113997fcc0..68036edb046a 100644 --- a/docs/docs/responses.mdx +++ b/docs/docs/responses.mdx @@ -276,8 +276,8 @@ responses: image: "https://i.imgur.com/vwv7aHN.png" - text: I am not sure of the whole week but I can see the sun is out today. ``` -All such response templates start with the `utter_` prefix followed by the retrieval intent name(`chitchat`) -and the associated response key(`ask_name` or `ask_weather`). +All such response templates (e.g. `utter_chitchat/ask_name`) start with the `utter_` prefix followed by the retrieval intent name (`chitchat`) +and the associated response key (`ask_name`). :::info Responses format The responses for retrieval intents use the same format as the [responses in the domain](responses.mdx). @@ -285,4 +285,3 @@ This means, you can also use buttons, images and any other multimedia elements i your responses. ::: - diff --git a/docs/docs/training-data-format.mdx b/docs/docs/training-data-format.mdx index 87ecce9231d8..3fb667c6de32 100644 --- a/docs/docs/training-data-format.mdx +++ b/docs/docs/training-data-format.mdx @@ -204,7 +204,7 @@ nlu: ``` All retrieval intents have a suffix added to them which identifies a particular response key for your assistant, in the -above example - `ask_name` and `ask_weather`. The suffix is separated from +above example `ask_name` and `ask_weather` are the suffixes. The suffix is separated from the retrieval intent name by a `/` delimiter. :::note Special meaning of `/` diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 89ebcefad868..f3a836ee1038 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -136,12 +136,14 @@ def is_retrieval_action(action_name: Text, retrieval_intents: List[Text]) -> boo The name for a retrieval action has an extra `utter_` prefix added to the corresponding retrieval intent name. + Args: action_name: Name of the action. retrieval_intents: List of retrieval intents defined in the NLU training data. - Returns: True or False depending on whether the resolved intent name - is present in the list of retrieval intents. + Returns: + `True` if the resolved intent name is present in the list of retrieval + intents, `False` otherwise. """ return ( @@ -154,7 +156,7 @@ def action_from_name( action_endpoint: Optional[EndpointConfig], user_actions: List[Text], should_use_form_action: bool = False, - retrieval_intents: List[Text] = [], + retrieval_intents: Optional[List[Text]] = None, ) -> "Action": """Return an action instance for the name.""" @@ -162,7 +164,7 @@ def action_from_name( if name in defaults and name not in user_actions: return defaults[name] - elif name.startswith(UTTER_PREFIX) and is_retrieval_action(name, retrieval_intents): + elif name.startswith(UTTER_PREFIX) and is_retrieval_action(name, retrieval_intents or []): return ActionRetrieveResponse(name) elif name.startswith(UTTER_PREFIX): return ActionUtterTemplate(name) @@ -178,7 +180,7 @@ def actions_from_names( action_names: List[Text], action_endpoint: Optional[EndpointConfig], user_actions: List[Text], - retrieval_intents: List[Text] = [], + retrieval_intents: Optional[List[Text]] = None, ) -> List["Action"]: """Converts the names of actions into class instances.""" @@ -259,12 +261,12 @@ def __init__(self, name: Text, silent_fail: Optional[bool] = False): self.silent_fail = silent_fail @staticmethod - def intent_name_from_action(action_name) -> Text: + def intent_name_from_action(action_name: Text) -> Text: """Resolve the name of the intent from the action name.""" return action_name.split(UTTER_PREFIX)[1] @staticmethod - def action_name_from_intent(intent_name) -> Text: + def action_name_from_intent(intent_name: Text) -> Text: """Resolve the action name from the name of the intent.""" return f"{UTTER_PREFIX}{intent_name}" From 466b7575a807915d65d812d5684d7de87cf3b3fa Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 17:59:48 +0200 Subject: [PATCH 15/25] code formatting, remove arguments from tests --- rasa/core/actions/action.py | 4 +++- .../nlu/training_data/test_training_data.py | 20 ++++++------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index f3a836ee1038..d6e5b74248ae 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -164,7 +164,9 @@ def action_from_name( if name in defaults and name not in user_actions: return defaults[name] - elif name.startswith(UTTER_PREFIX) and is_retrieval_action(name, retrieval_intents or []): + elif name.startswith(UTTER_PREFIX) and is_retrieval_action( + name, retrieval_intents or [] + ): return ActionRetrieveResponse(name) elif name.startswith(UTTER_PREFIX): return ActionUtterTemplate(name) diff --git a/tests/shared/nlu/training_data/test_training_data.py b/tests/shared/nlu/training_data/test_training_data.py index 954cccb1d96a..2c7a1007b6bf 100644 --- a/tests/shared/nlu/training_data/test_training_data.py +++ b/tests/shared/nlu/training_data/test_training_data.py @@ -119,23 +119,15 @@ def test_composite_entities_data(): assert td.number_of_examples_per_entity["role 'from'"] == 3 -@pytest.mark.parametrize( - "intent_response_key, template_key", - [["chitchat/ask_name", "utter_chitchat/ask_name"]], -) -def test_intent_response_key_to_template_key( - intent_response_key: Text, template_key: Text -): +def test_intent_response_key_to_template_key(): + intent_response_key = "chitchat/ask_name" + template_key = "utter_chitchat/ask_name" assert intent_response_key_to_template_key(intent_response_key) == template_key -@pytest.mark.parametrize( - "intent_response_key, template_key", - [["chitchat/ask_name", "utter_chitchat/ask_name"]], -) -def test_template_key_to_intent_response_key( - intent_response_key: Text, template_key: Text -): +def test_template_key_to_intent_response_key(): + intent_response_key = "chitchat/ask_name" + template_key = "utter_chitchat/ask_name" assert template_key_to_intent_response_key(template_key) == intent_response_key From d0bc08ac98cb82d103f8e56ff5ab5a21d1c30df8 Mon Sep 17 00:00:00 2001 From: Daksh Date: Wed, 9 Sep 2020 19:32:37 +0200 Subject: [PATCH 16/25] full sync of response templates between nlu data and domain --- docs/docs/responses.mdx | 4 +- .../responseselectorbot/data/responses.yml | 12 ---- examples/responseselectorbot/domain.yml | 10 +++ rasa/importers/importer.py | 62 ++++++++++++++++--- .../shared/nlu/training_data/training_data.py | 2 +- tests/importers/test_importer.py | 24 ++++++- 6 files changed, 88 insertions(+), 26 deletions(-) delete mode 100644 examples/responseselectorbot/data/responses.yml diff --git a/docs/docs/responses.mdx b/docs/docs/responses.mdx index 68036edb046a..580ac71adc15 100644 --- a/docs/docs/responses.mdx +++ b/docs/docs/responses.mdx @@ -262,9 +262,9 @@ responses: ## Responses for Retrieval Intents If you are using retrieval intents in your assistant, you also need to add response templates -for your assistant's replies to these intents in a separate file: +for your assistant's replies to these intents: -```yaml-rasa title="responses.yml" +```yaml-rasa responses: utter_chitchat/ask_name: - image: "https://i.imgur.com/zTvA58i.jpeg" diff --git a/examples/responseselectorbot/data/responses.yml b/examples/responseselectorbot/data/responses.yml deleted file mode 100644 index 93aa74cc95d9..000000000000 --- a/examples/responseselectorbot/data/responses.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "2.0" - -responses: - utter_chitchat/ask_name: - - image: "https://i.imgur.com/zTvA58i.jpeg" - text: hello, my name is retrieval bot. - - text: Oh yeah, I am called the retrieval bot. - - utter_chitchat/ask_weather: - - text: Oh, it does look sunny right now in Berlin. - image: "https://i.imgur.com/vwv7aHN.png" - - text: I am not sure of the whole week but I can see the sun is out today. diff --git a/examples/responseselectorbot/domain.yml b/examples/responseselectorbot/domain.yml index 35cfeb27093d..f637a032b36a 100644 --- a/examples/responseselectorbot/domain.yml +++ b/examples/responseselectorbot/domain.yml @@ -35,6 +35,16 @@ responses: utter_iamabot: - text: "I am a bot, powered by Rasa." + utter_chitchat/ask_name: + - image: "https://i.imgur.com/zTvA58i.jpeg" + text: hello, my name is retrieval bot. + - text: Oh yeah, I am called the retrieval bot. + + utter_chitchat/ask_weather: + - text: Oh, it does look sunny right now in Berlin. + image: "https://i.imgur.com/vwv7aHN.png" + - text: I am not sure of the whole week but I can see the sun is out today. + session_config: session_expiration_time: 60 # value in minutes carry_over_slots_to_new_session: true diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index 2d71de69bec5..07f884a3edb5 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -1,6 +1,6 @@ import asyncio from functools import reduce -from typing import Text, Optional, List, Dict, Set +from typing import Text, Optional, List, Dict, Set, Any import logging import rasa.shared.utils.common @@ -257,22 +257,32 @@ async def get_config(self) -> Dict: return reduce(lambda merged, other: {**merged, **(other or {})}, configs, {}) - async def get_domain(self) -> Domain: + async def _get_user_defined_domain(self) -> Domain: + """Construct domain out of user defined domain files.""" + domains = [importer.get_domain() for importer in self._importers] domains = await asyncio.gather(*domains) - combined_domain = reduce( + return reduce( lambda merged, other: merged.merge(other), domains, Domain.empty() ) + async def get_domain(self) -> Domain: + """Merge user defined domain with properties of retrieval intents in NLU data.""" + + combined_domain = await self._get_user_defined_domain() + user_defined_nlu_data = await self._get_user_defined_nlu_data() + # Check if NLU data has any retrieval intents, if yes # add corresponding retrieval actions with `utter_` prefix automatically - # to an empty domain and update the properties of existing retrieval intents. - nlu_data = await self.get_nlu_data() - if nlu_data.retrieval_intents: + # to an empty domain, update the properties of existing retrieval intents + # and merge response templates + if user_defined_nlu_data.retrieval_intents: domain_with_retrieval_intents = self._get_domain_with_retrieval_intents( - nlu_data.retrieval_intents, combined_domain + user_defined_nlu_data.retrieval_intents, + user_defined_nlu_data.responses, + combined_domain, ) combined_domain = combined_domain.merge(domain_with_retrieval_intents) @@ -281,7 +291,9 @@ async def get_domain(self) -> Domain: @staticmethod def _get_domain_with_retrieval_intents( - retrieval_intents: Set[Text], existing_domain: Domain + retrieval_intents: Set[Text], + response_templates: Dict[Text, List[Dict[Text, Any]]], + existing_domain: Domain, ) -> Domain: """Construct a domain consisting of retrieval intents listed in the NLU training data. @@ -311,7 +323,7 @@ def _get_domain_with_retrieval_intents( retrieval_intent_properties, [], [], - {}, + response_templates, action.construct_retrieval_action_names(retrieval_intents), [], ) @@ -332,7 +344,11 @@ async def get_stories( lambda merged, other: merged.merge(other), stories, StoryGraph([]) ) - async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: + async def _get_user_defined_nlu_data( + self, language: Optional[Text] = "en" + ) -> TrainingData: + """Fetch all the NLU data defined by the user.""" + nlu_data = [importer.get_nlu_data(language) for importer in self._importers] nlu_data = await asyncio.gather(*nlu_data) @@ -340,6 +356,32 @@ async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: lambda merged, other: merged.merge(other), nlu_data, TrainingData() ) + async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: + """Merge NLU data defined by the user and response templates defined in the domain.""" + + user_defined_nlu_data = await self._get_user_defined_nlu_data(language) + user_defined_domain = await self._get_user_defined_domain() + + return user_defined_nlu_data.merge( + self._get_nlu_data_with_responses(user_defined_domain.templates) + ) + + @staticmethod + def _get_nlu_data_with_responses( + response_templates: Dict[Text, List[Dict[Text, Any]]] + ) -> TrainingData: + """Construct training data object with only the response templates supplied. + + Args: + response_templates: Response templates the NLU data should + be initialized with. + + Returns: TrainingData object with response templates. + + """ + + return TrainingData(responses=response_templates) + class E2EImporter(TrainingDataImporter): """Importer which diff --git a/rasa/shared/nlu/training_data/training_data.py b/rasa/shared/nlu/training_data/training_data.py index 6dceb9c6466b..c16c8f5dbd01 100644 --- a/rasa/shared/nlu/training_data/training_data.py +++ b/rasa/shared/nlu/training_data/training_data.py @@ -156,7 +156,7 @@ def retrieval_intents(self) -> Set[Text]: return { ex.get(INTENT) for ex in self.training_examples - if ex.get(RESPONSE) is not None + if ex.get(INTENT_RESPONSE_KEY) } @lazy_property diff --git a/tests/importers/test_importer.py b/tests/importers/test_importer.py index 298262fb36c2..d924b47a97a8 100644 --- a/tests/importers/test_importer.py +++ b/tests/importers/test_importer.py @@ -297,7 +297,26 @@ async def test_adding_e2e_actions_to_domain(project: Text): assert all(action_name in domain.action_names for action_name in additional_actions) -async def test_adding_retrieval_intents_to_domain(project: Text): +async def test_nlu_data_domain_sync(project: Text): + config_path = os.path.join(project, DEFAULT_CONFIG_PATH) + domain_path = os.path.join(project, DEFAULT_DOMAIN_PATH) + data_paths = [os.path.join(project, DEFAULT_DATA_PATH)] + base_data_importer = TrainingDataImporter.load_from_dict( + {}, config_path, domain_path, data_paths + ) + + nlu_importer = NluDataImporter(base_data_importer) + core_importer = CoreDataImporter(base_data_importer) + + importer = CombinedDataImporter([nlu_importer, core_importer]) + domain = await importer.get_domain() + nlu_data = await importer.get_nlu_data() + + assert domain.retrieval_intents == [] + assert domain.templates == nlu_data.responses + + +async def test_nlu_data_domain_sync_with_retrieval_intents(project: Text): config_path = os.path.join(project, DEFAULT_CONFIG_PATH) domain_path = "data/test_domains/default_retrieval_intents.yml" data_paths = [ @@ -313,6 +332,9 @@ async def test_adding_retrieval_intents_to_domain(project: Text): importer = CombinedDataImporter([nlu_importer, core_importer]) domain = await importer.get_domain() + nlu_data = await importer.get_nlu_data() assert domain.retrieval_intents == ["chitchat"] assert domain.intent_properties["chitchat"].get("is_retrieval_intent") + assert domain.templates == nlu_data.responses + assert "utter_chitchat" in domain.action_names From 482d7429e9181c88e22f04cc1deb37b5b7292a12 Mon Sep 17 00:00:00 2001 From: Daksh Date: Thu, 10 Sep 2020 11:47:50 +0200 Subject: [PATCH 17/25] new data importer --- rasa/importers/importer.py | 95 +++++++++++++++++++------------- tests/importers/test_importer.py | 24 ++------ 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/rasa/importers/importer.py b/rasa/importers/importer.py index 07f884a3edb5..ca755a6c8ce7 100644 --- a/rasa/importers/importer.py +++ b/rasa/importers/importer.py @@ -153,7 +153,7 @@ def load_from_dict( ) ] - return E2EImporter(CombinedDataImporter(importers)) + return E2EImporter(RetrievalModelsDataImporter(CombinedDataImporter(importers))) @staticmethod def _importer_from_dict( @@ -243,7 +243,6 @@ 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. """ @@ -257,9 +256,7 @@ async def get_config(self) -> Dict: return reduce(lambda merged, other: {**merged, **(other or {})}, configs, {}) - async def _get_user_defined_domain(self) -> Domain: - """Construct domain out of user defined domain files.""" - + async def get_domain(self) -> Domain: domains = [importer.get_domain() for importer in self._importers] domains = await asyncio.gather(*domains) @@ -267,27 +264,66 @@ async def _get_user_defined_domain(self) -> Domain: lambda merged, other: merged.merge(other), domains, Domain.empty() ) + async def get_stories( + self, + template_variables: Optional[Dict] = None, + use_e2e: bool = False, + exclusion_percentage: Optional[int] = None, + ) -> StoryGraph: + stories = [ + importer.get_stories(template_variables, use_e2e, exclusion_percentage) + for importer in self._importers + ] + stories = await asyncio.gather(*stories) + + return reduce( + lambda merged, other: merged.merge(other), stories, StoryGraph([]) + ) + + async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: + nlu_data = [importer.get_nlu_data(language) for importer in self._importers] + nlu_data = await asyncio.gather(*nlu_data) + + return reduce( + lambda merged, other: merged.merge(other), nlu_data, TrainingData() + ) + + +class RetrievalModelsDataImporter(TrainingDataImporter): + """A `TrainingDataImporter` that sets up the data for training retrieval models. + + Synchronizes response templates between Domain and NLU + and adds retrieval intent properties from the NLU training data + back to the Domain. + """ + + def __init__(self, importer: TrainingDataImporter): + self._importer = importer + + async def get_config(self) -> Dict: + return await self._importer.get_config() + async def get_domain(self) -> Domain: - """Merge user defined domain with properties of retrieval intents in NLU data.""" + """Merge existing domain with properties of retrieval intents in NLU data.""" - combined_domain = await self._get_user_defined_domain() - user_defined_nlu_data = await self._get_user_defined_nlu_data() + existing_domain = await self._importer.get_domain() + existing_nlu_data = await self._importer.get_nlu_data() # Check if NLU data has any retrieval intents, if yes # add corresponding retrieval actions with `utter_` prefix automatically # to an empty domain, update the properties of existing retrieval intents # and merge response templates - if user_defined_nlu_data.retrieval_intents: + if existing_nlu_data.retrieval_intents: domain_with_retrieval_intents = self._get_domain_with_retrieval_intents( - user_defined_nlu_data.retrieval_intents, - user_defined_nlu_data.responses, - combined_domain, + existing_nlu_data.retrieval_intents, + existing_nlu_data.responses, + existing_domain, ) - combined_domain = combined_domain.merge(domain_with_retrieval_intents) + existing_domain = existing_domain.merge(domain_with_retrieval_intents) - return combined_domain + return existing_domain @staticmethod def _get_domain_with_retrieval_intents( @@ -334,36 +370,19 @@ async def get_stories( use_e2e: bool = False, exclusion_percentage: Optional[int] = None, ) -> StoryGraph: - stories = [ - importer.get_stories(template_variables, use_e2e, exclusion_percentage) - for importer in self._importers - ] - stories = await asyncio.gather(*stories) - return reduce( - lambda merged, other: merged.merge(other), stories, StoryGraph([]) - ) - - async def _get_user_defined_nlu_data( - self, language: Optional[Text] = "en" - ) -> TrainingData: - """Fetch all the NLU data defined by the user.""" - - nlu_data = [importer.get_nlu_data(language) for importer in self._importers] - nlu_data = await asyncio.gather(*nlu_data) - - return reduce( - lambda merged, other: merged.merge(other), nlu_data, TrainingData() + return await self._importer.get_stories( + template_variables, use_e2e, exclusion_percentage ) async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: - """Merge NLU data defined by the user and response templates defined in the domain.""" + """Update NLU data with response templates defined in the domain""" - user_defined_nlu_data = await self._get_user_defined_nlu_data(language) - user_defined_domain = await self._get_user_defined_domain() + existing_nlu_data = await self._importer.get_nlu_data(language) + existing_domain = await self._importer.get_domain() - return user_defined_nlu_data.merge( - self._get_nlu_data_with_responses(user_defined_domain.templates) + return existing_nlu_data.merge( + self._get_nlu_data_with_responses(existing_domain.templates) ) @staticmethod diff --git a/tests/importers/test_importer.py b/tests/importers/test_importer.py index d924b47a97a8..6cbdbb10f064 100644 --- a/tests/importers/test_importer.py +++ b/tests/importers/test_importer.py @@ -15,6 +15,7 @@ NluDataImporter, CoreDataImporter, E2EImporter, + RetrievalModelsDataImporter, ) from rasa.importers.rasa import RasaFileImporter @@ -297,25 +298,6 @@ async def test_adding_e2e_actions_to_domain(project: Text): assert all(action_name in domain.action_names for action_name in additional_actions) -async def test_nlu_data_domain_sync(project: Text): - config_path = os.path.join(project, DEFAULT_CONFIG_PATH) - domain_path = os.path.join(project, DEFAULT_DOMAIN_PATH) - data_paths = [os.path.join(project, DEFAULT_DATA_PATH)] - base_data_importer = TrainingDataImporter.load_from_dict( - {}, config_path, domain_path, data_paths - ) - - nlu_importer = NluDataImporter(base_data_importer) - core_importer = CoreDataImporter(base_data_importer) - - importer = CombinedDataImporter([nlu_importer, core_importer]) - domain = await importer.get_domain() - nlu_data = await importer.get_nlu_data() - - assert domain.retrieval_intents == [] - assert domain.templates == nlu_data.responses - - async def test_nlu_data_domain_sync_with_retrieval_intents(project: Text): config_path = os.path.join(project, DEFAULT_CONFIG_PATH) domain_path = "data/test_domains/default_retrieval_intents.yml" @@ -330,7 +312,9 @@ async def test_nlu_data_domain_sync_with_retrieval_intents(project: Text): nlu_importer = NluDataImporter(base_data_importer) core_importer = CoreDataImporter(base_data_importer) - importer = CombinedDataImporter([nlu_importer, core_importer]) + importer = RetrievalModelsDataImporter( + CombinedDataImporter([nlu_importer, core_importer]) + ) domain = await importer.get_domain() nlu_data = await importer.get_nlu_data() From ca1769f61cf8934beedce3e84e6e128a246dc79e Mon Sep 17 00:00:00 2001 From: Daksh Date: Thu, 10 Sep 2020 12:06:09 +0200 Subject: [PATCH 18/25] fix tests, NLU eval --- rasa/nlu/test.py | 4 ++-- rasa/shared/nlu/training_data/training_data.py | 9 ++++++--- tests/importers/test_importer.py | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rasa/nlu/test.py b/rasa/nlu/test.py index 34a14bd2c82f..ca486f258d5e 100644 --- a/rasa/nlu/test.py +++ b/rasa/nlu/test.py @@ -1286,9 +1286,9 @@ def get_eval_data( intent_results, entity_results, response_selection_results = [], [], [] response_labels = [ - e.get(RESPONSE) + e.get(INTENT_RESPONSE_KEY) for e in test_data.intent_examples - if e.get(RESPONSE) is not None + if e.get(INTENT_RESPONSE_KEY) is not None ] intent_labels = [e.get(INTENT) for e in test_data.intent_examples] should_eval_intents = ( diff --git a/rasa/shared/nlu/training_data/training_data.py b/rasa/shared/nlu/training_data/training_data.py index c16c8f5dbd01..75d76ceedab1 100644 --- a/rasa/shared/nlu/training_data/training_data.py +++ b/rasa/shared/nlu/training_data/training_data.py @@ -139,7 +139,7 @@ def intent_examples(self) -> List[Message]: @lazy_property def response_examples(self) -> List[Message]: - return [ex for ex in self.training_examples if ex.get(RESPONSE)] + return [ex for ex in self.training_examples if ex.get(INTENT_RESPONSE_KEY)] @lazy_property def entity_examples(self) -> List[Message]: @@ -169,7 +169,9 @@ def number_of_examples_per_intent(self) -> Dict[Text, int]: def number_of_examples_per_response(self) -> Dict[Text, int]: """Calculates the number of examples per response.""" responses = [ - ex.get(RESPONSE) for ex in self.training_examples if ex.get(RESPONSE) + ex.get(INTENT_RESPONSE_KEY) + for ex in self.training_examples + if ex.get(INTENT_RESPONSE_KEY) ] return dict(Counter(responses)) @@ -382,7 +384,8 @@ def sorted_intent_examples(self) -> List[Message]: """Sorts the intent examples by the name of the intent and then response""" return sorted( - self.intent_examples, key=lambda e: (e.get(INTENT), e.get(RESPONSE)) + self.intent_examples, + key=lambda e: (e.get(INTENT), e.get(INTENT_RESPONSE_KEY)), ) def validate(self) -> None: diff --git a/tests/importers/test_importer.py b/tests/importers/test_importer.py index 6cbdbb10f064..b4973992d84a 100644 --- a/tests/importers/test_importer.py +++ b/tests/importers/test_importer.py @@ -100,9 +100,9 @@ def test_load_from_dict( ) assert isinstance(actual, E2EImporter) - assert isinstance(actual.importer, CombinedDataImporter) + assert isinstance(actual.importer, RetrievalModelsDataImporter) - actual_importers = [i.__class__ for i in actual.importer._importers] + actual_importers = [i.__class__ for i in actual.importer._importer._importers] assert actual_importers == expected @@ -129,7 +129,7 @@ async def test_nlu_only(project: Text): ) assert isinstance(actual, NluDataImporter) - assert isinstance(actual._importer, CombinedDataImporter) + assert isinstance(actual._importer, RetrievalModelsDataImporter) stories = await actual.get_stories() assert stories.is_empty() From 2cf5b85d36044ac48412ccd4897b8306183f69ab Mon Sep 17 00:00:00 2001 From: Daksh Date: Thu, 10 Sep 2020 12:37:00 +0200 Subject: [PATCH 19/25] add test for evaluation. --- tests/nlu/test_evaluation.py | 46 +++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/nlu/test_evaluation.py b/tests/nlu/test_evaluation.py index 327f9a993dbd..e9bfd5054eae 100644 --- a/tests/nlu/test_evaluation.py +++ b/tests/nlu/test_evaluation.py @@ -53,8 +53,10 @@ import rasa.shared.nlu.training_data.loading from tests.nlu.conftest import DEFAULT_DATA_PATH from rasa.nlu.selectors.response_selector import ResponseSelector -from rasa.nlu.test import is_response_selector_present +from rasa.nlu.test import is_response_selector_present, get_eval_data from rasa.utils.tensorflow.constants import EPOCHS, ENTITY_RECOGNITION +from rasa.nlu import train +from rasa.importers.importer import TrainingDataImporter # https://github.com/pytest-dev/pytest-asyncio/issues/68 # this event_loop is used by pytest-asyncio, and redefining it @@ -356,6 +358,48 @@ def test_run_evaluation(unpacked_trained_moodbot_path): assert result.get("intent_evaluation") +async def test_eval_data(component_builder, tmpdir, project): + _config = RasaNLUModelConfig( + { + "pipeline": [ + {"name": "WhitespaceTokenizer"}, + {"name": "CountVectorsFeaturizer"}, + {"name": "DIETClassifier", "epochs": 2}, + {"name": "ResponseSelector", "epochs": 2}, + ], + "language": "en", + } + ) + + config_path = os.path.join(project, "config.yml") + data_importer = TrainingDataImporter.load_nlu_importer_from_config( + config_path, + training_data_paths=[ + "data/examples/rasa/demo-rasa.md", + "data/examples/rasa/demo-rasa-responses.md", + ], + ) + + (_, _, persisted_path) = await train( + _config, + path=tmpdir.strpath, + data=data_importer, + component_builder=component_builder, + persist_nlu_training_data=True, + ) + + interpreter = Interpreter.load(persisted_path, component_builder) + + data = await data_importer.get_nlu_data() + intent_results, response_selection_results, entity_results, = get_eval_data( + interpreter, data + ) + + assert len(intent_results) == 46 + assert len(response_selection_results) == 46 + assert len(entity_results) == 46 + + def test_run_cv_evaluation(pretrained_embeddings_spacy_config): td = rasa.shared.nlu.training_data.loading.load_data( "data/examples/rasa/demo-rasa.json" From fbc61cd9099a431a15a9575f51e6174a7dc5d41f Mon Sep 17 00:00:00 2001 From: Daksh Date: Thu, 10 Sep 2020 13:45:14 +0200 Subject: [PATCH 20/25] refactored retrieval action to use utter action class. --- rasa/core/actions/action.py | 101 +++++++++++++++++------------------- tests/core/test_actions.py | 16 ++++++ 2 files changed, 64 insertions(+), 53 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index d6e5b74248ae..0ea3952dc973 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -255,10 +255,49 @@ def __str__(self) -> Text: return "Action('{}')".format(self.name()) -class ActionRetrieveResponse(Action): +class ActionUtterTemplate(Action): + """An action which only effect is to utter a template when it is run. + + Both, name and utter template, need to be specified using + the `name` method.""" + + def __init__(self, name: Text, silent_fail: Optional[bool] = False): + self.template_name = name + self.silent_fail = silent_fail + + async def run( + self, + output_channel: "OutputChannel", + nlg: "NaturalLanguageGenerator", + tracker: "DialogueStateTracker", + domain: "Domain", + ) -> List[Event]: + """Simple run implementation uttering a (hopefully defined) template.""" + + message = await nlg.generate(self.template_name, tracker, output_channel.name()) + if message is None: + if not self.silent_fail: + logger.error( + "Couldn't create message for response '{}'." + "".format(self.template_name) + ) + return [] + message["template_name"] = self.template_name + + return [create_bot_utterance(message)] + + def name(self) -> Text: + return self.template_name + + def __str__(self) -> Text: + return "ActionUtterTemplate('{}')".format(self.name()) + + +class ActionRetrieveResponse(ActionUtterTemplate): """An action which queries the Response Selector for the appropriate response.""" def __init__(self, name: Text, silent_fail: Optional[bool] = False): + super().__init__(name, silent_fail) self.action_name = name self.silent_fail = silent_fail @@ -278,7 +317,7 @@ async def run( nlg: "NaturalLanguageGenerator", tracker: "DialogueStateTracker", domain: "Domain", - ): + ) -> List[Event]: """Query the appropriate response and create a bot utterance with that.""" response_selector_properties = tracker.latest_message.parse_data[ @@ -302,21 +341,15 @@ async def run( logger.debug(f"Picking response from selector of type {query_key}") selected = response_selector_properties[query_key] - possible_messages = selected[RESPONSE_SELECTOR_PREDICTION_KEY][ - RESPONSE_SELECTOR_RESPONSES_KEY - ] - # Pick a random message from list of candidate messages. - # This should ideally be done by the NLG class but that's not - # possible until the domain has all the response templates of the response selector. - picked_message_idx = random.randint(0, len(possible_messages) - 1) - picked_message = copy.deepcopy(possible_messages[picked_message_idx]) - - picked_message[RESPONSE_SELECTOR_TEMPLATE_NAME_KEY] = selected[ - RESPONSE_SELECTOR_PREDICTION_KEY - ][RESPONSE_SELECTOR_TEMPLATE_NAME_KEY] + # Override template name of ActionUtterTemplate + # with the complete template name retrieved from + # the output of response selector. + self.template_name = selected[RESPONSE_SELECTOR_PREDICTION_KEY][ + RESPONSE_SELECTOR_TEMPLATE_NAME_KEY + ] - return [create_bot_utterance(picked_message)] + return await super().run(output_channel, nlg, tracker, domain) def name(self) -> Text: return self.action_name @@ -325,44 +358,6 @@ def __str__(self) -> Text: return "ActionRetrieveResponse('{}')".format(self.name()) -class ActionUtterTemplate(Action): - """An action which only effect is to utter a template when it is run. - - Both, name and utter template, need to be specified using - the `name` method.""" - - def __init__(self, name: Text, silent_fail: Optional[bool] = False): - self.template_name = name - self.silent_fail = silent_fail - - async def run( - self, - output_channel: "OutputChannel", - nlg: "NaturalLanguageGenerator", - tracker: "DialogueStateTracker", - domain: "Domain", - ) -> List[Event]: - """Simple run implementation uttering a (hopefully defined) template.""" - - message = await nlg.generate(self.template_name, tracker, output_channel.name()) - if message is None: - if not self.silent_fail: - logger.error( - "Couldn't create message for response '{}'." - "".format(self.template_name) - ) - return [] - message["template_name"] = self.template_name - - return [create_bot_utterance(message)] - - def name(self) -> Text: - return self.template_name - - def __str__(self) -> Text: - return "ActionUtterTemplate('{}')".format(self.name()) - - class ActionBack(ActionUtterTemplate): """Revert the tracker state by two user utterances.""" diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index fec83de857a4..35a350746433 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -377,6 +377,11 @@ async def test_action_utter_retrieved_response( } }, ) + + default_domain.templates.update( + {"utter_chitchat/ask_name": [{"text": "I am a bot."}]} + ) + events = await ActionRetrieveResponse(action_name).run( default_channel, default_nlg, default_tracker, default_domain ) @@ -410,6 +415,11 @@ async def test_action_utter_default_retrieved_response( } }, ) + + default_domain.templates.update( + {"utter_chitchat/ask_name": [{"text": "I am a bot."}]} + ) + events = await ActionRetrieveResponse(action_name).run( default_channel, default_nlg, default_tracker, default_domain ) @@ -438,11 +448,17 @@ async def test_action_utter_retrieved_empty_response( "response": { "intent_response_key": "chitchat/ask_name", "response_templates": [{"text": "I am a bot."}], + "template_name": "utter_chitchat/ask_name", } } } }, ) + + default_domain.templates.update( + {"utter_chitchat/ask_name": [{"text": "I am a bot."}]} + ) + events = await ActionRetrieveResponse(action_name).run( default_channel, default_nlg, default_tracker, default_domain ) From 58899b08572b58de263c15c03235960f3047dc3c Mon Sep 17 00:00:00 2001 From: Daksh Date: Thu, 10 Sep 2020 14:01:29 +0200 Subject: [PATCH 21/25] make linter happy --- rasa/core/actions/action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 0ea3952dc973..6f0c4a054558 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -141,8 +141,8 @@ def is_retrieval_action(action_name: Text, retrieval_intents: List[Text]) -> boo action_name: Name of the action. retrieval_intents: List of retrieval intents defined in the NLU training data. - Returns: - `True` if the resolved intent name is present in the list of retrieval + Returns: + `True` if the resolved intent name is present in the list of retrieval intents, `False` otherwise. """ From 9c43df8cd281f9e151c4ca697b03b0eadbc7a00f Mon Sep 17 00:00:00 2001 From: Daksh Date: Thu, 10 Sep 2020 16:59:56 +0200 Subject: [PATCH 22/25] fix tests, change param to train on text --- .../demo-rasa-intents-2.md | 8 ------- .../demo-rasa-intents-3.md | 21 +++++++++++++++++++ rasa/core/actions/action.py | 2 -- rasa/nlu/selectors/response_selector.py | 2 +- rasa/nlu/test.py | 1 - .../shared/nlu/training_data/training_data.py | 2 +- tests/importers/test_importer.py | 4 ++-- tests/nlu/test_config.py | 2 +- 8 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 data/test/duplicate_intents_markdown/demo-rasa-intents-3.md diff --git a/data/test/duplicate_intents_markdown/demo-rasa-intents-2.md b/data/test/duplicate_intents_markdown/demo-rasa-intents-2.md index 7c960a8cd93b..0616acd6e826 100644 --- a/data/test/duplicate_intents_markdown/demo-rasa-intents-2.md +++ b/data/test/duplicate_intents_markdown/demo-rasa-intents-2.md @@ -34,14 +34,6 @@ - I am looking for [mexican indian fusion](cuisine) - [central](location) [indian](cuisine) restaurant -## intent:chitchat/ask_name -- What's your name? -- What can I call you? - -## intent:chitchat/ask_weather -- How's the weather? -- Is it too hot outside? - ## synonym:chinese + Chines * Chinese diff --git a/data/test/duplicate_intents_markdown/demo-rasa-intents-3.md b/data/test/duplicate_intents_markdown/demo-rasa-intents-3.md new file mode 100644 index 000000000000..a9293e8f3355 --- /dev/null +++ b/data/test/duplicate_intents_markdown/demo-rasa-intents-3.md @@ -0,0 +1,21 @@ + +## intent:affirm +- yes +- yep +- yeah +- indeed +- that's right +- ok +- great +- right, thank you +- correct +- great choice +- sounds really good + +## intent:chitchat/ask_name +- What's your name? +- What can I call you? + +## intent:chitchat/ask_weather +- How's the weather? +- Is it too hot outside? \ No newline at end of file diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 6f0c4a054558..9c111817bcfb 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -20,13 +20,11 @@ from rasa.nlu.constants import ( RESPONSE_SELECTOR_DEFAULT_INTENT, RESPONSE_SELECTOR_PROPERTY_NAME, - RESPONSE_SELECTOR_RESPONSES_KEY, RESPONSE_SELECTOR_PREDICTION_KEY, RESPONSE_SELECTOR_TEMPLATE_NAME_KEY, INTENT_RANKING_KEY, INTENT_NAME_KEY, ) -from rasa.shared.nlu.constants import INTENT_RESPONSE_KEY from rasa.core.events import ( UserUtteranceReverted, diff --git a/rasa/nlu/selectors/response_selector.py b/rasa/nlu/selectors/response_selector.py index 90481eb00d40..50dad38916ad 100644 --- a/rasa/nlu/selectors/response_selector.py +++ b/rasa/nlu/selectors/response_selector.py @@ -211,7 +211,7 @@ def required_components(cls) -> List[Type[Component]]: RETRIEVAL_INTENT: None, # Boolean flag to check if actual text of the response # should be used as ground truth label for training the model. - USE_TEXT_AS_LABEL: False, + USE_TEXT_AS_LABEL: True, # If you want to use tensorboard to visualize training and validation metrics, # set this option to a valid output directory. TENSORBOARD_LOG_DIR: None, diff --git a/rasa/nlu/test.py b/rasa/nlu/test.py index ca486f258d5e..5143a35c6b35 100644 --- a/rasa/nlu/test.py +++ b/rasa/nlu/test.py @@ -36,7 +36,6 @@ from rasa.shared.nlu.constants import ( TEXT, INTENT, - RESPONSE, INTENT_RESPONSE_KEY, ENTITIES, EXTRACTOR, diff --git a/rasa/shared/nlu/training_data/training_data.py b/rasa/shared/nlu/training_data/training_data.py index 75d76ceedab1..3783d31e19a1 100644 --- a/rasa/shared/nlu/training_data/training_data.py +++ b/rasa/shared/nlu/training_data/training_data.py @@ -521,7 +521,7 @@ def _split(_examples: List[Message], _count: int) -> None: examples = [ e for e in training_examples - if RESPONSE in e.data and e.data[RESPONSE] == response + if e.get(INTENT_RESPONSE_KEY) and e.get(INTENT_RESPONSE_KEY) == response ] _split(examples, count) training_examples = training_examples - set(examples) diff --git a/tests/importers/test_importer.py b/tests/importers/test_importer.py index b4973992d84a..0b2e982ca159 100644 --- a/tests/importers/test_importer.py +++ b/tests/importers/test_importer.py @@ -117,8 +117,8 @@ def test_load_from_config(tmpdir: Path): importer = TrainingDataImporter.load_from_config(config_path) assert isinstance(importer, E2EImporter) - assert isinstance(importer.importer, CombinedDataImporter) - assert isinstance(importer.importer._importers[0], MultiProjectImporter) + assert isinstance(importer.importer, RetrievalModelsDataImporter) + assert isinstance(importer.importer._importer._importers[0], MultiProjectImporter) async def test_nlu_only(project: Text): diff --git a/tests/nlu/test_config.py b/tests/nlu/test_config.py index c11a46d337a3..2f7af32ca62c 100644 --- a/tests/nlu/test_config.py +++ b/tests/nlu/test_config.py @@ -183,7 +183,7 @@ async def test_train_docker_and_docs_configs( ), ( "data/test_config/config_spacy_entity_extractor.yml", - "data/test/md_converted_to_json.json", + "data/test/duplicate_intents_markdown/demo-rasa-intents-2.md", [f"add one of {TRAINABLE_EXTRACTORS}"], ), ( From 05cee334d4ce9e0e7b52695038962ace644af321 Mon Sep 17 00:00:00 2001 From: Daksh Date: Fri, 11 Sep 2020 08:37:17 +0200 Subject: [PATCH 23/25] omit empty domain warning --- examples/responseselectorbot/data/nlu.yml | 11 +++++++++++ examples/responseselectorbot/domain.yml | 10 ---------- rasa/importers/rasa.py | 4 ++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/responseselectorbot/data/nlu.yml b/examples/responseselectorbot/data/nlu.yml index bbd4acc898b1..71ff79a9a081 100644 --- a/examples/responseselectorbot/data/nlu.yml +++ b/examples/responseselectorbot/data/nlu.yml @@ -103,3 +103,14 @@ nlu: - Does it look sunny outside today? - Oh, do you mind checking the weather for me please? - I like sunny days in Berlin. + +responses: + utter_chitchat/ask_name: + - image: "https://i.imgur.com/zTvA58i.jpeg" + text: hello, my name is retrieval bot. + - text: Oh yeah, I am called the retrieval bot. + + utter_chitchat/ask_weather: + - text: Oh, it does look sunny right now in Berlin. + image: "https://i.imgur.com/vwv7aHN.png" + - text: I am not sure of the whole week but I can see the sun is out today. diff --git a/examples/responseselectorbot/domain.yml b/examples/responseselectorbot/domain.yml index f637a032b36a..35cfeb27093d 100644 --- a/examples/responseselectorbot/domain.yml +++ b/examples/responseselectorbot/domain.yml @@ -35,16 +35,6 @@ responses: utter_iamabot: - text: "I am a bot, powered by Rasa." - utter_chitchat/ask_name: - - image: "https://i.imgur.com/zTvA58i.jpeg" - text: hello, my name is retrieval bot. - - text: Oh yeah, I am called the retrieval bot. - - utter_chitchat/ask_weather: - - text: Oh, it does look sunny right now in Berlin. - image: "https://i.imgur.com/vwv7aHN.png" - - text: I am not sure of the whole week but I can see the sun is out today. - session_config: session_expiration_time: 60 # value in minutes carry_over_slots_to_new_session: true diff --git a/rasa/importers/rasa.py b/rasa/importers/rasa.py index 7b009295a663..dd6e0a28bdf9 100644 --- a/rasa/importers/rasa.py +++ b/rasa/importers/rasa.py @@ -58,6 +58,10 @@ async def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: async def get_domain(self) -> Domain: domain = Domain.empty() + + # If domain path is None, return an empty domain + if not self._domain_path: + return domain try: domain = Domain.load(self._domain_path) domain.check_missing_templates() From 84db4cd5267c20ae755affa4ce2deaa3dc5b0185 Mon Sep 17 00:00:00 2001 From: Daksh Date: Fri, 11 Sep 2020 14:14:08 +0200 Subject: [PATCH 24/25] review comments on shared --- rasa/core/actions/action.py | 7 +++++-- rasa/core/actions/forms.py | 8 ++------ rasa/core/constants.py | 1 - rasa/core/processor.py | 2 +- rasa/core/training/interactive.py | 3 +-- rasa/shared/constants.py | 1 + rasa/shared/core/constants.py | 2 -- rasa/shared/core/domain.py | 5 ++--- rasa/shared/nlu/constants.py | 2 -- .../shared/nlu/training_data/training_data.py | 9 +++------ rasa/shared/nlu/training_data/util.py | 20 ++++++++++++++++--- rasa/validator.py | 3 +-- 12 files changed, 33 insertions(+), 30 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 92499f6d9940..5ce487c2baaa 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -19,7 +19,11 @@ INTENT_RANKING_KEY, ) -from rasa.shared.constants import DOCS_BASE_URL, DEFAULT_NLU_FALLBACK_INTENT_NAME +from rasa.shared.constants import ( + DOCS_BASE_URL, + DEFAULT_NLU_FALLBACK_INTENT_NAME, + UTTER_PREFIX, +) from rasa.shared.core.constants import ( USER_INTENT_OUT_OF_SCOPE, ACTION_LISTEN_NAME, @@ -32,7 +36,6 @@ ACTION_DEFAULT_ASK_REPHRASE_NAME, ACTION_BACK_NAME, REQUESTED_SLOT, - UTTER_PREFIX, ) from rasa.shared.nlu.constants import INTENT_NAME_KEY from rasa.shared.core.events import ( diff --git a/rasa/core/actions/forms.py b/rasa/core/actions/forms.py index 9b4a9cbda1b4..2cf23a2690e2 100644 --- a/rasa/core/actions/forms.py +++ b/rasa/core/actions/forms.py @@ -8,12 +8,8 @@ from rasa.shared.core.domain import Domain from rasa.core.actions.action import ActionExecutionRejection, RemoteAction -from rasa.shared.core.constants import ( - ACTION_LISTEN_NAME, - LOOP_VALIDATE, - REQUESTED_SLOT, - UTTER_PREFIX, -) +from rasa.shared.core.constants import ACTION_LISTEN_NAME, LOOP_VALIDATE, REQUESTED_SLOT +from rasa.shared.constants import UTTER_PREFIX from rasa.shared.core.events import Event, SlotSet, ActionExecuted from rasa.core.nlg import NaturalLanguageGenerator from rasa.shared.core.trackers import DialogueStateTracker diff --git a/rasa/core/constants.py b/rasa/core/constants.py index ff279b99feb6..63f8c6bd6093 100644 --- a/rasa/core/constants.py +++ b/rasa/core/constants.py @@ -29,7 +29,6 @@ # the priority intended to be used by form policies # it is the highest to prioritize form to the rest of the policies FORM_POLICY_PRIORITY = 5 -UTTER_PREFIX = "utter_" DIALOGUE = "dialogue" diff --git a/rasa/core/processor.py b/rasa/core/processor.py index e32ed9f4b00d..8060511d5f0a 100644 --- a/rasa/core/processor.py +++ b/rasa/core/processor.py @@ -20,7 +20,6 @@ ACTION_LISTEN_NAME, ACTION_SESSION_START_NAME, REQUESTED_SLOT, - UTTER_PREFIX, ) from rasa.shared.core.domain import Domain from rasa.shared.core.events import ( @@ -38,6 +37,7 @@ INTENT_MESSAGE_PREFIX, DOCS_URL_DOMAINS, DEFAULT_SENDER_ID, + UTTER_PREFIX, ) from rasa.core.nlg import NaturalLanguageGenerator from rasa.core.policies.ensemble import PolicyEnsemble diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py index 02944b9e6c6e..e12679fc7c17 100644 --- a/rasa/core/training/interactive.py +++ b/rasa/core/training/interactive.py @@ -31,7 +31,6 @@ LOOP_VALIDATE, LOOP_REJECTED, REQUESTED_SLOT, - UTTER_PREFIX, ) from rasa.core import run, train, utils from rasa.core.constants import DEFAULT_SERVER_FORMAT, DEFAULT_SERVER_PORT @@ -47,7 +46,7 @@ UserUtteranceReverted, ) import rasa.core.interpreter -from rasa.shared.constants import INTENT_MESSAGE_PREFIX, DEFAULT_SENDER_ID +from rasa.shared.constants import INTENT_MESSAGE_PREFIX, DEFAULT_SENDER_ID, UTTER_PREFIX from rasa.shared.core.trackers import EventVerbosity, DialogueStateTracker from rasa.shared.core.training_data import visualization from rasa.shared.core.training_data.visualization import ( diff --git a/rasa/shared/constants.py b/rasa/shared/constants.py index 91b105cae1c0..eb4054855cc2 100644 --- a/rasa/shared/constants.py +++ b/rasa/shared/constants.py @@ -29,3 +29,4 @@ ENV_LOG_LEVEL = "LOG_LEVEL" DEFAULT_SENDER_ID = "default" +UTTER_PREFIX = "utter_" diff --git a/rasa/shared/core/constants.py b/rasa/shared/core/constants.py index 8464e913a9d7..7f074f1f9fca 100644 --- a/rasa/shared/core/constants.py +++ b/rasa/shared/core/constants.py @@ -70,8 +70,6 @@ SLOT_LAST_OBJECT_TYPE = "knowledge_base_last_object_type" DEFAULT_KNOWLEDGE_BASE_ACTION = "action_query_knowledge_base" -UTTER_PREFIX = "utter_" - # the keys for `State` (USER, PREVIOUS_ACTION, SLOTS, ACTIVE_LOOP) # represent the origin of a `SubState` USER = "user" diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index 2123e2cf0d66..a658f9643011 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -15,7 +15,6 @@ import rasa.shared.utils.validation import rasa.shared.utils.io import rasa.shared.utils.common -from rasa.shared.utils.common import lazy_property from rasa.shared.core.events import SlotSet, UserUttered from rasa.shared.core.slots import Slot, UnfeaturizedSlot, CategoricalSlot @@ -337,7 +336,7 @@ def _transform_intent_properties_for_internal_use( return intent - @lazy_property + @rasa.shared.utils.common.lazy_property def retrieval_intents(self) -> List[Text]: """List retrieval intents present in the domain.""" return [ @@ -1123,7 +1122,7 @@ def check_missing_templates(self) -> None: utterances = [ a for a in self.action_names - if a.startswith(rasa.shared.core.constants.UTTER_PREFIX) + if a.startswith(rasa.shared.constants.UTTER_PREFIX) ] missing_templates = [t for t in utterances if t not in self.templates.keys()] diff --git a/rasa/shared/nlu/constants.py b/rasa/shared/nlu/constants.py index 1c440ddbc105..12da6e94ba58 100644 --- a/rasa/shared/nlu/constants.py +++ b/rasa/shared/nlu/constants.py @@ -25,5 +25,3 @@ ENTITY_ATTRIBUTE_START = "start" ENTITY_ATTRIBUTE_END = "end" NO_ENTITY_TAG = "O" - -UTTER_PREFIX = "utter_" diff --git a/rasa/shared/nlu/training_data/training_data.py b/rasa/shared/nlu/training_data/training_data.py index 86af3bbc7cc6..ceaff00873a1 100644 --- a/rasa/shared/nlu/training_data/training_data.py +++ b/rasa/shared/nlu/training_data/training_data.py @@ -24,10 +24,7 @@ INTENT_NAME, ) from rasa.shared.nlu.training_data.message import Message -from rasa.shared.nlu.training_data.util import ( - check_duplicate_synonym, - intent_response_key_to_template_key, -) +from rasa.shared.nlu.training_data import util DEFAULT_TRAINING_DATA_OUTPUT_PATH = "training_data.json" @@ -79,7 +76,7 @@ def merge(self, *others: "TrainingData") -> "TrainingData": lookup_tables.extend(copy.deepcopy(o.lookup_tables)) for text, syn in o.entity_synonyms.items(): - check_duplicate_synonym( + util.check_duplicate_synonym( entity_synonyms, text, syn, "merging training data" ) @@ -244,7 +241,7 @@ def _fill_response_phrases(self) -> None: continue # look for corresponding bot utterance - story_lookup_key = intent_response_key_to_template_key( + story_lookup_key = util.intent_response_key_to_template_key( example.get_full_intent() ) assistant_utterances = self.responses.get(story_lookup_key, []) diff --git a/rasa/shared/nlu/training_data/util.py b/rasa/shared/nlu/training_data/util.py index b6fd55ba3bdd..2c98d91b6b84 100644 --- a/rasa/shared/nlu/training_data/util.py +++ b/rasa/shared/nlu/training_data/util.py @@ -15,7 +15,7 @@ ENTITY_ATTRIBUTE_ROLE, ENTITY_ATTRIBUTE_GROUP, ) -from rasa.shared.nlu.constants import UTTER_PREFIX +from rasa.shared.constants import UTTER_PREFIX import rasa.shared.utils.io logger = logging.getLogger(__name__) @@ -105,12 +105,26 @@ def remove_untrainable_entities_from(example: Dict[Text, Any]) -> None: def intent_response_key_to_template_key(intent_response_key: Text) -> Text: - """Resolve the response template key for a given intent response key.""" + """Resolve the response template key for a given intent response key. + + Args: + intent_response_key: retrieval intent with the response key suffix attached. + + Returns: The corresponding response template. + + """ return f"{UTTER_PREFIX}{intent_response_key}" def template_key_to_intent_response_key(template_key: Text) -> Text: - """Resolve the intent response key for a given response template key.""" + """Resolve the intent response key for the given response template. + + Args: + template_key: Name of the response template. + + Returns: The corresponding intent response key. + + """ return template_key.split(UTTER_PREFIX)[1] diff --git a/rasa/validator.py b/rasa/validator.py index aa61fc33af3c..5f4873039b55 100644 --- a/rasa/validator.py +++ b/rasa/validator.py @@ -4,8 +4,7 @@ import rasa.core.training.story_conflict from rasa.constants import DOCS_URL_ACTIONS -from rasa.shared.constants import DOCS_BASE_URL, DOCS_URL_DOMAINS -from rasa.shared.core.constants import UTTER_PREFIX +from rasa.shared.constants import DOCS_BASE_URL, DOCS_URL_DOMAINS, UTTER_PREFIX from rasa.shared.core.domain import Domain from rasa.shared.core.events import ActionExecuted from rasa.shared.core.events import UserUttered From 4e04e4fe698caa78209ea848eee23153baa83bc1 Mon Sep 17 00:00:00 2001 From: Daksh Date: Fri, 11 Sep 2020 14:53:49 +0200 Subject: [PATCH 25/25] add missing import --- rasa/shared/nlu/training_data/training_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/shared/nlu/training_data/training_data.py b/rasa/shared/nlu/training_data/training_data.py index ceaff00873a1..535dfea2e99c 100644 --- a/rasa/shared/nlu/training_data/training_data.py +++ b/rasa/shared/nlu/training_data/training_data.py @@ -481,7 +481,7 @@ def _needed_responses_for_examples( responses = {} for ex in examples: if ex.get(INTENT_RESPONSE_KEY) and ex.get(RESPONSE): - key = intent_response_key_to_template_key(ex.get_full_intent()) + key = util.intent_response_key_to_template_key(ex.get_full_intent()) responses[key] = self.responses[key] return responses