Return both FollowupAction and list of Slotset

Hello everyone ,

How can i return FollowupAction and list of Slotset in a custom Action ?

Thank you,

@Juste

Hi @Ahmed. Can you be more specific with what you mean by ‘return’? If you want to include Slotset to your custom actions, you can add the following at the end of your custom action class:

return [SlotSet("matches", result if result is not None else [])]

You can find a full example here: Actions

Hi @Ahmed

in addition to @Juste 's post, returning a FollowUp Action works like this:

from rasa_sdk.events import FollowupAction

def your_custom_action(...):
    <code>
    return [FollowupAction(name='your_action_name')]

as documented here:

https://rasa.com/docs/rasa/api/events/#force-a-followup-action

If you provide more information, more help is awaiting you! :slight_smile:

Regards

But is there a way to do both at the same time?

E.G. I’d like to do return [FollowupAction(name='action_name'), SlotSet('slot1', value)]

As a workaround, I tried this approach, but then slot1 is never set…

class CustomAction(Action):

def run(...):
    SlotSet('slot1', value) ***
    return [FollowupAction(name='action_name')]

Would be nice to be able to set the slot outside of the return statement :slight_smile:

I actually have the same question, albeit slightly different. Theoretically (AFAIK), this is valid (and is what I tried):

return [SlotSet('slot_name', None), FollowupAction(name='a_new_form')]

The idea I have is to reset some specific slot before redirecting. However, none of them was removed; and more strangely with that, some slots are asked again.

Also I wonder if that construct above implies order (SlotSet before FollowupAction), or the order is undetermined (and we have to use something like await).

any solution? Still can’t set a slot and trigger a followup action, whether or not both are in the return statements.

Hi @liaeh,

can you elaborate where you actually want to return:

[FollowupAction(name='action_name'), SlotSet('slot1', value)]

I am asking because depending on the caller of the function that should return, the SDKs behaviour differs.

Kind regards
Julian

1 Like

@JulianGerhard Here’s an overview -

I want to find recipe suggestions. I have various slots, one of which is called disliked_ingredients. A separate action ActionFetchRecipes gets recipe suggestions using an API and stores the list of hits in a slot called found_recipes. Then, my custom action ActionSuggestRecipe is called, where I try to take an element from found_recipes. If I can’t find any recipes, then I want to relax the constraints of the search.

So, if searching for a recipe initially with disliked_ingredients = ['olives', 'cilantro'], then I want to remove one of the constraints, disliked_ingredients = ['olives'].

class ActionSuggestRecipe(Action):

