How can I quit from a Active Form

Hello everyone…

I am in doubt about how I can exit an active form using the user input (“fromText” method in slot type).

I tested the “/restart” command and saw that it takes precedence over all other methods, then closing the active form and clearing the slots.

This is exactly what I want, but the problem is that I would like to use another trigger word instead of “/restart”, such as “/cancel” or “/quit”.

Is there any way I can create custom actions that have a trigger that is a priority to all the others, in order to achieve this purpose, which is to close an active form from the user’s text input (using the “fromText” user text input method for the slot)?

there is default action action_deactivate_form that if predicted (you need to have corresponding stories) deactivates current active form.

another way is in your custom validate method return self._deactivate() if certain intent

3 Likes

Well, I tried returning self._deactivate() in my custom validation, and it deactivate but there is a problem: if there is a required field not filled yet in the form it will concatenate the self._deactivate() + set required_slot action to the next required field. So I don’t like this approach since, lets imagine this scenario:

  • I have a FormAction with 3 required fields:
  • TICKET_TYPE (using slot_mapping = self.from_text())
  • TICKET_DETAILS (using slot_mapping = self.from_text())
  • TICKET_QUANTITY (using slot_mapping = self.from_text())

So, when I enter in this Form it will ask for TICKET_TYPE, which expect any text from the user.

Ok then, now let’s suppose, when the bot asks me about TICKET_DETAILS (utter_ask_TICKET_DETAILS) I want to cancel the form with “/cancel”. If I cancel the form using the command “/cancel” (catching this string in the custom validation method) and then return self._deactivate(), it appears to concatenate the form deactivation command + setting a required slot, like:

[‘Form(None)’, ‘SlotSet(key: requested_slot, value: None)’, ‘SlotSet(key: requested_slot, value: TICKET_DETAILS)’]

When it should be:

[‘Form(None)’, ‘SlotSet(key: requested_slot, value: None)’]

So, what is the problem with canceling from inside the custom validation method? Basically, when it appends the “SlotSet(key: requested_slot, value: TICKET_DETAILS)”, it will call utter_ask_TICKET_DETAILS, which doesn’t make any sense in this context since the form is already canceled (and yet the bot will ask for the new field).

So the problem with this approach is the bot uttering ‘utter_ask_TICKET_DETAILS’ even after deactivating the form, which doesn’t make sense since the user is already out of the form (now deactivated).

This scenario and behavior don’t occur, however, when typing “/restart” in the user input.

1 Like

I see your point, so self._deactivate() should be returned in request_next_slot(...) then, instead of asking the next question

1 Like

Okay, but I will need to do some validation, no?

Because if I haven’t canceled it should request the next slot and not deactivate the form…

I thought in the “run” function, check if there is the deactivate form command, and if it exists, do not concatenate the command to request the next slot command, like (check TODO on what I was planning to do):

def run(self, dispatcher, tracker, domain):

    events = self._activate_if_required(tracker)
    events.extend(self._validate_if_required(dispatcher, tracker, domain))

    temp_tracker = tracker.copy()
    for e in events:
        if e['event'] == 'slot':
            temp_tracker.slots[e["name"]] = e["value"]

    next_slot_events = self.request_next_slot(dispatcher, temp_tracker,
                                              domain)
    #TODO: Develop an condition (if) that if the deactivate command combination exists ([‘Form(None)’, ‘SlotSet(key: requested_slot, value: None)’]) do not extend the events list with events.extend(next_slot_events)
    if next_slot_events is not None:
        events.extend(next_slot_events)
    else:
        events.extend(self.submit(dispatcher, temp_tracker, domain))
        events.extend(self._deactivate())

    return events

I’m not, however, very familiar with Python yet, and I do not know what would be the ideal and more performative way of doing this condition. It would be of great help if you could help me with the writing of this condition in the best practices.

Also, is there any way to dispatch an event? Like, from inside the custom validation method, dispatch some event which is immediately and synchronously executed and only after that return the events of the custom validation method?

there is no the best way. You can do validation, as you do now, then add condition in request_next_slot, smth along the line:

def request_next_slot(self,
                      dispatcher,  # type: CollectingDispatcher
                      tracker,  # type: Tracker
                      domain  # type: Dict[Text, Any]
                      ):
    # type: (...) -> Optional[List[Dict]]
    """Request the next slot and utter template if needed,
        else return None"""

    intent = tracker.latest_message.get("intent", {}).get("name")
    if intent == 'cancel':
        return self._deactivate()
    else:
        for slot in self.required_slots(tracker):
            if self._should_request_slot(tracker, slot):
                logger.debug("Request next slot '{}'".format(slot))
                dispatcher.utter_template("utter_ask_{}".format(slot),
                                          tracker,
                                          silent_fail=False,
                                          **tracker.slots)
                return [SlotSet(REQUESTED_SLOT, slot)]

        logger.debug("No slots left to request")
        return None
7 Likes

run method of a custom action returns a list of event that are executed afterwards

1 Like

Yes, I tried along the lines of what I’ve written (with a custom run method) but it is executed afterward so it doesn’t serve for solving this scenario.

I’ve now rewritten the custom action using a custom ‘request_next_slot’ along the lines of what you have written and I consider this a satisfactory solution for my scenario.

Thank you so much for the help!

1 Like

If the user raises another intent in the middle of a form, I’d like to switch intents. Can this also be accomplished with this approach?

I’m getting the following error when I call return self._deactivate()

AttributeError: ‘CustomerForm’ object has no attribute ‘_deactivate’

What version of rasa_core_sdk, do you use? We rename it to self.deactivate() at newer versions

I’m on v0.12.0

I don’t remember exactly in which version we changed it, please try without _

Please provide full action code

1 Like

NameError: name ‘logger’ is not defined

Im getting this error. when executing

1 Like

from rasa_sdk.forms import logger, REQUESTED_SLOT

first of import logger bt this line after that execute action file…i hope your problem will be solved.

when I’m using the deactivation custom action in my scenario.

Example: I’m giving cancel as input to deactivate the form. At that moment, the slots has been validating and giving default fallback question like ‘enter valid input’ along with form deactivation action.

I don’t want the validation question while deactivating the form. How to do it…

which version of Rasa are you use?

I’m using Rasa 1.10.12

@seera Can you share your code?

def request_next_slot(

    self,

    dispatcher,

    tracker,

    domain

    ):# type: (...) -> Optional[List[Dict]]

    

    "Request the next slot and utter template if needed, else return None"

    intent = tracker.latest_message.get("intent", {}).get("name")

    if intent == 'stop':

        dispatcher.utter_message(template="utter_restartflow")

        return self._deactivate()

    else:

        for slot in self.required_slots(tracker):

            if self._should_request_slot(tracker, slot):

                logger.debug("Request next slot '{}'".format(slot))

                dispatcher.utter_template("utter_ask_{}".format(slot),

                                          tracker,

                                          silent_fail=False,

                                          **tracker.slots)

                return [SlotSet(REQUESTED_SLOT, slot)]

        logger.debug("No slots left to request")

        return None