How to deal with conditional followup actions in Rasa Core

I have a use case that requires the user to be authenticated. If the user is already authenticated, he can just go straight to the action, however, if he isn’t logged in, I redirect him to a login using a FollowupAction.

The stories for the two scenarios could look like this:

Authenticated user:

* agent.add_wishlist_items{"shopping_item": "milk"}
    - slot{"shopping_item": ["milk"]}
    - action_search_items  # This will display buttons with milk items
* user.selected_products{"products": "33"}
    - slot{"products": ["33"]}
    - action_add_wishlist_items

Unauthenticated user:

* agent.add_wishlist_items{"shopping_item": "milk"}
    - slot{"shopping_item": ["milk"]}
    - action_search_items  # This will display buttons with milk items
* user.selected_products{"products": "33"}
    - slot{"products": ["33"]}
    - action_add_wishlist_items
    - action_login  # if user is not authenticated, which is checked in action_add_wishlist_items
* account_linked{"access_token": "2A2b0zdf614"}
    - slot{"access_token": "2A2b0zdf614"}
    - action_account_linked
* user.selected_products{"products": "33"}
    - slot{"products": ["33"]}
    - action_add_wishlist_items

Now I have two questions:

  1. When adding the stories just like that, the dialogue model is learning to sometimes show action_login after action_add_wishlist_items even for already authenticated users, right? How do I make it clear that action_login should only be executed if the user is not authenticated yet?
  2. The action_login is causing a ‘redirect’ here. What’s the best way to make sure that ‘milk is added to the wishlist’ after the login? So far, I just sent the user.selected_products message again. But is there a way that it’s executed again automatically after the action_login completed successfully?

Thanks a lot in advance!

I think you need to write a proper slot value indicating user is logged in after the action action_add_wishlist_items. Sucht that the algo learns to show only action_login if user is not logged in (using a categorical slot). You need to return this slot in action_add_wishlist_items

Btw as I am interested to use a log in feature in the futire too, how did you do that?

That makes a lot of sense. I’ll give it a try.

Regarding login: I’ve been working on three different authentication flows:

  • Basic Authentication
  • Token Authentication
  • OAuth2 Authentication

Unfortunately, the auth flows somewhat depend on the channel you’re using. For example, I read that Google Assistant and Alexa are fully OAuth2 compatible, whereas the Messenger Platform is not. So the authentication really depends on the details, but in general I have a setup similar to this:

  • Slot(s) indicating whether user is authenticated
  • If user tries to execute something without permissions, I send a FollowupAction(‘login’) that triggers a login form to be sent to the user. Login form asks for different things, e.g. login type (Basic, Token, OAuth2), username and password if necessary.
  • Afterwards, I sent an authorization request to my backend containing the credentials received through the form
  • The authorization webhook returns status code (success / failure) and, if necessary, an authorization token which I store in a slot
  • Credentials are then added to all future requests that need permissions

For example, when using the Messenger channel, I used OAuth2 and customized the account_linking method of the channels.facebook.Messenger class to link the OAuth2 token to Messenger’s PSID.

If you want, I could publish a small guide here over the next week or so.

2 Likes

So, this is a simple custom action which asks for credentials? Why do use a authorization webhook and how does it looks like?

What you mean by that?

That would be nice :smile:

Essentially depending on the authorization flow and channel I first send a login action. For Basic Auth and Token Auth, this could be a simple FormAction asking for username and password. For OAuth2 this is sending a Postback button that redirects the user to the login screen of my backend.

Then, the backend responds with either success (and potentially an access token) or failure that causes a Linked or Linking_Error action to be executed. In the success case the Linked action just sets some slots (login=True and potentially access_token=“xxx”) and redirects the user to the previous action (the one he needed to login for). In the Linking_Error case, I just utter some error message.

Cleaning up my authorization flows at the moment. I’ll try to write something and post it here for everyone once I’m done :slight_smile:

Want to bring up this issue again. What is the correct way to write a story with followup actions? I ran it in interactive mode and it exports to something like this (i shortened it):

* shopping.add_cart_product{"shopping.product_id": "23"}
    - slot{"shopping.product_id": "23"}
    - action_shopping.add_cart_product
    - followup{"name": "auth.login_form"}
    - slot{"auth.authenticated": false}
    - slot{"next": "action_add_cart_product"}
    - auth.login_form
    - form{"name": "auth.login_form"}
    - form: followup{"name": "action_auth.account_linked"}
    - form{"name": null}
    - slot{"requested_slot": null}
    - action_auth.account_linked
    - action_shopping.add_cart_product
* dialogue.thank_you
    - utter_dialogue.thank_you

In this run, the user wanted to add a product, which triggers the custom action action_shopping.add_cart_product. However, the action noticed the user isn’t authenticated, so it returned a followup action auth.login_form. The login form then takes the user through the login process, confirms with action_auth.account_linked and then runs the initial action_shopping.add_cart_product action again to now successfully add the product to the customers cart.

