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í:

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:

## intent:solicitar_restaurante  - estoy buscando un restaurante  - ¿Puedo conseguir comida [sueca](cocina) en cualquier área?  - un restaurante que sirve comida [caribeña](cocina)  - quisiera un restaurante  - Estoy buscando un restaurante que sirva comida [mediterránea](cocina)  - ¿Puedo encontrar un restaurante que sirva comida [china](cocina)?  - Reservame una mesa para tres en el restaurante [italiano](cocina)  - ¿Puedes reservar una mesa para 5?  - Me gustaría reservar una mesa para 2  - buscando una mesa en el restaurante [mexicano](cocina) para cinco  - búscame una mesa para 7 personas  - ¿Puedo conseguir una mesa para cuatro en algún lugar que sirvan comida [griega](cocina)?    ## intent:informar  - comida [afgana](cocina)  - qué tal [asiático oriental](cocina)  - ¿Qué pasa con la comida [india](cocina)?  - uh, ¿qué tal el tipo de comida [turca](cocina)?  - um [inglés](cocina)  - Estoy buscando comida [toscana](cocina)  - me gustaría comida [marroquí](cocina)  - quiero sentarme [al aire libre](asiento)  - quiero sentarme [interior](asiento)  - vamos [adentro](asiento)  - 2 personas  - para tres personas  - solo una persona    ## intent:charla  - ¿Cómo estás?  - ¿Cómo está el clima hoy?  - ¿Hace frío o calor?  - Hermoso día, ¿no?  - ¿Puedo preguntar quién te inventó?  - por favor dime la empresa que te creó  - por favor dime quien te creó

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:

  1. name: Este método debe retornar el nombre del formulario, como se declaró en domain.yml.
  2. required_slots: Aquí se definen los slots que requerirá el formulario durante su ejecución. El formulario intentará obtenerlos en el orden especificado.
  3. 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.
  4. 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.
  5. 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

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *