Creación de un bot conversacional usando herramientas low code
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.
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:
- Google Dialogflow. Se trata de una plataforma de procesamiento de lenguaje natural, incluyendo texto y voz, de tipo stateful y basada en una interfaz visual que permite diseñar los flujos.
- Microsoft Azure Bot Service. Proporciona un entorno de desarrollo integrado para la creación de bots, mediante una plataforma low code totalmente hospedada que nos permite diseñar los bots. También procesa texto y voz.
- Watson Assistant de IBM. Plataforma para la generación de asistentes conversacionales, multicanal y con procesamiento natural del lenguaje. Diseñada para usuarios de negocio, da acceso a sus herramientas a través de herramientas low code.
- Amazon Lex. Servicio de inteligencia artificial (IA) completamente administrado con modelos avanzados de lenguaje natural que sirve para diseñar, crear, probar e implementar interfaces de conversación en las aplicaciones.
- Wit built-in NPL for Facebook Messenger. Tecnología stateless con integración para Facebook Messenger.
Idea y diagrama de la solución
Nuestro chatbot residirá en una página de Facebook, donde gestionará los mensajes recibidos.
Será capaz de las siguientes funciones:
- Saludar y despedirse, por ej.
- hola => hola caracola!
- Traducir frases a lenguaje boomer, por ej.
- traduce 'me da igual' => a mi plin, duermo con Pikolín
- Acabar frases boomer, por ej.
- me piro => ...vampiro
- Contar chistes boomer, por ej.
- cuéntame el chiste del pato => Saben aquel que diu...
- un chiste de Chiquito, por favor => (vídeo de un chiste de Chiquito de la Calzada)
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:
- Tabla
system
. Incluye mensajes de sistema, que se devolverán en función del tag proporcionado a la API. Por ejemplo, si se piden mensajes con el tag "help" devolverá un mensaje de ayuda. Tiene las siguientes columnas:message
: mensaje de textolength
: longitud en caracteres. Campo autocalculado pensado para una posible integración con twittertags
: puede tomar valores como "help" o "nonsense" para responder a diferentes situaciones.
- Tabla
phrases
. Incluiye frases boomer que el bot devolverá ante un saludo, despedida, una petición de traducción, o una frase por acabar. Incluye las siguientes columnas:message
: mensaje de textolength
: longitud en caracterestags
: puede tomar valores como "saludo" o "despedida" para responder a diferentes situaciones.start
: cuando la frase se puede dividir en dos partes, por ejemplo dos partes que riman, este campo incluye la primera parteend
: este campo incluye la segunda parte de la frasetranslations
: traducciones a lenguaje actual de la frase
- Tabla
jokes
. Incluye chistes pasados de moda, de autores de la época de los 80' y 90'. Consta de las columnas:message
: mensaje de textolength
: longitud en caracterestags
: puede tomar valores como "colegio" o "muerte" para buscar chistes por tema.title
,url
eimageUrl
. Algunos chistes no tienen texto sino un vídeo. Para mostrar el vídeo en Facebook Messenger necesitamos un título y un screenshot, además de la url del vídeo.
Ejemplo de tabla excel 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:
system_by_tag.json
: devuelve todos los mensajes de sistema coincidentes con un tag, por ejemplo"help"
phrases_by_tag.json
: devuelve todas las frases coincidentes con un tag, por ejemplo"saludo"
phrases_by_translation.json
: devuelve todas las frases coincidentes con una traducción, por ejemplo"cómo estás"
phrases_starting_with
: devuelve todas las frases que empiezan por una frase, por ejemplo"qué listo"
jokes_all
: devuelve todos los chistesjokes_by_author
: devuelve todos los chistes de un determinado autor, por ejemplo"eugenio"
jokes_by_tag
: devuelve todos los chistes coincidentes con un tag, por ejemplo"colegio"
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.
Diseño y entrenamiento del chatbot
El diseño y entrenamiento de un chatbot con Wit.ai sigue los siguientes pasos:
- Diseñamos los intents con los que queremos que el bot clasifique el texto de entrada.
- Diseñamos las entities que queremos que el bot extraiga del texto de entrada.
- Generamos frases de ejemplo llamadas utterances y ayudamos al bot a reconocer el intent asociado a ese ejemplo y a extraer las entities que contenga.
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:
greeting
- Corresponde a frases tipo "hola"
farewell
- Corresponde a frases tipo "Adiós"
jokeRandom
- Corresponde a frases tipo "Cuéntame un chiste"
jokeAbout
- Corresponde a frases tipo "Cuéntame el chiste sobre el colegio"
- Incluye la entidad
tag
que identifica el tema del chiste, en este caso "colegio". Esta entidad es de tipo keyword, ya que se debe buscar dentro de un conjunto de palabras predefinidas.
jokeFrom
- Corresponde a frases tipo "Cuéntame un chiste de Chiquito"
- Incluye la entidad
author
que identifica el autor del chiste, en este caso "Chiquito". Esta entidad es de tipo keyword, ya que se debe buscar dentro de un conjunto de palabras predefinidas.
phraseStartingWith
- Corresponde a frases tipo "a otra cosa..." que el bot acabará con "mariposa"
- Incluye la entidad
phrase_start
que identifica el principio de la frase, en este caso "a otra cosa". Esta entidad es de tipo keyword, ya que se debe buscar dentro de un conjunto de frases predefinidas.
phraseTranslation
- Corresponde a frases tipo "tradúceme 'dar una vuelta'" que el bot traducirá como "dar un voltio"
- Incluye la entidad
phrase_to_translate
que identifica la frase a tracudir, en este caso "dar una vuelta"
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:
He aquí una muestra de cómo se enseña al algoritmo a reconocer intents y entities dentro de las 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:
.env
. Variables de entorno incluyendo las API keys y tokens de las APIs de Facebook, Wit y Tinybird. Este archivo solo es visible para usuarios autorizadosapp.js
. Script principal, ejecutado al levantar el servidorapi_handler.js
. Funciones auxiliares para interrogar nuestras APIs de Tinybird y obtener un resultado aleatorio de entre las filas retornadasbot_handler.js
. Funciones auxiliares para interrogar la API de Wit.ai y clasificar los mensajes de usuario usando sus capacidades de procesamiento natural del lenguaje (NLP)facebook_handler.js
. Funciones auxiliares para interrogar enviar mensajes de texto e imagen a Facebook Messenger en nombre del botpackage.json
. Enumera las dependencias javascript en forma de paquetes de node e incluye el script de inicio
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:
- Necesita bastante entrenamiento si queremos que entienda bien al usuario. A veces textos similares no son correctamente interpretados. En cualquier caso las herramientas de Wit.ai hace fácil mejorar continuamente el motor con las entradas de usuario recibidas.
- Se trata de un motor stateless, lo que nos obliga a programar a mano el flujo, si nuestro bot debe almacenar estado. Por ejemplo, si quisiéramos entablar una conversación en la que el usuario indica que tiene un problema, le solicitamos el número de pedido, le preguntamos si el problema es en relación al envío, etc.
Créditos
Foto por Lana Codes en Unsplash.