Creando un chatbot con Rasa

0
158

En este tutorial nos adentraremos un poquito en el mundo de los chatbots conversacionales de la mano de Rasa.

Rasa viene en dos sabores: una versión Open Source y otra Pro que extiende las capacidades de la versión Open Source, destacando el uso de CALM, pero esto lo dejaremos para otro momento.

Rasa nos permite construir asistentes virtuales cuyo objetivo es ayudarnos a realizar una tarea concreta (closed-domain) como contratar un seguro o pedir cita con el centro de salud mediante un diálogo (Task oriented Dialogue system), pero no para echar un rato charlando de filosofía, política o religión, (chit-chat bot).

Imaginad un momento que tenemos que construir nosotros desde cero algo como esto: ¿qué problemas principales se nos plantearían?. Os ayudo un poco y voy incluyendo algunos conceptos en cursiva que forman parte de Rasa y del mundo de los chatbots en general:
Debemos entender lo que el usuario quiere hacer, Intents, cómo interpretar sus mensajes en el contexto del problema en el que el chatbot se mueve, Domain, extraer la información relevante, Entities, y guardar esa información para poderla usar durante la conversación, Slots.
Esta primera parte corresponde al procesamiento del lenguaje natural, NLP, en nuestro caso más concreto procesar el texto de entrada no estructurado del usuario y convertirlo en algo entendible por una máquina NLU (Natural Language Understanding).

Hago un paréntesis, antes de seguir. Con la aparición de los LLM, Large Language Models, la forma de gestionar esta parte ha evolucionado hacia su uso, pero en este tutorial nos centraremos en esta primera forma más tradicional de gestionar la entrada del usuario. En futuros tutoriales nos adentraremos en el uso de Language Models, sus mejoras y sus handicaps, haciendo uso de CALM (Conversational AI with Language Models).

Siguiendo con los problemas que se nos plantean, tenemos que ser capaces de gestionar la conversación de manera eficiente, DM (Dialogue Management), para poder obtener del usuario información necesaria y poder completar una operación, o quizás hacer nuevas propuestas en función del estado de la conversación.
Además, no podemos dejar de lado que nuestras respuestas, Responses, han de ser coherentes y naturales, esto corresponde a lo que se denomina NLG (Natural Language Generation).

Supongo que ya os habréis dado cuenta de que esto no es trivial, y que hay mucho trabajo detrás de un chatbot. Pero no os preocupéis, Rasa nos facilita mucho la tarea.
Sin más preámbulos, vamos a ver cómo podemos construir un chatbot sencillo. Antes de nada, os dejo todo el código fuente de este tutorial en este repositorio de GitHub.

Índice

  1. Entorno
  2. Preparando lo necesario
  3. El ejemplo
  4. Conclusiones

1. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15″ (Intel Core i9, 32GB, 1TB SSD).
  • Sistema Operativo: macOS Sonoma 14.6.1

2. Preparación del entorno, instalación de Rasa y creación del proyecto

Para trabajar con Rasa necesitamos tener instalado Python. Para gestionar el entorno de Python y las dependencias estoy usando pipenv.
Podéis encontrar más información en este tutorial de Juan Antonio Ortiz.
En el momento de la realización de este tutorial la última versión de Rasa disponible es la 3.6.20, siendo Rasa compatible con la versión 3.10 de python como versión más reciente y esa será la combinación que usaremos.

Cread una carpeta para el proyecto creamos un entorno virtual con pipenv y activamos el entorno virtual:

    > pipenv --python 3.10
    > pipenv shell (activamos el entorno virtual)

Ahora instalamos Rasa:

    > pipenv install rasa

Al instalar Rasa se ha instalado también la SDK de Rasa para poder crear nuestras Custom Actions

    > pip list

Si todo ha ido bien, deberíamos ser capaces de ejecutar el comando:

    > rasa --version

En este momento, podéis usar el comando rasa init para crear un proyecto de Rasa desde cero y también te preguntará si quieres entrenar el modelo del ejemplo.
Un modelo de ejemplo que podéis entrenar y probar, especialmente los más impacientes.

