Form deactivation inside validate function

I want to change the validate function so that I can deactivate my form and delete the value of requested_slot before raise ActionExecutionRejection(...).

def validate(self, dispatcher, tracker, domain):
    # type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
    """Extract and validate value of requested slot.

    If nothing was extracted reject execution of the form action.
    Subclass this method to add custom validation and rejection logic
    """
    # extract other slots that were not requested
    # but set by corresponding entity or trigger intent mapping
    slot_values = self.extract_other_slots(dispatcher, tracker, domain)

    # extract requested slot
    slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
    if slot_to_fill:
        slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))

        if not slot_values:
	     tracker.active_form = {}
	     tracker.slots["requested_slot"] = None

            # reject to execute the form action
            # if some slot was requested but nothing was extracted
            # it will allow other policies to predict another action
            raise ActionExecutionRejection(
                self.name(),
                "Failed to extract slot {0} "
                "with action {1}"
                "".format(slot_to_fill, self.name()),
            )
    logger.debug("Validating extracted slots: {}".format(slot_values))
    return self.validate_slots(slot_values, dispatcher, tracker, domain)

I added these two lines:

tracker.active_form = {}
tracker.slots["requested_slot"] = None

But, they didn’t work and nothing changed. Do you know what is wrong?

I think if not slot_values: means no entities are recognized by the bot, and

The way this works with forms is that a form will raise an ActionExecutionRejection if the user didn’t provide the requested information.

If the bot didn’t recognize any entities, especially for the requested slot, then isn’t the requested slot’s value still None ?

So these two lines didn’t do anything:

tracker.active_form = {}
tracker.slots["requested_slot"] = None

That’s my guess, i’m not so sure of what you are trying to achieve, but in my opinion if the ActionExecutionRejection is raised, no slots will be assigned any value.

Yes, if not slot_values: means that your user didn’t provide the requested information so the value of slot_to_fill ( slot_to_fill or the value of requested_slot slot) is None.

When the user doesn’t provide the requested information, an ActionExecutionRejection will raise and rasa will try to predict another action but this form remains active and the value of requested_slot slot remains the same. Rasa continues its path but It always waits for the value of requested_slot to finish the form and deactivate it. Until that time you can not activate another form.

I want to be able to deactivate the form. I don’t want to get the value of requested_slot slot form user anymore. So, I tried to deactivate this form by tracker.active_form = {} and tell rasa to move on and stop waiting for the value of tracker.slots["requested_slot"]. But this is not the right way to achieve this goal.

Hi @mac_71128,

I am currently using this to deactivate a form:

def validate(self, dispatcher, tracker, domain):
    # type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
    """Extract and validate value of requested slot.

    If nothing was extracted reject execution of the form action.
    Subclass this method to add custom validation and rejection logic
    """

    slot_to_fill = tracker.get_slot(REQUESTED_SLOT)

    reset = self._check_reset_form_action_on_intent(tracker=tracker, dispatcher=dispatcher)
    if reset:
        return self.deactivate() 

Where self._check_reset_form_action_on_intent(tracker=tracker, dispatcher=dispatcher) is implemented like this:

def _check_reset_form_action_on_intent(self, tracker: Tracker, dispatcher):
    """

    :param tracker:
    :param dispatcher:
    :return:
    """
    last_intent = tracker.latest_message['intent'].get('name')
    if last_intent == 'reset_dialogue':
        dispatcher.utter_message('Okay, gern. Was kann ich sonst fĂźr Sie tun?')
        return True
    else:
        return False

Of course there won’t be any need of using such a method - you can return the deactivate statement right out of validate, if that suits your case.

Did that help you?

Regards

2 Likes

Hi @JulianGerhard. Thanks for your reply.

This will deactivate my form but you can only use it when you want to give your user an option to cancel your form. But, I want to give my users an option to change the topic of conversation whenever they want. For example, a user is in the middle of a restaurant form. Bot ask him about something to find the value of REQUESTED_SLOT but the user reply like this:

‘Forget it, Can you get me a taxi, please?’

User changes the topic. I want to deactivate restaurant form. Then try to predict another action. For example, rasa can answer this by activating a taxi form. Then my bot can ask about user location. For example:

‘What is your location?’

By using functions like _check_reset_form_action_on_intent, you can only deactivate your form(based on some conditions like reset_dialogue intent) but I want to answer my users too. I want to answer them automatically and based on our predefined policies and stories,…

When ActionExecutionRejection raises, rasa tries to predict another action(which is exactly what I want), but the problem is what I mentioned before(form remained active). I solved it by changing the library of rasa. I add deactivate_form=True to ActionExecutionRejection:

raise ActionExecutionRejection(
    self.name(),
    "Failed to extract slot {0} "
    "with action {1}"
    "".format(slot_to_fill, self.name()),
    deactivate_form=True
)

and handle this deactivation inside this section: https://github.com/RasaHQ/rasa/blob/master/rasa/core/actions/action.py#L412.

I solved this by changing the library of rasa because this seemed to be the only way. Do you have another suggestion?

Hi @mac_71128,

