Inside active form, bot responds to chitchat but doesn't request slot

I have chitchat (ResposeSelector) implemented with FormAction, similar to what @JulianGerhard mentions in this post, but the behavior of my bot seems different and I can’t understand why. I want the bot to respond to the chitchat but, in the same turn, repeat the slot filling prompt, but it only responds to the chitchat and then listens.

Versions:
rasa==1.10.10
rasa-sdk==1.10.2

  - name: TEDPolicy
    max_history: 5
    epochs: 100

Desired behavior:

User: I want to make an appointment
Assistant: Please give me your email address.
User: What is the weather like today? (CHITCHAT)
Assistant: I don’t know. Try looking out the window. (CHITCHAT RESPONSE)
Assistant: Please give me your email address. (SLOT FILLING RESPONSE)
User: boca@gmail.com.ar
Assistant: please give me your full name… etc

Current behavior:

User: I want to make an appointment
Assistant: Please give me your email address.
User: What is the weather like today? (CHITCHAT)
Assistant: I don’t know. Try looking out the window. (CHITCHAT RESPONSE ONLY)
(and the user is not prompted to give his or her email again)

I assumed the problem was with TED and I just needed more stories, but I have seven stories (including three of just this first turn around the email slot request) and it doesnt seem to do any good. Here’s and example of a story:

## chitchat at beginning, telefono
* consulta_turno
    - datos_personales_form
    - form{"name": "datos_personales_form"}
    - slot{"requested_slot": "email"}
* chitchat
    - respond_chitchat
    - datos_personales_form
    - slot{"requested_slot": "email"}
* form: inform_email{"email": "boca@gmail.com.ar"}
    - slot{"email": "boca@gmail.com.ar"}
    - form: datos_personales_form
    - slot{"email": "boca@gmail.com.ar"}
    - slot{"requested_slot": "name"}
* form: inform_name{"PER": "Carlos Alberto Tevez"}
    - form: datos_personales_form
    - slot{"name": "Carlos Alberto Tevez"}
    - slot{"requested_slot": "telefono"}
* form: inform_telefono{"telefono": "891231234"}
    - slot{"telefono": "891231234"}
    - form: datos_personales_form
    - slot{"telefono": "891231234"}
    - slot{"requested_slot": "obra_social"}
* form: inform_obra_social{"obra_social": "Swiss Medica"}
    - slot{"obra_social": "Swiss Medica"}
    - form: datos_personales_form
    - slot{"obra_social": null}
    - slot{"requested_slot": "obra_social"}
* form: inform_obra_social{"obra_social": "swiss medical"}
    - slot{"obra_social": "swiss medical"}
    - form: datos_personales_form
    - slot{"obra_social": "swiss medical"}
    - form{"name": null}
    - slot{"requested_slot": null}
* ask_confirm_datos OR affirm
    - action_confirm_datos

I feel like there must be some other policy taking priority or other element at play here. Any ideas? What else could I check? Something to do with MemoizationPolicy?

