DEV Community

Cover image for RASA - Custom submit action
Petr Janik
Petr Janik

Posted on • Updated on

RASA - Custom submit action

Eventually, we would like to do more than just print the user's responses to the screen.

In this article, we will learn how to send the form data to Airtable API.

Create an account on Airtable, if you already don't have one. A free account is enough.

On the Account overview page, generate an API key. Note this key aside.

Airtable account page

Then visit https://airtable.com/shrnO6866oXVl7adV. Click Copy base. This will create a new base named Rasa dev tutorial with one table named Table 1.

In the upper right corner, click Help, then click API documentation. This opens an autogenerated API documentation for the base.

Airtable API

Copy the base ID starting with app. You will need it soon.

In the project root, create a new .env file:

AIRTABLE_API_KEY=key*****
BASE_ID=app*****
TABLE_NAME=Table%201
Enter fullscreen mode Exit fullscreen mode

The TABLE_NAME is the name of the table in your Rasa dev tutorial base. Because it contains a space, the space is encoded for URL as %20.

Put your own AIRTABLE_API_KEY and BASE_ID in.
These values should be kept secret. Add the .env file to .gitignore, so that you don't accidentaly commit it to your version control system.

Add the following to .gitignore.

# keep secret keys out of VCS
.env
Enter fullscreen mode Exit fullscreen mode

We will load those variables in the actions/actions.py with this code:

# actions/actions.py
load_dotenv()

airtable_api_key = os.getenv("AIRTABLE_API_KEY")
base_id = os.getenv("BASE_ID")
table_name = os.getenv("TABLE_NAME")
Enter fullscreen mode Exit fullscreen mode

Make sure you have your python virtual environment activated (source venv/bin/activate).
Install two new packages. We will use them soon.

pip install python-dotenv requests
Enter fullscreen mode Exit fullscreen mode

Add them to the requirements.txt file:

rasa==2.3.1
python-dotenv==0.15.0
requests==2.25.1
Enter fullscreen mode Exit fullscreen mode

In actions/actions.py, you will need these imports:

# actions/actions.py
from typing import Text, List, Optional, Dict, Any
from rasa_sdk.forms import FormValidationAction
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk import Tracker, Action
from dotenv import load_dotenv
import os
import requests
import json
import uuid
Enter fullscreen mode Exit fullscreen mode

In actions/actions.py, create a new method.

# actions/actions.py
def create_newsletter_record(email, frequency, notifications, can_ask_age, age):
    request_url = f"https://api.airtable.com/v0/{base_id}/{table_name}"

    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": f"Bearer {airtable_api_key}",
    }

    data = {
        "fields": {
            "Id": str(uuid.uuid4()),
            "Email": email,
            "Frequency": frequency,
            "Notifications?": notifications,
            "Can ask age?": can_ask_age,
            "Age": age,
        }
    }

    try:
        response = requests.post(
            request_url, headers=headers, data=json.dumps(data)
        )
        response.raise_for_status()
    except requests.exceptions.HTTPError as err:
        raise SystemExit(err)

    print(f"Response status code: {response.status_code}")
    return response
Enter fullscreen mode Exit fullscreen mode

This method creates a URL, HTTP header and body and sends a POST request to Airtable API endpoint.
The body contains data we have obtained during the form loop from the user.
uuid.uuid4() is used to generate a unique identifier for the record.

Also in actions/actions.py, create a new class.
This class is a custom action. It extends an Action class. We can call this action in our rules and stories with - action: submit_newsletter_form, since submit_newsletter_form is what the name method returns.

# actions/actions.py
class SubmitNewsletterForm(Action):

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

    async def run(
            self, dispatcher, tracker: Tracker, domain: Dict[Text, Any],
    ) -> List[Dict[Text, Any]]:
        email = tracker.get_slot("email")
        frequency = tracker.get_slot("frequency")
        notifications = tracker.get_slot("notifications")
        can_ask_age = tracker.get_slot("can_ask_age")
        age = tracker.get_slot("age")

        response = create_newsletter_record(email, frequency, notifications, can_ask_age, age)

        dispatcher.utter_message("Thanks, your answers have been recorded!")

        return []
Enter fullscreen mode Exit fullscreen mode

The run method is called when the action is called.
First, we get the slot values.
Then, we send them to the Airtable API using our previous method create_newsletter_record.
Lastly, we use dispatcher to utter a custom message.

The return value is a list of events that should follow. There are none in this case.

Don't forget to add this action to domain.yml.

# domain.yml
# ... previous content ...
actions:
  # ... previous actions ...
  - submit_newsletter_form
Enter fullscreen mode Exit fullscreen mode

Call this action after the utter_subscribed action in the data/stories.yml and data/rules.yml.

# data/stories.yml
# ... previous content ...
stories:
  # ... previous stories ...
  - story: interactive_story_1
    steps:
      - intent: subscribe
      - action: newsletter_form
      - active_loop: newsletter_form
      - slot_was_set:
          - requested_slot: email
      - intent: chitchat
      - action: utter_ask_continue
      - intent: affirm
      - action: newsletter_form
      - slot_was_set:
          - requested_slot: email
      - slot_was_set:
          - requested_slot: frequency
      - intent: chitchat
      - action: utter_ask_continue
      - intent: affirm
      - action: newsletter_form
      - slot_was_set:
          - requested_slot: frequency
      - active_loop: null
      - action: utter_subscribed
      - action: submit_newsletter_form
Enter fullscreen mode Exit fullscreen mode
# data/rules.yml
# ... previous content ...
rules:
  # ... previous rules ...
  - rule: submit form
    condition:
      - active_loop: newsletter_form
    steps:
      - action: newsletter_form
      - active_loop: null
      - action: utter_subscribed
      - action: submit_newsletter_form
Enter fullscreen mode Exit fullscreen mode

Add the action also to the tests.

Retrain the model: rasa train.
Start the actions server: rasa run actions.
In a new terminal window, start the rasa shell: rasa shell.

Newletter form chat

The submit action created a new record in the table.

Airtable record

You can learn more about actions in the documentation.

In the next chapter, we will look at messaging channels.

Please note that I needed to make some changes to the notifications slot. Those changes can be seen on GitHub.

Repository for this tutorial:

You can checkout the state of the repository at the end of this tutorial by running:

git clone --branch 18-custom-submit-action git@github.com:petr7555/rasa-dev-tutorial.git
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
joshuaalvarez profile image
JoshuaAlvarez • Edited

Hello Petr, great tutorial.
Edit: SOLVED
I'm having problems with Rasa actions. Could you help me out?
After I answer the chatbot with my age the terminal window for "rasa run actions" prints:
Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "C:\Users\...\Documents\rasa-dev-tutorial\actions\actions.py", line 69, in create_newsletter_record
response.raise_for_status()
File "c:\users\...\documents\rasa-dev-tutorial\venv-rasa-devto\lib\site-packages\requests\models.py", line 943, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 422 Client Error: Unprocessable Entity for url: https://api.airtable.com/v0/<baseID>/<tableId>

I would appreciate any help. Thanks!

Collapse
 
joshuaalvarez profile image
JoshuaAlvarez

The problem was my "Notifications" column was missing a question mark "?" in Airtable as it can be seen in the last image.
Such a silly oversight from my part. If anyone else has a similar issue make sure the column's field type in Airtable matches the entity type.
✌️

Collapse
 
petr7555 profile image
Petr Janik • Edited

I'm glad you figured it out. Thank you for posting the solution. I've fixed the base template.