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

Hi everybody, i’m new in rasa so please be patient with me :slight_smile:

The thing is that i was working in a BOT and in the 1.10 version i was able to change dynamically the requested slots in the required_slots method of the form actions based in a conditional and the validations work by temselves.

When i made the update to the new version i read in the docs that the new way to implement this kind of form is by using a custom action and in a github post (Custom action class for validating slots with one custom action · Issue #6819 · RasaHQ/rasa · GitHub) i read that for making the validations is needed to run the validations methods inside the run method of the action maybe i don’t understood it well. So i make this action with the validatios methods with the idea of request the slots depending of the slot_nit_conditional value and making the validation of all the needed slots.

i’m almost sure that is not well implemented but i didn’t found the corret way of implement the validations and the conditional slot request in the same action one of the problems with my custom action if i understod well is that the active_loop ends whan all the slots are filled so the form nevers makes the validation of the second slot in the case it is requested and the second issue is that even if i the condition is not True (in this case the required_slots only contain “slot_client_id”) the BOT also ask me for the second slot i’m sure this is a logic problem in my code but could’nt found the correct way of implementing this form. In the domain i have the form declaration in this way.

The validation methods are inside the same class of the run method in all the cases, maybe this is not important but i’m working in Ubuntu 20.04 and with a Python venv with Python 3.7.9 installed.

i’m trying to use the validation methods that i already have since version 1.10, so the returns of the methods looks like these.

Hi @Joseesc24, welcome to the forum!

I tried to implement a minimalistic example of what you’re trying to achieve and I got it working. In your case, the issue is probably in your implementation of the validation class. But first things first: Can you confirm that

  • the name of your validation action is listed under actions in your domain file
  • your action server is running
  • an endpoint for the action server is specified in endpoints.yml
  • the validation action gets executed each time a requested slot gets filled from user message (it should run every time, not just after the form gets deactivated)

If you answered yes to the 4 points and things still don’t work as they should, then let’s do some debugging :slight_smile: First, when you define the form in your domain, try to include only the slots that will always be required. In other words, don’t write the slot_nit_number there, you will only request it from your validation action if needed.

Then, the code of the first for-loop in your validation action will need to change slightly: Currently, this for-loop returns a SlotSet event for the validated slot, but it doesn’t set requested_slot when slot_nit_number should be requested. Only your second for-loop does that, but it’s too late if the function returned already from the first for-loop :slight_smile:

Let me know if this makes sense and if you feel confident that you can now fix the issue.

Hi @SamS, thanks for your help :slight_smile:

I searched errors in all the points that you mention and i think that the first three are well maybe i have a doubt about the first one cause i have a validate_{form} as the action name and validate_{slot} as methods inside this one for validating the slot values. i got a little lost with the miggration from 1.10 so i dont know if maybe i should define the validate_{slot} methods also in the domain or just the validate_{form} ones.

In the fourth one i have the problem that the validations are not working every thime the slot get filled, or at least not by themselves, ¿this sould work in this way or is because the validation action is not well defined?, i have other validation actions that are extended to FormValidationAction class instead of Action and these ones works perfectly, i think that is because the run mehtod but i need this for changing the slots that the form asks for and i only know these way for achive this. mi idea was to embedd the logic inside the form for making this form working in every scenario of this kind instead defining two forms with just the required slots variation and the different stories for make both work.

About the active_loop i’m confused ¿i need to return a SlotSet(“requested_slot”, any_slot) in every loop to keep it working and a SlotSet(“requested_slot”, None) for finish it or it keeps working by themselve while any of the form slots are not filled?

It makes sense for me but i’m still a little bit confused.

Hey @Joseesc24, let’s go through your questions:

You need to include only the validate_{form} action in your domain file.

Regarding the validation action not getting executed: This is interesting. Could you try to run interactive learning in the debug mode using a command like this:

rasa interactive --debug

When you run it, what is the output shown after a slot gets filled and the form action (form_client_account_creation) runs? In my case, the output looks like below, clearly showing that the validation action was called (note that my_form is just the name of the form I have):

If you can see your validation action being called, you can do some further debugging by setting some special slots. For instance, you can define another slot in your domain called debug and set it to different values before each of the different return calls in your validation action.

If you don’t see the validation action being called, then it sounds like a mystery and maybe you could share here (or send me) your domain file, config file and the actions script. Also, which version of Rasa are you running?

About the looping mechanism: You don’t need to return any SlotSet events and the form will continue automatically, requesting the next unfilled slot. Returning a SlotSet event is useful only if:

  • your validation method changes the extracted slot value (to a different value, or to None if the value was unacceptable…) – then you return a SlotSet that changes the extracted slot
  • you want to change the “flow” of the form by requesting a different next slot (this is what you’re trying to achieve) – then you return a SlotSet that changes requested_slot

I understand that working with loops and forms is not the easiest and it’s okay to be confused at first. Let me know how it goes :slight_smile:

Hello again @SamS, since your last reply i followed the steps that you suggested me and i got the next results:

First of all i confirm that my validation action is being called when the form gets executed and the required slot is being filled correctly, i think that that’s good :slight_smile:

After that i made the debugg with another slot that i alreaddy had deffined in my domain file (slot_delivery_direction), i inderstood that the idea was to change the debugging slot value every time the validation action gets executed so i made the next modification in my run method.

This modification also works well, the value of the debbuging slot changes with every loop

but the validation methods still don’t working, i know it because if they were working that values would never be accepted. :frowning_face: Maybe this is not important or is obvious but the validation methods are defined in this way inside the validation action class togehter with the run method.

I have the doubt, ¿in this cases the slot validation method should be call inside the run method or the validation methods works by itselves?

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}
2 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