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)]