Exiting rasa form in the middle

So I’m working on a chatbot that accepts free form text from the user. There are 5 slots they can fill. However, I want them to be able to opt-out at any point by typing “done” on a blank line. So I set up a done intent in the nlu. And I created stories that after the done intent deactivate the loop (see below). But the form is not getting deactivated. The done intent get’s detected but it returns to filling the form even though the story deactivates the loop. Can someone tell me what I’m doing wrong? @rctatman

Stories.yml

- story: done in reflection -- add more
  steps:
  - intent: reflection_submit
  - action: utter_reflection_submit
  - action: reflection_form
  - active_loop: reflection_form
  - intent: done
  - action: action_deactivate_loop
  - active_loop: null
  - action: action_process_reflections
  - action: utter_reflection_continue
  - intent: reflection_retry
  - action: utter_reflection_retry
  - action: utter_reflection_text
  - action: reflection_form
  - active_loop: reflection_form
  
- story: done in reflection -- submit
  steps:
  - intent: reflection_submit
  - action: utter_reflection_submit
  - action: reflection_form
  - active_loop: reflection_form
  - intent: done
  - action: action_deactivate_loop
  - active_loop: null
  - action: action_process_reflections
  - action: utter_reflection_continue
  - intent: submit_as_is
  - action: utter_submit_as_is

nlu.yml

- intent: done
  examples: |
    - done
    - Done 
    - I'm Done
    - Im done
    - be done

domain.yml

utter_submit_options:
    - text: What do you want to submit?
      buttons:
      - payload: /kritik_submit
        title: Article analysis to Kritik.io
      - payload: /reflection_submit
        title: Lecture Reflection
  utter_reflection_submit:
    - text: Ok! Tell me your thoughts about the lecture.
  utter_reflection_continue:
    - text: Would you like to add more?
      buttons:
      - payload: /reflection_retry
        title: Add More
      - payload: /submit_as_is
        title: Submit As Is
  utter_reflection_retry:
    - text: Ok! Here's your current reflection. You can copy and paste it below and then add to it. Hit enter and type "done" when you are finished
  utter_reflection_text:
    - text: '{full_reflection}'
  utter_submit_as_is:
  - text: Your reflection has been recorded, you've gotten credit for this assignment.
actions:
- action_check_analysis
- action_get_date
- action_look_up
- action_wait
- utter_anything_else
- utter_articleanalysis
- utter_ask_email_form_email
- utter_greet
- utter_wait_continue
- validate_email_form
- action_purge_done
- action_process_reflections
forms:
  email_form:
    email:
    - type: from_entity
      entity: email
  reflection_form:
    reflection1:
    - type: from_text
    reflection2:
    - type: from_text
    reflection3:
    - type: from_text
    reflection4:
    - type: from_text
    reflection5:
    - type: from_text

So, I think I have this solved. There is a lot of old information in the forums about this so I am going to try to compile what works in 2.7 YMMV

First to exit a form, you do this during slot validation. Every slot has to have its own validation logic but as I have done, you can use a subroutine. I use 5 reflection slots (labeled reflection1 to reflection5) and then each runs a subroutine which does the same thing for each. For my form, if the user enters “done” in an empty slot, it will deactivate the form. Deactivation is currently very simple, you set

return {"requested_slot":None}

as indicated here. Below is my code in actions.py. I start the form using a rule, and then deactivate it as well which then runs a custom action. (see below). The one thing about my use case, which may be of interest to others, is that I wanted it to be that if the student had not fulfilled the minimum word requirement, it would allow them to continue adding to the form. That was a little tricky. Just restarting the form wouldn’t work. I needed to do two things in a custom action. Change the slot where they typed “done” to None, and then change the “requested_slot” slot to the name of that slot. If I just set the requested slot to the slot that had “done” it wouldn’t work (presumably because there was something already in it). So both steps were necessary to make it work. (code below)

Validation code

class ValidateReflectionForm(FormValidationAction):
    def name(self)-> Text:
        return "validate_reflection_form"

    def process_reflections(self, dispatcher: CollectingDispatcher,Tracker,slot_value) -> List[Text]:
        start_key="reflection"
        full_reflection=""
        for i in range(1,6):
            key=start_key+str(i)
            reflection=Tracker.get_slot(key)
            if reflection and reflection!="done":
                full_reflection+=reflection
        if slot_value.lower()=="done":
            return full_reflection
        return
    def validate_reflection1(self,
        slot_value: Any,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: DomainDict,
    ) -> Dict[Text, Any]:
        full_reflection=self.process_reflections(dispatcher,tracker,slot_value)
        if full_reflection:
            return {"reflection1":slot_value,"full_reflection":full_reflection, "requested_slot":None}
        else:
            return {"reflection1":slot_value}

    def validate_reflection2(self,
        slot_value: Any,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: DomainDict,
    ) -> Dict[Text, Any]:
        full_reflection=self.process_reflections(dispatcher,tracker,slot_value)
        if full_reflection:
            return {"reflection2":slot_value,"full_reflection":full_reflection, "requested_slot":None}
        else:
            return {"reflection2":slot_value}

    def validate_reflection3(self,
        slot_value: Any,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: DomainDict,
    ) -> Dict[Text, Any]:
        full_reflection=self.process_reflections(dispatcher,tracker,slot_value)
        if full_reflection:
            return {"reflection3":slot_value,"full_reflection":full_reflection, "requested_slot":None}
        else:
            return {"reflection3":slot_value}
    def validate_reflection4(self,
        slot_value: Any,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: DomainDict,
    ) -> Dict[Text, Any]:
        full_reflection=self.process_reflections(dispatcher,tracker,slot_value)
        if full_reflection:
            return {"reflection4":slot_value, "full_reflection":full_reflection, "requested_slot":None}
        else:
            return {"reflection4":slot_value}
    def validate_reflection5(self,
        slot_value: Any,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: DomainDict,
    ) -> Dict[Text, Any]:
        full_reflection=self.process_reflections(dispatcher,tracker,slot_value)
        if full_reflection:
            return {"reflection5":slot_value, "full_reflection":full_reflection, "requested_slot":None}
        else:
            return {"reflection5":slot_value}

rules.yml

- rule: start reflection
  steps:
  - intent: reflection_submit
  - action: utter_reflection_submit
  - action: reflection_form
  - active_loop: reflection_form
   
- rule: deactivate reflection form
  condition: 
  - active_loop: reflection_form
  steps:
  - action: reflection_form
  - slot_was_set: 
    - full_reflection: this is about robots
  - active_loop: null
  - action: action_process_reflections
  - action: utter_reflection_continue

reset form action code

lass ActionResetReflectionForm(Action):

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

    def run(self, dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
        start_key="reflection"
        for i in range(1,6):
            key="reflection"+str(i)
            reflection=tracker.get_slot(key)
            if reflection=="done":
                next_slot=key
                print(f"reset requested_slot to {next_slot}")

        return [SlotSet("requested_slot",next_slot), SlotSet(next_slot,None)]

I have the same issue with you, and I found out that action_deactivate_loop only work for the simple form defined in domain.yml without any custom validation, example this simple_form story work like a charm.

stories:
- story: Simple Form Stop
  steps:
  - intent: start_form
  - action: simple_form
  - active_loop: simple_form
  - intent: stop_application
  - action: action_deactivate_loop
  - active_loop: null
  - action: utter_goodbye

By having a form with custom validation, example leaves_form in my case defining story as below:

stories:
- story: Apply Leave Stop
  steps:
  - intent: apply_leave
  - action: leaves_form
  - active_loop: leaves_form
  - intent: stop_application
  - action: action_deactivate_loop
  - active_loop: null
  - action: utter_goodbye

This story will stuck at - active_loop: leaves_form and ignored the later intent/action as long as the custom form validation return value for requested_slot

I comes up 2 solutions, one similar with you, either

  1. implement a condition check that would trigger requested_slot: None in all validate_slot or
  2. overide validate function in form validation to add the condition check for every trigger

For my opinion, I think both solutions are weird in nature, one applying duplicate code to all slots function, and another one override the core validation function which might cause issue later
Just a question, do we able to trigger deactivation in a more general way?