3. El ejemplo

En mi caso os propongo un ejemplo en el que vamos a crear un sencillo asistente virtual que nos permita realizar algunas operaciones con una supuesta cuenta bancaria en un banco inventado: ACME Bank.
Nuestro asistente virtual será capaz de realizar las siguientes operaciones:

  • Mostrar el saldo, ya sea de nuestra cuenta o de nuestra tarjeta
  • Recuperar los últimos movimientos de nuestra cuenta o de nuestra tarjeta
  • Realizar una transferencia

Empezaremos definiendo el dominio de la solución en el fichero domain.yml.
Primero veamos los intents que vamos a usar en nuestro asistente virtual. En nuestro caso, vamos a definir los siguientes:

    ...
    intents:
      - greeting
      - bye
      - balance
      - last_transactions
      - transfer
      - info_transfer_account_number
      - info_transfer_amount
    ...

Saludo, despedida, saldo, últimos movimientos, transferencia, información de la cuenta destino y cantidad a transferir (estos dos últimos los usaremos para completar la información de la transferencia).

A continuación, definiremos los slots y las entidades que vamos a usar durante la conversación:

    ...
    slots:
      product_type:
        type: text
        mappings:
          - type: from_entity
            entity: product_type

      balance:
        type: float
        mappings:
          - type: custom
            action: action_balance

      last_transactions:
        type: text
        mappings:
          - type: custom
            action: action_last_transactions

      transfer_amount:
        type: float
        influence_conversation: true
        mappings:
          - type: from_entity
            entity: transfer_amount

      transfer_account_number:
        type: text
        influence_conversation: true
        mappings:
          - type: from_entity
            entity: transfer_account_number

    entities:
      - product_type
      - transfer_amount
      - transfer_account_number

    ...

Vemos que hemos definido un slot para guardar el tipo de producto del que queremos consultar el saldo o los movimientos, el valor del saldo consultado, el valor de los últimos movimientos la cantidad a transferir y el número de cuenta destino.
En algunos casos hemos configurado que el slot sea rellenado por una entidad, en otros casos lo haremos desde una acción personalizada.
Las entidades que vamos a usar para describir qué información extraer son el tipo de producto, la cantidad a transferir y el número de cuenta destino.

Ahora creo que es un buen momento para ver cómo podríamos entrenar nuestro modelo para que sea capaz de entender las frases que el usuario nos envíe en este contexto. Nos vamos al fichero nlu.yml y encontramos, entre otros, los siguientes ejemplos:

    ...
  - intent: balance
    examples: |
      - ¿cuánto dinero tengo?
      - ¿cuánto dinero tengo en mi cuenta?
      - ¿cuánto dinero me queda?
      - ¿cuál es mi saldo?
      - ¿cuál es el saldo de mi [cuenta](product_type)?
      - ¿cuál es el saldo de mi [tarjeta](product_type)?

  - intent: last_transactions
    examples: |
      - movimientos recientes
      - últimas operaciones
      - últimos movimientos
      - últimos movimientos en mi cuenta
      - ¿cuáles son los últimos movimientos?
      - ¿cuáles son las últimas operaciones?
      - ¿cuáles son los últimos movimientos en mi cuenta?
      - ¿cuáles son los últimos movimientos en la [cuenta](product_type)?
      - ¿cuáles son los últimos movimientos en la [tarjeta](product_type)?

  - intent: transfer
    examples: |
      - haz una transferencia
      - quiero hacer una transferencia
      - quiero transferir dinero
      - quiero enviar dinero
      - quiero hacer una transferencia a otra cuenta
      - quiero transferir dinero a otra cuenta
      - quiero transferir [234.89](transfer_amount) a la cuenta [40381121613111761246](transfer_account_number)
      - quiero transferir [234.8] euros
      - transferir [234] euros (transfer_amount)
      - enviar [234.87] euros (transfer_amount)
      - transferir dinero a [40381121613111761246](transfer_account_number)
    ...

