Avoiding the MappingPolicy Trap of Repetitive Replies--MultiLevel Mapped Actions

Mapping Policy is a powerful tool to deal with with users interrupting the “happy path” with a set of well-known and non-dialogue-affecting questions, e.g. “are you a bot?” that are meant to receive the same response regardless of the state of the dialogue. By setting Mapping Policy priority higher than any other (with the exception of Fallback Policy to deal with ambiguous intent identification), we can keep the users on the “happy path” without having to generate large number of storiers with different interruptions interspersed throughout the dialogue.

This functionality has an inherent drawback–it is very easy for users to expose the non-human functionality of the bot by asking the same question over and over again and getting the same response. For example, a bot has intent “ask_are_you_bot” mapped to “action_i_am_bot” configured in the domain.yml:

  • ask_are_you_bot: triggers: utter_i_am_bot

and the following template for “utter_i_am_bot”, where we will use multiple utterance instances for more human-like behavior. utter_i_am_bot:

  • text: Yes, I am a bot
  • text: Indeed, this is a bot

Still the human behavior of the bot can be easily “broken” as below:

User: are you a bot?

Bot: Yes, I am a bot

User: are you a computer?

Bot: Indeed, this is a bot

User: you are a stupid bot

Bot: Yes I am a bot

User: are you a bot

Bot: Indeed, this is a bot

User: You’re a really dumb bot

Bot: Yes I am a bot

That is problematic from a few points of view. First of all, sour bots should be as human as possible, whiule repetitive answers are not human behavior. Second, in case the user is not really interested in the conversation, our bot should not be wasting its resources on a dialogue that goes nowhere. In Vocinity’s application, a voicebot with per-minute charges for voice channel usage, this is especially relevant. And, lastly, by replying the same way over and over again we are losing out on the opportunity to convince the user to cease their non-cooperative behavior.

To overcome all these problems we introduce the MappedLevelAction subclass of the Action class that should be triggered by intents using the Mapping Policy. The MappedLevelAction class conventionally would have have 3 levels: green, yellow and red, although you can define as many levels as you want. In the three-level green/yellow/red model, as long as user behavior is “green”, the standard response would be given. When the user is in the “yellow” zone, the bot will give the user a warning that they need to change their behavior and stop asking this question. In the “red” zone, the bot will ask the user if they want to continue the conversation or just end it. A “classifier” method is used to determine which behavior “zone” the user’s currently in. The “classifier” uses the number of consecutive questions of the same kind as the criteria, although other criteria, such as the total number of questions of this kind asked during the dialogue, could be used too.

In a bot, different actions that are mapped to intents would be created as instances of the MappedLevelAction class and the default methods overridden if necessary. We will give some code examples later, but first, let’s look at the desired conversation output and give an example of a MappedLevelAction at work:

Let’s say we’re asked the most common of bot questions–“are you a bot”? We will use the MappedLevelAction with “green” zone of 2 consecutive questions, “yellow” zone of 3 consecutive questions and “red” zone of 4 consecutive questions. When faced with the user’s repetitive questions that would generate the following behavior:

User: are you a bot? #first time user asks the question, green zone

Bot: Yes, I am

User: Are you a bot? #second time user asks the question, still green zone

Bot. Yes, I am

User: Are you really a bot? # third time user asks the question, yellow zone, different response, warning the user that he’s misbehaving

Bot: You’ve asked me this twice already. Yes, I am a bot and I think you know that by now.

User: Whoa, you are a smart bot! # fourth time user asks the question, red zone, ask the user if they want to continue or not.

Bot: This is not going anywhere, would you like to move on or will you just keep talking about how I’m a bot?

User: OK, ok, let’s go on.

Now that we understand the desired behavior, let’s look at the pseudocode:

  1. Define green/yellow/red responses. These are empty methods in the MappedAction class and would have to be overwritten in the specific action instance.
  2. Run “counter” method to determine the number of consecutive and total times the triggering intent was seen: a) Find out which intent invoked this action (this is necessary as multiple intents can be mapped to the same action) b) go back through the event list in the tracker and find the consecutive number of intents that invoked the currently called function as well as the total number of times the intent was called and return them.
  3. Determine the “color” state based on the consecutive and total numbers and return it. In our case, we will only use consecutive number of intents, and define “green” as less than 3 consecutive intents, “yellow” as 3 consecutive intents and “red” as 4 consecutive intents.
  4. Based on the color, invoke the appropriate response.

