Criar fulfillment usando webhook

No Dialogflow, o fulfillment é um serviço, app, feed, conversa ou outra lógica que pode resolver uma solicitação do usuário. No nosso caso, precisamos de fulfillment que possa agendar um horário para a loja de bicicletas com as informações de data e hora fornecidas pela intent Agendar horário.

Para essa configuração, fornecemos um webhook como um serviço de back-end que pode receber os parâmetros de data e hora da intent e criar um evento no Google Agenda usando a API. Para isso, precisamos realizar duas tarefas:

  • Receber credenciais para a API Google Calendar
  • Criar uma nova agenda e configurar o código no webhook

Criar um webhook com o editor in-line

O Dialogflow tem um editor in-line no console que permite escrever diretamente o código NodeJS, que pode ser implantado e executado como um webhook no Firebase.

Para criar um webhook usando o editor in-line do Dialogflow, siga estas etapas:

  1. Clique no intent Criar atendimento.
  2. Na seção Fulfillment, clique no botão Ativar chamada de webhook para essa intent.
  3. Clique em SALVAR.
  4. Clique na guia Fulfillment na barra de navegação para acessar a página de fulfillment.
  5. Alterne o botão do editor in-line para ATIVADO.
  6. Exclua o conteúdo existente na guia package.json do editor in-line.
  7. Copie e cole o conteúdo JSON abaixo na guia package.json do editor in-line:

    {
      "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. Exclua o código da guia index.js do editor in-line.

  9. Copie e cole o código abaixo na guia index.js do editor in-line:

    /**
     * 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. Clique em IMPLANTAR.

    Figura 7. Fluxograma mostrando a conexão com a função de webhook makeAppointment().

Agora temos a intent Agendar horário associada à função makeAppointment() no webhook. Vamos analisar a seguinte parte do código no 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);
});

No código acima, observe a linha

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

A função set() é chamada em um objeto Map: intentMap. Ela vincula uma intent a uma função específica no código. Nesse caso, a chamada estabelece o mapeamento entre a intent Agendar horário e a função makeAppointment().

A função makeAppointment(agent){} lê os valores de parâmetro de data e hora do objeto de entrada agent por meio de agent.parameters.date e agent.parameters.time. Depois de analisar e formatar os valores de data e hora, a função chama uma função personalizada createCalendarEvent(), que faz uma chamada de API ao Google Agenda para criar um evento na agenda.

Por fim, a função agent.add() é usada para criar uma string personalizada como uma resposta ao usuário. Ao contrário do uso do console do Dialogflow para fornecer respostas, com um webhook, é possível usar a lógica do código para criar respostas extremamente dinâmicas. Por exemplo, quando o agente marca um horário, ele responde da seguinte forma:

Ok. Tenho um horário agendado para ${appointmentDateString} às ${appointmentTimeString}. Nos vemos em breve. Tchau.

No entanto, se o agente não conseguir agendar o horário na hora e data específicas fornecidas, ele retornará a seguinte resposta:

Não há horários disponíveis em ${appointmentDateString} às ${appointmentTimeString}. Há mais alguma coisa que eu possa fazer por você?

No momento, não é possível testar o código corretamente porque ele não tem acesso à API Google Calendar. Temos as variáveis a seguir no código para personalizar a configuração da API Google Calendar:

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

Em seguida, precisamos configurar o código para acessar a API Google Calendar.

Receber credenciais para a API Google Calendar

Para receber as credenciais da API Google Calendar, siga estas etapas:

  1. Na barra de navegação do Dialogflow, clique no botão de configuração ⚙ ao lado do nome do agente.
  2. Na tabela PROJETO DO GOOGLE, clique no link do ID do projeto para abrir o console do Google Cloud Platform.
  3. No console, clique no botão de menu ☰ e selecione APIs e serviços > Biblioteca.
  4. Clique no cartão API Google Calendar.

  5. Clique em ATIVAR na página da API Google Calendar.

  6. Na barra de navegação, clique em Credenciais.

  7. Clique em Criar credenciais e selecione o item Chave da conta de serviço no menu suspenso.

  8. Clique no menu suspenso Conta de serviço e selecione o item Nova conta de serviço.

  9. No campo de entrada Nome da conta de serviço, insira bike-shop-calendar.

  10. No campo Papel, selecione Projeto > Proprietário no menu suspenso.

  11. Clique em Criar. Um arquivo JSON que contém a chave da sua conta de serviço é salvo.

Criar uma nova agenda e configurar o código no webhook

Em seguida, crie uma nova agenda para que a loja de bicicletas acompanhe os horários agendados. Usamos as informações do arquivo JSON salvo para integrar o código do webhook do agente à nova agenda do Google. Verifique se você consegue visualizar e copiar o conteúdo do arquivo JSON antes de continuar seguindo as instruções.

Para criar uma nova agenda e concluir a integração, siga estas etapas:

  1. Abra o Google Agenda.
  2. Na barra de navegação do Google Agenda, clique no botão + ao lado do campo de entrada Adicionar a agenda de um amigo.
  3. Selecione o item Nova agenda no menu suspenso.
  4. No campo Nome, use Mary's Bike Shop.
  5. Clique no link CRIAR AGENDA. A nova agenda Mary's Bike Shop é criada na janela Configurações das minhas agendas, na barra de navegação.
  6. Clique em Loja de bicicletas da Maria e selecione Compartilhar com pessoas específicas.
  7. Na seção Compartilhar com pessoas específicas, clique em ADICIONAR PESSOAS. A janela pop-up Compartilhar com pessoas específicas é exibida na tela.
  8. Abra o arquivo JSON salvo anteriormente e copie o endereço de e-mail no campo client_email sem as aspas:

    {
      "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. Na janela pop-up Compartilhar com pessoas específicas, cole o endereço de e-mail de client_email no campo de entrada Adicionar e-mail ou nome.

  10. No menu suspenso Permissões, selecione o item Fazer alterações nos eventos.

  11. Clique em ENVIAR.

  12. Role a seção Integrar agenda e copie o conteúdo de ID da agenda.

  13. No console do Dialogflow, acesse a página Fulfillment do agente.

  14. Na guia index.js do editor in-line, localize a variável calendarId.

    const calendarId = '<INSERT CALENDAR ID HERE>';
    
  15. Cole o conteúdo de Código da agenda para substituir <INSERT CALENDAR ID HERE>, conforme mostrado no código a seguir:

    const calendarId = 'fh5kgikn3t4vvmc73423875rjc@group.calendar.google.com';
    
  16. Na guia index.js do editor in-line, localize a variável serviceAccount.

    const serviceAccount = {};
    
  17. Revise o arquivo JSON salvo anteriormente e copie todo o conteúdo, incluindo as chaves externas ({}).

  18. Cole o conteúdo para substituir o valor de serviceAccount, conforme mostrado no snippet de código a seguir:

    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. Clique em IMPLANTAR.

  20. Teste o agente para verificar se novos eventos foram criados corretamente na agenda.

Outras melhorias

Atualmente, seu agente pode fornecer serviços somente quando os usuários cooperam. Ele não lida bem com conversas em que as pessoas dizem frases que não estão incluídas nas caixas de diálogo com script. Em interfaces de conversação, grande parte do esforço concentra-se em tornar um agente capaz de responder a declarações inesperadas do usuário. Recomendamos que você conheça o recurso avançado do Dialogflow, Contextos, em que um agente pode controlar com eficiência o fluxo da conversa.