How to avoid default re-asking of slot if slot validation fails?

I have a form with a slot called time, along with a response utter_ask_time which asks “What time works for you?” I also have a FormValidationAction which validates that the filled slot includes “am” or “pm.” If not, I’d want to ask the user to specify am/pm. However, I don’t want to re-ask `utter_ask_time’. How can I achieve this logic?

Currently, I’ve defined the validate_time method inside my FormValidationAction like this:

    def validate_time(self, slot_value, dispatcher, tracker, domain):
        time = tracker.get_slot("time")
        if not ('am' in time or 'pm' in time):
            dispatcher.utter_message(text="Could you please specify am or pm?")
            return {"time": None}
        return {"time": time}

but as it’s currently defined, utter_ask_time would be automatically asked again since the time slot remains unfilled; this is what I want to avoid. However I still want to wait for user input, and use that to set the time slot.

1 Like

I have a similar problem. I want to customize the message when the user inputs wrong answer for a slot request. For example: If the bot asks “what kind of training are you looking at?” and the user gives wrong answer, then the bot should reply: “This is not a valid training type. Please enter correct training type.” and not just repeat the same sentence: “what kind of training are you looking at?”. I used to handle these in RASA 1.7 using request_next_slot method of the form action. But, in RASA 2.2 I am not sure how to do that.

Hi there! There are a couple of ways to do this, depending on the complexity of your case.

@alexyuwen, you could get clarification from the user as follows:

  • in the validation method for time dynamically add a slot e.g. clarify_time to the be the next slot in requested_slots if the value of a slot isn’t quite sufficient.
  • return {"time":"value"} as usual (so not nullifying the value, and not reasking for it.)
  • use some bot utterance to ask the user to clarify the time ("Could you please specify am or pm?")
  • in the validation method for clarify_time, update the original value of time with the clarified time.

@samvatsar, I think your case might be better suited for buttons. If you have a finite number of training types, it might be easiest to just ask the user to select the right one, rather than relying on them to type it in correctly. If you don’t want to do this, you could override request_next_slot and put a condition in to dispatch a different method.

You could also (this is slightly more complicated), implement a custom action for asking about a slot, and perform validation in there. If the slot wasn’t validated, you could dispatch a custom message depending on what was wrong with it.

How does that sound?

3 Likes

Thank you for the suggestion. I think the request _next_slot should be used when we are using the FormAction (rasa.core.actions) as the base class. However, I am using FormValidationAction from rasa_sdk.actions. Your other solution: “implementing a custom action for asking about a slot, and perform validation in there. If the slot wasn’t validated, you could dispatch a custom message depending on what was wrong with it.” helped me. I am able to achieve what I intended. Below is the action that I am using to solve it.

class AskForTrainingTypeAction(Action):

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

def run(
    self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict
) -> List[EventType]:
    requested_slot = tracker.get_slot("requested_slot")
    training_form_config = domain.get("forms",
                                   {}).get("enroll_training_form", {})
    training_form_required_slots = list(training_form_config.keys())
    count_val = 0 if tracker.get_slot("count") is None else int(
        tracker.get_slot("count"))
    for slot in training_form_required_slots:
        if count_val > 2:
            logger.debug(
                "*************end the form*****************************")
            dispatcher.utter_message(
                template='utter_second_wrong_' + slot)
            dispatcher.utter_message(template='utter_ask_something_else')

            return [ActiveLoop(None)]
        if requested_slot not in training_form_required_slots:
            logger.debug('coming to if condition')
            dispatcher.utter_message(
                template=f"utter_ask_{slot}")
            return []

        dispatcher.utter_message(
            template=f"utter_first_wrong_{requested_slot}")

        return []
1 Like

Hi Felicia, thank you for these suggestions, this seems like the best option. I am just not sure how to dynamically add a slot to be the next slot in requested_slots within a validation method, as you suggested in the first bullet point. This is easy to do by manipulating the return value of the required_slots method, but that won’t work for what I’m trying to achieve. Would you be able to elaborate on your first bullet point?

Hi Alex, no problem! Why won’t this work for what you’re trying to achieve? You can introduce a condition in the required_slots method like below. Of course, you need to remember to set self.need_clarification to False once you have the clarification. You could also use needs_clarification slot for this instead (similar to the outdoor_seating slot in this example.

async def required_slots(
        self,
        slots_mapped_in_domain: List[Text],
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: "DomainDict",
    ) -> Optional[List[Text]]:
        required_slots = slots_mapped_in_domain
        if self.need_clarification is True:
                  required_slots.append("clarify_time")
        return required_slots

Does that help?

Actually, this makes perfect sense. Thanks!

1 Like

hey can you please elaborate the last point with an example probably?

@ridasaleem0, I’d be happy to. Which point do you mean, the needs_clarification slot?

yes please. Also clarify_time and needs_clarification are two new slots, right? I am trying to make it work into my bot.

Okay, I’ll assume you have @alexyuwen’s scenario. You should do something like this: (note I didn’t test this code, but you should get the general idea). In this scenario, you have the slots: needs_clarification, clarify_time, and time. You’ll also need to define a response:

utter_ask_clarify_time: Did you mean am or pm?
async def required_slots(
        self,
        slots_mapped_in_domain: List[Text],
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: "DomainDict",
    ) -> Optional[List[Text]]:
        required_slots = slots_mapped_in_domain
        if tracker.slots("needs_clarification"):
                  required_slots.append("clarify_time")
        return required_slots

def validate_time(self, slot_value, dispatcher, tracker, domain):
        time = tracker.get_slot("time")
        if not ('am' in time or 'pm' in time):
                  dispatcher.utter_message(template='utter_ask_clarify_time')
                  return {"time": time, "needs_clarification": True}
        return {"time": time, "needs_clarification": False}
1 Like

@fkoerner My question is , what would be the slot mapping for clarify_time ? what would be the Extraction method for this slot?

I’m getting the error that dict object is not callable in the line

if tracker.slots(“needs_clarification”)

@natsucr7 tracker.slots is a dictionary where the keys are the slot names and the values are the slot values. So you will need to index the dictionary either with .get() or with square brackets

Thank you! The code worked for me but instead of going to the clarify slot it goes to the next slot in the form, is there any way to fix this?

Interesting. I also have this problem, and I think it may be a bug, so I created a new post for it: Dynamically added slots aren't waiting for user input

Hi, @alexyuwen, @natsucr7, I think this is because the dynamically added slot is added to the end of the list of required slots. Would you try adding it to the front of the list? (This is not very efficient, but I’m guessing the list is short enough so that it is not a big problem).

1 Like

Yes clarify_time should be added right before the time slot and on successful validation of clarify_time copy its value in the time slot and set clarify_time to None.

@fkoerner any suggestions on that? I think in this case I need to extract the default slot mappings in extract method for clarify_time that will be the slot mapping of time slot actually. what do you think?

Hi @noman, not sure if I’ve understood your questions, but let me give it a shot, please let me know if you’re looking for something else:

I would define utter_ask_clarify_time as something like:

responses:
  utter_ask_clarify_time:
    - text: "Did you mean AM or PM?"

and the clarify_time slot like:

slots:
    clarify_time:
        type: categorical
        values:
        - am
        - pm

You wouldn’t include this slot in the form since you don’t always want to ask for it.

As for extraction, I’m thinking you could do something like the below. Bear in mind that there are many other ways to extract the slots, or handle them. You could use a text slot, or a categorical slot.

In addition, you may need to cover cases like the user clarifying without saying the words “am” or “pm”, for example if they say “I meant in the morning”. The best way to figure this out is to practice CDD to find out what real people would say in response to your bot’s question :slight_smile:

And the usual disclaimer, I haven’t actually run this code, it’s very possible there are typos, it’s meant more as a suggestion than for you to copy it verbatim.

async def extract_clarify_time(
        self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict
    ) -> Dict[Text, Any]:
        text_of_last_user_message = tracker.latest_message.get("text")
        time_clarification = "am" if "am" in text_of_last_user_message.lower() else "pm"
        unclear_time = tracker.get_slot("time")
        clear_time = f"{unclear_time} {time_clarification}" # or however else you want to adjust the time with the new info
        return {"clarify_time": time, "time": clear_time}