How to practically design chatbot with repeated parts of a story

Hi rasa community,

Please find below a simplified version of what I am struggled to when trying to design my rasa chatbot.

Typically, I have a story whose large part of it is iteratively repeated until a variable number of times (here fixed to 3 iterations). More specifically, I have multiple loops within my story and some of them are even nested inside my big loop on the scheme…

Based on this, I wondered how to efficiently design those loops within rasa. I saw checkpoints but could read that it was not optimal regarding high use of memory. And I saw people suggesting Forms. The problem is that I can’t imagine how to design this as a Form since I don’t have typical slots to fill until completion of the form, but rather have a sequence of custom actions that I have to iteratively repeat several times for a user within a session.

Could anyone give me lights about this design ?

Thanks !

When dealing with loops, you should use Forms. Instead of using “typical slots” , you can use a counter slot that counts how many repetitions has occurred. E.g. you could have one required slot called counter that contains the integer number of how many repetitions there have been, and another required slot continue (or something) that only gets filled when the value in counter exceeds three. This will then complete the form and carry on to the next action.

That suggestion makes a few assumptions about what you are trying to achieve. If you can give a concrete example of the behaviour you expect, I may be able to help you better.

Hi @mloubser thank you for your answer.

Intuitively, I have indeed defined two slots counter (float, doesn’t influence the conversation) and end_loop_session (bool, influence the conversation) that I update within the custom action action_propose_subjects as following:

index_iter = tracker.get_slot("counter")

if index_iter < 3:
  index_iter += 1
  return [SlotSet("counter", index_iter), SlotSet("end_loop_session", False)]
elif index_iter == 3:
  index_iter = 1
  return [SlotSet("counter", index_iter), SlotSet("end_loop_session", True)]

However, I can’t manage to deal with these slots within Forms, so I defined multiple rules as following:

rules:
  - rule: Welcome
    # This rule only applies to the start of a session.
    conversation_start: True
    steps:
      - intent: greet
      - action: utter_welcome
      - action: action_propose_subjects

  - rule: Propose subjects
    steps:
      - action: action_propose_subjects
      - intent: affirm
      - action: utter_precontent

  - rule: Propose content
    steps:
      - action: utter_precontent
      - intent: affirm
      - action: utter_content
      - action: utter_feedback

  - rule: Answer feedback and continue session
    condition:
      - slot_was_set:
          - end_loop_session: false
    steps:
      - action: utter_feedback
      - intent: affirm
      - action: action_propose_subjects

  - rule: Answer feedback and close session
    condition:
      - slot_was_set:
          - end_loop_session: true
    steps:
      - action: utter_feedback
      - intent: affirm
      - action: utter_goodbye

This system seems to loop until the third iteration but the conversation doesn’t go to the end wanted which is action: utter_goodbye from rule: Answer feedback and close session, but stops at intent: affirm from rule: Propose subjects.

Could you have any advice on this problem and how to deal with these within Forms?

@mloubser would you have any time for helping me?

Thanks!

You can actually use just 1 counter slot, and use a validation action on the last required slot to check and iterate it’s value. I pulled the basis for each customization directly from the Advanced Usage forms docs.

Responses are just to make it clear what step it’s on, you could customize these or make them custom actions as you need. Result is this conversation, code is below:

Your input ->  /greet                                        
welcome
Round 1: Proposed subjects are x,y,z, is that fine?
Your input ->  /affirm                                        
precontent pls affirm?
Your input ->  /affirm                                       
content pls affirm?
Your input ->  /affirm                                       
feedback pls affirm?
Your input ->  /affirm                                       
Round 2: Proposed subjects are x,y,z, is that fine?
Your input ->  /affirm                                       
precontent pls affirm?
Your input ->  /affirm                                       
content pls affirm?
Your input ->  /affirm                                       
feedback pls affirm?
Your input ->  /affirm                                       
Round 3: Proposed subjects are x,y,z, is that fine?
Your input ->  /affirm                                       
precontent pls affirm?
Your input ->  /affirm                                       
content pls affirm?
Your input ->  /affirm                                       
feedback pls affirm?
Your input ->  /affirm                                       
goodbye

