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_timeNaturally, 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:
- response - utter that needs to be returned to the user.
- text - text returned to the user.
- image - URL or path to the image file that will be sent to the user.
- attachment - URL or path to the attachment file to be sent to the user.
- json_message - json dictionary that can be sent to a specific channel.
- buttons - button menu.
dispatcher.utter_message(buttons = [
{"payload": "/affirm", "title": "Yes"},
{"payload": "/deny", "title": "No"},
])
The object tracker has the following properties
- sender_id - The unique ID of the person talking to the bot.
- slots - The list of slots that can be filled as defined in the domains.
- latest_message - A dictionary containing the attributes of the latest message: intent, entities and text.
- events - A list of all previous events.
- active_loop - The name of the currently active loop.
- latest_action_name - The name of the last action the bot executed.
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']))