Here a sample rudimentary implementation of MappedLevelAction class and the ActionIAmBot instance:

class MappedLevelAction(Action): def name(self): return ‘action_mapped_action’

def counter(self, tracker, domain):
    #sets the counters for consecutive & total number of mapped_action invocations
    event_list = reversed(tracker.events)
   
    #find the intent just invoked by the user
    for event in event_list :
        if event['event']  == 'user' :
            latest_intent = event['parse_data']['intent']['name']
            break
    #Sanity check--now see if the latest intent has the current action mapped to it
    for intent in domain['intents'] :
        try:
            if intent[latest_intent]['triggers'] == self.name() :
                was_mapped = True
                break
        except:
            pass
    consec_number = 0
    total_number = 0
    if was_mapped:
        #go back and see how many current user intents there were until another user intent
        event_list = reversed(tracker.events)
        for event in event_list:
            try:
                if event['event'] == 'user' and event['parse_data']['intent']['name'] == latest_intent :
                    #if event['name']  == self.name() :
                    consec_number += 1
                elif event['event'] == 'user':
                    break
                else:
                    pass
            except:
                pass
        #calculate the total number of times this action was invoked
        event_list = reversed(tracker.events)
        for event in event_list:
            try:
                if event['event'] == 'user' and event['parse_data']['intent']['name'] == latest_intent :
                    total_number += 1
                else:
                    pass
            except:
                pass

    return [ consec_number, total_number ]

def color_classifier(self, tracker, default_color, consec_number, total_number):
    if consec_number <= 2:
        color = 'green'
    elif consec_number == 3:
        color = 'yellow'
    elif consec_number > 3:
        color = 'red'
    else:
        color = default_color
    return color

def green_response(self, tracker, dispatcher):
    return [] 

def yellow_response(self, tracker, dispatcher):
    return []

def red_response(self, tracker, dispatcher):
    return []

def run(self, dispatcher, tracker, domain):
    default_color = 'green'
    consec_number, total_number = self.counter(tracker, domain)
    color = self.color_classifier(tracker, default_color, consec_number, total_number)
    if color == 'green' :
        event_list = self.green_response(tracker, dispatcher)
    elif color == 'yellow' :
        event_list = self.yellow_response(tracker, dispatcher)
    elif color == 'red' :
        event_list = self.red_response(tracker, dispatcher)
    return event_list

So in the following domain file: intents:

  • ask_are_you_bot: triggers: action_i_am_bot
  • greet
  • affirm

“action_i_am_bot” would be built as subclasses of the MappedLevelAction class. Note that we will use the default color classifier values defined in our MappedLevelAction class and will only overwrite the name and response methods. In this case we have a bot that is going through a single form. If there’s a more complicated dialogue, we can return UserUtteranceReverted event as is customary with mapped actions. We can also send multiple “yellow” responses:

class ActionIAmBot(MappedLevelAction): def name(self): return ‘action_i_am_bot’

def green_response(self, tracker, dispatcher):
    dispatcher.utter_template('utter_i_am_bot', tracker)
    return [FollowupAction(tracker.active_form['name'])]

def yellow_response(self, tracker, dispatcher):
    message = [
        " You've asked me this twice already.  Yes, I am a bot and I think you know that by now",
        "That's not nice!  I already told you I am a bot and you are just being silly. "
     ]
    dispatcher.utter_message(random.choice(message))
    return [FollowupAction(tracker.active_form['name'])]

def red_response(self, tracker, dispatcher):
     message = 'This is not going anywhere, would you like to move on or just keep talking  about how I'm a bot?'
    dispatcher.utter_message(message)
    return [FollowupAction('continue_form')]
1 Like

where did you define FollowupAction()

FollowupAction is a rasa event that will force the bot to go tot he Action specified inside the parenthesis. in case of FollowupAction(‘continue_form’)] weinstruct the bot to use action “continue_form” which has been defined somewhere else.