RASA - Custom forms
Tags: rasa, chatbot, nlp, ai
Published: 2021-06-18 05:53:10

What if we wanted to have a question in our form that should be asked only if the answer to the previous question was *"yes"*?

This is where custom actions come in handy.

> A custom action can run any code you want, including API calls, database queries etc. They can turn on the lights, add an event to a calendar, check a user's bank balance, or anything else you can imagine.

You can learn more about custom actions in the [documentation](https://rasa.com/docs/rasa/custom-actions).

We will add two questions to our existing newsletter subscription form. First one asking if the user is OK with telling us their age and second one asking the age. Naturally, we want to ask the second question only if the user is fine with it.

To store the information from the user, we need to have the corresponding slots.

```yml
# domain.yml
# ... previous content ...
slots:
  # ... previous slots ...
  can_ask_age:
    type: bool
    influence_conversation: false  
  age:
    type: text
    influence_conversation: false
```

We also need to add the slots to the form, so that the corresponding questions are asked during the form loop.

```yml
# domain.yml
# ... previous content ...
forms:
  newsletter_form:
    # ... previous slots ...
    can_ask_age:
      - type: from_intent
        intent: affirm
        value: true
      - type: from_intent
        intent: deny
        value: false
    age:
      - type: from_entity
        entity: age
```

**can_ask_age** is a **bool** slot. If the intent is **affirm**, the **value** assigned to that slot will be **true**. If the intent is **deny**, the **value** assigned to that slot will be **false**.

You can learn more about boolean slots in the [documentation](https://rasa.com/docs/rasa/domain#boolean-slot).

Notice that we can use any number of **type** keys telling how the slot should be filled. The first one from the top that is matched is used.

Now let's add the questions. Remember, the name must be either `utter_ask_<form_name>_<slot_name>` or `utter_ask_<slot_name>`.

```yml
# domain.yml
# ... previous content ...
responses:
  # ... previous responses ...
  utter_ask_can_ask_age:
    - text: Can I ask what your age is?
  utter_ask_age:
    - text: What is your age?
```

Right now, the *"What is your age?"* question is always asked. This is because all slots that are part of a form are mandatory and have to be filled before the form ends. Luckily for us, there is an easy way to tell the chatbot what the mandatory slots are based on what we already know – like that the user does not want to share their age.

This is accomplished through custom actions. Custom actions live in `actions/actions.py` file.

For our use case, we will use a form validation action.
Form validation action is a class that extends a `FormValidationAction` class.
It has to implement a `name` method, which returns the name of that action. If we name it `validate_<form_name>`, it will be run automatically after each step of the form.

You can learn more about validation actions in the [documentation](https://rasa.com/docs/rasa/forms#validating-form-input).

We then override the `required_slots` method to tell what slots are mandatory and have to be filled.

You can learn more about this dynamic behaviour in the [documentation](https://rasa.com/docs/rasa/forms#dynamic-form-behavior). 

```python
# actions/actions.py
class ValidateNewsletterForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_newsletter_form"

    async def required_slots(
            self,
            slots_mapped_in_domain: List[Text],
            dispatcher: "CollectingDispatcher",
            tracker: "Tracker",
            domain: "DomainDict",
    ) -> Optional[List[Text]]:
        if not tracker.get_slot("can_ask_age"):
            slots_mapped_in_domain.remove("age")

        return slots_mapped_in_domain
```

`slots_mapped_in_domain` parameter is a list of names of slots in our form. We remove the slot **age** from required slots if the value of **can_ask_age** slot is not **True**.

Any custom action that we want to use in our stories must be added into the actions section of our domain.

```yml
# domain.yml
# ... previous content ...
actions:
  - validate_newsletter_form
```

Lastly, uncomment the following code in `endpoints.yml`. This tells Rasa where to look for custom actions.

```yml
# endpoints.yml
action_endpoint:
  url: "http://localhost:5055/webhook"
```

Because we are using custom actions, we have to run an actions server. Run `rasa run actions` in a new terminal window.

You can learn more about this command in the [documentation](https://rasa.com/docs/action-server/running-action-server).

In another terminal, run `rasa train && rasa shell`.

![Chat can ask age](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0172dl8r0kou6rf12ue9.png)

![Chat cannot ask age](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0hchvtm7v104q3i1tswo.png)

We can see that when a user answers *"no"*, the age is not asked, and the value is *None*.

You can learn more about the action server in the [documentation](https://rasa.com/docs/action-server/).

In the next chapter, we will look at custom submit action.

Repository for this tutorial:
{% github petr7555/rasa-dev-tutorial no-readme %}

You can checkout the state of the repository at the end of this tutorial by running:
```bash
git clone --branch 17-custom-forms git@github.com:petr7555/rasa-dev-tutorial.git
```