Issue with handling conditional slot logic and validations in the same action in Rasa 2.0

Thank you. This solution is worked with me but I do not understand it :laughing:

1 Like

@Abdelrahman-Ahmed @Joseesc24 @rajp4690 could you try out this pattern that works for me? In my case, it handles a simple form that asks for day and colour, but Friday is an invalid day and if you enter Monday, then it also asks for name before asking for colour. The pattern seems to work for me – it reacts well to invalid day values and also adds colour to required slots when appropriate.

class ValidateMyForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_my_form"

    def getRequiredSlots(self,tracker):
        """A list of required slots that the form has to fill"""
        if tracker.slots.get("day", "") == "Monday":
            return ["day", "name", "colour"]
        else:
            return ["day", "colour"]

    async def run(self, dispatcher, tracker, domain):
        events = await self.validate(dispatcher, tracker, domain)
        # let the form do its job if it just started or if the last slot didn't pass validation
        if len(events) == 0 or (len(events) > 0 and events[-1]["value"] is None):
            return events
        # if the slot passed validation, we step in and choose next slot to be filled
        else:
            required_slots = self.getRequiredSlots(tracker)
            for slot_name in required_slots:
                if tracker.get_slot(slot_name) is None:
                    events.append(SlotSet("requested_slot", slot_name))
                    return events
            # all slots filled, we're finished!
            events.append(SlotSet("requested_slot", None))
            return events

    def validate_day(self,value,dispatcher,tracker,domain):
        if value not in ["Monday", "Tuesday"]:
            return {"day": None}
        else:
            return {"day": value}

    def validate_name(self,value,dispatcher,tracker,domain):
        return {"name": value}

Hello again @SamS i tested your run method, but in my case it started asking me the slot that i don’t want to fill, maybe is my fail, i couldn’t test it to much. :cry:

I also updated my run method implementation, mi last one was to much complicated and it was not working as good as i thinked, the thing whit that method was that if the value that the validation method return in case that the value is valid is the same that is currently in the slot it works nice, but if in the validation method we over write it that value would never be overwrited cause the method was deleting that events, now that i think about it it doesn’t have much sense, mi implementation updated if you want to test it looks like this it would be nice for me if it also works for you. :grin:

Hmmm, this is interesting. @Joseesc24 may I ask what the slot_data_successful_validation does and where does it get set? Also, could you show the entire code of the validate_form_client_account_creation class, especially the validation methods you have implemented?

Hi @SamS the slot_data_successful_validation is not important. is part of a new functionality that i’m tring to give to my bot for avoid it to stop working in case that i spend all my requests of an API, i would like to share my code but painfully i can’t share all the code of the validations for now (and also i don’t know how to share it as code :grimacing:) but i can share some of the parts that i think that could be importants like the returns, i also could explain the general working of the validation.

basically the validation of the slot slot_client_id takes a free text, a number (maped as an entity) or and image (that is gived as an url) and search valid id numbers on it, then using the API that i mention before for checking if the number is valid, if the number was given as a number the validation preserves the slot value, if the slot was filled with an url before and the image has a valid id number the slot is overwrited with that number, and with the text is almost the same using a regex, i also set the slot slot_data_successful_validation to False in case that the API that is used to confirm the id numbers reject mi validation request couse in that case i want to stop the form and ask to the client to try to fill it again later, the method that validate slot_nit_number is almost the same (using a diferent tipe of entity) excluding the image recognition, i also return in the slot a list of id numbers in case that in the text or in the image more than 1 is valid. the returns looks like this:

Captura de pantalla de 2020-10-30 11-05-52

I have like 10 returns in my validation methods, especially the one that validate the slot slot_client_id but all of then are the same that i share before in different situations, i sorry if is not helpful, if you need some more specific part maybe i could share it. :cry:

1 Like

Thanks @Joseesc24! I think I’ll leave it here since you’ve found a way that works for you. However, I think the docs will change at some point to show a simple, recommended way to change the required slots dynamically and also do slot validation (@rajp4690 let’s wait with updating the docs; there’s a discussion going on and we need to agree on what’s going to be the recommended way of doing things (this could include small changes to FormValidationAction)).

It would be great if the rasa team can update the doc since conditional logic is really important and I believe most of us use it a lot in the previous version of rasa, I’m suffering with a different problem:

def validate_name(self, value, dispatcher, tracker, domain):
        PERSON = tracker.get_slot("PERSON")
        if PERSON:
            dispatcher.utter_message(text="Thanks, {}".format(PERSON))
            return {"name": PERSON}
            
        else:
            dispatcher.utter_template("utter_not_name", tracker)
            return {"name": None, "name1": value } 

You can see that I want to set two slots during the validation and then use slot “name1” to do the conditional slot logic for the next loop, but it seems only name is updated when I use tracker.get_slot, both two slots are being set at the same time when I use the previous rasa version, not sure what’s wrong with my method, I’ll need to change most of my conditional logic in forms if I can’t solve this

Hi @JialiuXu, I think the thing you’re trying to achieve works for my simple bot. Is the slot name1 defined in your domain? And if you changed the run() method of the custom action that contains the validate_name method, could you share the code of that run() method? I think it might contain some issues.

I agree that it’s not particularly easy to migrate from Rasa 1.x to 2.0 when it comes to slot mapping or validation, and we’re working on making it clearer and easier (see e.g. this GitHub issue).

Hi, Sam, thanks for your quick reply, here’s my run() method:

def required_slots(self, tracker: Tracker) -> List[Text]:
       name = tracker.get_slot("name")
       name1 = tracker.get_slot("name1")     
        if name and (not name1):
            return []
        elif name and name1:
            return ["gender", "ethnic"]
        else:
            return ["name", "gender", "ethnic", "DOB", "region"]
            
async def run(self, dispatcher, tracker, domain):
    events = await self.validate(dispatcher, tracker, domain)
    logger.info("events:{}".format(events))
    required_slots = self.required_slots(tracker)

    for slot_name in required_slots:
        if tracker.slots.get(slot_name) is None:
            # The slot is not filled yet. Request the user to fill this slot next.
            events.append(SlotSet("requested_slot", slot_name))
            return events

    # All slots are filled.
    events.append(SlotSet("requested_slot", None))
    return events

I’ve defined everything in the domain file, actually, the second slot “name1” has been set, but later than required_slots = self.required_slots(tracker) generated, that’s why I can’t use it for conditional logic

Alright, I see what you mean. I think you can still make it work if you pass events to your required_slots() method. Of course, inside that method, you’ll need to extract the name1 value a bit differently from the events list than you would from a tracker (tracker.get_slot()). But it should do the job and enable you to condition on the slot’s value :slight_smile:

Thanks for your reply.

That’s the last thing I want to do :sweat_smile:, I got about 50 forms and this one’s logic is the easiest, it will be a nightmare and not easy for debugging + I don’t have a deep understanding of “events”, there might be some other issues during this process

Thank you again for your help, looking forward to see the new doc updates

@JialiuXu if you’ve got 50 forms, then I guess your use case is pretty advanced and you would benefit from inheriting from Action instead of FormValidationAction. The latter class is currently meant mostly to make things easier if you want to validate slot values while writing little custom code, but not if you want full customisability. But we could improve FormValidationAction if there is a pattern that many people want to use…

By the way, if you look at the list of SlotSet events that validate() returns, it shouldn’t be too difficult to extract particular slot values from there. After all, tracker.get_slot() does something very similar under the hood…

1 Like

Hi, Sam, I understand what you are saying, but the problem is all my custom codes are running in prod and we have proved the stability after a long perod of user test process, I can’t repeat this testing process for any changes I made at the moment, so I really want to find a common and safe way to migrant my custom code with minimal change, or I’ll probably choose to stay on 1.x since I can’t take any risk that will influence the prod environment

So for the problem I mentioned before, when I print tracker.events after:

def validate_name(self, value, dispatcher, tracker, domain):
        PERSON = tracker.get_slot("PERSON")
        if PERSON:
            dispatcher.utter_message(text="Thanks, {}".format(PERSON))
            return {"name": PERSON}
            
        else:
            dispatcher.utter_template("utter_not_name", tracker)
            return {"name": value, "name1": value } 