Two things worth noting: 1) I have another form in this assistant and the chitchat + slot fill request works fine, and 2) I have tried both with and without the raise ActionExecutionRejection( and still no luck.

Here’s the form code, if that helps (warning: it is kind of behemoth):

class DatosPersonalesForm(FormAction):
    """Solicitar datos para agendar al usuario"""

    def name(self) -> Text:
        """Unique identifier of the form"""
        return "datos_personales_form"

    @staticmethod
    def required_slots(tracker: Tracker) -> List[Text]:
        """A list of required slots that the form has to fill"""
        return ["email", "name", "telefono", "obra_social"]

    def slot_mappings(self) -> Dict[Text, Any]:
        """A dictionary to map required slots to
        - an extracted entity
        - intent: value pairs
        - a whole message
        or a list of them, where a first match will be picked"""

        return {
            "email": self.from_entity(entity="email", intent=None),
            "name": self.from_entity(entity="PER", intent=None),
            "telefono": self.from_entity(entity="tel", intent=None),
            "obra_social": [
                self.from_entity(entity="obra_social", intent=None),
                self.from_intent(
                    intent="inform_pago_particular", value="Pago particular"
                ),
            ],
        }

    @staticmethod
    def is_letters(string):
        """String cleaning"""
        if re.search("[0-9@]+", string) is not None:
            return False
        return True

    @staticmethod
    def clean_number(string):
        """String cleaning"""
        pattern = re.compile(r"(\s)|(,)|(\.)|(-)")
        string = re.sub(pattern, "", string)
        return string

    @staticmethod
    def is_int(string):
        """Check if a string is an integer"""
        try:
            int(string)
            return True
        except ValueError:
            return False

    @staticmethod
    def email_regex(mail):
        """email checking"""
        regex = r"^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$"
        if re.search(regex, mail):
            return True
        return False

    def validate_email(
        self,
        value: Text,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> Dict[Text, Any]:
        """Check to see if an email meets requirements"""
        if self.email_regex(value):
            return {"email": value}
        else:
            dispatcher.utter_template("utter_email_fail", tracker)
            return {"email": None}

    def validate_name(
        self,
        value: Text,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> Dict[Text, Any]:
        """Check to see if a name meets requirements."""
        # value es una lista si ambos extractores obtuvieron una entity
        # -> [SpacyEntity, DIETEntity]
        logger.debug(f"ENTITIES: {value} ")
        if isinstance(value, list):
            value = value[-1]

        # Filter out names that are doctors + Hola
        doctors = [
            apellido + ", " + nombre
            for doc_id, apellido, nombre, msp_id in HospitalDAO().get_doctors()
        ]
        doctors.extend(("hola", "Hola"))

        if value in doctors:
            return {"name": None}

        if self.is_letters(value):
            return {"name": value.title()}
        else:
            dispatcher.utter_template("utter_name_fail", tracker)
            return {"name": None}

    def validate_telefono(
        self,
        value: Text,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> Dict[Text, Any]:
        """Check to see if a tel meets requirements."""
        value = self.clean_number(value)
        if self.is_int(value) and (15 > len(value) > 6):
            return {"telefono": value}
        else:
            dispatcher.utter_template("utter_telefono_fail", tracker)
            return {"telefono": None}

    def validate_obra_social(
        self,
        value: Text,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
        hin_in_crm=False,
    ) -> Dict[Text, Any]:
        """Valida si la obra social es de las requeridas"""
        hos_id = tracker.get_slot("hos_id")
        if hos_id is None:
            return {}
        obras_sociales = HospitalDAO().get_health_insurance(hos_id=hos_id)
        if value.lower() in map(lambda os: os[1].lower(), obras_sociales):
            return {"obra_social": value.upper()}
        else:
            template = "utter_obra_social_fail"
            if hin_in_crm:
                template = "utter_obra_social_saved_fail"
            buttons = format_buttons("obra_social", query=obras_sociales)
            dispatcher.utter_message(template=template, buttons=buttons)
            return {"obra_social": None, "flag_obra_social": True}

    async def validate(
        self,
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: Dict[Text, Any],
    ) -> List[EventType]:
        """Extract and validate value of requested slot.

        If nothing was extracted reject execution of the form action.
        Subclass this method to add custom validation and rejection logic
        """
        slot_values = self.extract_other_slots(dispatcher, tracker, domain)

        # extract requested slot
        slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
        if slot_to_fill:
            slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))

            # if not slot_values:
            #     # reject to execute the form action
            #     # if some slot was requested but nothing was extracted
            #     # it will allow other policies to predict another action

            #     raise ActionExecutionRejection(
            #         self.name(),
            #         f"Failed to extract slot {slot_to_fill} with action {self.name()}",
            #     )
        logger.debug(f"Validating extracted slots: {slot_values}")
        return await self.validate_slots(slot_values, dispatcher, tracker, domain)

    def request_next_slot(
        self,
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: Dict[Text, Any],
    ) -> Optional[List[EventType]]:
        """Request the next slot and utter template if needed,
        else return None"""

        leer_crm = (
            not self._should_request_slot(tracker, "email")
            and self._should_request_slot(tracker, "name")
            and self._should_request_slot(tracker, "telefono")
            and self._should_request_slot(tracker, "obra_social")
        )  # punto flojo ver!
        if leer_crm:
            email = tracker.get_slot("email")
            result = IntegracionCRM.getContacto(email)
            # consulta si el usuario esta en el crm
            if result is not None:
                # si esta -> trae datos, valida, llena los slots, va al submit
                dispatcher.utter_message(text="Ya te encuentras registrado.")
                set_slots = [
                    SlotSet("contactid", result["contactid"]),
                    SlotSet("name", result["nombre"]),
                    SlotSet("telefono", result["telefono"]),
                ]

                validation = self.validate_obra_social(
                    result["obra_social"], dispatcher, tracker, domain, hin_in_crm=True
                )
                if validation.get("obra_social") is None:
                    set_slots.extend(
                        [SlotSet("obra_social", None), FollowupAction("action_listen")]
                    )
                else:
                    set_slots.extend(
                        [
                            SlotSet("obra_social", result["obra_social"]),
                            SlotSet(REQUESTED_SLOT, None),
                            FollowupAction("datos_personales_form"),
                        ]
                    )

                return set_slots

        flag_obra_social = tracker.get_slot("flag_obra_social")
        for slot in self.required_slots(tracker):
            if self._should_request_slot(tracker, slot):
                if slot == "obra_social" and flag_obra_social:
                    # y si vengo de validar obra social
                    return [FollowupAction("action_listen")]

                logger.debug(f"Request next slot '{slot}'")
                dispatcher.utter_message(template=f"utter_ask_{slot}", **tracker.slots)
                return [SlotSet(REQUESTED_SLOT, slot)]

        # no more required slots to fill
        return None

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

        hos_id = tracker.get_slot("hos_id")
        if hos_id is None:
            return [FollowupAction("action_failure")]
        email = tracker.get_slot("email")
        name = tracker.get_slot("name")
        telefono = tracker.get_slot("telefono")
        obra_social = tracker.get_slot("obra_social")

        channel = tracker.get_latest_input_channel()
        if channel == "twilio":
            confirmar_message = f"Muchas gracias! Tus datos son: \n▪️ Nombre: {name.title()} \n▪️ Email: {email} \n▪️ Teléfono: {telefono} \n▪️ Obra social: {obra_social} \n\n*Indícame cual dato quieres cambiar o si quieres confirmarlos*"
            dispatcher.utter_message(text=confirmar_message)
            return []

        buttons = [
            {"title": "Confirmar", "payload": "/ask_confirm_datos"},
            {"title": name.title(), "payload": "/ask_reset_slot_name"},
            {"title": email, "payload": "/ask_reset_slot_email"},
            {"title": telefono, "payload": "/ask_reset_slot_telefono"},
            {"title": obra_social, "payload": "/ask_reset_slot_obra_social"},
        ]
        dispatcher.utter_message(template="utter_confirm_datos", buttons=buttons)

        return [SlotSet("flag_obra_social", None)]

Hi @bubjanes Can you list all the policies you have in your config?

Hola! Here they are:

policies:
  - name: FormPolicy
  - name: MemoizationPolicy
  - name: TEDPolicy
    max_history: 5
    epochs: 100
  - name: MappingPolicy
  - name: TwoStageFallbackPolicy
    nlu_threshold: 0.4
    core_threshold: 0.5
    fallback_core_action_name: action_default_fallback
    fallback_nlu_action_name: action_default_ask_affirmation