ML: RASA Chatbot: Actions


Introduction

In some cases, the responses of the RASA chatbot RASA require some logical analysis or working with external data. All this can be programmed in Python. This document is dedicated to creating such responses, called action (action).


Preparation

To work with actions, you need to make several preliminary settings. First of all, uncomment the following two lines in the file endpoints.yml:

action_endpoint:                            # endpoints.yml
  url: "http://localhost:5055/webhook"
Then, in the actions section of the file domain.yml, list the actions used. For now, there will be only one action named action_show_time, which tells the user the current time:
actions:                                    # domain.yml
  - action_show_time

To make action testing more meaningful, add examples to the file data/nlu.yml for the user's intent to know the current time:

nlu:                                        # data/nlu.yml
- intent: what_time_is_it
  examples: |
    - Tell me what time it is?
    - What time is it?    
    - What time is it now?
    - What time is it?

For simplicity, in data/rues.yml we will add a strict rule, requiring the bot to always call the action_show_time action for the what_time_is_it intent:

rules:                                      # data/rues.yml
- rule: Tell me what time it is
  steps:
  - intent: what_time_is_it
  - action: action_show_time
Naturally, the action can also be called in stories along with utter_... responses. Unlike simple responses, action response names begin with the prefix action_...


Action Script

Now we will program the actual action action_show_time. To do this, write the following code in actions/actions.py:


from rasa_sdk          import Action, Tracker
from rasa_sdk.events   import SlotSet
from rasa_sdk.executor import CollectingDispatcher

from datetime import datetime as dt
from typing import Any, Text, Dict, List

class ActionShowTime(Action):

    def name(self) -> Text:              # register the action name
        return "action_show_time"

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

        # when the action is called, return a response with the current time: 
        dispatcher.utter_message(text=f'Now {dt.now().strftime("%H:%M")}')

        return []
In the ActionShowTime class, the name method must return the action name, and in the run method the bot's response is generated by calling the utter_message function. Instead of the text argument, you can call a standard response, assigning it to the response argument (you can combine text and response):
dispatcher.utter_message(response="utter_goodbye")

Such a bot is trained as usual (rasa train). Before testing it, you need to start the RASA server in the project root with the command "rasa run actions". Then the dialogue with the bot is launched (rasa shell or !agent.py).

If the action script changes, there is no need to retrain, but you need to restart the server (rasa run actions), interrupting the previous one. Errors in the script are visible in the server window. Debug calls to the print function are also visible there.


Complicating the Action

The run method receives the tracker variable, from which you can extract the last message from the client. For example, before telling the time, in the ActionShowTime class we will thoughtfully repeat the person's question:

    def run(self, dispatcher:CollectingDispatcher, tracker:Tracker, domain:Dict[Text,Any])
        -> List[Dict[Text, Any]]:
        
        text  = tracker.latest_message['text']
        utter = f'For your question "{text}" I will answer: {dt.now().strftime("%H:%M")}'
        dispatcher.utter_message(text=utter)

        return []
Naturally, the text variable can be used with greater benefit, for example, for deeper (compared to intent classification) semantic analysis.


The run method returns a list into which you can place the value of slots (memory cells). Slots must be listed in the file domain.yml, so we add a slot for the time of the last action call to it:

slots:
  LAST_TIME_ASKING:
    type: text
After that, you need to run training again (rasa train), otherwise when trying to set the slot value, a warning about its absence will appear.

Now we modify the ActionShowTime.run method so that the time of the last call (in dt_frm format as text) is saved in the slot (the return operator), which (the get_slot function) will affect the action's response:

    def run(self, dispatcher:CollectingDispatcher, tracker:Tracker, domain:Dict[Text,Any])
        -> List[Dict[Text, Any]]:
        
        dt_frm = "%Y-%m-%d %H:%M:%S.%f"                  # date and time format
        
        prev  = tracker.get_slot("LAST_TIME_ASKING")     # value of the "LAST_TIME_ASKING" slot
        
        utter = f'Now: {dt.now().strftime("%H:%M")}.'
        if prev:                                         # if not None (already filled), then:
            prev = dt.strptime(prev, dt_frm)             # restore the time
            secs = (dt.now() - prev).total_seconds()     # difference in seconds from the current time
            
            utter += " You asked about this %d seconds ago." % (secs)
            
        dispatcher.utter_message(text=utter)

        return [SlotSet("LAST_TIME_ASKING", dt.now().strftime(dt_frm))] # fill the slot
An example of a bot that tells the current time is in the file Bot04_Action.zip.


Dispatcher, Tracker and domain

The object dispatcher (dispatcher), available in the action, sends messages to the user using the utter_message method. It has the following optional arguments:

For example, a button menu can be sent as follows:
dispatcher.utter_message(buttons = [
                {"payload": "/affirm", "title": "Yes"},
                {"payload": "/deny", "title": "No"},
            ])

The object tracker has the following properties

The domain dictionary contains all properties from the file domain.yml.

Here is an example of getting the text of the last message, its intent and the entities extracted from it from latest_message:

print(tracker.latest_message['text'])
print(tracker.latest_message['intent']['name'], "[%.2f]" % 
              (tracker.latest_message['intent']['confidence']))
if 'entities' in tracker.latest_message:
    for e in tracker.latest_message['entities']:
        print("- [%.2f] entity:%s, value:%s pos:[%d,%d]" %  
             (e['confidence_entity'],  e['entity'],  e['value'], e['start'], e['end']))


Useful Information