user photo

Desarrollando un chatbot con Javascript

Published on
Sergio Perea · 13 min read
javascript

En general muchos chatbots se construyen a partir de plataformas Saas. El motivo es que su funcionamiento suele ser muy similar y puede ser complicado implementarlos desde cero. De este modo, parece razonable que el modelo de distribución de muchos chatbots se base en servicios accesibles a través de la nube que simplemente deban ser parametrizados.

Sin embargo, hay ocasiones en las que te pueda interesar desarrollar tu propio chatbot sin recurrir a uno de estos servicios. Uno de los motivos suele ser que queremos centralizar la lógica de nuestra aplicación en un único bot que se comunique con diversas plataformas, de modo que podemos cubrir un amplio abanico de canales para comunicarnos con nuestros clientes.

Un día me gustaría mostrarte como plantear uno de estos chatbots en React o Vue.js usando web sockets, pero hoy te voy a mostrar una manera mucho más sencilla de empezar en este mundillo a través de Javascript: BotKit.

Botkit: ¿qué es?

Botkit es una librería Js y un conjunto de herramientas y complementos (o sea, un framework), que proporciona a los desarrolladores de bots una interfaz independiente de la plataforma con la que construir chatbots. De este modo el programador puede olvidarse de ciertos detalles técnicos esenciales para centrarse en la creación de las características propias de su chatbot y luego utilizar esa funcionalidad en diferentes aplicaciones.

En dfinitiva, es un kit muy potente y sencillo para crear interfaces de usuario conversacionales, que cuenta con funciones intuitivas como hears (), ask () y reply () que hacen básicamente lo que dicen que hacen. Yo creo que no puede ser más sencillo.

Botkit, como veremos ahora, cuenta con un sistema muy flexible que nos permitirá manejar diálogos con guiones y conversaciones transaccionales que involucren preguntas, lógica de ramificación y otros comportamientos dinámicos. El límite sólo lo pondrá tu imaginación y tu pericia programando.

Instalación

La instalación, tal como muestra su página web, es bastante simple, así que no entraré en muchos detalles. Simplemente, para empezar a probar, ejecuta esto:

    mkdir mybot
    cd mybot
    yo botkit

Tras un sencillo asistente (deberemos decidir, entre otras cosas, bajo qué plataforma funcionará nuestro bot), ya podemos ejecutar nuestro proyecto, que por defecto escuchará en el puerto 3000:

npm start
    
Chat with me: http://localhost:3000
    
o 
    
Webhook endpoint online:  http://localhost:3000/api/messages 
    

El controlador

El controlador podríamos definirlo como el “cerebro” de nuestro bot. Dicho controlador es nuestra interfaz con todas las funcionalidades de BotKit. Incluso los eventos de nuestro chat deben ser adjuntados al controlador. Algo así como trozos de código en los que le decimos a nuestro bot: «cuando el usuario haga esto tú tienes que hacer eso«.

A continuación te muestro las funciones que tiene el controlador, aunque obviamente para profundizar en ellas tendrás que navegar por la documentación oficial, porque las posibilidades de BotKit son más grandes de lo que parece a simple vista.

  • addDep()
  • addDialog()
  • addPluginExtension()
  • afterDialog()
  • completeDep()
  • getConfig()
  • getLocalView()
  • handleTurn()
  • hears()
  • interrupts()
  • loadModule()
  • loadModules()
  • on()
  • publicFolder()
  • ready()
  • saveState()
  • shutdown()
  • spawn()
  • trigger()
  • usePlugin()

Una de las ventajas de que este framework tenga esta arquitectura, es que el controlador podemos desarrollarlo de forma independiente de la plataforma. De este modo, podremos utilizar Botkit para desarrollar bots en las siguientes plataformas:

  • Un chatbot web creado con React o Vue.js
  • Un bot para slack
  • Un bot para equipos webex.
  • Un bot para Google Hangouts
  • Un bot para Facebook Messenger
  • Un bot para Twilio SMS.

Cada tipo de bot requeriría dedicar un artículo, porque el tema es bastante extenso. Pero en general el uso del controlador será muy parecido. Veamos como podemos programar un controlador de forma general en Botkit para que haga de endpoint para cualquier chat de la plataforma que sea:

    const { Botkit } = require('botkit');
    
    const controller = new Botkit({
        webhook_uri: '/api/messages',
    });
    
    controller.hears('.*','message', async(bot, message) => {
    
        await bot.reply(message, 'I heard: ' + message.text);
    
    });
    
    controller.on('event', async(bot, message) => {
        await bot.reply(message,'I received an event of type ' + message.type);
    
    });

