Tabla de contenidos

Introducción

Los chatbots están aquí, y han venido para quedarse. Son una nueva manera más natural de interactuar con los usuarios, y además viven en los canales que los usuarios ya usan.

Una de las frameworks más famosas para construir chatbots es la Microsoft Bot Framework y uno de los lenguajes que soporta es Node.js. Durante este artículo veremos como construir chatbots inteligentes utilizando ambos.

Canales

Hay muchos canales en los que un chatbot puede vivir, incluyendo aplicaciones móviles, la web o el mail. Y desde luego, los canales de mensajería como Facebook Messenger, Telegram o Slack. El problema con la existencia de tantos canales, es que cada uno tiene una manera diferente de comunicarse, y eso supone que para desplegar un bot en varios hay que hacer mucho desarrollo diferente solamente para adaptarse a la comunicación de cada canal. Esta es una de las ventajas de usar Microsoft Bot Framework: desplegas un único bot, y puedes conectarlo a los canales sin desarrollo extra, y Microsoft es el responsable de conectarse a cada canal y traducir los mensajes a un formato común. Para ello hay que desplegar en Azure un componente llamado Bot Channel Registration, que tiene una interfaz gráfica sencilla para añadir el bot a los diferentes canales:

Dos notas importantes:

  • Mucha gente cree que los bots desarrollados con Microsoft Bot Framework obligatoriamente hay que desplegarlos en Azure. En realidad se pueden desplegar en cualquier plataforma de tu gusto (Google Cloud, AWS, Digital Ocean, Heroku,…) y lo que sí se despliega en Azure es el Bot Channel Registration y se hace que apunte al backend de tu bot.
  • El uso de Bot Channel Registration es gratuito para canales que no sean premium. Los canales premium son aquellos en los que Microsoft no solamente hace de conector sino que también tiene que almacenar trozos de conversación y manejarla, es decir: Web y Direct Line API (genera una API, es la manera ideal de integrar bots en móvil). Los canales premium pueden ser desplegados como F0 (gratuito pero limitado a 10.000 mensajes al mes) o S1 (0.42$ por cada 1000 mensajes. También es importante saber que un mensaje se considera no solamente del usuario al bot, sino también del bot hacia el usuario.

UI, Clickbots, Inteligencia

Los chatbots de hoy en día no son solamente de texto, sino que se han ido añadiendo elementos gráficos para interactuar con el usuario, como botones, imágenes, localización, carruseles,… y estos elementos gráficos siguen evolucionado a día de hoy.

Para probar estos elementos gráficos, puedes probar la web de Adaptative Cards, la propuesta de Microsoft para ser capaces de usar estos elementos de una manera común para diferentes canales.

Pero también hay una manera de añadir inteligencia a tus chatbots: NLP (Natural Language Processing). El NLP se compone de dos partes:

  • NLU: Natural Language Understanding. Es la parte que a partir de una utterance (frases tal y como las dicen los humanos) es capaz de determinar el intent (acción que esa utterance significa). Además es capaz de extraer las entidades mediante algo llamado NER (Named Entity Recognition).
  • NLG: Natural Language Generation. Es la parte para producir frases hacia el usuario. No es solamente devolver una frase y punto, hay que tener en cuenta el género del usuario, los plurales, el nivel de confianza, el contexto de la conversación,…

Lo entenderemos mejor con un ejemplo:

En este ejemplo la utterance es I want to travel to Barcelona tomorrow, que el NLU interpreta como el intent user.travel. Además, debe reconocer dos entidades: la localización a la que se quiere viajar y la fecha. Hay que pensar en el intent como el nombre de la acción que queremos que el chatbot ejecute, y las entidades como los parámetros necesarios para realizar dicha acción.

node-nlp

Hay muchos productos para la parte del NLP, como DialogFlow (Google), Wit.ai (Facebook) o LUIS.ai (Microsoft). Puedes encontrar comparativas por internet, aunque no todas son fiables porque dependen mucho del idioma que se pruebe y de la fecha en la que se haya probado, porque son tecnologías en contínua evolución.

Una de las grandes diferencias que hacen de DialogFlow el líder, es el uso de contextos, de manera que el procesamiento de los intents varía en función de parámetros de la conversación, y da herramientas para definir las respuestas de los usuarios. Esto es muy importante porque por ejemplo otro líder del sector, LUIS.ai, no da contexto ni manera de definir las respuestas, sino solamente los intents, y las respuestas y el contexto los dejan para la parte del desarrollo dentro del código del bot.

También puedes haber oído hablar de QnA Maker. QnA Maker es muy bueno para poder hacer rápidamente bots a partir de una FAQ (Frequently Asked Questions), pero es importante entender que no es un NLP. Cuando usas QnA Maker en profundidad, parece no estar utilizando técnicas de inteligencia artificial para tomar las decisiones, sino más bien algoritmos de distancia de cadenas de texto. En mi caso lo probé entrenándolo con frases en lengua negra, una lengua inventada de El Señor de los Anillos, y el ejemplo funcionaba, con lo cual internamente no tiene en cuenta el idioma utilizado (de hecho nunca lo pregunta).

También existen productos que en lugar de en la nube pueden ser desplegados on premise, como RASA o Snips. La principal razón para no tener un NLP en la nube sino dentro de u compañía, suele ser la seguridad y temas legales: evitar que información confidencial del usuario sea enviada a compañías de terceros. Esto es muy importante sobre todo en chatbots cuya área es la salud: pensad en un chatbot al que el usuario le comunica sus enfermedades, o incluso sus alergias. También puede haber otros motivos para tener tu NLP dentro de tu compañía, como personalización, precio, performance, privacidad…

En mi caso para los ejemplos utilizaré node-nlp, que es una libreía de Node.js basada en Natural. Las razones para usar una libería son varias, pero básicamente lo que nos permite es una total personalización del NLP, integrar el NLG directamente en el contexto del bot para cada conversación, integración sencilla, no tener que desplegar de manera separada y la performance, además de permitirnos hacer entrenamiento del bot en tiempo real.

Sobre la performance, tener el NLP como libería significa que no hay que llamar a ninguna API para resolver la información, sino que está directamente accesible en el programa, así que no hay TTL. Esto es importante porque cuando se envía una respuesta a un usuario, muchas veces para calcularla hay proceso de negocio intermedios que ya tardan un tiempo, así que agregar incluso más tiempo puede hacer esperar al usuario. Una pequeña comparación del mismo bot hecho con node-nlp y con LUIS:

¡Menos de 2 milisegundos para calcular la respuesta!

Creando un bot con node-nlp

Para crear un bot usando node-nlp, básicamente hay que crear un bot como siempre, y utilizar node-nlp para cargar el modelo, bien sea programándolo, desde un fichero de modelo o desde un excel. Es importante que una vez el modelo está entrenado, guardarlo para no tener que volver a perder el tiempo que se tarda en entrenarlo, porque es un proceso que utiliza mucha CPU.

const builder = require('botbuilder');  const express = require('express');  const fs = require('fs');  const { Recognizer } = require('node-nlp');    const modelName = './smalltalk.nlp';  const excelName = './smalltalk.xls';    // Creates a connector for the chatbot  const connector = new builder.ChatConnector({    appId: process.env.BOT_APP_ID,    appPassword: process.env.BOT_APP_PASSWORD,  });    // Creates a node-nlp recognizer for the bot  const recognizer = new Recognizer();  if (fs.existsSync(modelName)) {    recognizer.load(modelName);  } else {    recognizer.loadExcel(excelName);    recognizer.save(modelName);  }    // Creates the bot using a memory storage, with a main dialog that  // use the node-nlp recognizer to calculate the answer.   const bot = new builder.UniversalBot(connector, (session) => {    recognizer.recognize(session, (err, data) => {      session.send(data.answer || 'I don\'t understand');    });  }).set('storage', new builder.MemoryBotStorage());    // Creates the express application  const app = express();  const port = process.env.PORT || 3000;  app.post('/api/messages', connector.listen());  app.listen(port);

Para modificar el comportamiento del bot, simplemente edita el excel que viene con su código. En ese excel podrás cambiar las utterances, intents y respuestas:

Una vez modificado solamente tienes que reentrenar a tu bot con el nuevo excel.

Para probar el bot antes de desplegarlo, te sugiero que uses Microsoft Bot Emulator, que te permitirá usarlo en local y hacer todas las pruebas necesarias. El bot del ejemplo ejcutaría así:

Intents y Diálogos

Si ya has desarrollado chatbots usando Microsoft Bot Framework y LUIS, sabrás que la manera en la que la framework usa el NLP es muy diferente de la que se muestra en el ejemplo. Como LUIS no tiene NLG con lo cual no devuelve la respuesta sino el intent, Microsoft Bot Framework propone trabajar con disparadores: cada mensaje que llega al bot es enviado a LUIS, y si LUIS reconoce el mensaje como un intent, y existe un diálogo marcado como que debe ejecutarse cuando suceda ese intent, entonces se dispara ese diálogo. Un ejemplo de código:

const luisAppId = process.env.LuisAppId;  const luisAPIKey = process.env.LuisAPIKey;  const luisAPIHostName = process.env.LuisAPIHostName || 'westus.api.cognitive.microsoft.com';    // Create a recognizer that gets intents from LUIS, and add it to the bot  bot.recognizer(new builder.LuisRecognizer(`https://${luisAPIHostName}/luis/v2.0/apps/${luisAppId}?subscription-key=${luisAPIKey}`));    bot.dialog('GreetingDialog', (session) => {    session.send(`You reached the Greeting intent. You said '${session.message.text}'`);    session.endDialog();  }).triggerAction({ matches: 'Greeting' });

Dentro de node-nlp hay una clase Recognizer que puede ser usada perfectamente en lugar del LUIS Recognizer de Microsoft. Un ejemplo usando node-nlp recognizer:

const builder = require('botbuilder');  const express = require('express');  const { Recognizer } = require('node-nlp');    // Creates a connector for the chatbot  const connector = new builder.ChatConnector({    appId: process.env.BOT_APP_ID,    appPassword: process.env.BOT_APP_PASSWORD,  });    // Creates a node-nlp recognizer for the bot  const recognizer = new Recognizer();  recognizer.nlpManager.addLanguage('en');  recognizer.nlpManager.addDocument('en', 'Hello', 'Greeting');  recognizer.nlpManager.addDocument('en', 'Hey', 'Greeting');  recognizer.nlpManager.addDocument('en', 'Hi!', 'Greeting');  recognizer.nlpManager.addDocument('en', 'I need help', 'Help');  recognizer.nlpManager.addDocument('en', 'Help me', 'Help');  recognizer.nlpManager.addDocument('en', 'help', 'Help');  recognizer.nlpManager.addDocument('en', 'I want to cancel', 'Cancel');  recognizer.nlpManager.addDocument('en', 'cancel', 'Cancel');  recognizer.nlpManager.addDocument('en', 'please cancel', 'Cancel');  recognizer.train();    // Creates the bot using a memory storage, with a main dialog that  // use the node-nlp recognizer to calculate the answer.   const bot = new builder.UniversalBot(connector, (session) => {    session.send(`You reached the default message handler. You said '${session.message.text}'.`);  }).set('storage', new builder.MemoryBotStorage());    bot.recognizer(recognizer);    bot.dialog('GreetingDialog', (session) => {    session.send(`You reached the Greeting intent. You said '${session.message.text}'.`);    session.endDialog();  }).triggerAction({ matches: 'Greeting' });    bot.dialog('HelpDialog', (session) => {    session.send(`You reached the Help intent. You said '${session.message.text}'.`);    session.endDialog();  }).triggerAction({ matches: 'Help' });    bot.dialog('CancelDialog', (session) => {    session.send('You reached the Cancel intent. You said \'%s\'.', session.message.text);    session.endDialog();  }).triggerAction({ matches: 'Cancel' });    // Creates the express application  const app = express();  const port = process.env.PORT || 3000;  app.post('/api/messages', connector.listen());  app.listen(port);  console.log(`Chatbot listening on port ${port}`);

En este ejemplo hemos entrenado al bot directamente desde el código para enseñar que no solamente se puede hacer desde el excel, y además para enseñar que al poder cambiar programáticamente el NLP, significa que podemos cambiar los intents y respuestas y reentrenar en tiempo real. El bot funcionando:

Híbrido

El problema viene cuando tienes que hacer un chatbot más complejo, en el que tienen que convivir un árbol de diálogos complejo con una inteligencia contextualizada. En esos casos, la integración del NLP es muy complicada usando Microsoft Bot Framework y LUIS. Gran parte del trabajo duro viene causado por el hecho de que el reconocedor está integrado en la lógica core de Microsoft Bot Framework, calculando cual será el siguiente diálogo a mostrar, suponiendo que no se integra ningún NLG para calcular ese enrutado.

Pero hay una manera de cambiar el comportamiento de ese enrutado cambiado un método llamado “disambiguate route”, de manera que se puede reemplazar el comportamiento por defecto de Microsoft Bot Framework con tu propia manera de calcular la ruta basada en la pila de diálogos de la conversación y la información de sesión.

Todo esto que suena complicado, node-nlp ya es capaz de hacerlo internamente, añadiendo la capacidad de tener respuestas, o incluso de que que si la respuesta empieza por / entonces significa que no es una respuesta de texto sino la ruta hacia un diálogo.

Para conseguirlo, en lugar de llamar a bot.recognizer(recognizer) como en el ejemplo anterior, hay que llamar a recognizer.setBot(bot, true, 0.7), que le dirá al recognizer el bot al que debe configurarse, el true significa que active el enrutado del recognizer sustituyendo al de Miccrosoft, y 0.7 es el umbral de certeza que debe tener como mínimo al reconocer el intent para que se intent sea disparado en lugar de considerar que la respuesta pertenece a la ejecución que ya había del diálogo.

Mejor verlo en código:

const builder = require('botbuilder');  const express = require('express');  const fs = require('fs');  const { Recognizer } = require('node-nlp');    const modelName = './hybrid.nlp';  const excelName = './hybrid.xls';    // Creates a connector for the chatbot  const connector = new builder.ChatConnector({    appId: process.env.BOT_APP_ID,    appPassword: process.env.BOT_APP_PASSWORD,  });    // Creates a node-nlp recognizer for the bot  const recognizer = new Recognizer();  if (fs.existsSync(modelName)) {    recognizer.load(modelName);  } else {    recognizer.loadExcel(excelName);    recognizer.save(modelName);  }    // Creates the bot using a memory storage, with a main dialog that  // use the node-nlp recognizer to calculate the answer.   const bot = new builder.UniversalBot(connector, (session) => {    session.send(`You reached the default message handler. You said '${session.message.text}'.`);  }).set('storage', new builder.MemoryBotStorage());    recognizer.setBot(bot, true);    bot.dialog('/GreetingDialog', (session) => {    session.send(`You reached the Greeting intent. You said '${session.message.text}'.`);    session.endDialog();  });    bot.dialog('/HelpDialog', (session) => {    session.send(`You reached the Help intent. You said '${session.message.text}'.`);    session.endDialog();  });    bot.dialog('/CancelDialog', (session) => {    session.send('You reached the Cancel intent. You said \'%s\'.', session.message.text);    session.endDialog();  });    // Creates the express application  const app = express();  const port = process.env.PORT || 3000;  app.post('/api/messages', connector.listen());  app.listen(port);  console.log(`Chatbot listening on port ${port}`);

El bot ejecutándose:

Conclusión

Es muy importante tener un buen enrutado en el chatbot de manera que ambos mundos, el de los chatbots inteligentes y el de los clickbots, puedan vivir juntos en el mismo bot para lograr la mejor experiencia de usuario.

También es muy importante separar la generación de respuestas del código, porque eso es parte del NLG, y nos permitirá cambiar el coportamiento del bot sin tener que cambiar su código ni desplegarlo de nuevo. Permitir además que desde el NLG podamos controlar la pila y flujo de diálogos es de lo más potente, y es la manera en la que podemos conseguir hacer bots muchísimo más complejos pero que en realidad al usuario le resulten intuitivos.

Código y Librerías:

Todos los ejemplos de código pueden ser encontrados en este repositorio:

  • https://github.com/jseijas/bot-nlp
  • https://github.com/axa-group/nlp.js
  • https://github.com/NaturalNode/natural
  • https://github.com/Microsoft/botbuilder

Por Jesús Seijas de la Fuente

Enfocado en proyectos de IA, principalmente IA conversacional y Visión por Computador. Autor de NLP.js, un conjunto de librerías NLP para AI conversacional, capaz de hacer clasificación, con normalizadores, tokenizadores y lematizadores para 41 idiomas, que es capaz de entrenar y clasificar en el navegador y móvil sin conexión a internet, e integrado con BERT. La red neuronal está diseñada para el problema de clasificación de PNL en términos de precisión y rendimiento.

Deja una respuesta

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