def run(self, dispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

    hits = tracker.get_slot("found_recipes")

    try:
        suggest = hits.pop()
        .... carry on

    except (IndexError, AttributeError):  # there are no more hits to suggest
        # remove something from disliked ingredients and search again
            constraints = tracker.get_slot('disliked_ingredients')
        if len(constraints ) > 1:
            constraints.pop()
            return [SlotSet('disliked_ingredients', constraints), FollowupAction("action_fetch_recipe")]

Hi @liaeh,

at the risk of overlooking it, but your UseCase seems like the perfect match for a FormAction itsself. Why don’t you actually use one? If you have to do it via “normal” Actions, we will figure it out, but the way the SlotFilling behaviour is managed inside a FormAction fits your needs quite well…

If you would be open to use a FormAction, I can come up with a simple implementation if you want me to. If not, give me a sign and I’ll take a look at the CustomAction thing.

Kind regards
Julian

Hi @JulianGerhard - I guess I should’ve mentioned that I am using a FormAction already to fill the slots incl disliked_ingredients from the user input :slight_smile: Here’s a broader overview of how things flow with a story:

## happy path recipe search
* greet
    - utter_greet
* affirm
    - utter_ask_number_users
* inform{"num_ppl" : "1"}
    - action_decide_which_form
    - recipe_form
    - form{"name": "recipe_form"}
    - form{"name": null}
    - action_fetch_recipes
    - action_suggest_recipe
* affirm
    - action_share_recipe
* affirm
    - utter_goodbye
* goodbye
- action_slot_reset

So my FormAction Recipe_Form delegates fetching and suggesting a recipe once the form is filled. From how I saw it, the purpose of the form is to gather all necessary information. Do you have another suggestion?

Thanks :smiley:

Hi @liaeh,

ok - good that you mentioned it now! :smiley:

I assume that fetching the recipes is an asynchronous process. Can you tell me how long the request lasts?

Kind regards
Julian

@JulianGerhard It is actually executed synchronously - the request is really quick.

Hi @liaeh,

ok - then we would have several options to actually achieve your goal. Let me propose one of them:

  1. Implement your fetch_recipes method inside the FormAction
  2. Start the Form and get every necessary slot filled
  3. In the validator of the last slot to be filled, execute fetch_recipes based on the trackers slots filled
  4. If no recipes could be found, you can choose between: a. taking the list of disliked ingredients, remove one value and execute fetch_recipes again or b. setting the value of the last slot to None (or even more), resulting in the Form not being able to be submitted. This way you would be able to ask for certain slots again, modify your way of asking for them and lots of more.

I really like holding a logic inside a FormAction because the behaviour is controlled and you can influence it at almost every point.

Keep in mind: If your FormAction is designed such that every slot can be filled at any time, then there won’t be a “last slot” to be filled. If so, you can simply call fetch_recipes inside every validator and in your method, you can check if every necessary is actually filled before executing the API call itsself.

Did you get the idea? If you are unsure how to implement it, you can contact me via Slack if you want to.

Kind regards
Julian

1 Like

Thanks for the suggestion @JulianGerhard!

I see it’s possible this way - but it sounds like it’s a workaround because you cannot modify the state of the tracker. I guess this doesn’t answer the original question of why

return [FollowupAction(name='action_name'), SlotSet('slot1', value)]

doesn’t work in any custom action, in general. I can think of a few more use cases where being able to modify the tracker would also be helpful, so I want to find a way to modify a slot and return a followup action anytime, or at least be able to change slot values outside of a form/outside of a return statement like here:

def run(...):
    SlotSet('slot1', value) ***
    return [FollowupAction(name='action_name')]

Any ideas? :thinking: thanks again!

Hi @liaeh,

I don’t think it’s a workaround but I get your point.

Let’s say we have to do it via a normal CustomAction in which we simply want to set one slot and force the next action - correct? I can reproduce that without your usecase if you are okay with that!?

Let me check this in detail and get back to you asap. I need to take the dog for a walk, but will respond straight afterwards if that would be ok!?

Kind regards
Julian

Yes, totally fine. Thanks - looking forward to seeing what you find :slight_smile:

Hi @liaeh,

so here is what I did. I have a story:

## repeat_last_utterance
* repeat_last_question
  - action_repeat_last_utterance
  - action_restart 

The action repeat_last_utterance is implemented as follows:

class RepeatLastUtterance(Action):
    def name(self) -> Text:
        return "action_repeat_last_utterance"

    def run(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> List[Dict[Text, Any]]:

        return [SlotSet('test_slot', 'RepeatLastUtterance'), FollowupAction('test_action')]

The action test_action is implemented as follows:

class TestAction(Action):
    def name(self) -> Text:
        return "test_action"

    def run(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> List[Dict[Text, Any]]:
        x = tracker.get_slot('test_slot')
        dispatcher.utter_message(text='SLOT VALUE IS: {}'.format(x))
        return []

So actually, if that what you want to achieve, would work, the bot would have to utter:

[
    {
        "recipient_id": "default",
        "text": "SLOT VALUE IS: RepeatLastUtterance"
    }
]

Which, you may have guessed it, he did. Here is the traceback:

2020-04-01 17:26:38 DEBUG    rasa.core.actions.action  - Calling action endpoint to run action 'action_repeat_last_utterance'.
2020-04-01 17:26:39 DEBUG    rasa.core.processor  - Action 'action_repeat_last_utterance' ended with events '[<rasa.core.events.SlotSet object at 0x000001CB999BC438>, <rasa.core.events.FollowupAction object at
0x000001CB999BC470>]'.
2020-04-01 17:26:39 DEBUG    rasa.core.processor  - Current slot values:
        test_slot: RepeatLastUtterance
2020-04-01 17:26:39 DEBUG    rasa.core.processor  - Predicted next action 'test_action' with confidence 1.00.
2020-04-01 17:26:39 DEBUG    rasa.core.actions.action  - Calling action endpoint to run action 'test_action'.
2020-04-01 17:26:40 DEBUG    rasa.core.processor  - Action 'test_action' ended with events '[BotUttered('SLOT VALUE IS: RepeatLastUtterance', {"elements": null, "quick_replies": null, "buttons": null, "attachme
nt": null, "image": null, "custom": null}, {}, 1585754800.156991)]'.
2020-04-01 17:26:40 DEBUG    rasa.core.processor  - Current slot values:
        test_slot: RepeatLastUtterance
2020-04-01 17:26:40 DEBUG    rasa.core.policies.fallback  - NLU confidence threshold met, confidence of fallback action set to core threshold (0.3).
2020-04-01 17:26:40 DEBUG    rasa.core.policies.memoization  - Current tracker state [None, {}, {'prev_action_listen': 1.0, 'intent_repeat_last_question': 1.0}, {'intent_repeat_last_question': 1.0, 'prev_action
_repeat_last_utterance': 1.0}, {'prev_test_action': 1.0, 'intent_repeat_last_question': 1.0}]
2020-04-01 17:26:40 DEBUG    rasa.core.policies.memoization  - There is no memorised next action
2020-04-01 17:26:40 DEBUG    rasa.core.policies.form_policy  - There is no active form
2020-04-01 17:26:40 DEBUG    rasa.core.policies.mapping_policy  - There is no mapped action for the predicted intent, 'repeat_last_question'.
2020-04-01 17:26:40 DEBUG    rasa.core.policies.ensemble  - Predicted next action using policy_0_TEDPolicy
2020-04-01 17:26:40 DEBUG    rasa.core.processor  - Predicted next action 'action_listen' with confidence 0.96.
2020-04-01 17:26:40 DEBUG    rasa.core.processor  - Action 'action_listen' ended with events '[]'.
2020-04-01 17:26:40 DEBUG    rasa.core.lock_store  - Deleted lock for conversation 'default'.

So this seems fine for me. Does it for you? If not, where is the misunderstanding?

Regarding the tracker: The Tracker is a “simple” dictionary. Without returning it, how would the caller know about its changes? The actions should be designed stateless, so actually you would need a bit of engineering to write an update-method that works systemwide.

Kind regards
Julian

1 Like

Hi Julian, oh I see - so the way the tracker is designed, setting a slot outside the return statement would cause side effect changes… :neutral_face: Makes sense now why changes are made as they are!!

What version of rasa are you using? your repeat_last_utterance action is exactly what I tried but didn’t work! Curious why.

Hi @liaeh,

actually I am using the latest Rasa version which is 1.9.5. I wondered from the beginning why you had the impression that these things won’t work so I assume that the problem resides in your code.

Can you post your exact custom action the Rasa version you are using?

Kind regards
Julian

I have the same issue here. I want to run followupaction and setslot at the same time, but it never worked. Has anyone solved this issue here? Thanks.