Una vez has entendido este trozo de código, ya estás preparado para entender cómo funciona tu chatbot. De momento, vamos a ir desgranando diferentes partes de este código.

Escuchando mensajes

Botkit cuenta con un controlador de eventos para gestionar la interacción con el usuario. Una de sus funciones se llama hears () y básicamente nos ayudará a configurar el bot para que dispare funciones cuando “escuche” algo del usuario.

Vamos a verlo con un ejemplo, aunque luego profundizaremos un poco más:

    controller.hears('hola','message',async(bot, message) => {
        // do something!
        await bot.reply(message, 'Hola Mundo')
    });

Como ves, no puede ser más simple: cuando el controlador escucha que el usuario escribe «hola», le responde: «Hola Mundo». ¡Ya tenemos nuestro Hola Mundo!

Gestión de eventos

Hears() nos ayudará a responder a eventos conversacionales, pero nuestro bot también podrá responder a otro tipo de eventos. Por ejemplo: cuando un usuario se une al canal, cuando se hace click en un botón o cuando se sube algún archivo. El patrón para manejar estos eventos es muy intuitivo para cualquiera que conozca javascript.

    controller.on('channel_join', async(bot, message) => {
        await bot.reply(message,'Bienvenido a mi canal');
    });

Una vez el usuario se conecta a nuestro chatbot, el bot recibirá un constante flujo de eventos que van desde mensajes de texto, notificaciones, o cambios de presencia del usuario (entra, sale del chat, se desconecta, etc.). Incluso podremos crearnos nuestros propios eventos.

Tal como habrás intuido hasta ahora, para responder a los eventos usamos controller.on ().

Extendiendo Botkit con un middleware

Además de ejecutar acciones en base a respuestas a un determinado mensaje o evento, Botkit también puede llevar a cabo acciones sobre los mensajes de forma pasiva interceptándolos a través de middlewares. Las funciones de middleware pueden modificar en tiempo real los mensajes, agregar nuevos campos, disparar eventos alternativos y modificar el comportamiento del Bot.

A continuación te muestro un sencillo ejemplo donde el middleware intercepta mensajes enviados y recibidos para mostrar en consola su contenido.

    // Log every message received
    controller.middleware.receive.use(function(bot, message, next) {
    
        // log it
        console.log('RECEIVED: ', message);
    
        // modify the message
        message.logged = true;
    
        // continue processing the message
        next();
    
    });
    
    // Log every message sent
    controller.middleware.send.use(function(bot, message, next) {
    
        // log it
        console.log('SENT: ', message);
    
        // modify the message
        message.logged = true;
    
        // continue processing the message
        next();
    
    });

El middleware básicamente dispone de tres endpoints:

  • ingest: es como receive pero se ejecuta antes de que dicho mensaje sea procesado.
  • receive
  • send

Esta funcionalidad es muy importante de cara a integrar tu bot en muchas plataformas, ya que te permite, entre otras cosas, crear pipelines y procesos que gestionen el mensaje sin preocuparse de su origen. En definitiva, es determinante dependiendo de la arquitectura que quieras implementar para tu aplicación.

Piensa, por ejemplo, que un mensaje llega en bruto pero a lo mejor necesitas realizar un proceso de transformación del mismo para adaptarlo a cada plataforma en la que está integrado el bot. Puede que algunos mensajes debas enviarlos a través de web sockets, y otros simplemente los gestiones a través de un webhook. Puede que quieras que tu chat sea cifrado extremo a extremo o que el tratamiento de los mensajes sea diferente en función del tipo de usuario.

En estas y muchas otras situaciones, el middleware será una herramienta esencial

Control avanzado de las conversaciones

Botkit cuenta con funciones más vanzadas, entre las que está BotkitConversation, que te permitirá crear interfaces basadas en diálogos, botones, opciones, etc.

Los diálogos se crean usando funciones como convo.ask () y convo.say (), y las acciones dinámicas se pueden implementar usando un sistema de enlace (convo. before (), convo.after () y convo.onChange ()) que proporciona contexto de conversación y un worker del bot en puntos clave donde hay que ejecutar código.

    const { BotkitConversation } = require('botkit');
    
    // define the conversation
    const onboarding = new BotkitConversation('onboarding');
    
    onboarding.say('Buenos días');
    onboarding.ask('¿Cómo puedo dirigirme a ti?', async(answer) => { 
        // no hagas nada, sólo espera una respuesta
    }, {key: 'name'});
    
    // recolectar posibles valores. Cuidado con lo que respondes.
    onboarding.ask('¿Te gusta la tortilla con cebolla o sin cebolla?', [
        {
            pattern: 'con cebolla',
            handler: async function(answer, convo, bot) {
                await convo.gotoThread('me_gusta_con_cebolla');
            }
        },
        {
            pattern: 'sin cebolla',
            handler: async function(answer, convo, bot) {
                await convo.gotoThread('me_gusta_sin_cebolla');
            }
        }
    ],{key: 'tortilla'});
    
    onboarding.addMessage('Me representas', 'me_gusta_con_cebollas');
    
    onboarding.addMessage('Eso no es tortilla ni es nada', 'me_gusta_sin_cebolla');
    
    onboarding.after(async(results, bot) => {
        const name = results.name;
    });
    
    controller.addDialog(onboarding);
    
    controller.hears(['hello'], 'message', async(bot, message) => {
        bot.beginDialog('onboarding');
    });

Bueno, pues con esta pequeña introducción espero que te esté picando el gusanillo para poder experimentar. De momento ve asimilándolo y en sucesivos artículos intentaré explicarte como ir más allá hasta que desarrolles un chatbot completo.

Testeando nuestro chatbot

Desarrolla usando TDD

El Desarrollo Guiado por Pruebas (TDD) es especialmente útil en este tipo de software, ya que los chatbots generalmente requieren aplicar iteraciones muy cortas y rápidas de refactorización. De lo contrario, a medida que vayamos añadiendo o quitando partes de nuestro flujo de conversación nos irá quedando un montón de código descolgado, inútil y «spaguetti».

Necesitamos tener mucho control en el código para no acabar escribiendo más de lo que realmente se necesita y sobre todo debemos ser capaces de detectar fallos cada vez que cambiamos algo en el flujo de las conversaciones, pues es frecuente que un pequeño cambio influya en el funcionamiento de otra parte del programa.

Parece entonces bastante razonable escribir test unitarios para nuestro Chat Bot. Pues veamos la manera de hacerlo:

Opción 1: Botkit-Mock

Uno de los primeros problemas que nos podemos encontrar con BotKit es que depende demasiado de adaptadores con otras aplicaciones (Slack, Facebook, MS Teams, etc.). Por ello existe Botkit-Mock, que es una extensión de Botkit que pretende proporcionar una interfaz para aceptar mensajes de usuario a través de .usersInput.

Lamentablemente, la librería es demasiado nueva y en el momento de escribir este artículo sólo podremos utilizar Botkit-Mock con un adaptador para Slack, de modo que sólo te resultará útil si construyes un bot para Slack.

Para utilizarlo sólo tendremos que instalarlo en nuestro proyecto:

npm install --save botkit-mock

e incluirlo en nuestro proyecto:

const { BotMock } = require('botkit-mock');

const fileBeingTested = require("./indexController")