okay - got it. How about handling in your story? Imagine this one:

  • start_restaurant_form
    • restaurant_form
    • form{“name”: “restaurant_form”}
    • slot{“requested_slot”: “style”}
  • ask_for_a_taxi
    • action_deactivate_form
    • taxi_form
    • form{“name”: “taxi_form”}
    • slot{“requested_slot”: “location”}

where action_deactivate_form simply ends the current form before starting anything else, documented here.

Did that help you? Regards

It should work, but imagine if he has 10 more intents and forms and the user can switch between any of them, it would be a pain creating all the stories. I solved it by subclass the FormAction to create a CustomFormAction. In which if the ActionExecutionRejection is raised, i will return self.deactivate() and follow up with action_redirect (a custom action that follow up to a form based on the last intent). Doing that i am able to switch to any form, from anywhere. There are some conditions need to be checked but generally that’s the idea.

Exactly. I can’t handle it using an unhappy story for each possible intent because I have 50 other intents. I have to write 50 stories for each slot of my form.

Thank you @fuih for sharing your idea but I’m confused. What is the content of action_redirect? Did you check the intent of the last message inside this action and trigger another action based on that intent?

Exactly.

In the validate function, instead of just raising the Exception, i check if the user’s intent is a form intent (i define a list of them). If it does, i deactivate the form and follow up to the action_redirect. Otherwise i raise the Exception normally (in case the user enters some nonsense).

Then the action_redirect does exactly what you said. I have a dictionary mapping intents and their corresponding forms. Yes defining a 50 elements dict is still annoying, but more doable than writing tons of stories :smile:.

Actually, you can do it without the action_redirect, the form policy should just predicts the next form after you end the previous form i think. I just happen to need setting some slots before activating a form so i make the action_redirect

Hi @fuih,

that’s surely true but I think he asked for an update-secure solution that follows the rasa conventions and the CustomFormAction will e.g. result in adaptive work if rasa decides to change something in the above layer. However, there are usually several ways leading to Rome! :slight_smile:

Regards Julian

1 Like

Hi @amn41. Is it possible to have this feature in the next rasa versions?

I deactivate my forms in the following way:

  1. Create a deactivate_intent with NLU data.
  2. Create a custom form_deactivation_action.
  3. Create a story for deactivation only using deactivate_intent and form_deactivation_action.

It is easy to create one single intent for all the cases where you want to invoke deactivation_intent.

The code for run method of custom action form_deactivation_action is as follows:

dispatcher.utter_message('Current form deactivated')
return [Form(None), SlotSet('requested_slot', None)]

Even when a form is active in a conversation, I can invoke my deactivate_intent at any point by writing a message similar to NLU data of deactivate_intent. Rest is handled by Rasa.

1 Like

Hey, I follow same method but It didn’t work for me. How you invoke deactivation_intent ?

@ tonysinghmss Thanks for your reply, But as I told to Julian, I want to deactivate my form without defining a deactivate_intent. I want to handle the context switch. For example, a user is in the middle of a restaurant form. Bot ask him about something to find the value of REQUESTED_SLOT(for example cuisine) but the user reply like this:

‘Can you get me a taxi, please?’

User changes the topic. I want to deactivate restaurant form, then switch to taxi form and start to ask him about the location.

I can’t handle it using unhappy stories I have at least 50 other intents. So writing tons of stories is impossible for me!

I solved my problem by changing a couple of lines of 4 files of rasa and rasa_sdk libraries. But I don’t like to change libraries!

@ gaurangubhatt As Julian and Manishankar mentioned:

  1. Define your deactivation_intent
  2. Define your custom form
  3. Inside validate function of your form, check the intent of last message and deactivate your form if the intent of last message was deactivate_intent:
    def validate(
        self,
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: Dict[Text, Any],
    ) -> List[EventType]:
        """Extract and validate value of requested slot.

        If nothing was extracted reject execution of the form action.
        Subclass this method to add custom validation and rejection logic
        """

        # extract other slots that were not requested
        # but set by corresponding entity or trigger intent mapping
        slot_values = self.extract_other_slots(dispatcher, tracker, domain)

        # extract requested slot
        slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
        if slot_to_fill:
            slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))

            if not slot_values:
                if tracker.latest_message.get("intent", {}).get("name")=='deactivation':
                       self.deactivate()
                # reject to execute the form action
                # if some slot was requested but nothing was extracted
                # it will allow other policies to predict another action
                raise ActionExecutionRejection(
                    self.name(),
                    "Failed to extract slot {0} "
                    "with action {1}"
                    "".format(slot_to_fill, self.name()),
                )
        logger.debug("Validating extracted slots: {}".format(slot_values))
        return self.validate_slots(slot_values, dispatcher, tracker, domain)
2 Likes

Hey @JulianGerhard, thank you for the solution, works perfectly ! Is there a way I can reset all slots before returning self.deactivate ? When I call the form again, the bot continues from where we stopped, I don’t want that.

Thanks a lot !

To reset all slots you can define a custom action:

class ActionFlushSlots(Action):

    def name(self) -> Text:
        return "action_flush_slots"

    def run(self, dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

        slots = []
        for key, value in tracker.current_slot_values().items():
            if value is not None:
                slots.append(SlotSet(key=key, value=None))

        return slots

You can invoke this action wherever you want in your stories and rules. Alternatively, you can add to your validate function the lines of code inside the run method above.

2 Likes