Creación de un bot conversacional usando herramientas low code

29 de junio de 2022

En este post usaremos herramientas low code para crear un chatbot de Facebook Messeger con procesamiento del lenguaje natural (NPL).

Para ello generaremos una API con Tinybird que almacenará todas las posibles respuestas del bot, en función de etiquetas, palabras o frases parciales encontradas en el texto de entrada del usuario, y de la intención de este texto (saludo, pregunta, ayuda, etc.).

Las entradas de usuario serán clasificadas en intents -intenciones- y entities -entidades- a través de la inteligencia artificial del algoritmo proporcionado por Wit.ai, que nosotros mismos entrenaremos.

Hasta aquí nada de código, solo utilizaremos un servidor node.js para unir todas las piezas, conectarnos a la API de Facebook Messenger y servir las respuestas que nuestro bot devolverá desde su propia página de Facebook.

Todo esto suena muy serio y hasta aburrido para quien no esté profundamente interesado en los chatbots. Por ello, y para pasar yo mismo un buen rato, he decidido crear un chatbot nostálgico que habla y cuenta chistes en lenguaje boomer de los 80' y 90', llamado OK Boomer Bot.

OK Boomer Bot

Podéis visitarlo y decirle cosas bonitas en su página:

https://www.facebook.com/OK-Boomer-Bot-104044359006300

Qué es un chatbot con procesamiento natural del lenguaje (NPL)

