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

Hey @Joseesc24, it’s good to see that things are starting to work for you! I think you’re very close to having it all working correctly.

The validation methods need to be called explicitly from within the run() method. I think that before your current for-loop you could add another for loop that would just call the validation methods and check if the extracted value is invalid. If it is, then requested_slot should be set to that slot (so the user needs to re-enter some value) and the function should return. If all extracted values are valid, then the second for-loop will run, requesting the first found slot that isn’t filled yet :slight_smile:

Hi @SamS & @Joseesc24, I have faced the same issue while trying to migrate to rasa 2.0, but I found the following way which works fine for me as of now, it might not be the right way though.

So I think when we try to use custom method validate_my_form which inherits Action class, it only executes the run() method and will not validate any slot values.

After digging into the source code I see FormValidationAction class in it’s run() method just calls the self.validate() to get events and returns it.

So Instead of inheriting Action class, I am inheriting FormValidationAction class and using run() method to use existing self.validate() functionality and also to get requested_slots.

Take a look at following template about how you can implement for your form:

class ValidateMyForm(FormValidationAction):

    def name(self):
        """Unique identifier of the form"""

        return "validate_my_form"

    def getRequiredSlots(tracker):
        """A list of required slots that the form has to fill"""
	
        return ["slot1", "slot2"]

    async def run(self, dispatcher, tracker, domain):
        events = await self.validate(dispatcher, tracker, domain)
        required_slots = self.getRequiredSlots(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

    def validate_slot1(self,value,dispatcher,tracker,domain):
        """Validate slot1 value."""

        return {"slot1": validValue}
	
    def validate_slot2(self,value,dispatcher,tracker,domain):
        """Validate slot1 value."""

        return {"slot2": validValue}
3 Likes

@rajp4690 thanks for sharing your insight! :slight_smile: Do you also dynamically change the set of required slots while the loop is active? If yes, then maybe it’s worth updating the docs to mention that this is possible with FormValidationAction.

@Joseesc24 looks like something that could simplify your code a tiny bit, though inheriting from Action (instead of FormValidationAction) is legit and can be the preferred thing to do if you want full customisation :wink:

2 Likes

I’ve finally got it working :laughing:, thanks a lot @SamS and @rajp4690, without your help probably i would have had to go back to 1.10 while an example of this appeared in the documentation or in a forum response, i was stuck in this part for almost two weeks, at the end mi FormValidationAction looks like this (omitting the validation methods):

Probably theres a best way to achive this behavior in a form but this works for me. :grimacing:

4 Likes

@SamS Yes, I am able to dynamically change the required slots while loop is active. Please let me know how can I contribute to update the doc. Thank you.

Hey all, I currently have some issues with conditional slot logic as well and tried your solutions but cannot get it to work properly. I also created a separate post for it here.

My required slots look like this and I tried implementing the run method by @Joseesc24 but now my form only allows me to fill in my first slot:

def required_slots(tracker: Tracker) -> List[Text]:
"""A list of required slots that the form has to fill."""

if (tracker.get_slot("box_damaged_slot") is False and tracker.get_slot("product_damaged_slot") is False) or (
        tracker.get_slot("box_damaged_slot") is True and tracker.get_slot("product_damaged_slot") is False):
    return ["box_damaged_slot", "product_damaged_slot"]
else:
    if tracker.get_slot("multiple_product_slot") is False:
        if tracker.get_slot("return_product_slot") is False:
            return ["box_damaged_slot", "product_damaged_slot",
                    "multiple_product_slot",
                    "value_product_slot", "return_product_slot"]
        else:
            return ["box_damaged_slot", "product_damaged_slot",
                    "multiple_product_slot",
                    "value_product_slot", "return_product_slot",
                    "product_or_money_slot"]
    else:
        if tracker.get_slot("return_product_slot") is False:
            return ["box_damaged_slot", "product_damaged_slot",
                    "multiple_product_slot", "products_damaged_slot",
                    "value_product_slot", "return_product_slot"]
        else:
            return ["box_damaged_slot", "product_damaged_slot",
                    "multiple_product_slot", "products_damaged_slot",
                    "value_product_slot", "return_product_slot",
                    "product_or_money_slot"]

Can somebody help me with this please.

Hi @fabrice-toussaint if the problem is inside the run method that i posted before i think i can help you a little bit, in other case i think that @SamS or @rajp4690 could help you a lot more. :grimacing:

¿The run method is getting stuck somewhere or it’s just asking you for the first slot and finishing after filling it?

@Joseesc24 I got it working! Thanks for you help :D.

@fabrice-toussaint glad to see that it’s working now! Do you mind sharing what was the problem that you had to fix?

Also, please, @fabrice-toussaint can you comment in the other thread, possibly with a solution? So that the thread is resolved properly :slight_smile:

@SamS I had two problems:

(1) I could only fill in my first slot but this was because I did not remove the @staticmethod from the run method as described by @Joseesc24.

(2) In my domain my slot could take two values based on different intents (True / False for Affirmation / Decline respectively). However, I did not handle this properly and therefore my story did not run as it should.

1 Like

Hello, I have the same issue and i tried @rajp4690 solution but it did not work. It is only ask for the first slot and ignoring the rest. So can anyone help me please with this issue, because it was working fine in rasa version 1. The following picture was for version 1

And this is the new code for version 2 which is not working as version 1

Hi @Abdelrahman-Ahmed, Could you please provide following info:

  1. Did you add validate_debit_date_and_grace_form under actions in your domain file?

  2. Can you please confirm if instruction_options slot is having value Signup after you provide the value?

Hello, @rajp4690 The problem was in Signup and i fixed it and the required slots are changed dynamically. But I faced another problem.

The validation of the slots is not working correctly. for example: when I respond to slot question with wrong answer, the slot is set null, but it is moving to the following slot question

@Abdelrahman-Ahmed can you post your code here? Especially the run() method.

While debugging, I found that when I enter a wrong answer to the slot, the validation works and set slot to null but it moves to the next slot question. And at the end, it goes to the null slots and asks for them again. So, I need it to stuck in the slot until the user enter the right answer (like the behavior in Rasa version 1)

Here is my code:

Actions.py

class ValidateDebitGraceForm(FormValidationAction): def name(self) -> Text: return “validate_debit_date_and_grace_form”

def getRequiredSlots(self, tracker):
    if tracker.get_slot("instruction_options") == "signup":
        required_slots = ["instruction_options", "debit_date", "grace_period"]
    else:
        required_slots = ["instruction_options"]
    
    print(required_slots)
    return required_slots


async def run(self, dispatcher, tracker, domain):

    events = await self.validate(dispatcher, tracker, domain)
    required_slots = self.getRequiredSlots(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


def validate_instruction_options(
    self,
    slot_value: Any,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: DomainDict,
) -> Dict[Text, Any]:
    
    if ("signup" in slot_value or "sign me up" in slot_value or "Sign Me Up" in slot_value) or (slot_value == "1"):
        slot_value = "signup"
    elif ("Send me a copy of the quote, I’m not sure yet" in slot_value) or (slot_value == "2"):
        slot_value = "Send me a copy of the quote"
    elif ("No, Thanks" in slot_value or "no thanks" in slot_value or "No thanks" in slot_value) or (slot_value == "3"):
        slot_value = "No thanks"
    else:
        dispatcher.utter_message(template="utter_enter_valid_number")
        return{"instruction_options": None}
    
    return{"instruction_options": slot_value}



def validate_debit_date(
    self,
    slot_value: Any,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: DomainDict,
) -> Dict[Text, Any]:

     
    try:
        amount = int(slot_value)
        if amount > 0 and amount < 32:
            break
        else:
            dispatcher.utter_message(template="utter_enter_valid_number")
            return {"debit_date": None}
    except ValueError:
        dispatcher.utter_message(template="utter_enter_valid_number")
        return {"debit_date": None}

    return {"debit_date": slot_value}



def validate_grace_period(
    self,
    slot_value: Any,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: DomainDict,
) -> Dict[Text, Any]:

    
    try:
        amount = int(slot_value)
        if amount == 1 or amount == 2:
            break
        else:
            dispatcher.utter_message(template="utter_enter_valid_number")
            return {"grace_period": None}
    except ValueError:
        dispatcher.utter_message(template="utter_enter_valid_number")
        return {"grace_period": None}

    dispatcher.utter_message(template="utter_sent_doc_and_ask_anything_else")
    dispatcher.utter_message(template="utter_ask_help")
    return {"grace_period": slot_value}

domain.yml

forms:

debit_date_and_grace_form:
instruction_options:
- type: from_text
  intent: inform
debit_date:
- type: from_text
  intent: inform
grace_period:
- type: from_text
  intent: inform

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