Con estos ejemplos, preparamos al modelo para que sea capaz de entender las frases que el usuario nos envíe, las clasifique en los intents que hemos definido y pueda extraer las entidades que nos interesan para que luego sean almacenadas en los slots.
Cuantos más ejemplos tengamos y más similares sean a ejemplos reales, mejor será el modelo que obtendremos.

Ahora, vamos a ayudar al modelo a entender cómo se desarrolla una conversación. En el fichero stories.yml tenemos la posibilidad de definir tanto Rules como Stories. Ambas se usan para entrenar el DM, pero las Rules son más sencillas y se usan para definir comportamientos concretos, mientras que las Stories se usan para definir flujos de conversación más raros o complejos. Por ahora, a nosotros nos sobra con las Rules:

    rules:
      - rule: greeting and Help
        steps:
          - intent: greeting
          - action: utter_greeting
          - action: utter_help

      - rule: ask the user to try again
        steps:
          - intent: nlu_fallback
          - action: utter_default
          - action: utter_help

      - rule: goodbye
        steps:
          - intent: bye
          - action: utter_bye

      - rule: get balance
        steps:
          - intent: balance
          - action: action_balance

      - rule: get last transactions
        steps:
          - intent: last_transactions
          - action: action_last_transactions

Cada una de estas reglas define un flujo de conversación para el intent que se ha interpretado.
En la primera regla, greeting and Help, saludamos al usuario y le ofrecemos ayuda si lo que se ha interpretado es que el usuario solo quiere saludar. En la segunda, ask the user to try again, si el usuario no ha dicho nada que podamos entender, le decimos que lo intente de nuevo (fallback). En la tercera, goodbye nos despedimos amablemente. En la cuarta, get balance, si el usuario nos pide el saldo, le decimos cuánto tiene y en la quinta, get last transactions, si el usuario nos pide los últimos movimientos, se los mostramos.
Ahora nos queda definir las acciones en algún lugar. Vemos como algunas acciones son respuestas sencillas y que empiezan con el prefijo utter_. Esas debemos definirlas en el fichero responses.yml:

    ...
    responses:
      utter_greeting:
        - text: "Hola, bienvenido al Chatbot de ACME Bank."
          image: "https://i.etsystatic.com/34273753/r/il/86989b/4784709033/il_1588xN.4784709033_oabn.jpg"

      utter_default:
        - text: "No te he entendido bien. ¿Puedes intentarlo de nuevo?"

      utter_help:
        - text: "Puedes pedirme cosas como: \n"
          buttons:
            - title: "¿Cuál es mi saldo?"
              payload: "/balance"
            - title: "¿Cuáles son los últimos movimientos en mi cuenta?"
              payload: "/last_transactions"

      utter_bye:
        - text: "Adiós, espero haberte ayudado. ¡Hasta la próxima!"

      utter_balance:
        - text: "El saldo actual de tu {product_type} es de {balance} EUR. ¿En qué más puedo ayudarte?"

      utter_last_transactions:
        - text: "Los últimos movimientos en tu {product_type} han sido: \n {last_transactions}. ¿En qué más puedo ayudarte?"


      utter_ask_transfer_account_number:
        - text: "Por favor, indícame el número de cuenta de destino."

      utter_ask_transfer_amount:
        - text: "Por favor, indícame el importe de la transferencia."

      utter_transfer:
        - text: "Se ha realizado una transferencia de {transfer_amount} EUR a la cuenta {transfer_account_number}. ¿En qué más puedo ayudarte?"
    ...

Además de texto (que permite además sustituir el valor de los slots), podemos incluir imágenes y botones en nuestras respuestas. Esto se interpretará de manera diferente en función del canal e incluso podemos definir mensajes por canal, así como múltiples mensajes para una misma acción (utter_).

Otras veces necesitamos definir acciones personalizadas que se ejecutarán en el servidor de acciones de Rasa. Estas comienzan con el prefijo action_.
Para usarlas necesitaremos hacer dos cosas: definir la acción en el fichero actions.py:

    ...

    class ActionGetBalance(Action):

    def name(self) -> Text:
        return "action_balance"

    def run(self, dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
        slots = []
        product_type = tracker.get_slot("product_type")

        if product_type is None:
            slots.append(SlotSet(key="product_type", value="Cuenta"))

        slots.append(SlotSet(key="balance", value=1000))

        dispatcher.utter_message(response="utter_balance")

        return slots

    ...

y registrar la acción en el fichero domain.yml

    ...
    actions:
    - action_balance
    - action_last_transactions
    - action_transfer
    ...

Ya por último, antes de probar nuestro ejemplo, usaremos los formularios como soporte a la recolección de información para la transferencia. Para eso hemos definido el formulario en domain.yml indicando la información que se necesita para completar el formulario así como los intents que se ignorarán durante el proceso de recolección de información:

    ...
    forms:
      transfer_form:
        ignored_intents:
          - greeting
          - bye
          - balance
          - last_transactions
        required_slots:
          - transfer_account_number
          - transfer_amount
    ...

Describiremos qué regla debe activar el formulario en el fichero stories.yml y cuando se debe desactivar para dar paso a la ejecución de la acción de transferencia (básicamente cuando se tiene la información necesaria):

  - rule: activate transfer form
    steps:
      - intent: transfer
      - action: transfer_form
      - active_loop: transfer_form

  - rule: make transfer
    condition:
      - active_loop: transfer_form
    steps:
      - action: transfer_form
      - active_loop: null
      - slot_was_set:
          - requested_slot: null
      - action: action_transfer

Añadimos además la acción para transferir dinero en el fichero actions.py:

    ...
    class ActionTransfer(Action):

        def name(self) -> Text:
            return "action_transfer"

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

            # Make the transfer...
            dispatcher.utter_message(response="utter_transfer")

            return []

    ...

Los formularios nos permiten añadir también validaciones y mensajes personalizados, usando convenciones de nombres para definirlos y que se usarán en el momento adecuado. Veamos el fichero domain.yml:

    ...
    intents:
    ...
        - info_transfer_account_number
        - info_transfer_amount
    ...

    responses:
      utter_ask_transfer_account_number:
        - text: "Por favor, indícame el número de cuenta de destino."

      utter_ask_transfer_amount:
        - text: "Por favor, indícame el importe de la transferencia."
    ...

    actions:
    ...
        - validate_transfer_form
    ...

Y la acción de validación en el fichero actions.py:

    ...
    class ValidateTransferForm(FormValidationAction):
        def name(self) -> Text:
            return "validate_transfer_form"

        def validate_transfer_account_number(self, slot_value: str, dispatcher: CollectingDispatcher, tracker: Tracker,
                                             domain: Dict[Text, Any]) -> Dict[Text, Any]:

            if not validate_account_number(slot_value):
                dispatcher.utter_message(text="El número de cuenta proporcionado no es válido")
                return {"transfer_account_number": None}

            return {"transfer_account_number": slot_value}

        def validate_transfer_amount(self, slot_value: float, dispatcher: CollectingDispatcher, tracker: Tracker,
                                     domain: Dict[Text, Any]) -> Dict[Text, Any]:

            if slot_value <= MIN_TRANSFER_AMOUNT:
                dispatcher.utter_message(text="Debe indicar una cantidad mayor a {} euros".format(MIN_TRANSFER_AMOUNT))
                return {"transfer_amount": None}

            return {"transfer_amount": slot_value}
    ...

En el ejemplo que os he proporcionado se han añadido también sinónimos y expresiones regulares para mejorar la recuperación de información del usuario.

Ahora, revisemos el fichero de configuración dónde hemos configurado el pipeline para que funcione a nuestro gusto:

    ...
    language: es

    pipeline:
      - name: WhitespaceTokenizer
      - name: LanguageModelFeaturizer
        model_name: "bert"
        model_weights: "rasa/LaBSE"
      - name: RegexFeaturizer
      - name: DIETClassifier
        epochs: 100
        learning_rate: 0.001
      - name: FallbackClassifier
        threshold: 0.7
      - name: EntitySynonymMapper

    policies:
      - name: MemoizationPolicy
      - name: TEDPolicy
        max_history: 5
        epochs: 100
      - name: RulePolicy
        core_fallback_threshold: 0.4
        core_fallback_action_name: "action_default_fallback"
        enable_fallback_prediction: True
    ...

En este caso, hemos usado un modelo de lenguaje preentrenado, LaBSE, para extraer características de las frases que el usuario nos envíe. Hemos añadido un RegexFeaturizer para que el modelo pueda extraer características de las expresiones regulares que hemos definido en el fichero nlu.yml. Hemos añadido un FallbackClassifier para que el modelo pueda gestionar mejor las frases que no entienda. Hemos añadido un EntitySynonymMapper para que el modelo pueda entender sinónimos de las entidades que hemos definido en el fichero nlu.yml. Hemos añadido un TEDPolicy para que el modelo pueda gestionar mejor la conversación y un RulePolicy para que el modelo pueda gestionar mejor las reglas que hemos definido en el fichero stories.yml.

Hasta aquí hemos definido el comportamiento que queremos de nuestro chatbot.

Ahora vamos a entrenar el modelo con los datos que hemos definido. Para ello, ejecutamos el siguiente comando:

    > rasa train
    ...
    2024-09-16 17:25:58 INFO     rasa.engine.training.hooks  - Finished training component 'TEDPolicy'.
    Your Rasa model is trained and saved at 'models/20240916-172513-district-team.tar.gz'.
    ...

Ahora, probemos nuestro asistente. Asegúrate de que el servidor de acciones está en marcha, ya que hemos usado acciones personalizadas. Si no lo está, abre otra shell y arráncarlo con el siguiente comando:

    > rasa run actions
    ...
    2024-09-16 17:26:38 INFO     rasa_sdk.executor  - Registered function for 'action_transfer'.
    2024-09-16 17:26:38 INFO     rasa_sdk.executor  - Registered function for 'validate_transfer_form'.
    2024-09-16 17:26:38 INFO     rasa_sdk.endpoint  - Starting plugins...
    2024-09-16 17:26:38 INFO     rasa_sdk.endpoint  - Action endpoint is up and running on http://0.0.0.0:5055
    ...

Y ahora ya si, probemos nuestro asistente. Tenemos varias opciones para hacerlo, pero por ahora usemos el comando rasa shell:

    > rasa shell
    ...
    2024-09-16 17:29:07 INFO     root  - Rasa server is up and running.
    Bot loaded. Type a message and press enter (use '/stop' to exit):
    Your input ->

Empecemos por saludar:

    Your input ->  Qué tal?
    Hola, bienvenido al Chatbot de ACME Bank.
    Image: https://i.etsystatic.com/34273753/r/il/86989b/4784709033/il_1588xN.4784709033_oabn.jpg
    ? Puedes pedirme cosas como:
     (Use arrow keys)
     » 1: ¿Cuál es mi saldo? (/balance)
       2: ¿Cuáles son los últimos movimientos en mi cuenta? (/last_transactions)
     Type out your own message...

Nos saluda y nos ofrece ayuda junto con algunas opciones (botones).

Y empecemos a hacer preguntas a nuestro asistente virtual. Por ejemplo, podemos preguntarle por nuestro saldo:

Voy a usar un botón para preguntar por el saldo de mi cuenta:

     1: ¿Cuál es mi saldo? (/balance)
    El saldo actual de tu Cuenta es de 1000 EUR. ¿En qué más puedo ayudarte?

Ahora preguntemos por los movimientos de nuestra tarjeta:

     Your input ->  Cuales son los movimientos de mi tarjeta?
     Los últimos movimientos en tu tarjeta han sido:

        06/03/2024 |  -60.45 EUR | Gasolina
        06/03/2024 | -189.34 EUR | Restaurante con amigos
        02/03/2024 | -250.34 EUR | Compra en Grandes Almacenes
        01/03/2024 |  +23.00 EUR | Pago de nómina marzo :)
        . ¿En qué más puedo ayudarte?

Vamos a preguntarle cosas de la vida, a ver qué nos dice:

    Your input ->  ¿Si fueras entrenador del Madrid, qué usarías 433 o 442?
    No te he entendido bien. ¿Puedes intentarlo de nuevo?
    ? Puedes pedirme cosas como:
     (Use arrow keys)
     » 1: ¿Cuál es mi saldo? (/balance)
       2: ¿Cuáles son los últimos movimientos en mi cuenta? (/last_transactions)
       Type out your own message...

Parece que el fútbol no le va. Probemos ahora a hacer una transferencia:

     Your input ->  quiero enviar dinero
     Por favor, indícame el número de cuenta de destino.

Voy a meter una cuenta que empiece por «9» (que es lo que hemos definido como inválida en la acción):

     Your input ->  90311122763110761275
     El número de cuenta proporcionado no es válido
     Por favor, indícame el número de cuenta de destino.

Vamos a meter una cuenta válida y una cantidad válida:

     Por favor, indícame el número de cuenta de destino.
     Your input ->  401813265738888761242
     Por favor, indícame el importe de la transferencia.
     Your input ->  234.45
     Se ha realizado una transferencia de 234.45 EUR a la cuenta 401813265738888761242. ¿En qué más puedo ayudarte?

Nos despedimos y paramos la prueba:

    Your input ->  adios
    Adiós, espero haberte ayudado. ¡Hasta la próxima!
    Your input ->  /stop
    2024-09-16 18:08:56 INFO     root  - Killing Sanic server now.

