Crea un chatbot meteorológico en NodeJS con Microsoft Bot Framework

Monday, January 14, 2019

Crea un chatbot meteorológico en NodeJS con Microsoft Bot Framework

Escrito por Enrique Blanco, investigador del equipo de Ideas Locas CDO de Telefónica.

El pasado 12 de diciembre de 2018, en el último LUCA Talk del año, tuvimos la oportunidad de ver cómo crear un conjunto muy sencillos de chatbots en node.js haciendo uso de Microsoft Bot Framework. Si no pudisteis uniros, en el siguiente enlace tendréis la oportunidad de ver lo sencillo que resulta crear un bot con funcionalidades muy interesantes con unas mínimas nociones de programación en JavaScript.

En este artículo, resumiremos los principales pasos necesarios para crear un chatbot a través de los recursos disponibles con Microsoft Bot Framework y terminaremos mostrando cómo crear un chatbot que nos ofrezca el tiempo actual en una ciudad a elección del usuario.

Imagen por satélite de una tormenta tropical


Los chatbots son agentes inteligentes, guiados bien por reglas o por Inteligencia Artificial, diseñados para responder automáticamente a las peticiones de usuario a través de interfaces conversacionales.
La inteligencia de los primeros reside en la calidad de su programación. Su capacidad de respuesta está limitada a unos casos restringidos. Los chatbots basados en machine Learning, cada vez más numerosos cuentan con funcionalidades basadas en Inteligencia Artificial, como Natural Language Processing. Aprenden conforme el usuario interacciona con ellos. La comunicación suele ser más humana.

Desde 1966 con ELIZA, pasando por ALICE y SmarterChild a principios del S.XXI y con la llegada de IBM con Watson, Apple con Siri y Google con Google Now, en los últimos años el mundo de los chatbots y los asistentes virtuales han iniciado un boom al que se suman Amazon con Alexa, Windows con Cortana, Microsoft con Microsoft Botbuilder, Facebook con Chatfuel, principilamente soportados gracias a su integración en múltiples canales generalistas.
Los chatbots son un recurso cada vez más usados en la actualidad. ¿Qué promueve actualmente el desarrollo masivo de estas herramientas?
  1. Por un lado, la expansión de las aplicaciones de mensajería. Se estima que aproximadamente 2,500M usuarios tienen al menos una app de mensajería. En 2014 se mandaba la nada desdeñable cantidad de 1,300M mensajes/día; en 2016 ya eran unos 7,600M mensajes/día.
  2. Además, nos hallamos en una etapa en la que la evolución del Machine Learning y la Inteligencia Artificial está siendo muy rápida y efectiva. Nos encontramos con una sustancial mejora de funcionalidades soportadas por este tipo de servicios, además de que se ha agilizado el despliegue y la eficiencia de estos bots.
  3. Cada vez contamos con ecosistemas más robustos, y cada vez más apps de mensajería fomentan el despliegue tanto de bots propios como customizados por el usuario.
Figura 1. Aplicaciones destacadas de chatbots 

Antes de comenzar con la creación de bots, necesitamos satisfacer los siguientes requisitos en nuestro sistema:
  • Tener instalado Node.js 
  • Instalar un editor de código como Visual Studio Code 
  • Descargar Bot Framework Emulator, para hacer uso de la misma como aplicación que permite testear los bots tanto en localhost como en remoto. En este breve tutorial nos centraremos en pruebas en local.
Una vez que tengamos todos los requisitos satisfechos, podremos lanzar nuestro primer bot de manera rápida y sencilla.

1. Abre un terminal con privilegios elevados.
2. Si aún no contamos con un directorio para bots en JavaScript, creamos uno y nos situamos en el mismo.
md myJsBots
cd myJsBots
3. Nos aseguramos de que la versión de npm está actualizada.
npm install -g npm
4. Después, instalamos Yeoman y el generador para JavaScript.
npm install -g yo generator-botbuilder
5. A continuación, podemos usar el generador para crear un bot de eco.
yo botbuilder
Yeoman solicitará alguna información adicional con la que crear el bot. Si optamos por los valores predeterminados, obtendremos una plantilla básica de Chatbot.
  • Escriba un nombre para el bot. (a la elección del usuario)
  • Escriba una descripción. (a la elección del usuario)
  • Elija el idioma del bot. (JavaScript)
  • Elija la plantilla que se va a usar. (Echo
Si decide crear un bot Basic, necesitará un modelo de lenguaje LUIS. No es lo que nos ocupa en este simple tutorial, por lo que nos restringiremos al tipo Echo.
Gracias a la plantilla, el proyecto contiene todo el código necesario para crear el bot en esta guía de inicio rápido. Realmente no necesita escribir ningún código adicional.

El generador Yeoman crea una aplicación web de tipo restify. Si vamos a la guía de inicio rápido de restify en su documentación, verá una aplicación similar al archivo index.js generado. A continuación se describe cómo se suele estructurar una aplicación de este tipo. Contamos con cuatro pilares principales: los archivos package.json, .env, index.js, bot.js y un fichero .bot

package.json
package.json especifica las dependencias del bot y sus versiones asociadas. Esto viene configurado por la plantilla y el sistema.
Figura 2. Ejemplo de fichero package.json

archivo .env
El archivo .env especifica la información de configuración del bot, como el número de puerto, el identificador de aplicación y la contraseña, entre otras cosas. Si utiliza determinadas tecnologías o si usa este bot en producción, deberá agregar las claves o dirección URL específicas a esta configuración. Para este bot de eco, no obstante, no necesita hacer nada aquí ahora mismo; el identificador de aplicación y la contraseña se pueden dejar sin definir en este momento.
Para usar el archivo de configuración .env la plantilla necesita incluir un paquete adicional. En primer lugar, obtenga el paquete dotenv de npm:
npm install dotenv
Figura 3. Ejemplo de fichero .env 
index.js
El archivo index.js configura el bot y el servicio de hospedaje que reenviará las actividades a la lógica del bot.

Por lo general, en la parte superior del archivo index.js encontrarán la serie de módulos o bibliotecas que van a ser necesarios para el funcionamiento del mismo. Estos módulos le darán acceso a un conjunto de funciones que tal vez desee incluir en la aplicación.

La siguiente parte carga información del archivo de configuración de bot.

En este fichero también se incluyen tanto el adaptador del bot como el servidor HTTP y el estado del bot. En esta parte se configura el servidor y el adaptador que permiten al bot comunicarse con el usuario y enviar respuestas. El servidor escuchará en el puerto especificado en el archivo de configuración BotConfiguration.bot o volverá al 3978 (que resulta el puerto por defecto) para la conexión con el emulador. El adaptador actúa como el director del bot, dirige la comunicación entrante y saliente y la autenticación, entre otras.
También se crea un objeto de estado que usa MemoryStorage como proveedor de almacenamiento. Este estado se define como ConversationState, lo que significa simplemente que mantiene el estado de la conversación. ConversationState almacenará en memoria la información que le interesa, que en este caso es simplemente un contador de turnos.

Dentro de este archivo o haciendo llamada a uno diferente, se define la lógica del bot con toda la estructura de los diálogos.

Archivo .bot
El archivo .bot contiene información, incluidos el punto de conexión, el identificador de aplicación, la contraseña y las referencias a los servicios que el bot utiliza. Este archivo se crea automáticamente al iniciar la creación de un bot desde una plantilla, pero puede crear el suyo propio con el emulador u otras herramientas. Puede especificar el archivo .bot que se va a utilizar al probar el bot con el emulador.
Figura 4. Ejemplo de fichero .bot

Un archivo .bot no es un requisito para crear bots con el SDK Bot Builder. También puede usar appsettings.json, web.config, env, un almacén de claves o cualquier mecanismo que le convenga para realizar un seguimiento de las referencias de servicio y las claves de las que depende el bot. Sin embargo, para probar el bot con el emulador, necesitará un archivo .bot. La buena noticia es que el emulador puede crear un archivo .bot para las pruebas. Si no haces uso de un asistente como Yeoman, puedes fabricarte fácilmente tu propio fichero .bot.


Funcionamiento de los bots. Detalles HTTP, avisos en cascada y enriquecimiento de contenido

Detalles HTTP
Las actividades llegan al bot desde Bot Framework Service mediante una solicitud POST HTTP. El bot responde a la solicitud POST entrante con un código de estado HTTP 200. Las actividades que se envían desde el bot al canal se envían en una solicitud POST HTTP independiente a Bot Framework Service. Esta se confirma de vuelta con un código de estado HTTP 200.

Diálogos y avisos de cascada
Dada la naturaleza de la interacción solicitud-respuesta, para implementar un aviso se requieren al menos dos pasos en un diálogo de cascada: uno para enviar el aviso y otro para capturar y procesar la respuesta. Si tiene un aviso adicional, en ocasiones puede combinar estos pasos mediante una única función para procesar primero la respuesta del usuario y luego iniciar el siguiente aviso.
Un diálogo de cascada se compone de una secuencia de pasos de cascada. Cada paso es un delegado asincrónico que toma un parámetro de contexto de paso de cascada (también llamado step). 
Se puede controlar el valor devuelto desde un diálogo, por ejemplo dentro de un paso de la cascada de un diálogo. Dentro de un paso de la cascada, el diálogo proporciona el valor devuelto en la propiedad result del contexto del paso. 
Una cascada avanza paso a paso en la secuencia con que las funciones están definidas en la matriz. La primera función dentro de una cascada puede recibir los argumentos que se pasan al diálogo. Cualquier función dentro de una cascada puede usar la función next para avanzar al paso siguiente sin pedirle al usuario que intervenga.

Incorporación de datos adjuntos de tarjetas enriquecidas a mensajes
Varios canales, como Skype y Facebook, admiten el envío de tarjetas gráficas enriquecidas a los usuarios con botones interactivos en los cuales el usuario hace clic para iniciar una acción. El SDK proporciona varias clases de generadores de tarjetas y mensajes que se pueden usar para crear y enviar tarjetas. El servicio Bot Framework Connector representará estas tarjetas con un esquema nativo para el canal, compatible con la comunicación multiplataforma. Si el canal no es compatible con las tarjetas, como SMS, Bot Framework hará todo lo posible para representar una experiencia razonable a los usuarios.

Veremos a continuación un ejemplo de gestión de contenidos muy sencillo para aprender a definir este tipo de tarjetas enriquecidas.

Weather Chatbot

Afortunadamente, hay miles de módulos que se pueden agregar a nuestro proyecto y que pueden hacer casi cualquier cosa. En este proyecto, usaremos los módulos de BotBuilder (esta es la biblioteca de Microsoft que le permite crear robots), Restify (le permite crear una API REST en solo unas pocas líneas de código) y, finalmente, Request (que le permite crear hacer peticiones HTTP a otros sitios) para poder obtener el tiempo en el momento actual haciendo llamada tanto a las APIs de OpenWeatherMap como de AEMET.
Figura 5. Lógica del bot de consulta de tiempo creado para el tutorial

Creamos un archivo app.js en el directorio de su proyecto, este archivo será el punto de entrada de nuestra aplicación. Contendrá toda la lógica de nuestro robot.

Abra el archivo que acaba de crear en el modo de edición, ¡es hora de comenzar a programar! Lo primero que debe hacer es cargar los módulos Node.js que necesitaremos
var request = require('request');
var restify = require('restify');
var builder = require('botbuilder');
//------------------------------------------------------
// Configuration
//------------------------------------------------------
// Loading your environment variables is a one-liner:
require('dotenv').config();
Luego, debemos definir algunas variables de configuración. Para ello, leeremos todos los datos que tenemos almacenados en el fichero .env. Lo que nos interesa es OPEN_WEATHER_MAP_APP_ID y AEMET_APP_ID para poder hacer llamadas a las APIs de los servicios de meteorología.
// OpenWeatherMap API Key
var openWeatherMapAppId = process.env.OPEN_WEATHER_MAP_APP_ID;
// AEMET API Key
var aemetApiKey = process.env.AEMET_APP_ID;
const inMemoryStorage = new builder.MemoryBotStorage();

Dijimos que el robot era en realidad una API web simple, por lo que necesitaremos un servidor HTTP:
Ha llegado el momento de crear el robot al indicar el conector que tendrá que usar. El conector es lo que permite al robot interactuar con los usuarios. Hay varios tipos de conectores, incluido el ConsoleConnector que le permite interactuar con el robot directamente desde la consola. Aquí, usaremos el ChatConnector, que permite conectar el robot al Conector de Bot de Microsoft (Skype, Facebook, Slack, etc.) y al Emulador de canales de Framework de Bot (herramienta para simular un servicio de mensajería para realizar pruebas).
//-------------------------------------------------------
// Setup
//-------------------------------------------------------
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
   console.log('%s listening to %s', server.name, server.url); 
});

Luego tenemos que decirle a nuestro servidor HTTP que es el robot el que responderá las llamadas REST.
// Create connector and bot
const connector = new builder.ChatConnector({
    appId: process.env.MICROSOFT_APP_ID,
    appPassword: process.env.MICROSOFT_APP_PASSWORD
});
// Listen for messages from users 
server.post('/api/messages', connector.listen());
Para seleccionar las acciones que se realizarán en función de los mensajes que se le envíen, nuestro robot tendrá que determinar qué intención está oculta detrás de cada mensaje. Aquí empezamos a describir la estructura conversacional que queremos que siga nuestro bot.
// Declaring the bot and manually setting the memory storage
const bot = new builder.UniversalBot(connector, [
    (session, results, next) => {
        session.preferredLocale('es', (err) => {
          if (err) {
            session.error('error');
          }
          session.send("Bienvenido al weatherBot de LUCA");
          next();
        });
      },
    (session, results) => {
        builder.Prompts.text(session, 'Por favor, indique para qué ciudad desea conocer el tiempo en este momento');
        //next();
      },
    (session, results, next) => {
        openweathermap(results.response, function(success, previsions) {
            if (!success) return session.send('Se ha producido un error en la llamada a la API de OpenWeatherMaps. Por favor, inténtelo de nuevo.');
            
            var message = 'Actualmente en ' + previsions.city + ' tenemos ' + previsions.description + ' con:\n\n' +
                          'Temperatura  : ' + previsions.temperature + '°C\n\n' + 
                          'Grado de humedad : ' + previsions.humidity + '%\n\n' +
                          'Velocidad del viento : ' + previsions.wind + 'km/h';
                          
            session.send(message);
            //session.endConversation()
        });
        next();
      },
    (session, results) => {
        aemetapi(function(success, weatherMapUrl){
            if (!success) return session.send('Se ha producido un error en la llamada a la API de AEMET. Por favor, inténtelo de nuevo.');
            //console.log('URL en el diálogo -->' + weatherMapUrl)
            const msg = new builder.Message(session).addAttachment(createHeroCard(session, weatherMapUrl));
            session.send(msg);
            session.endConversation()
        });
    }
]).set('storage', inMemoryStorage);
En el fragmento de código que se está viendo, deberías haber notado que usamos las funciones openweathermap y aemetapi , que aún no están declaradas. Estas funciones se usarán para recuperar el clima de una ciudad usando la API de Open Weather Map y el mapa de isobaras más reciente del Atlántico Norte, aquí está su código:

//-------------------------------------------------------
// Call to OpenWeatherMap API
//-------------------------------------------------------

var openweathermap = function (city, callback){
    //console.log(openWeatherMapAppId)
    var url = 'https://api.openweathermap.org/data/2.5/weather?q=' + city + '&lang=es&units=metric&appid=' + openWeatherMapAppId;   
    request(url, function(err, response, body){
        try{        
            var result = JSON.parse(body);
            
            if (result.cod != 200) {
                callback(false);
            } else {
                
                var previsions = {
                    description : result.weather[0].description,
                    temperature : Math.round(result.main.temp),
                    humidity : result.main.humidity,
                    wind: Math.round(result.wind.speed * 3.6),
                    city : result.name,
                };
                        
                callback(true, previsions);
            }
        } catch(e) {
            callback(false); 
        }
    });
}

//-------------------------------------------------------
// Call to AEMET API
//-------------------------------------------------------

var aemetapi = function (callback){
    //console.log(aemetApiKey)
    var url = 'https://opendata.aemet.es/opendata/api/mapasygraficos/analisis/?api_key=' + aemetApiKey;   
    request(url, function(err, response, body){
        try{        
            var result = JSON.parse(body);
            //console.log(result)
            
            if (result.estado != 200) {
                callback(false);
            } else {
                
                var weatherMapUrl = result.datos;
                //console.log('URL del mapa --> ' + weatherMapUrl)
                        
                callback(true, weatherMapUrl);
            }
        } catch(e) {
            callback(false); 
        }
    });
}

La imagen devuelta por la api de AEMET en forma de URL puede embeberse fácilmente ne una HeroCard. El ejemplo de código que se muestra a continuación es muy intuitivo y permite familiarizarse con el manejo de este tipo de recurso fácilmente:

//-------------------------------------------------------
// Definition of a HeroCard with buttons for redirecting to Weather Map from AEMET
//-------------------------------------------------------

function createHeroCard(session, url_map) {
    return new builder.HeroCard(session)
      .title('Mapas y Gráficos de Europa y Atlántico Norte')
      //.subtitle('Using a Chart as Image service...')
      //.text(statusLabel)
      .buttons([
          builder.CardAction.openUrl(session, url_map, 'Presión en superficie en Europa y Atlántico Norte'),
          builder.CardAction.openUrl(session, 'https://opendata.aemet.es/dist/index.html?#!/mapas-y-graficos/Mapas_de_an%C3%A1lisis_%C3%9Altima_pasada', 'Detalles sobre la API de AEMET')
      ]);
  
  }

Con los recursos presentados en este post, os animamos a crear vuestros chatbots. Como haber visto, con un dos llamadas a dos APIs y un diálogo en cascada muy sencillo se ha podido obtener un chatbot capaz de ofrecer una gran cantidad de información personalizada para el usuario.

No te pierdas ninguno de nuestros post. Suscríbete a LUCA Data Speaks.

Para mantenerte al día con LUCA visita nuestra página web, y no olvides seguirnos en TwitterLinkedIn YouTube.


No comments:

Post a Comment