I want to change the validate function so that I can deactivate my form and delete the value of requested_slot before raise ActionExecutionRejection(...).
def validate(self, dispatcher, tracker, domain):
# type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
"""Extract and validate value of requested slot.
If nothing was extracted reject execution of the form action.
Subclass this method to add custom validation and rejection logic
"""
# extract other slots that were not requested
# but set by corresponding entity or trigger intent mapping
slot_values = self.extract_other_slots(dispatcher, tracker, domain)
# extract requested slot
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
if slot_to_fill:
slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))
if not slot_values:
tracker.active_form = {}
tracker.slots["requested_slot"] = None
# reject to execute the form action
# if some slot was requested but nothing was extracted
# it will allow other policies to predict another action
raise ActionExecutionRejection(
self.name(),
"Failed to extract slot {0} "
"with action {1}"
"".format(slot_to_fill, self.name()),
)
logger.debug("Validating extracted slots: {}".format(slot_values))
return self.validate_slots(slot_values, dispatcher, tracker, domain)
Thatâs my guess, iâm not so sure of what you are trying to achieve, but in my opinion if the ActionExecutionRejection is raised, no slots will be assigned any value.
Yes, if not slot_values: means that your user didnât provide the requested information so the value of slot_to_fill ( slot_to_fill or the value of requested_slot slot) is None.
When the user doesnât provide the requested information, an ActionExecutionRejection will raise and rasa will try to predict another action but this form remains active and the value of requested_slot slot remains the same. Rasa continues its path but It always waits for the value of requested_slot to finish the form and deactivate it. Until that time you can not activate another form.
I want to be able to deactivate the form. I donât want to get the value of requested_slot slot form user anymore. So, I tried to deactivate this form by tracker.active_form = {} and tell rasa to move on and stop waiting for the value of tracker.slots["requested_slot"]. But this is not the right way to achieve this goal.
def validate(self, dispatcher, tracker, domain):
# type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
"""Extract and validate value of requested slot.
If nothing was extracted reject execution of the form action.
Subclass this method to add custom validation and rejection logic
"""
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
reset = self._check_reset_form_action_on_intent(tracker=tracker, dispatcher=dispatcher)
if reset:
return self.deactivate()
Where self._check_reset_form_action_on_intent(tracker=tracker, dispatcher=dispatcher) is implemented like this:
def _check_reset_form_action_on_intent(self, tracker: Tracker, dispatcher):
"""
:param tracker:
:param dispatcher:
:return:
"""
last_intent = tracker.latest_message['intent'].get('name')
if last_intent == 'reset_dialogue':
dispatcher.utter_message('Okay, gern. Was kann ich sonst fĂźr Sie tun?')
return True
else:
return False
Of course there wonât be any need of using such a method - you can return the deactivate statement right out of validate, if that suits your case.
This will deactivate my form but you can only use it when you want to give your user an option to cancel your form. But, I want to give my users an option to change the topic of conversation whenever they want. For example, a user is in the middle of a restaurant form. Bot ask him about something to find the value of REQUESTED_SLOT but the user reply like this:
âForget it, Can you get me a taxi, please?â
User changes the topic. I want to deactivate restaurant form. Then try to predict another action. For example, rasa can answer this by activating a taxi form. Then my bot can ask about user location. For example:
âWhat is your location?â
By using functions like _check_reset_form_action_on_intent, you can only deactivate your form(based on some conditions like reset_dialogue intent) but I want to answer my users too. I want to answer them automatically and based on our predefined policies and stories,âŚ
When ActionExecutionRejection raises, rasa tries to predict another action(which is exactly what I want), but the problem is what I mentioned before(form remained active). I solved it by changing the library of rasa. I add deactivate_form=True to ActionExecutionRejection:
It should work, but imagine if he has 10 more intents and forms and the user can switch between any of them, it would be a pain creating all the stories. I solved it by subclass the FormAction to create a CustomFormAction. In which if the ActionExecutionRejection is raised, i will return self.deactivate() and follow up with action_redirect (a custom action that follow up to a form based on the last intent). Doing that i am able to switch to any form, from anywhere. There are some conditions need to be checked but generally thatâs the idea.
Exactly. I canât handle it using an unhappy story for each possible intent because I have 50 other intents. I have to write 50 stories for each slot of my form.
Thank you @fuih for sharing your idea but Iâm confused. What is the content of action_redirect? Did you check the intent of the last message inside this action and trigger another action based on that intent?
In the validate function, instead of just raising the Exception, i check if the userâs intent is a form intent (i define a list of them). If it does, i deactivate the form and follow up to the action_redirect. Otherwise i raise the Exception normally (in case the user enters some nonsense).
Then the action_redirect does exactly what you said. I have a dictionary mapping intents and their corresponding forms. Yes defining a 50 elements dict is still annoying, but more doable than writing tons of stories .
Actually, you can do it without the action_redirect, the form policy should just predicts the next form after you end the previous form i think. I just happen to need setting some slots before activating a form so i make the action_redirect
thatâs surely true but I think he asked for an update-secure solution that follows the rasa conventions and the CustomFormAction will e.g. result in adaptive work if rasa decides to change something in the above layer. However, there are usually several ways leading to Rome!
Create a story for deactivation only using deactivate_intent and form_deactivation_action.
It is easy to create one single intent for all the cases where you want to invoke deactivation_intent.
The code for run method of custom action form_deactivation_action is as follows:
dispatcher.utter_message('Current form deactivated')
return [Form(None), SlotSet('requested_slot', None)]
Even when a form is active in a conversation, I can invoke my deactivate_intent at any point by writing a message similar to NLU data of deactivate_intent. Rest is handled by Rasa.
@ tonysinghmss Thanks for your reply, But as I told to Julian, I want to deactivate my form without defining a deactivate_intent. I want to handle the context switch. For example, a user is in the middle of a restaurant form. Bot ask him about something to find the value of REQUESTED_SLOT(for example cuisine) but the user reply like this:
âCan you get me a taxi, please?â
User changes the topic. I want to deactivate restaurant form, then switch to taxi form and start to ask him about the location.
I canât handle it using unhappy stories I have at least 50 other intents. So writing tons of stories is impossible for me!
I solved my problem by changing a couple of lines of 4 files of rasa and rasa_sdk libraries. But I donât like to change libraries!
@ gaurangubhatt
As Julian and Manishankar mentioned:
Define your deactivation_intent
Define your custom form
Inside validate function of your form, check the intent of last message and deactivate your form if the intent of last message was deactivate_intent:
def validate(
self,
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: Dict[Text, Any],
) -> List[EventType]:
"""Extract and validate value of requested slot.
If nothing was extracted reject execution of the form action.
Subclass this method to add custom validation and rejection logic
"""
# extract other slots that were not requested
# but set by corresponding entity or trigger intent mapping
slot_values = self.extract_other_slots(dispatcher, tracker, domain)
# extract requested slot
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
if slot_to_fill:
slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))
if not slot_values:
if tracker.latest_message.get("intent", {}).get("name")=='deactivation':
self.deactivate()
# reject to execute the form action
# if some slot was requested but nothing was extracted
# it will allow other policies to predict another action
raise ActionExecutionRejection(
self.name(),
"Failed to extract slot {0} "
"with action {1}"
"".format(slot_to_fill, self.name()),
)
logger.debug("Validating extracted slots: {}".format(slot_values))
return self.validate_slots(slot_values, dispatcher, tracker, domain)
Hey @JulianGerhard, thank you for the solution, works perfectly ! Is there a way I can reset all slots before returning self.deactivate ? When I call the form again, the bot continues from where we stopped, I donât want that.
To reset all slots you can define a custom action:
class ActionFlushSlots(Action):
def name(self) -> Text:
return "action_flush_slots"
def run(self, dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
slots = []
for key, value in tracker.current_slot_values().items():
if value is not None:
slots.append(SlotSet(key=key, value=None))
return slots
You can invoke this action wherever you want in your stories and rules. Alternatively, you can add to your validate function the lines of code inside the run method above.