Un bot conversacional o chatbot nos permite ofrecer a nuestros clientes un acceso más humano y personalizado a diferentes servicios. Entre sus ventajas, podemos ofrecer una gran cantidad de servicios sin tener que crear un menú con todas las opciones; es posible responder a comandos de voz, y también responder con generación de voz; es posible entender el lenguaje natural, es decir, las diferentes maneras que tiene un humano de expresar una misma idea ("¿qué tiempo hace en Madrid?", "¿va a llover en Madrid?", "¿lloverá en la capital de España?...).

Un ejemplo de chatbot lo proporciona nuestra solución Flowable Engage, que permite el uso de chatbots para el soporte a clientes, e incluso derivar el ticket a un operador humano cuando el chatbot no puede procesar satisfactoriamente la petición del usuario (ver video).

Otros ejemplos notables son OK Google, Alexa, pero también decenas de chatbots que han irrumpido en páginas de empresas, y chats de Whatsapp, Telegram, Facebook o cuentas de Twitter que responden a cosas tan curiosas como colorear una foto en blanco y negro (@Colorize_Bot).

Los chatbots pueden ser stateless, es decir, responden a cada frase de forma independiente, o también stateful, recordando detalles importantes de la conversación hasta el momento y permitiendo operaciones complejas que requieren más de una interacción entre el usuario y el bot.

Algunas tecnologías extendidas para la programación de chatbots incluyen:

Idea y diagrama de la solución

Nuestro chatbot residirá en una página de Facebook, donde gestionará los mensajes recibidos.

Diagrama de la solución

Será capaz de las siguientes funciones:

Una aplicación node.js orquestará la interacción entre Facebook Messenger, la inteligencia artificial de Wit.ai que analiza los mensajes y las respuestas predefinidas que almacenamos en una API de Tinybird.

Creación de la API

Vamos a crear una API basándonos en herramientas no-code. El caso de uso sería tener una persona o un departamento no técnico al cargo del mantenimiento del chatbot. Estas personas se encargarían de actualizar tablas de excel en Google Drive, que contienen las frases que va a utilizar el chatbot junto con etiquetas que permitirán clasificarlas.

Alguien con conocimientos técnicos medios podría usar Tinybird para generar las APIs que nuestro chatbot consumirá, y mantenerlas actualizadas cada vez que las tablas excel sean modificadas.

Para nuestro chatbot usaremos 3 tablas diferentes, con varias columnas:

Ejemplo de tabla excel en Google Drive:

Tabla en Google Drive

Exportamos estas tablas a CSV, desde el propio Google Drive, lo cual nos genera una url, que importaremos en Tinybird, y a partir de las 3 tablas generamos varias APIs:

Como ya se mencionó, es muy sencillo actualizar las APIs y no se necesitan grandes conocimientos técnicos. Por un lado es necesario modificar las tablas en Google Drive y volver a exportar a una url CSV, para después reemplazar las fuentes en Tinybird por la nueva url. Si el formato de las tablas no ha cambiado -nombre de las columnas y tipos de datos- los nuevos datos sustituyen a los antiguos y las APIs siguen funcionando casi sin interrupción.

Aquí podéis ver un ejemplo de una de las pipes creadas en Tinybird.

Un pipe de Tinybird

Diseño y entrenamiento del chatbot

El diseño y entrenamiento de un chatbot con Wit.ai sigue los siguientes pasos:

Por ejemplo, dentro de la utterance "qué temperatura hace en Valencia", el intent podría ser getTemperature si así lo hubiésemos llamado y habría una entity de tipo location en la palabra "Valencia".

El proceso puede ser a la inversa, es decir, vamos generando frases, y sobre ellas generamos los intents y las entities. Una vez generadas varias utterances se debe entrenar el algoritmo. Asimismo, cada vez que la API de Wit.ai reciba nuevas peticiones de clasificación de mensajes, estos aparecerán como nuevas utterances que podemos corregir en caso de que el bot las haya interpretado de forma errónea, volver a entrenar al bot, y así ir mejorando su rendimiento conforme se usa más.

Para nuestro chatbot OK Boomer Bot, generamos los siguientes intents y entities:

Intents

Veamos un ejemplo de entity tipo keyword. En este caso debemos enumerar todos los posibles valores que la entity puede tomar, incluidos sinónimos que cada keyword puede recibir en el contexto:

Entities

He aquí una muestra de cómo se enseña al algoritmo a reconocer intents y entities dentro de las utterances.

Entrenamiento del bot mediante utterances

Desarrollo del servidor node.js

Como este proyecto es un divertimento personal, para el servidor node.js he escogido un servidor alojado en Glitch, que me permite editar los ficheros directamente mediante su interfaz de usuario sin necesidad de crear un repositorio.

El servidor se queda dormido cuando nadie lo usa por un período de tiempo y por ello el bot tarda un poco en responder la primera vez que le escribimos.

El código está estructurado en varios ficheros:

Los scripts de integración con las APIs de Facebook y Tinybird carecen de interés. Quien desee conocer más detalles puede mirar el código fuente en Glitch:

https://glitch.com/edit/#!/changeable-mercury-foxtrot

El script de la app expone un webhook que consumirá Facebook. Un webhook (también conocido como «API inversa») es una herramienta que permite que un sistema o aplicación envíe notificaciones sobre un evento específico a otro sistema o aplicación en tiempo real.

En nuestro caso el webhook expone un endpoint de tipo GET para la verificación del webhook desde Facebook. Esta llamada incluye un token definido por nosotros, que solo nuestra app y Facebook conocen. Cuando Facebook presenta el token correcto, nuestro webhook devuelve un status 200 indicando que está verificado.

Por otro lado se expone un endpoint de tipo POST para recibir los eventos de Facebook. Si el evento es un mensaje recibido a través de la página de Facebook procesaremos el texto con nuestro bot y enviaremos la respuesta a Facebook Messenger.

const express = require("express");
const bodyParser = require("body-parser");
const fetch = require("node-fetch");
const facebookHandler = require("./facebook-handler");
const botHandler = require("./bot-handler");

// Creates express http server
const app = express().use(bodyParser.json());
app.listen(process.env.PORT || 1337, () => console.log("webhook is listening"));

// Adds support for GET requests to our webhook
app.get("/webhook", (req, res) => {
  // Parse the query params
  let mode = req.query["hub.mode"];
  let token = req.query["hub.verify_token"];
  let challenge = req.query["hub.challenge"];

  // Checks if a token and mode is in the query string of the request
  if (mode && token) {
    // Checks the mode and token sent is correct
    if (mode === "subscribe" && token === process.env.FB_VERIFY_TOKEN) {
      // Responds with the challenge token from the request
      console.log("WEBHOOK_VERIFIED");
      res.status(200).send(challenge);
    } else {
      // Responds with '403 Forbidden' if verify tokens do not match
      console.log("WEBHOOK_NOT_VERIFIED");
      res.sendStatus(403);
    }
  }
});

// Creates the endpoint for our webhook
app.post("/webhook", (req, res) => {
  let body = req.body;

  // Checks this is an event from a page subscription
  if (body.object === "page") {
    // Iterates over each entry - there may be multiple if batched
    body.entry.forEach(function (entry) {
      // Gets the message. entry.messaging is an array, but
      // will only ever contain one message, so we get index 0
      const event = entry.messaging[0];

      // Yay! We got a new message!
      // We retrieve the Facebook user ID of the sender
      const id = event.sender.id;
      const message = event.message.text;
      console.log(`received message: ${message}`);

      botHandler
        .processMessage(message)
        .then((response) => {
          console.log(`sent response: ${JSON.stringify(response)}`);
          const { text, title, url, image_url } = response;
          facebookHandler
            .sendFacebookMessage({ id, text, title, url, image_url })
            .catch(console.error);
        })
        .catch((err) => {
          console.error(`Oops! Got an error from Wit: `, err.stack || err);
        });
    });

    // Returns a '200 OK' response to all requests
    res.status(200).send("EVENT_RECEIVED");
  } else {
    // Returns a '404 Not Found' if event is not from a page subscription
    res.sendStatus(404);
  }
});

Veamos parte del bot handler para entender qué hacemos con los intents y las entities. En general el intent debería ser suficiente para clasificar el mensaje del usuario. Sin embargo, Wit.ai devuelve a veces el intent vacío pero es capaz de reconocer un entity dentro del texto. En este caso haremos la clasificación nosotros a partir de la entity, ya que en nuestro modelo a cada entity le corresponde un solo intent.

No incluiremos el código entero del bot handler, tan solo la identificación de la tarea a realizar mediante el intent y la entity, así como una tarea de ejemplo (enviar un chiste de un autor). Como vemos al final del listado de código, se llama a una de las APIs de Tinybird con el parámetro author, y el resultado puede incluir los valores text, title, url, image_url. Esto es porque algunos chistes que devuelve la API tienen un vídeo con title, url, image_url mientras que otros son solo texto (text).

const Wit = require("node-wit").Wit;
const log = require("node-wit").log;
const apiHandler = require("./api-handler");

// Setting up our bot
const wit = new Wit({
  accessToken: process.env.WIT_TOKEN,
  logger: new log.Logger(log.INFO),
});

exports.processMessage = (message) =>
  wit.message(message).then((res) => responseFromWit(res));

const responseFromWit = (data) => {
  let intent, entity;
  try {
    intent = data.intents.length && data.intents[0];
    entity = Object.values(data.entities)[0][0];
  } catch (err) {}

  if (intent) {
    return handleIntent(intent, data);
  } else if (entity && entity.confidence > 0.9) {
    return handleEntity(entity, data);
  }

  return handleSystemMessageRandom();
};

const handleIntent = (intent, data) => {
  switch (intent.name) {
    case "greeting":
      return handleGreeting();
    case "farewell":
      return handleFarewell();
    case "phraseStartingWith":
      return handlePhraseStartingWith(data);
    case "phraseTranslation":
      return handlePhraseTranslation(data);
    case "jokeRandom":
      return handleJokeRandom();
    case "jokeFrom":
      return handleJokeFrom(data);
    case "jokeAbout":
      return handleJokeAbout(data);
  }
};

const handleEntity = (entity, data) => {
  switch (entity.role) {
    case "phrase_start":
      return handlePhraseStartingWith(data);
    case "phrase_to_translate":
      return handlePhraseTranslation(data);
    case "author":
      return handleJokeFrom(data);
    case "tag":
      return handleJokeAbout(data);
  }
};

const handleJokeFrom = (data) => {
  const entities = data.entities["author:author"];
  const author = entities.length && entities[0].value;
  const authorName = entities.length && entities[0].body;

  if (!author) {
    return handleSystemMessageRandom();
  }

  return apiHandler.getJokeByAuthor(author).then((joke) => {
    const { message, title, url, image_url } = joke;
    const text = !message
      ? undefined
      : `Aquí va un chiste de ${authorName}:
    
${message}`;
    return { text, title, url, image_url };
  });
};

Conclusiones

Hemos aprendido que con las herramientas hoy disponibles es relativamente sencillo crear un chatbot. Con muy poco código hemos creado un bot en Facebook Messenger que responde a un usuario, siendo capaces de interpretar el lenguaje natural.

Además este bot sería sencillo de mantener por personas con bajo nivel técnico.

La inteligencia artificial de Wit.ai tiene algunas limitaciones:

Créditos

Foto por Lana Codes en Unsplash.

Sobre el autor: Juangui Jordán

Desarrollador full stack, cada vez más especializado en frontend y UX. Siempre dispuesto a aprender y enseñar todo lo que tengo que ofrecer a los demás.

Comments
Únete a nosotros