Activate Forms inside a Form or Action until a Condition is met

Hello,

My question boils down to two things:

  • How can I activate a Form in another Form or Action?
  • Is there a better way to organize this?

I’m getting a bit stuck in architecting my chatbot and would love to get some opinions from the group. My use case involves food, multiple entities per utterance, and variable required slots.

Here is an example:

U: Can I get a (burrito)[item] and a (pepsi)[item]?
C: What flavor burrito would you like?
U: Bacon
C: What size pepsi would you like?
U: Large

So far I’m pretty sure I need a Form to handle each individual item.

My thinking is if I keep a slot with a list of items, and pop from that list as I activate forms, I will eventually know I am done when that list of the items is empty. I just don’t know how to activate forms and recursively call back to a Form or Action until that list is empty.

Thank you for any thoughts on this.

Here is some simplified code:

The problem is, this doesn’t activate the Form loop and rerun the custom action as expected:

return slot_set_actions+[
                ActiveLoop("item_form"),
                ActionUpdateTicket()
                ]
def get_item_followups(item):
    if item == 'burrito':
        return ['flavor']
    if item == 'pepsi':
        return ['size']
    return []


class ActionUpdateTicket(Action):
    def name(self):
        return "action_update_ticket"

    def run(self, dispatcher, tracker, domain):
        slot_set_actions = []
        requested_items = tracker.get_slot("item")
        if requested_items:
            slot_set_actions.append(SlotSet('item', None))
        else:
            requested_items = tracker.get_slot("requested_items")
        
        if requested_items:
            active_item = requested_items.pop(0)
            
            required_slots = get_item_followups(active_item)

            follow_up_slot_reset = [SlotSet(slot, None) for slot in required_slots]
            slot_set_actions = slot_set_actions+follow_up_slot_reset
            slot_set_actions.append(SlotSet("active_item", active_item))
            slot_set_actions.append(SlotSet("required_slots", required_slots))
            slot_set_actions.append(SlotSet("requested_items", requested_items))
            
            return slot_set_actions+[
                ActiveLoop("item_form"),
                ActionUpdateTicket()
                ]
        else:
            return []
        
class ItemForm(FormAction):
    """Add an item to the ticket"""
    def name(self) -> Text:
        """Unique identifier of the form"""

        return "item_form"
    
    def required_slots(
        self,
        tracker: "Tracker"
        ) -> List[Text]:
        
        return tracker.get_slot("required_slots") 

    def submit(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
        ) -> List[Dict]:
        """Define what the form has to do
            after all required slots are filled"""
        # Checkout confirm submit form flow, can I use the list of items to unset slots if still pending
        
        ticket_item = {}
        ticket_item['type'] = tracker.get_slot('active_item')
        for slot in tracker.get_slot("required_slots"):
            ticket_item[slot] = tracker.get_slot(slot)
        return [SlotSet("ticket", tracker.get_slot("ticket")+[ticket_item])]

Seems like you would be better of doing this in a single form and use the FormValidationAction to control what slots need to be filled. You can read about this in Dynamic Form Behavior.

Thanks for the response Greg. I do like the idea of something like what you’ve linked. To make that work, a challenge I run into is that I essentially have slot requirements per item. There could be overlap, so I really need to go one item at a time.

If I could set slot values within my FormValidationAction, required_slots definition, I think I could accomplish this, but I don’t think that’s possible.

Here is what I’ve done to accomplish this, but it’s less story-driven, so I’d prefer to shift to a Dynamic Form:

class ActionUpdateTicket(Action):
    def name(self):
        return "action_update_ticket"

    async def run(self, dispatcher, tracker, domain):
        slot_set_actions = []
        requested_items = tracker.get_slot("item")
        if requested_items:
            slot_set_actions.append(SlotSet('item', None))
        else:
            requested_items = tracker.get_slot("requested_items")
        
        if requested_items:
            active_item = requested_items.pop(0)
            
            required_slots = get_item_followups(active_item)
            follow_up_slot_reset = [SlotSet(slot, None) for slot in required_slots]
            slot_set_actions = slot_set_actions+follow_up_slot_reset
            slot_set_actions.append(SlotSet("active_item", active_item))
            slot_set_actions.append(SlotSet("required_slots", required_slots))
            slot_set_actions.append(SlotSet("requested_items", requested_items))
            item_form = ItemForm()
            item_form_events = await (item_form.run(dispatcher, tracker, domain))

            return [
                FollowupAction("action_update_ticket")
                ]+item_form_events+slot_set_actions
        else:
            return []
        
class ItemForm(FormAction):
    """Add an item to the ticket"""
    def name(self) -> Text:
        """Unique identifier of the form"""

        return "item_form"
    
    def required_slots(
        self,
        tracker: "Tracker"
        ) -> List[Text]:
        
        return tracker.get_slot("required_slots") 

    def submit(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
        ) -> List[Dict]:
        """Define what the form has to do
            after all required slots are filled"""
        # Checkout confirm submit form flow, can I use the list of items to unset slots if still pending
        
        ticket_item = {}
        ticket_item['type'] = tracker.get_slot('active_item')
        for slot in tracker.get_slot("required_slots"):
            ticket_item[slot] = tracker.get_slot(slot)
        return [SlotSet("ticket", tracker.get_slot("ticket")+[ticket_item])]

I’ve been poking around, and I think what this issue boils down to is, I need a way to fill Composite Entity slots in my Form