Aquí tendrías un ejemplo de un test:

    
    'use strict';
    const assert = require('assert');
    const
    {
      BotMock,
      SlackApiMock
    } = require('../../../lib');
    const
    {
      SlackAdapter,
      SlackMessageTypeMiddleware,
      SlackEventMiddleware
    } = require('botbuilder-adapter-slack');
    const fileBeingTested = require(
      './dialog');
    describe('create dialog in a thread',
        () =>
        {
          const initController =
            () =>
            {
              const adapter =
                new SlackAdapter(
                {
                  clientSigningSecret: "some secret",
                  botToken: "some token",
                  debug: true,
                });
              adapter.use(
                new SlackEventMiddleware()
              );
              adapter.use(
                new SlackMessageTypeMiddleware()
              );
              this.controller =
                new BotMock(
                {
                  adapter: adapter,
                });
              SlackApiMock
                .bindMockApi(
                  this
                  .controller
                );
              fileBeingTested(this
                .controller);
            };
          beforeEach(() =>
          {
            this.userInfo = {
              slackId: 'user123',
              channel: 'channel123',
            };
          });
          describe('create_service',
            () =>
            {
              beforeEach(
                () =>
                {
                  initController
                    ();
                });
              it(`should reply in a correct sequence through message`,
                async() =>
                {
                  await this
                    .controller
                    .usersInput(
                      [
                      {
                        type: 'message',
                        user: this
                          .userInfo
                          .slackId, //user required for each direct message
                        channel: this.userInfo.channel, // user channel required for direct
                        message
                        messages: [
                        {
                          text: 'create_dialog_service',
                          isAssertion: true
                        }]
                      }]);
                  assert.strictEqual(this.controller.detailed_answers[this.userInfo.channel][0].text, `Howdy!`);
                });
            });

Opción 2: TestMyBot

Dadas las limitaciones de la opción anterior, te propongo otra. TestMyBot es un framework de automatización de prueba para chatbots. Es agnóstico respecto a las herramientas involucradas en tu desarrollo. Y además es gratis y de código abierto.

Las herramientas de captura y reproducción registrarán tus casos de prueba y se ejecutarán contra la implementación de tu chatbot automáticamente una y otra vez. Está planteado para que lo puedas usar en tu delivery pipeline.

Veamos como instalar TestMyBot para utilizarlo junto con la librería Jasmine (framework para desarrollo de test):

    npm install testmybot --save-dev
    npm install jasmine --save-dev
    ./node\_modules/.bin/jasmine init/code>

Añade un archivo spec/testmybot.spec.js con este contenido:

        const bot = require('testmybot');
        const botHelper = require('testmybot/helper/jasmine');
    
        botHelper.setupJasmineTestSuite(60000);

Añade también un archivo testmybot.json a tu carpeta de proyecto:

    { 
      "containermode": "local"
    }

El archivo jasmine.js es precisamente el que se encarga de conectar el código de tu chatbot con el código de TestMyBot. Genera una conversación y un informe en XML.

TestMyBot viene con helpers integrados para Jasmine y Mocha, pero también se puede usar con otras librerías. Incluso cuando se usa con Docker, también se puede usar con proyectos de chatbot escritos en otros lenguajes de programación.

En general, nuestros casos de prueba no serán más que conversaciones que el chatbot debería poder manejar. La transcripción de la conversación debe ejecutarse automáticamente, y cualquier diferencia con respecto a la transcripción debe informarse como un error.

En general, estos casos de pruebas constituyen un conjunto de pruebas de regresión y nos garantiza, antes de desplegar después de cualquier cambio, que el chatbot todavía funciona correctamente después de que se cambió o se conectó con otro software.

Por tanto, debes escribir test que garanticen que cualquier cambio en tu chatbot no romperá sus flujos de conversación. Obviamente, esto implica que tendrás que plantear test para todos los flujos de conversación posibles.

TestMyBot IDE

Para interactuar con nuestro ChatBot, contaremos con una herramienta llamada TestMyBot IDE, que nos proporciona una interfaz en el navegador para registrar y organizar nuestros casos de prueba e interactuar con él.

Además, la conversación la guardará en un fichero de texto que posteriormente nos servirá para comprobar que todo ha ido según lo esperado. Por supuesto, cuenta también con herramientas de captura y reproducción que registran tus casos de prueba y pueden ejecutarse contra la implementación dl Chatbot en cualquier momento.

Línea de comandos

Si lo del entorno gráfico no te gusta, TestMyBot incluye una interfaz de línea de comandos para interactuar con tu chatbot. Es muy útil en entornos de servidor.

Veamos como funciona

    npm install testmybot --save-dev
    npm install testmybot-fbmock --save-dev
    npm install jasmine --save-dev
    ./node\_modules/.bin/jasmine init

Añade un archivo “testmybot.json” a la carpeta de tu proyecto. También será necesaria una configuración básica (la siguiente va sobre Docker):

    {
      "docker": {
        "container": {
          "testmybot-fbmock": {
            "env": {
              "TESTMYBOT\_BOTKIT\_WEBHOOKPORT": 3000,
              "TESTMYBOT\_BOTKIT\_WEBHOOKPATH": "webhook"
            }
          }
        }
      }
    }

Imagina un chatbot muy simple que está programado para que cuando tú le digas “Hola” él te responda: «Mundo». El caso de prueba desarrollado con jasmine (pec/testmybot.spec.js) para este flujo conversacional sería el siguiente:

        describe('Hello World Bot', function() {
          beforeEach(function() {
            this.bot = require('testmybot').setup();
          });
          it('di hola', function() {
           expect(this.bot.hears('Hola').says().text())).toMatch(/mundo/);
          });
        });

Tampoco te olvides de incluir el script en tu package.json.

    "scripts": {
      "start\_testmybot": "node index.js",
    },

Como ves, se trata de un objeto que envía texto a tu chatbot (podría ser otro tipo de contenido), y recibe lo que tu bot responde .

Para ejecutar tus test con Jasmine:

    ./node\_modules/.bin/jasmine init