Fill form slot with intent of initial message

not in submit though, then using trigger_intent in tracker, it’ll be possible to change output of required_slots so that correct next slot will be asked

I’m not sure I understand exactly what you mean. Having thought about it, I think a combination of both a self.trigger_intent and a from_trigger_intent might be best. Validate could then use self.trigger_intent to populate a slot and the user can specify how to name the slot.

For example with a FormAction like this:

class FormAction(Action):
    @staticmethod
    def required_slots(tracker):
        return ['order_type', 'action', ...]

    def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict]]]:
    return {
        "order_type": [
            self.from_text(intent='inform'),
            self.from_trigger_intent(intent='add_drink', value='drink'),
            self.from_intent(intent='add_drink', value='drink'),
            ...
        ],
        "action": [
            self.from_text(intent='inform'),
            self.from_trigger_intent(intent='add_drink', value='add'),
            self.from_intent(intent='add_drink', value='add'),
            ...
        ],
        "other_slots": ...
    }

    def validate(...):
        # would require to now also handle .from_trigger_intent

    def submit(self, dispatcher: CollectingDispatcher, tracker: Tracker,
               domain: Dict[Text, Any]) -> List[Dict]:
        action = tracker.get_slot('action')
        order_type = tracker.get_slot('order_type')

        # Some logic to trigger correct task
        tasks = {
            "add": {
                "drink": add_drink,  # or some other function call
                ...
            },
            ...
        }
        task = tasks.get(action, {}).get(order_type)
        if task:
            # execute task
            res = task(dispatcher, tracker, domain)
        else:
            # handle error
        
        return []

… one could handle all kinds of cases like this

* order
 - cart_form
 - slot{"requested_slot": "order_type"}
* form: drink
 - cart_form
 - slot{"requested_slot": "action"}
* form: add
 - cart_form  # adds a drink

* add_drink  
 - cart_form  # directly adds drink

Then you could even fill multiple slots with the trigger_intent and have the form fill them for you if the request is not specific enough.

What do you think?

we cannot have self.trigger_intent because the Action should be stateless

submit is called only after all required_slots are populated

Ah. Ok I get it. Isn’t tracker.latest_message['intent'] working though?

I suggest doing similar to Slot Filling

tracker.latest_message[‘intent’] is the latest message, so on the second question it is not the message that triggered the form

You’re right, but if we have a .from_trigger_intent() it could populate the slots right on the first .validate() call. And afterwards the slots will be filled by the .from_intent() slot mappings.

But of course we’d lose the self.trigger_intent functionality. But the user could add a:

"trigger_intent": [self.from_trigger_intent(...)]

If he still needs access to trigger intent

I’m not sure what you mean

following my suggestion, it would be always available in tracker.active_form.get('trigger_message', {}).get("intent", {}).get("name")

1 Like

Yes, I’m fine with that. What I meant is that we can use your suggestion to fill slots with some value based on the trigger intent, e.g. if we modified the intent_is_desired and extract_requested_slot functions like this

@staticmethod
def intent_is_desired(requested_slot_mapping, tracker):
    # **add this**
    mapping_type = requested_slot_mapping.get("type")
    if mapping_type == 'from_trigger_intent':
      # **your suggestion**
      intent = tracker.active_form.get(
        'trigger_message', {}).get("intent", {}).get("name")
    else:
      intent = tracker.latest_message.get("intent",
                                        {}).get("name")


    return ((not mapping_intents and intent not in mapping_not_intents) or
            intent in mapping_intents)

def extract_requested_slot(self,
                           dispatcher,  # type: CollectingDispatcher
                           tracker,  # type: Tracker
                           domain  # type: Dict[Text, Any]
                           ):
    ...
            elif mapping_type == "from_intent":
                value = requested_slot_mapping.get("value")
            # **add this**
            elif mapping_type == "from_trigger_intent":
                value = requested_slot_mapping.get("value")
            ....
    return {}

one could just turn on / turn off whether he wants slots to be filled from the trigger intent as well. Enabling stories like the one above:

I see, yes, this looks fine, but we need this trigger_intent first

Do you mind creating PRs to both rasa_core and rasa_core_sdk to enable this capability?

Sure, I’ll try to submit one by the end of today. Just to avoid any upfront issues:

  • for core, I’ll just update the change_form_to method here
  • for sdk: If rasa_core is updated, shouldn’t SDK’s tracker be initialized with the new active form dictionary already? Isn’t the rasa_core_sdk's tracker.active_form just initialized with whatever is received as JSON here

And then of course I’ll add the changes from above.

in core you also need to update Form event to take trigger_message together with form name

indeed for sdk, active_form should be init correctly.

but in sdk Form event should be updated according to core changes in this event and forms in sdk should be updated to pass message to Form event during activation

If I do this, wouldn’t this require that the story format for forms changes as well? E.g.

* order_drinks
  - form_order  # Should now return [Form(self.name(), trigger_message)]
  - form{"name": "form_order", "trigger_message": ...}  # Isn't this necessary then?
  - form{"name": null}

Or how would the Form(Event) be initialized? And in Form.__init__() we also don’t have access to the tracker to get the latest message.

If so, what would be the issue with modifying just the change_form_to method? E.g.

# rasa_core.trackers.py
def change_form_to(self, form_name: Text) -> None:
    """Activate or deactivate a form"""
    if form_name is not None:
        self.active_form = {'name': form_name,
                            'validate': True,
                            'rejected': False,
                            'trigger_message': self.latest_message}
    else:
        self.active_form = {}

This method is only called on activation or deactivation of a form, right? So when a form gets activated, we’d set the trigger_message in the tracker.active_form attribute to tracker.latest_message (which now should be the last user message before the form got activated). Isn’t it sufficient that only the tracker sets the trigger_message but the Form(Event) handling stays the same?

yes but in the forms.py, so that the stories will be the same

    def _activate_if_required(self, tracker):
        # type: (Tracker) -> List[Dict]
        """Return `Form` event with the name of the form
            if the form was called for the first time"""

        if tracker.active_form.get('name') is not None:
            logger.debug("The form '{}' is active"
                         "".format(tracker.active_form))
        else:
            logger.debug("There is no active form")

        if tracker.active_form.get('name') == self.name():
            return []
        else:
            logger.debug("Activated the form '{}'".format(self.name()))
            return [Form(self.name(), tracker.latest_message)]

rasa_core.trackers.py

def change_form_to(self, form_name: Text, message) -> None:
    """Activate or deactivate a form"""
    if form_name is not None:
        self.active_form = {'name': form_name,
                            'validate': True,
                            'rejected': False,
                            'trigger_message': message}
    else:
        self.active_form = {}

@Ghostvv added two PRs:

I believe PR is not final yet, but feel free to review general idea already.

Thanks, I’ll take a look