domain.yml

intents: 
- affirm
- greet

slots: 
# None of the slots used to fill the form need to be featurized
  proposed_subjects:
    type: any
  precontent:
    type: any
  content:
    type: any
  feedback:
    type: any
  counter:
    type: any
    initial_value: 1

forms:
  feedback_loop:
    proposed_subjects:
    - intent: affirm
      type: from_intent
      value: yes
    precontent:
    - intent: affirm
      type: from_intent
      value: yes
    content:
    - intent: affirm
      type: from_intent
      value: yes
    # we will use a validation method for `feedback` that increases the counter and resets slots if necessary
    feedback:
    - intent: affirm
      type: from_intent
      value: yes

actions:
- validate_feedback_loop
- action_ask_feedback_loop_proposed_subjects

responses:
  utter_welcome:
  - text: welcome
  utter_goodbye:
  - text: goodbye
# There's no `utter_ask` for proposed_subjects slot becuase 
# the custom action `action_ask_feedback_loop_proposed_subjects` should be called instead
# this should do what your action `action_proposed subjects` was doing
  utter_ask_feedback_loop_precontent:
  - text: precontent pls affirm?
  utter_ask_feedback_loop_content:
  - text: content pls affirm?
  utter_ask_feedback_loop_feedback:
  - text: feedback pls affirm?

rules.yml

I didn’t add a second way to trigger the form since you didn’t list one, but you could add more rules that also trigger the form under other circumstances.

rules:
  - rule: Welcome
    # This rule only applies to the start of a session.
    conversation_start: True
    steps:
      - intent: greet
      - action: utter_welcome
      - action: feedback_loop
      - active_loop: feedback_loop

  - rule: Feedback loop completion
    condition:
    - active_loop: feedback_loop
    steps:
      - action: feedback_loop
      - active_loop: null # indicates form is completed
      - action: utter_goodbye

actions.py

from rasa_sdk import Action, FormValidationAction

class FeedbackLoop(FormValidationAction):

    def name(self):
        """Unique identifier of the action"""
        return "validate_feedback_loop"

    def validate_feedback(self, value, dispatcher, tracker, domain):
        """Check value of counter slot, reset all slots if counter is not at 3 yet"""

        current_counter = tracker.get_slot("counter")
        if current_counter < 3:
            counter = current_counter + 1
            iterate_slots = {"counter": counter}
            for slot in ["proposed_subjects", "precontent", "content", "feedback"]:
                iterate_slots[slot] = None
            return iterate_slots

        elif current_counter == 3:
            feedback = tracker.get_slot("feedback")
            return {"feedback": feedback, "counter": 1}
            
## You can create custom actions like the one below to ask for other slots too if they need to vary e.g. per iteration

class ActionAskFeedbackLoopProposedSubjects(Action):
    def name(self):
        return "action_ask_feedback_loop_proposed_subjects"

    def run(self, dispatcher, tracker, domain):
        # your logic for proposed subjects goes here
        # as a mock test I'll use the counter slot
        count = tracker.get_slot("counter")
        dispatcher.utter_message(text=f"Round {count}: Proposed subjects are x,y,z, is that fine?")
        return []

1 Like

counter_loop_bot.zip (5.3 KB)

You can train this with rasa train core (no NLU data in the repo) on rasa==2.4.2 (patch prob doesn’t matter, but just for the record) to see the behaviour above by using hardcoded intents (/greet, /affirm)

1 Like

Thanks @mloubser this is awesome!

I have two following questions:

  1. Are the slots linked to the form automatically reset after this form has been completed?
  2. How could I deal with forms within forms, i.e. how could I implement nested loops?
  1. Nope, but you could definitely do that in a final custom action
  2. Depending what you want to achieve you could do something with activating and deactivating forms based on each other, so it’s more like form-interrupts-form-returns to form

@mloubser could you indicate me examples of form-interrupts-form-returns to form?

I use this as a reference when I was dealing with forms in Rasa 2.4.x . Check out their stories and the usage of next_form_name slot.

Hope this helps.

2 Likes