So now I was wondering whether this story is messing up training for users who are already logged in where the story would be like this (since action_shopping.add_cart_product won’t issue a FollowupAction):

* shopping.add_cart_product{"shopping.product_id": "23"}
        - slot{"shopping.product_id": "23"}
        - action_shopping.add_cart_product
* dialogue.thank_you
        - utter_dialogue.thank_you

@akelad @souvikg10: Any ideas on this? I’d like to write a guide on this sooner or later since others seem to struggle with login related issues as well, but want to make sure I fully understand it myself.

Bringing this up one more time with an additional question. To summarize, I have many actions that are protected and require the user to be logged in. So a simple use case like this:

  • shopping.list_products_in_cart
    • action_shopping.list_products_in_cart

Might end up like this if the user isn’t logged in already:

  • shopping.list_products_in_cart
    • action_shopping.list_products_in_cart
    • followup{“name”: “action_auth.login”}
    • slot{“next”: “action_shopping.list_products_in_cart”}
    • action_auth.login
  • auth.account_linked{“access_token”: “abc”}
    • slot{“access_token”: “abc”}
    • action_auth.account_linked
    • slot{“auth.authenticated”: true}
    • followup{“name”: “action_shopping.list_products_in_cart”}
    • action_shopping.list_products_in_cart

Now I’m still unclear about two things:

  1. Do I need to add these long stories to the training data? Or does it not matter, since the followup actions will be executed anyway?
  2. If I need to add these protected paths to the stories, is there a way I can generalize it? Let’s say I have a dozen such stories for listing products, adding products, removing products, wishlists, etc. I would have to duplicate these stories a dozen times, but only 4 lines would change. Or is there a way to do something like that:
  • shopping.list_products_in_cart OR shopping.add_product_to_cart OR
    • action_shopping.list_products_in_cart OR action_shopping.add_product_to_cart OR
    • followup{“name”: “action_auth.login”}
    • slot{“next”: “action_shopping.list_products_in_cart”}
    • action_auth.login
  • auth.account_linked{“access_token”: “abc”}
    • slot{“access_token”: “abc”}
    • action_auth.account_linked
    • slot{“auth.authenticated”: true}
    • followup{“name”: “action_shopping.list_products_in_cart”}
    • action_shopping.list_products_in_cart OR shopping.add_product_to_cart OR

I’d just like to train the core model that sometimes, an action might cause a login to be required, but that it should continue with the original action afterwards.

Any comments, thoughts or ideas?

I have need of similar logic and tried to control the flow in story with least repetitive way and the solution end up being the best was to do this control within FormAction programatically (so predictable…) So my story look like this

## share secret
* select_menu{"main_menu":"share_secret"}
- auth_form
- form{"name": "auth_form"}
- form{"name": null}
- utter_share_secret

Within this auth_form, I set / check authenticated slot and determine whether user need authentication or not. With this way, I can provide finer control of how long the session is good for etc by setting authenticated to False if it elapsed more than 5 min since last authenticated etc.

In FormAction, to set authenticated slot to True if token is validated. I do this in validate func. Then in request_next_slot function, if authenticated is set to True, I simply return None so that it gets out of auth_form and next actionutter_share_secret is correctly predicted to move on. So yeah, every action that requires authentication you will need to repeat 3 lines

- auth_form
- form{"name": "auth_form"}
- form{"name": null}

but it seems to work best for my purpose especially I like the fine / predictable control which it is pretty important for action like authentication.

Full example of auth_form action is here

Hi @naoko, I initially thought about this process as well. However, in this case you rely on core to predict the auth_form as next action. Therefore, you could run into a situation, where core directly jumps from * select_menu to utter_share_secret without requiring login through the auth_form. If you need to have 100% prove that utter_share_secret is not executed without being logged in, you need a different solution.

Hi @smn-snkl! Did you manage to implement properly an authentication process for RASA in the end ? If yes, may I ask you to share your final experience please.

Thank you!

Hello @smn-snkl, did you find any better solution?

Hello @smn-snkl did u find the solution. I also need to authenticate users before executiong some action

Hello @smn-snkl did u find the solution. I also need to authenticate users before executiong some action

Hi @smn-snkl!

I’m bumping this thread and reminding you at the same time. If you or anyone else has a robust solution, please share it.

If you want a story to execute conditionally, add a slot command to the story. For example, I have a profile slot (categorical with values “vip”, “standard”, “anonymous”).

In my stories I have something like this:

## greet_vip_story
* greet
  - slot{"profile" : "vip"}
  - utter_vip_greeting

## greet_standard_story
* greet
  - slot{"profile": "standard"}
  - utter_standard_greeting

## greet_anonymous_story
* greet
  - slot{"profile": "anonymous"}
  - utter_please_log_in

In this case, I’m assuming the slot already contains the correct value. Have a look here: Rasa documentation on slots, to see what can be detected. (A string for example, can’t be matched, you can just check if it is set or not)

Does this solve the problem for you?