I saw: [{'event': 'action', 'timestamp': 1604363313.3726287, 'name': 'name_form', 'policy': None, 'confidence': None}, {'event': 'slot', 'timestamp': 1604363313.372596, 'name': 'name', 'value': 'pat'}], only the first slot value I was trying to set updated in tracker.events, the second one didn’t

@JialiuXu I understand that it’s stressful and you want the simplest solution possible. However, I think you would benefit from looking at how FormValidationAction is implemented (i.e. what code it adds on top of Action). There, you will see why tracker.events isn’t updated. In my previous message, I suggested working not with tracker.events, but with the list of events that validate() returns. Inside the list you should see all the slots, even those that aren’t updated in the tracker yet.

Let me know how it goes :slight_smile:

Hi, Sam, thanks for your reply, I tired this solution, nearly working, but still not fully meet my requirement, there’s probably misunderstanding in my code:

async def run(self, dispatcher, tracker, domain):
    events = await self.validate(dispatcher, tracker, domain)
    logger.info("event:{}".format(events))
    temp_tracker = tracker.copy()
    for e in events:
            if e["event"] == "slot":
                temp_tracker.slots[e["name"]] = e["value"]
    required_slots = self.required_slots(temp_tracker)
    logger.info("required_slots:{}".format(required_slots))
    for slot_name in required_slots:
        if temp_tracker.slots.get(slot_name) is None:
            # The slot is not filled yet. Request the user to fill this slot next.
            events.append(SlotSet("requested_slot", slot_name))
            logger.info("events:{}".format(events))
            return events

    # All slots are filled.
    events.append(SlotSet("requested_slot", None))
    return events

I can set both slots at the the same time by doing this, but another unexpected problem occured here, the validate_slot funtion start validate from the very beginning of the run method and replace some of the slot values in the original events, which cause trouble when setting required_slots

I’m sure there’s another way to do that, but I don’t really want to modify the SDK source code, all I want to achieve is the same logic as the old form action

@JialiuXu I’m afraid your use case is starting to grow quite hairy here… The current behaviour, I think, is the desired one – a validate_{slot} function should update the tracker. If there’s a reason why you don’t want to update the tracker, then maybe you can pass a new tracker (or a copy of the current tracker) to validate()? By the way, if you don’t want to modify the code of FormValidationAction, you can always inherit from Action. That way, you can create a class that will be almost like FormValidationAction but some parts of it will be customized. If you look at what code FormValidationAction adds on top of Action, it should be possible to adapt these for your purposes…

Thanks for your reply, Sam @SamS Sorry, I didn’t make myself clear, I didn’t mean validate_{slot} function not update the tracker, but it shouldn’t validate the slots I set from previous actions and already in the tracker, what happens here is:

  1. The bot make an API call and get the user name: name/guest user
  2. use SlotSet update tracker: name/None if guest user
  3. I want to use this form collect user info,
def required_slots(self, tracker: Tracker) -> List[Text]:
       name = tracker.get_slot("name")
       name1 = tracker.get_slot("name1")     
        if name and (not name1):
            return ["name1"]
        elif name and name1:
            return ["gender", "ethnic" ,"DOB", "region"]
        else:
            return ["name", "gender", "ethnic", "DOB", "region"]

so when I already have the name slot filled, instead of validate, the form should direct ask name1 and then gender and ethnic, etc, but the form still trying to validate the name slot which cause the wrong logic I don’t think my user case is hairy, it’s a very simple logic and make sense to me, it works well all the time in rasa 1.x, but become so hard to achieve in rasa 2.0 I think what I’ll do is just find the 1.x SDK and compare old formaction and FormValidationAction, and revert the FormValidationAction function to 1.x formaction

Alright @JialiuXu I think I understand now. You can validate just the recently set slots if you follow the code of FormValidationAction: in validate(), it iterates only over the recent SlotSet events because only those are returned by tracker.slots_to_validate().

In Rasa 2.1 there’s now an updated version of FormValidationAction that should make many of the issues tackled above easy. See in particular the docs on advanced usage of forms.