Esto parece que funciona.

Vamos a dar un pasito más y en lugar de la shell, vamos a probar a integrar el chatbot en una aplicación Web. Tenemos varias opciones para hacerlo, desde hacerlo nosotros usando la API, se puede usar alguna solución externa como Rasa Webchat o podemos usar el Widget oficial que proporciona Rasa, que es lo que vamos a hacer. En estos dos últimos casos debemos configurar WebSockets:
En el fichero credentials.yml debemos habilitar el canal de WebSocket indicando los nombres de los eventos que se usarán para la comunicación:

    ...
    socketio:
        user_message_evt: user_uttered
        bot_message_evt: bot_uttered
        session_persistence: false
    ...

Debemos incluir el widget en algún fichero HTML. En este caso, vamos a usar el siguiente código:

...
<div id="rasa-chat-widget" data-websocket-url="http://localhost:5005" />
<script src="https://unpkg.com/@rasahq/rasa-chat" type="application/javascript" />
...

Ahora debemos arrancar el servidor de Rasa, con el que comunicará el widget:

    > rasa run --cors "*" --debug

El servidor de acciones personalizadas, con el que comunicará el servidor de Rasa:

    > rasa run actions

También arrancaremos un servidor Web que servirá nuestra aplicación (en el directorio web está nuestra aplicación):

    > python -m http.server -d web

Et voilà! Ya tenemos nuestro chatbot funcionando en una aplicación web:
muestra el chatbot embebido en una página web que simula un banco

4. Conclusiones

Yo creo que esto es más que suficiente para dar nuestros primeros pasos en el mundillo de los chatbots conversacionales y lo hemos hecho construyendo juntos un asistente virtual sencillo con Rasa.
Como habréis imaginado, esto abre un mundo de posibilidades, además de otra gran cantidad de cosas por resolver como la gestión de la información, la seguridad, la privacidad, la integración con otros sistemas y canales, el entrenamiento continuado del chatbot, etc.
Espero al menos haber despertado vuestro interés por este mundo y que os animéis a seguir explorando. ¡Hasta pronto!

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad