Crea entregas mediante webhook

En Dialogflow, la entrega es un servicio, una app, un feed, una conversación, o cualquier otra lógica que puede resolver una solicitud del usuario. En nuestro caso, necesitamos una entrega que pueda programar una cita con la tienda de bicicletas, dada la información de fecha y hora proporcionada por el intent Acordar una cita (Make Appointment).

Para esta configuración, proporcionamos un webhook como un servicio de backend que puede recibir los parámetros de fecha y hora del intent y crear un evento en el Calendario de Google con la API. Para lograrlo, debemos realizar las siguientes dos tareas:

  • Obtener credenciales para la API del Calendario de Google
  • Crear un calendario nuevo y configurar el código en el webhook

Crea un webhook con el editor directo

Dialogflow tiene un editor directo en la consola que te permite escribir directamente el código NodeJS, que luego puede implementarse para que se ejecute como un webhook en Firebase.

Para crear un webhook con el editor directo de Dialogflow, sigue estos pasos:

  1. Haz clic en el intent Programar una cita.
  2. En la sección Entrega (Fulfillment), activa el botón Habilitar llamada de webhook para este intent (Enable Webhook call for this intent).
  3. Haz clic en GUARDAR (SAVE).
  4. Haz clic en la pestaña Entrega (Fulfillment) en la barra de navegación para ir a la página de entrega.
  5. Activa el botón del editor directo de modo que quede HABILITADO (ENABLED).
  6. Borra el contenido existente en la pestaña package.json del editor directo.
  7. Copia y pega el siguiente contenido JSON en la pestaña package.json del editor directo:

    {
      "name": "DialogflowFirebaseWebhook",
      "description": "Firebase Webhook dependencies for a Dialogflow agent.",
      "version": "0.0.1",
      "private": true,
      "license": "Apache Version 2.0",
      "author": "Google Inc.",
      "engines": {
        "node": "6"
      },
      "scripts": {
        "lint": "semistandard --fix \"**/*.js\"",
        "start": "firebase deploy --only functions",
        "deploy": "firebase deploy --only functions"
      },
      "dependencies": {
        "firebase-functions": "^2.0.2",
        "firebase-admin": "^5.13.1",
        "googleapis": "^27.0.0",
        "actions-on-google": "2.2.0",
        "dialogflow-fulfillment": "0.5.0"
      }
    }
    
  8. Borre el código existente en la pestaña index.js del editor directo.

  9. Copia y pega el siguiente código en la pestaña index.js del editor directo:

    /**
     * Copyright 2017 Google Inc. All Rights Reserved.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    'use strict';
    
    const functions = require('firebase-functions');
    const {google} = require('googleapis');
    const {WebhookClient} = require('dialogflow-fulfillment');
    
    // Enter your calendar ID and service account JSON below.
    const calendarId = '<INSERT CALENDAR ID HERE>'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
    const serviceAccount = {}; // The JSON object looks like: { "type": "service_account", ... }
    
    // Set up Google Calendar service account credentials
    const serviceAccountAuth = new google.auth.JWT({
      email: serviceAccount.client_email,
      key: serviceAccount.private_key,
      scopes: 'https://www.googleapis.com/auth/calendar'
    });
    
    const calendar = google.calendar('v3');
    process.env.DEBUG = 'dialogflow:*'; // It enables lib debugging statements
    
    const timeZone = 'America/Los_Angeles';  // Change it to your time zone
    const timeZoneOffset = '-07:00';         // Change it to your time zone offset
    
    exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
      const agent = new WebhookClient({ request, response });
    
      function makeAppointment (agent) {
        // Use the Dialogflow's date and time parameters to create Javascript Date instances, 'dateTimeStart' and 'dateTimeEnd',
        // which are used to specify the appointment's time.
        const appointmentDuration = 1;// Define the length of the appointment to be one hour.
        const dateTimeStart = convertParametersDate(agent.parameters.date, agent.parameters.time);
        const dateTimeEnd = addHours(dateTimeStart, appointmentDuration);
        const appointmentTimeString = getLocaleTimeString(dateTimeStart);
        const appointmentDateString = getLocaleDateString(dateTimeStart);
        // Check the availability of the time slot and set up an appointment if the time slot is available on the calendar
        return createCalendarEvent(dateTimeStart, dateTimeEnd).then(() => {
          agent.add(`Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye.`);
        }).catch(() => {
          agent.add(`Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?`);
        });
      }
      let intentMap = new Map();
      intentMap.set('Make Appointment', makeAppointment);  // It maps the intent 'Make Appointment' to the function 'makeAppointment()'
      agent.handleRequest(intentMap);
    });
    
    function createCalendarEvent (dateTimeStart, dateTimeEnd) {
      return new Promise((resolve, reject) => {
        calendar.events.list({  // List all events in the specified time period
          auth: serviceAccountAuth,
          calendarId: calendarId,
          timeMin: dateTimeStart.toISOString(),
          timeMax: dateTimeEnd.toISOString()
        }, (err, calendarResponse) => {
          // Check if there exists any event on the calendar given the specified the time period
          if (err || calendarResponse.data.items.length > 0) {
            reject(err || new Error('Requested time conflicts with another appointment'));
          } else {
            // Create an event for the requested time period
            calendar.events.insert({ auth: serviceAccountAuth,
              calendarId: calendarId,
              resource: {summary: 'Bike Appointment',
                start: {dateTime: dateTimeStart},
                end: {dateTime: dateTimeEnd}}
            }, (err, event) => {
              err ? reject(err) : resolve(event);
            }
            );
          }
        });
      });
    }
    
    // A helper function that receives Dialogflow's 'date' and 'time' parameters and creates a Date instance.
    function convertParametersDate(date, time){
      return new Date(Date.parse(date.split('T')[0] + 'T' + time.split('T')[1].split('-')[0] + timeZoneOffset));
    }
    
    // A helper function that adds the integer value of 'hoursToAdd' to the Date instance 'dateObj' and returns a new Data instance.
    function addHours(dateObj, hoursToAdd){
      return new Date(new Date(dateObj).setHours(dateObj.getHours() + hoursToAdd));
    }
    
    // A helper function that converts the Date instance 'dateObj' into a string that represents this time in English.
    function getLocaleTimeString(dateObj){
      return dateObj.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true, timeZone: timeZone });
    }
    
    // A helper function that converts the Date instance 'dateObj' into a string that represents this date in English.
    function getLocaleDateString(dateObj){
      return dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: timeZone });
    }
    
  10. Haz clic en IMPLEMENTAR.

    Figura 7. Diagrama de flujo que muestra la conexión con la función webhook makeAppointment().

Ahora tenemos el intent Acordar una cita conectado a la función makeAppointment() en el webhook. Examinemos el siguiente fragmento de código en el webhook:

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });

  function makeAppointment (agent) {
    // Calculate appointment start and end datetimes (end = +1hr from start)
    const dateTimeStart = convertParametersDate(agent.parameters.date, agent.parameters.time);
    const dateTimeEnd = addHours(dateTimeStart, 1);
    const appointmentTimeString = getLocaleTimeString(dateTimeStart);
    const appointmentDateString = getLocaleDateString(dateTimeStart);
    // Check the availability of the time, and make an appointment if there is time on the calendar
    return createCalendarEvent(dateTimeStart, dateTimeEnd).then(() => {
      agent.add(`Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye.`);
    }).catch(() => {
      agent.add(`Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?`);
    });
  }
  let intentMap = new Map();
  intentMap.set('Make Appointment', makeAppointment);
  agent.handleRequest(intentMap);
});

En el código anterior, observa la línea

intentMap.set('Make Appointment', makeAppointment);

La función set() se llama en un objeto Map, intentMap. Esta función vincula un intent a una función específica en el código. En este caso, la llamada establece la asignación entre el intent Acordar una cita (Make Appointment) y la función makeAppointment().

La función makeAppointment(agent){} lee los valores del parámetro de fecha y hora del objeto de entrada agent mediante agent.parameters.date y agent.parameters.time. Después de analizar y formatear los valores de fecha y hora, la función llama a una función personalizada createCalendarEvent(), que realiza una llamada a la API del Calendario de Google para crear un evento en el calendario.

Por último, la función agent.add() se utiliza para entregar una string personalizada como respuesta al usuario. A diferencia de usar la consola de Dialogflow para proporcionar respuestas, con un webhook, podemos usar la lógica del código a fin de construir respuestas altamente dinámicas. Por ejemplo, cuando el agente programa una cita con éxito, responde con lo siguiente:

Entendido. Tengo tu cita programada para el día ${appointmentDateString} a las ${appointmentTimeString}. Nos vemos pronto. Adiós. (Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye).

Sin embargo, si el agente no logra acordar una cita en la hora y fecha específicas proporcionadas, muestra la siguiente respuesta:

Lo sentimos, no tenemos disponibilidad el ${appointmentDateString} a las ${appointmentTimeString}. ¿Hay algo más que pueda hacer por ti? (Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?).

En este punto, no podemos probar el código correctamente porque no tiene acceso a la API del Calendario de Google. Ten en cuenta que tenemos las siguientes variables en el código para personalizar la configuración de la API del Calendario de Google:

const calendarId = '<INSERT CALENDAR ID HERE>'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
const serviceAccount = {}; // The JSON object looks like: { "type": "service_account", ... }

A continuación, debemos configurar el código para acceder a la API del Calendario de Google.

Obtén credenciales para la API del Calendario de Google

Para obtener las credenciales de la API del Calendario de Google, sigue estos pasos:

  1. En la barra de navegación de Dialogflow, haz clic en el botón de configuración ⚙ junto al nombre del agente.
  2. En la tabla PROYECTO DE GOOGLE (GOOGLE PROJECT), haz clic en el vínculo del ID del proyecto para abrir Google Cloud Platform Console.
  3. En Google Cloud Platform Console, haz clic en el botón de menú ☰ y selecciona API y servicios> Biblioteca.
  4. Haz clic en la tarjeta Google Calendar API (API del Calendario de Google).

  5. Haz clic en ENABLE (HABILITAR) en la página de la API del Calendario de Google.

  6. En la barra de navegación, haz clic en Credenciales.

  7. Haz clic en Crear credenciales y selecciona el elemento Clave de cuenta de servicio del menú desplegable.

  8. Haz clic en el menú desplegable Cuenta de servicio y selecciona el elemento Nueva cuenta de servicio.

  9. En el campo de entrada Service account name (Nombre de la cuenta de servicio), ingresa bike-shop-calendar.

  10. En el campo Función, selecciona Proyecto > Propietario en el menú desplegable.

  11. Haz clic en Crear. (Se descargará un archivo JSON que contiene la clave de tu cuenta de servicio).

Crea un calendario nuevo y configura el código en el webhook

Luego, necesitamos crear un nuevo calendario para que la tienda de bicicletas haga un seguimiento de las citas. Utilizaremos la información del archivo JSON descargado para integrar el código de webhook del agente en el nuevo Calendario de Google. Asegúrate de poder ver y copiar el contenido del archivo JSON antes de continuar con el siguiente conjunto de instrucciones.

Para crear un calendario nuevo y completar la integración, sigue estos pasos:

  1. Abre el Calendario de Google.
  2. En la barra de navegación del Calendario de Google, haz clic en el botón + junto al campo de entrada Agregar el calendario de un amigo (Add a friend's calendar).
  3. Selecciona el elemento Nuevo calendario del menú desplegable.
  4. En el campo Nombre, ingresa Mary's Bike Shop.
  5. Haz clic en el vínculo CREAR CALENDARIO. (El nuevo calendario Mary's Bike Shop se creará en la ventana Configuración de mis calendarios de la barra de navegación).
  6. Haz clic en Tienda de bicicletas de Mary (Mary's Bike Shop) y selecciona Compartir con personas específicas.
  7. En la sección Compartir con personas específicas, haz clic en AGREGAR PERSONAS. La ventana emergente Compartir con personas específicas aparecerá en la pantalla.
  8. Abre el archivo JSON descargado previamente y copia la dirección de correo electrónico en el campo client_email, sin las comillas:

    {
      "type": "service_account",
      "project_id": "marysbikeshop-...",
      "private_key_id": "...",
      "private_key": "...",
      "client_email": "bike-shop-calendar@<project-id>.iam.gserviceaccount.com",
      "client_id": "...",
      "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      "token_uri": "https://accounts.google.com/o/oauth2/token",
      "auth_provider_x509_cert_url": "https://www.googleapis.com...",
      "client_x509_cert_url": "https://www.googleapis.com/robot/v..."
    }
    
  9. En la ventana emergente Compartir con personas específicas, pega la dirección de correo electrónico de client_email en el campo de entrada Agregar un nombre o correo electrónico.

  10. En el menú desplegable Permisos, selecciona el elemento Realizar cambios en los eventos.

  11. Haz clic en ENVIAR.

  12. Desplázate hacia abajo en la sección Integrar calendario y copia el contenido de ID del calendario.

  13. En la consola de Dialogflow, ve a la página Entrega (Fulfillment) de tu agente.

  14. En la pestaña index.js del editor directo, busca la variable calendarId.

    const calendarId = '<INSERT CALENDAR ID HERE>';
    
  15. Pega el contenido de ID del calendario para reemplazar <INSERT CALENDAR ID HERE>, como se muestra en el siguiente código:

    const calendarId = 'fh5kgikn3t4vvmc73423875rjc@group.calendar.google.com';
    
  16. En la pestaña index.js del editor directo, busca la variable serviceAccount.

    const serviceAccount = {};
    
  17. Revisa el archivo JSON descargado previamente y copia todo el contenido, incluidas las llaves más externas ({}).

  18. Pega el contenido para reemplazar el valor de serviceAccount, como se muestra en el siguiente fragmento de código:

    const serviceAccount = {
      "type": "service_account",
      "project_id": "marysbikeshop-...",
      "private_key_id": "...",
      "private_key": "...",
      "client_email": "bike-shop-calendar@<project-id>.iam.gserviceaccount.com",
      "client_id": "...",
      "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      "token_uri": "https://accounts.google.com/o/oauth2/token",
      "auth_provider_x509_cert_url": "https://www.googleapis.com...",
      "client_x509_cert_url": "https://www.googleapis.com/robot/v..."
    };
    
  19. Haz clic en IMPLEMENTAR (DEPLOY).

  20. Prueba el agente para verificar que los eventos nuevos se creen correctamente en el calendario.

Mejoramiento adicional

Actualmente, tu agente puede proporcionar servicios solo cuando los usuarios cooperan bien; no maneja bien las conversaciones si aquello que los usuarios dicen está fuera de los diálogos programados. En las interfaces de conversación, una gran parte del esfuerzo se dedica a lograr que un agente sea capaz de recuperarse de las declaraciones inesperadas de los usuarios. Te recomendamos que explores la función potente de Dialogflow, Contextos, que permite que un agente controle eficazmente el flujo de la conversación.