Este tutorial es la tercera parte de mi serie de tutoriales de RASA en español. Si te gustaría leer las partes anteriores, haz click aquí:
Tabla de contenidos
Introducción
En las partes anteriores demostré los conceptos básicos necesarios para construir chatbots con el framework Rasa, tales como los archivos requeridos, su estructura, y las principales características que ofrece.
En esta ocasión revisaremos los forms, que a mi parecer son de los componentes más útiles y versátiles que ofrece Rasa. Los forms son un conjunto de slots que deberán llenarse durante la conversación para finalmente ejecutar una tarea en específico, lo cual es muy útil cuando se busca obtener información puntual sobre el usuario.
Si bien en los casos anteriores no fue necesario mucho código, en Rasa, el concepto de los forms o formularios involucra el uso del rasa_sdk, que es la misma librería de Python utilizada para construir las acciones personalizadas.
Este chatbot es la traducción al español del formbot, un bot que Rasa provee como ejemplo en su repositorio de GitHub.
Antes de empezar
Para utilizar este bot debes instalar RASA y descargar el modelo de español de Spacy, puedes utilizar pip para hacerlo.
Puedes ver todo el código original en mi repositorio.
Nota:
Si deseas usar RASA 2.0, visita la guía de migración. Dado que esta versión aún se encuentra en sus etapas iniciales, seguiré usando la versión 1.10.
Datos de entrenamiento
El objetivo del formbot de Rasa es demostrar la versatilidad de los formularios, y el ejemplo se trata de desarrollar un chatbot que pida al usuario los datos suficientes para realizar una búsqueda de restaurantes. Los datos que el usuario podrá ingresar son los siguientes:
- Tipo de cocina
- Número de personas
- Asiento exterior
- Preferencias
- Comentarios
Un extracto de los datos de entrenamiento para el lenguaje natural es el siguiente:
El intentsolicitar_restaurante tiene el objetivo de comunicar que el usuario desea buscar un restaurante, mientras que informar proporciona información sobre la búsqueda más en específico. En ambos casos se proporcionan entities que corresponden a los slots que serán requeridos por el formulario.
El intent charla contiene frases que no son relevantes para la conversación o que no están contempladas en el alcance del chatbot. Más adelante usaremos este intent.
El formato para las historias de entrenamiento de la conversación se verá de la siguiente forma:
## camino feliz * saludar - utter_saludar * solicitar_restaurante - restaurante_form - form{"name": "restaurante_form"} - form{"name": null} - utter_valores_slots * gracias - utter_denada ## charla, detenerse pero continuar * solicitar_restaurante - restaurante_form - form{"name": "restaurante_form"} * charla - utter_charla - restaurante_form * detener - utter_ask_continuar * afirmar - restaurante_form - form{"name": null} - utter_valores_slots * gracias - utter_denada
En primera instancia analicemos cómo se incluyen los formularios en las historias de entrenamiento. Tenemos este fragmento:
* solicitar_restaurante - restaurante_form - form{"name": "restaurante_form"} - form{"name": null}
El anterior extracto indica que cuando se detecte el intentsolicitar_restaurante, se correrá la acción de restaurante_form, la cual activará el formulario del mismo nombre, y una vez que se llenen todos los campos necesarios, el formulario se desactivará.
El formulario por sí solo se encargará de pedir los valores al usuario y de extraer la información, sin embargo, el formulario se puede interrumpir con otros intents y se puede regresar al formulario nuevamente, como en el siguiente fragmento:
* solicitar_restaurante - restaurante_form - form{"name": "restaurante_form"} * charla - utter_charla - restaurante_form * detener - utter_ask_continuar * afirmar - restaurante_form - form{"name": null} - utter_valores_slots * gracias - utter_denada
En este ejemplo, el formulario se ve interrumpido por el intent charla, sin embargo se retoma la ejecución del formulario. Más adelante, se detiene con el intentdetener, se pregunta si se desea continuar, y ante la afirmación, se continua y finaliza la ejecución del formulario.
Dominio y configuración
Ahora veremos cómo deben ser los archivos de dominio y configuración para poder hacer uso de los formularios en Rasa. En primer lugar, en el archivo domain.yml debemos declarar los slots que llenará nuestro formulario. Estos deben ser de tipo unfeaturized y con auto_fill en falso. Asimismo, se deben añadir respuestas con nombres de la forma utter_ask_{slot} para cada slot que llene el formulario.
entities: - cocina - asiento - comentarios - number # duckling slots: cocina: type: unfeaturized auto_fill: false numero_personas: type: unfeaturized auto_fill: false asiento_exterior: type: unfeaturized auto_fill: false preferencias: type: unfeaturized auto_fill: false comentarios: type: unfeaturized auto_fill: false requested_slot: type: unfeaturized responses: utter_ask_cocina: - text: "¿Qué tipo de cocina?" utter_ask_numero_personas: - text: "¿Para cuántas personas?" utter_ask_asiento_exterior: - text: "¿Quieres un asiento en el exterior?" utter_ask_preferencias: - text: "Por favor proporciona preferencias adicionales" utter_ask_comentarios: - text: "Por favor proporciona comentarios sobre tu experiencia hasta ahora" forms: - restaurante_form
Tomando como ejemplo el anterior fragmento, tenemos el slot cocina, entonces agregamos el utter_ask_cocina. Estas respuestas se mostrarán automáticamente cuando el formulario necesite los llenar los slots.
Es importante mencionar que se debe agregar el slotrequested_slot, que es en donde se almacenará el nombre del slot que se buscará llenar en el formulario. También se deben agregar todos los formularios que se vayan a usar bajo el apartado forms.
En el archivo config.yml debemos incluir FormPolicy en las policies para poder hacer uso de los formularios.
language: es pipeline: - name: SpacyNLP - name: SpacyTokenizer - name: SpacyFeaturizer - name: RegexFeaturizer - name: DucklingHTTPExtractor url: http://localhost:8000 dimensions: - number - name: DIETClassifier epochs: 100 - name: EntitySynonymMapper policies: - name: FallbackPolicy - name: MemoizationPolicy - name: FormPolicy - name: MappingPolicy
Acciones
El último paso para poder hacer uso de nuestro formulario es agregar el código para controlar su comportamiento, haciendo uso del rasa_sdk, el cual es sumamente flexible.
El siguiente código muestra la implementación de un FormAction para nuestro formulario.
from typing import Dict, Text, Any, List, Union, Optional from rasa_sdk import Tracker from rasa_sdk.executor import CollectingDispatcher from rasa_sdk.forms import FormAction class RestaurantForm(FormAction): def name(self) -> Text: return "restaurante_form" @staticmethod def required_slots(tracker: Tracker) -> List[Text]: return ["cocina", "numero_personas", "asiento_exterior", "preferencias", "comentarios"] def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict]]]: return { "cocina": self.from_entity(entity="cocina", not_intent="chitchat"), "numero_personas": [ self.from_entity( entity="number", intent=["informar", "solicitar_restaurante"] ), ], "asiento_exterior": [ self.from_entity(entity="asiento"), self.from_intent(intent="afirmar", value=True), self.from_intent(intent="negar", value=False), ], "preferencias": [ self.from_intent(intent="negar", value="Sin preferencias adicionales"), self.from_text(not_intent="afirmar"), ], "comentarios": [self.from_entity(entity="comentarios"), self.from_text()], } @staticmethod def cocina_db() -> List[Text]: return [ "caribeña", "china", "francesa", "griega", "india", "italiana", "mexicana", ] @staticmethod def is_int(string: Text) -> bool: try: int(string) return True except ValueError: return False def validate_cocina( self, value: Text, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> Dict[Text, Any]: if value.lower() in self.cocina_db(): return {"cocina": value} else: dispatcher.utter_message(template="utter_cocina_equivocada") return {"cocina": None} def validate_numero_personas( self, value: Text, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> Dict[Text, Any]: if self.is_int(value) and int(value) > 0: return {"numero_personas": value} else: dispatcher.utter_message(template="utter_numero_personas_equivocada") return {"numero_personas": None} def validate_asiento_exterior( self, value: Text, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> Dict[Text, Any]: if isinstance(value, str): if "afuera" in value: return {"asiento_exterior": True} elif "adentro" in value: return {"asiento_exterior": False} else: dispatcher.utter_message(template="utter_asiento_exterior_equivocado") return {"asiento_exterior": None} else: return {"asiento_exterior": value} def submit( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> List[Dict]: dispatcher.utter_message(template="utter_submit") return []
¡Vaya! Tal vez parece mucho código pero su funcionamiento es muy sencillo. Veamos paso a paso qué es lo que hace este código.
Primero importamos las clases necesarias y declaramos una nueva clase RestaurantForm que hereda de FormAction. Una vez hecho esto, definiremos una serie de métodos para esta clase:
- name: Este método debe retornar el nombre del formulario, como se declaró en domain.yml.
- required_slots: Aquí se definen los slots que requerirá el formulario durante su ejecución. El formulario intentará obtenerlos en el orden especificado.
- slot_mappings: Esta es la forma en la que se llenará cada slot del formulario. Hay varias maneras de asignar valores a los slots: from_entity (se especifica qué entidad tomar de los mensajes), from_intent (si se identifica este intent se asigna un valor especificado) y from_text (se toma todo el texto del mensaje del usuario).
Si se asignan varios mappings, el formulario tomará la primer coincidencia. - submit: Este método se ejecuta una vez que todos los slots han sido llenados. Aquí se puede ejecutar cualquier código haciendo uso de los valores recabados durante la ejecución del formulario.
Para obtener los valores se usa tracker.get_slot(), con el nombre del slotque se desea obtener. - validate_{slot}: Estos métodos, de los cuales puede haber uno por cada slot, validarán los valores que se recojan antes de asignarlos. Yo considero que estas funciones son las más importantes en los formularios, ya que permiten tener un control total sobre los valores que se guardarán para posteriormente utilizarlos.
Cada función de validación debe retornar un diccionario con el nombre del slot que se valida y el valor que se asigna. En caso de que la entrada sea inválida, se retorna con un valor nulo (None), en cuyo caso el formulario volverá a preguntar por este valor. Por ejemplo:
def validate_cocina( self, value: Text, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> Dict[Text, Any]: if value.lower() in self.cocina_db(): return {"cocina": value} else: dispatcher.utter_message( template="utter_cocina_equivocada") return {"cocina": None}
Si el tipo de cocina solicitado no se encuentra en la “base de datos” de cocinas, se arroja un mensaje al usuario y se volverá a preguntar por el tipo de cocina. En el caso contrario, se acepta la entrada y se continúa con el flujo.
Ejecución
Una vez listos todos los archivos, podemos proceder a probar nuestro chatbot.
En este bot haremos uso de Duckling para extraer datos numéricos (notarás que no incluimos datos de entrenamiento para este caso), entonces primero debemos correr una instancia local de este servicio con:
docker run -p 8000:8000 -d rasa/duckling
Ahora, para poder hacer uso de las acciones que escribimos, corramos un servidor de acciones de Rasa con el comando:
rasa run actions &
Y finalmente podremos interactuar con el chatbot. Con el comando siguiente podremos iniciar una sesión de conversación en la terminal:
rasa shell
Aquí tenemos un ejemplo de un “camino feliz”:
Demostrando la flexibilidad de los formularios:
Finalmente un “camino infeliz”:
Espero que este tutorial te haya sido de utilidad, si tienes algún problema con el tutorial, por favor, escríbeme y con gusto te ayudaré.
Recuerda que todo el código se encuentra en mi GitHub:
jaimeteb – Overview
Mechatronics Engineer with a career in Software Development and Machine Learning. Made in Mexico. 🇲🇽 Arctic Code…
github.com