def _find_action_from_rules(self, tracker: DialogueStateTracker, domain: Domain) -> Optional[Text]: tracker_as_states = self.featurizer.prediction_states([tracker], domain) states = tracker_as_states[0] logger.debug(f"Current tracker state: {states}") rule_keys = self._get_possible_keys(self.lookup[RULES], states) predicted_action_name = None best_rule_key = "" if rule_keys: # TODO check that max is correct # if there are several rules, # it should mean that some rule is a subset of another rule best_rule_key = max(rule_keys, key=len) predicted_action_name = self.lookup[RULES].get(best_rule_key) active_form_name = tracker.active_form_name() if active_form_name: # find rules for unhappy path of the form form_unhappy_keys = self._get_possible_keys( self.lookup[RULES_FOR_FORM_UNHAPPY_PATH], states) # there could be several unhappy path conditions unhappy_path_conditions = [ self.lookup[RULES_FOR_FORM_UNHAPPY_PATH].get(key) for key in form_unhappy_keys ] # Check if a rule that predicted action_listen # was applied inside the form. # Rules might not explicitly switch back to the `Form`. # Hence, we have to take care of that. predicted_listen_from_general_rule = ( predicted_action_name == ACTION_LISTEN_NAME and ACTIVE_FORM_PREFIX + active_form_name not in best_rule_key) if predicted_listen_from_general_rule: if DO_NOT_PREDICT_FORM_ACTION not in unhappy_path_conditions: # negative rules don't contain a key that corresponds to # the fact that active_form shouldn't be predicted logger.debug( f"Predicted form '{active_form_name}' by overwriting " f"'{ACTION_LISTEN_NAME}' predicted by general rule.") return active_form_name # do not predict anything predicted_action_name = None if DO_NOT_VALIDATE_FORM in unhappy_path_conditions: logger.debug("Added `FormValidation(False)` event.") tracker.update(FormValidation(False)) if predicted_action_name is not None: logger.debug( f"There is a rule for the next action '{predicted_action_name}'." ) else: logger.debug("There is no applicable rule.") return predicted_action_name
def predict_action_probabilities(self, tracker: DialogueStateTracker, domain: Domain) -> List[float]: """Predicts the corresponding form action if there is an active form""" result = [0.0] * domain.num_actions if tracker.active_form.get("name"): logger.debug("There is an active form '{}'".format( tracker.active_form["name"])) if tracker.latest_action_name == ACTION_LISTEN_NAME: # predict form action after user utterance if tracker.active_form.get("rejected"): if self.state_is_unhappy(tracker, domain): tracker.update(FormValidation(False)) return result idx = domain.index_for_action(tracker.active_form["name"]) result[idx] = 1.0 elif tracker.latest_action_name == tracker.active_form.get("name"): # predict action_listen after form action idx = domain.index_for_action(ACTION_LISTEN_NAME) result[idx] = 1.0 else: logger.debug("There is no active form") return result
def predict_action_probabilities( self, tracker: DialogueStateTracker, domain: Domain, interpreter: NaturalLanguageInterpreter = RegexInterpreter(), **kwargs: Any, ) -> List[float]: """Predicts the corresponding form action if there is an active form""" result = self._default_predictions(domain) if tracker.active_loop.get("name"): logger.debug( "There is an active form '{}'".format(tracker.active_loop["name"]) ) if tracker.latest_action_name == ACTION_LISTEN_NAME: # predict form action after user utterance if tracker.active_loop.get("rejected"): if self.state_is_unhappy(tracker, domain): tracker.update(FormValidation(False)) return result idx = domain.index_for_action(tracker.active_loop["name"]) result[idx] = 1.0 elif tracker.latest_action_name == tracker.active_loop.get("name"): # predict action_listen after form action idx = domain.index_for_action(ACTION_LISTEN_NAME) result[idx] = 1.0 else: logger.debug("There is no active form") return result
def predict_action_probabilities( self, tracker: DialogueStateTracker, domain: Domain, interpreter: NaturalLanguageInterpreter, **kwargs: Any, ) -> List[float]: """Predicts the corresponding form action if there is an active form""" result = self._default_predictions(domain) if tracker.active_loop_name: logger.debug("There is an active form '{}'".format( tracker.active_loop_name)) if tracker.latest_action_name == ACTION_LISTEN_NAME: # predict form action after user utterance if tracker.active_loop.get(LOOP_REJECTED): if self.state_is_unhappy(tracker, domain): tracker.update(FormValidation(False)) return result result = self._prediction_result(tracker.active_loop_name, tracker, domain) elif tracker.latest_action_name == tracker.active_loop_name: # predict action_listen after form action result = self._prediction_result(ACTION_LISTEN_NAME, tracker, domain) else: logger.debug("There is no active form") return result
def predict_action_probabilities(self, tracker: DialogueStateTracker, domain: Domain) -> List[float]: """Predicts the corresponding form action if there is an active form""" result = [0.0] * domain.num_actions if tracker.active_form.get('name'): logger.debug("There is an active form '{}'" "".format(tracker.active_form['name'])) if tracker.latest_action_name == ACTION_LISTEN_NAME: # predict form action after user utterance if tracker.active_form.get('rejected'): # since it is assumed that training stories contain # only unhappy paths, notify the form that # it should not be validated if predicted by other policy tracker_as_states = self.featurizer.prediction_states( [tracker], domain) states = tracker_as_states[0] memorized_form = self.recall(states, tracker, domain) if memorized_form == tracker.active_form['name']: logger.debug("There is a memorized tracker state {}, " "added `FormValidation(False)` event" "".format(self._modified_states(states))) tracker.update(FormValidation(False)) return result idx = domain.index_for_action(tracker.active_form['name']) result[idx] = 1.0 elif tracker.latest_action_name == tracker.active_form.get('name'): # predict action_listen after form action idx = domain.index_for_action(ACTION_LISTEN_NAME) result[idx] = 1.0 else: logger.debug("There is no active form") return result
async def test_form_unhappy_path_no_validation_from_story(): form_name = "some_form" handle_rejection_action_name = "utter_handle_rejection" domain = Domain.from_yaml(f""" intents: - {GREET_INTENT_NAME} actions: - {UTTER_GREET_ACTION} - {handle_rejection_action_name} - some-action slots: {REQUESTED_SLOT}: type: unfeaturized forms: - {form_name} """) unhappy_story = TrackerWithCachedStates.from_events( "bla", domain=domain, slots=domain.slots, evts=[ # We are in an active form ActionExecuted(form_name), ActiveLoop(form_name), # When a user says "hi", and the form is unhappy, # we want to run a specific action UserUttered(intent={"name": GREET_INTENT_NAME}), ActionExecuted(handle_rejection_action_name), ActionExecuted(ACTION_LISTEN_NAME), # Next user utterance is an answer to the previous question # and shouldn't be validated by the form UserUttered(intent={"name": GREET_INTENT_NAME}), ActionExecuted(form_name), ActionExecuted(ACTION_LISTEN_NAME), ], ) policy = RulePolicy() policy.train([unhappy_story], domain, RegexInterpreter()) # Check that RulePolicy predicts no validation to handle unhappy path conversation_events = [ ActionExecuted(form_name), ActiveLoop(form_name), SlotSet(REQUESTED_SLOT, "some value"), ActionExecuted(ACTION_LISTEN_NAME), UserUttered("haha", {"name": GREET_INTENT_NAME}), ActionExecutionRejected(form_name), ActionExecuted(handle_rejection_action_name), ActionExecuted(ACTION_LISTEN_NAME), UserUttered("haha", {"name": GREET_INTENT_NAME}), ] tracker = DialogueStateTracker.from_events("casd", evts=conversation_events, slots=domain.slots) action_probabilities = policy.predict_action_probabilities( tracker, domain, RegexInterpreter()) # there is no rule for next action assert max(action_probabilities) == policy._core_fallback_threshold # check that RulePolicy added FormValidation False event based on the training story assert tracker.events[-1] == FormValidation(False)
async def test_form_unhappy_path_no_validation_from_rule(): form_name = "some_form" handle_rejection_action_name = "utter_handle_rejection" domain = Domain.from_yaml(f""" intents: - {GREET_INTENT_NAME} actions: - {UTTER_GREET_ACTION} - {handle_rejection_action_name} - some-action slots: {REQUESTED_SLOT}: type: unfeaturized forms: - {form_name} """) unhappy_rule = TrackerWithCachedStates.from_events( "bla", domain=domain, slots=domain.slots, evts=[ # We are in an active form ActiveLoop(form_name), SlotSet(REQUESTED_SLOT, "bla"), ActionExecuted(RULE_SNIPPET_ACTION_NAME), ActionExecuted(ACTION_LISTEN_NAME), # When a user says "hi", and the form is unhappy, # we want to run a specific action UserUttered(intent={"name": GREET_INTENT_NAME}), ActionExecuted(handle_rejection_action_name), # Next user utterance is an answer to the previous question # and shouldn't be validated by the form ActionExecuted(ACTION_LISTEN_NAME), UserUttered(intent={"name": GREET_INTENT_NAME}), ActionExecuted(form_name), ActionExecuted(ACTION_LISTEN_NAME), ], is_rule_tracker=True, ) policy = RulePolicy() # RulePolicy should memorize that unhappy_rule overrides GREET_RULE policy.train([GREET_RULE, unhappy_rule], domain, RegexInterpreter()) # Check that RulePolicy predicts action to handle unhappy path conversation_events = [ ActionExecuted(form_name), ActiveLoop(form_name), SlotSet(REQUESTED_SLOT, "some value"), ActionExecuted(ACTION_LISTEN_NAME), UserUttered("haha", {"name": GREET_INTENT_NAME}), ActionExecutionRejected(form_name), ] action_probabilities = policy.predict_action_probabilities( DialogueStateTracker.from_events("casd", evts=conversation_events, slots=domain.slots), domain, RegexInterpreter(), ) assert_predicted_action(action_probabilities, domain, handle_rejection_action_name) # Check that RulePolicy predicts action_listen conversation_events.append(ActionExecuted(handle_rejection_action_name)) action_probabilities = policy.predict_action_probabilities( DialogueStateTracker.from_events("casd", evts=conversation_events, slots=domain.slots), domain, RegexInterpreter(), ) assert_predicted_action(action_probabilities, domain, ACTION_LISTEN_NAME) # Check that RulePolicy triggers form again after handling unhappy path conversation_events.append(ActionExecuted(ACTION_LISTEN_NAME)) tracker = DialogueStateTracker.from_events("casd", evts=conversation_events, slots=domain.slots) action_probabilities = policy.predict_action_probabilities( tracker, domain, RegexInterpreter()) assert_predicted_action(action_probabilities, domain, form_name) # check that RulePolicy added FormValidation False event based on the training rule assert tracker.events[-1] == FormValidation(False)
async def test_persist_form_story(tmpdir): domain = Domain.load("data/test_domains/form.yml") tracker = DialogueStateTracker("", domain.slots) story = ("* greet\n" " - utter_greet\n" "* start_form\n" " - some_form\n" ' - form{"name": "some_form"}\n' "* default\n" " - utter_default\n" " - some_form\n" "* stop\n" " - utter_ask_continue\n" "* affirm\n" " - some_form\n" "* stop\n" " - utter_ask_continue\n" " - action_listen\n" "* form: inform\n" " - some_form\n" ' - form{"name": null}\n' "* goodbye\n" " - utter_goodbye\n") # simulate talking to the form events = [ UserUttered(intent={"name": "greet"}), ActionExecuted("utter_greet"), ActionExecuted("action_listen"), # start the form UserUttered(intent={"name": "start_form"}), ActionExecuted("some_form"), Form("some_form"), ActionExecuted("action_listen"), # out of form input UserUttered(intent={"name": "default"}), ActionExecutionRejected("some_form"), ActionExecuted("utter_default"), ActionExecuted("some_form"), ActionExecuted("action_listen"), # out of form input UserUttered(intent={"name": "stop"}), ActionExecutionRejected("some_form"), ActionExecuted("utter_ask_continue"), ActionExecuted("action_listen"), # out of form input but continue with the form UserUttered(intent={"name": "affirm"}), FormValidation(False), ActionExecuted("some_form"), ActionExecuted("action_listen"), # out of form input UserUttered(intent={"name": "stop"}), ActionExecutionRejected("some_form"), ActionExecuted("utter_ask_continue"), ActionExecuted("action_listen"), # form input UserUttered(intent={"name": "inform"}), FormValidation(True), ActionExecuted("some_form"), ActionExecuted("action_listen"), Form(None), UserUttered(intent={"name": "goodbye"}), ActionExecuted("utter_goodbye"), ActionExecuted("action_listen"), ] [tracker.update(e) for e in events] assert story in tracker.export_stories()
def predict_action_probabilities( self, tracker: DialogueStateTracker, domain: Domain, interpreter: NaturalLanguageInterpreter = RegexInterpreter(), **kwargs: Any, ) -> List[float]: """Predicts the next action the bot should take after seeing the tracker. Returns the list of probabilities for the next actions. If memorized action was found returns 1 for its index, else returns 0 for all actions. """ result = self._default_predictions(domain) if not self.is_enabled: return result # Rasa Open Source default actions overrule anything. If users want to achieve # the same, they need to a rule or make sure that their form rejects # accordingly. rasa_default_action_name = _should_run_rasa_default_action(tracker) if rasa_default_action_name: result[domain.index_for_action(rasa_default_action_name)] = 1 return result active_form_name = tracker.active_form_name() active_form_rejected = tracker.active_loop.get("rejected") should_predict_form = (active_form_name and not active_form_rejected and tracker.latest_action_name != active_form_name) should_predict_listen = (active_form_name and not active_form_rejected and tracker.latest_action_name == active_form_name) # A form has priority over any other rule. # The rules or any other prediction will be applied only if a form was rejected. # If we are in a form, and the form didn't run previously or rejected, we can # simply force predict the form. if should_predict_form: logger.debug(f"Predicted form '{active_form_name}'.") result[domain.index_for_action(active_form_name)] = 1 return result # predict `action_listen` if form action was run successfully if should_predict_listen: logger.debug( f"Predicted '{ACTION_LISTEN_NAME}' after form '{active_form_name}'." ) result[domain.index_for_action(ACTION_LISTEN_NAME)] = 1 return result possible_keys = set(self.lookup.keys()) tracker_as_states = self.featurizer.prediction_states([tracker], domain) states = tracker_as_states[0] logger.debug(f"Current tracker state: {states}") for i, state in enumerate(reversed(states)): possible_keys = set( filter(lambda _key: self._rule_is_good(_key, i, state), possible_keys)) if possible_keys: # TODO rethink that key = max(possible_keys, key=len) recalled = self.lookup.get(key) if active_form_name: # Check if a rule that predicted action_listen # was applied inside the form. # Rules might not explicitly switch back to the `Form`. # Hence, we have to take care of that. predicted_listen_from_general_rule = recalled is None or ( domain.action_names[recalled] == ACTION_LISTEN_NAME and f"active_form_{active_form_name}" not in key) if predicted_listen_from_general_rule: logger.debug(f"Predicted form '{active_form_name}'.") result[domain.index_for_action(active_form_name)] = 1 return result # Since rule snippets inside the form contain only unhappy paths, # notify the form that # it was predicted after an answer to a different question and # therefore it should not validate user input for requested slot predicted_form_from_form_rule = ( domain.action_names[recalled] == active_form_name and f"active_form_{active_form_name}" in key) if predicted_form_from_form_rule: logger.debug("Added `FormValidation(False)` event.") tracker.update(FormValidation(False)) if recalled is not None: logger.debug(f"There is a rule for next action " f"'{domain.action_names[recalled]}'.") result[recalled] = 1 else: logger.debug("There is no applicable rule.") return result