首页 > 解决方案 > DialogFlow - 修改后的“自行车商店”示例不起作用

问题描述

我在让这个 Bike Shop 示例的修改版本(或原始版本,就此而言)工作时遇到了一些严重的困难。

我正在尝试复制基本功能,但将姓名、电话号码、电子邮件等字段添加到日历事件中。

也许是因为这是我第一次使用 Node.js,但事实证明这比在热水泥中沐浴更不愉快。

我将快速总结一下我遇到的主要问题:

获取要在日历中填充的事件

多亏了@Prisoner,我几乎把所有事情都搞定了。我的主要问题是我的上下文是大写的,因此没有被识别。将大部分意图转换为顶层也有帮助。现在,我可以始终如一地获得履行响应来填写并给我我的第一条确认消息,但我总是得到我的第二条“错误”响应,并且日历中没有发生任何事件。

未显示在日历上 如何更新 googleapis 库?只需将 package.json 中的 ^27 更改为 ^30 即可?

上下文 没有.getContext()被弃用吗?这就是它在这里所说的(https://dialogflow.com/docs/contexts/contexts-fulfillment),这就是我昨天尝试时错误所说的。

带有连字符的实体 我更改了名称(我不知道名称不仅仅是一个标签),但为了将来的参考和清晰,它是.parameters,.properties还是.params?

仅供参考,这是我的意图流程: Scheduleappointment >

FirstLast(获取名字和姓氏,分配给系统实体)>

ServiceNeeded(获取所需的服务,分配给开发者实体)>

Date Time MeetingPlace Email PhoneN(获取日期、时间、电话号码和电子邮件:分配给系统实体;获取位置:分配给开发人员实体)

我的 index.js:

/**
 * 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.
// See https://github.com/dialogflow/bike-shop/blob/master/README.md#calendar-setup
const calendarId = 'm6t2gekgnv2qro9ln8genh10o8@group.calendar.google.com'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
const serviceAccount = {
  "type": "service_account",
  "project_id": "appsoft-lead-net",
  "private_key_id": "863c577bd9249f09cbce540cf082fb7c1f7349ac",
  "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n",
  "client_email": "id-appsoft-chatbot-v1-calendar@appsoft-lead-net.iam.gserviceaccount.com",
  "client_id": "113420745582984409565",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/id-appsoft-chatbot-v1-calendar%40appsoft-lead-net.iam.gserviceaccount.com"
}; // 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/New_York';  // Change it to your time zone

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

  // This function receives the date and time values from the context 'MakeAppointment-followup'
  // and calls the createCalendarEvent() function to mark the specified time slot on Google Calendar.
  function makeAppointment (agent) {
    // Get the contexts
    const contextF = agent.context.get('firstlast');
    const contextS = agent.context.get('serviceneeded');
    const contextD = agent.context.get('datetimemeetingplaceemailphonen-followup');
    // This variable needs to hold an instance of Date object that specifies the start time of the appointment.
    const dateTimeStart = convertTimestampToDate(contextD.parameters.date, contextD.parameters.time);
    // This variable holds the end time of the appointment, which is calculated by adding an hour to the start time.
    const dateTimeEnd = addHours(dateTimeStart, 1);
    // Convert the Date object into human-readable strings.
    const appointmentTimeString = getLocaleTimeString(dateTimeStart);
    const appointmentDateString = getLocaleDateString(dateTimeStart);
// set properties to variables
const appointmentLocationString = contextD.parameters.meetingPlace;
const appointmentEmail = contextD.parameters.email;
const appointmentService = contextS.parameters.ServiceNeeded;
const appointmentFullName = contextF.parameters.givenName + " " + contextF.parameters.lastName;
const appointmentFirstName = contextF.parameters.givenName;
const appointmentPhoneString = contextD.parameters.phoneNumber;
    // Delete the context 'MakeAppointment-followup'; this is the final step of the path.
    agent.context.delete('datetimemeetingplaceemailphonen-followup');
    // The createCalendarEvent() function checks the availability of the time slot and marks the time slot on Google Calendar if the slot is available.
    return createCalendarEvent(agent, dateTimeStart, dateTimeEnd, appointmentFullName, appointmentPhoneString, appointmentLocationString, appointmentEmail, appointmentService).then(() => {
        agent.context.delete('serviceneeded');
        agent.context.delete('firstlast');
        agent.context.delete('schedule');
      agent.add(`Got it! I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}—we'll contact you shortly to confirm the deets! See you soon, ${appointmentFirstName}. Good-bye!`);
    }).catch(() => {
      agent.add(`Sorry, ${appointmentFirstName}, something went wrong—I couldn't book ${appointmentDateString} at ${appointmentTimeString}. Try trying again! If that doesn't work, let us know—Mitch probably just messed up something...`);
    });
  }

  // This function receives the date and time values from the context 'MakeAppointment-followup'
  // and calls the checkCalendarAvailablity() function to check the availability of the time slot on Google Calendar.
  function checkAppointment (agent) {
      // Get the contexts
      const contextF = agent.context.get('firstlast');
      const contextS = agent.context.get('serviceneeded');
    // This variable needs to hold an instance of Date object that specifies the start time of the appointment.
    const dateTimeStart = convertTimestampToDate(agent.parameters.date, agent.parameters.time);
    // This variable holds the end time of the appointment, which is calculated by adding an hour to the start time.
    const dateTimeEnd = addHours(dateTimeStart, 1);
    // Convert the Date object into human-readable strings.
    const appointmentTimeString = getLocaleTimeString(dateTimeStart);
    const appointmentDateString = getLocaleDateString(dateTimeStart);
    // set properties into variables
    const appointmentLocationString = agent.parameters.meetingPlace;
    const appointmentEmail = agent.parameters.email;
    const appointmentService = contextS.parameters.ServiceNeeded;
    const appointmentFullName = contextF.parameters.givenName + " " + contextF.parameters.lastName;
    const appointmentFirstName = contextF.parameters.givenName;
    const appointmentPhoneString = agent.parameters.phoneNumber;
    // The checkCalendarAvailablity() function checks the availability of the time slot.
    return checkCalendarAvailablity(dateTimeStart, dateTimeEnd).then(() => {
        // The time slot is available.
       // The function returns a response that asks for the confirmation of the date and time.
       agent.add(`Okay, ${appointmentFullName}, so you've said that you'd like your appointment on ${appointmentDateString} at ${appointmentTimeString}. We'll call ${appointmentPhoneString} and/or email ${appointmentEmail} to confirm this appointment ${appointmentLocationString} about ${appointmentService}. Did I get that right?`);
     }).catch(() => {
       // The time slot is not available.
       agent.add(`Sorry, ${appointmentFirstName}, we're booked up on ${appointmentDateString} at ${appointmentTimeString}. Huge bummer, I know =/ But is there another time you'd like to schedule your appointment?`);
       // Delete the context 'MakeAppointment-followup' to return the flow of conversation to the beginning.
       agent.context.delete('datetimemeetingplaceemailphonen-followup');
   });
  }
  // Mapping of the functions to the agent's intents.
  let intentMap = new Map();
  intentMap.set('Date Time MeetingPlace Email PhoneN', checkAppointment);
  intentMap.set('Date Time MeetingPlace Email PhoneN - yes', makeAppointment);
  agent.handleRequest(intentMap);
});

// This function checks for the availability of the time slot, which starts at 'dateTimeStart' and ends at 'dateTimeEnd'.
// 'dateTimeStart' and 'dateTimeEnd' are instances of a Date object.
function checkCalendarAvailablity (dateTimeStart, dateTimeEnd) {
  return new Promise((resolve, reject) => {
    calendar.events.list({
      auth: serviceAccountAuth, // List events for time period
      calendarId: calendarId,
      timeMin: dateTimeStart.toISOString(),
      timeMax: dateTimeEnd.toISOString()
    }, (err, calendarResponse) => {
      // Check if there is an event already on the Calendar
      if (err || calendarResponse.data.items.length > 0) {
        reject(err || new Error('Requested time conflicts with another appointment'));
      }else {
        resolve(calendarResponse);
      }
    });
  });
}

// This function marks the time slot on Google Calendar. The time slot on the calendar starts at 'dateTimeStart' and ends at 'dateTimeEnd'.
// 'dateTimeStart' and 'dateTimeEnd' are instances of a Date object.
function createCalendarEvent (agent, dateTimeStart, dateTimeEnd, appointmentFullName, appointmentPhoneString, appointmentLocationString, appointmentEmail, appointmentService) {

// assign values to variables
    appointmentPhoneString = agent.parameters.phoneNumber;
    appointmentLocationString = agent.parameters.meetingPlace;
    appointmentEmail = agent.parameters.email;
    appointmentService = agent.parameters.ServiceNeeded;
    appointmentFullName = agent.parameters.givenName + " " + agent.parameters.lastName;

  return new Promise((resolve, reject) => {
    calendar.events.list({
      auth: serviceAccountAuth, // List events for time period
      calendarId: calendarId,
      timeMin: dateTimeStart.toISOString(),
      timeMax: dateTimeEnd.toISOString()
    }, (err, calendarResponse) => {
      // Check if there is an event already on the Calendar
      if (err || calendarResponse.data.items.length > 0) {
        reject(err || new Error('Requested time conflicts with another appointment'));
      } else {
        // Create event for the requested time period
        calendar.events.insert({ auth: serviceAccountAuth,
          calendarId: calendarId,
          resource: {
           summary: 'Appsoft Appointment',
           start: {
             dateTime: dateTimeStart
           },
           end: {
             dateTime: dateTimeEnd
           },
           attendees:[ {
             displayName: appointmentFullName,
             email: appointmentEmail,
           }],
           location: appointmentLocationString,
           description: 'Phone Number: ' + appointmentPhoneString + '; Service Needed: ' + appointmentService}
        }, (err, event) => {
          err ? reject(err) : resolve(event);
        }
        );
      }
    });
  });
}

// A helper function that receives Dialogflow's 'date' and 'time' parameters and creates a Date instance.
function convertTimestampToDate(date, time){
  // Parse the date, time, and time zone offset values from the input parameters and create a new Date object
  return new Date(Date.parse(date.split('T')[0] + 'T' + time.split('T')[1].split('-')[0] + '-' + time.split('T')[1].split('-')[1]));
}

// 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 });
}

我的 package.json:

{
  "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.6.1"
  }
}

在此先感谢您的帮助!

{固定编辑:终于让它工作了,结果上下文名称需要完全小写才能被识别。createCalendarEvent另外,我无缘无故地不正确地声明了变量。}

标签: parameterssplitpropertiesdialogflow-es

解决方案


很多问题,让我们看看我们能解决什么问题。

无法读取属性“拆分”

第 192 行是

return new Date(Date.parse(date.split('T')[0] + 'T' + time.split('T')[1].split('-')[0] + '-' + time.split('T')[1].split('-')[1]));

其中有很多对 的调用split(),因此尚不清楚究竟是哪一个导致了问题,但发生这种情况是因为未定义其中一个date或(或两者)。time

包含这一行的convertTimestampToDate()函数是从两个不同的地方调用的。你的makeAppointment()checkAppointment()函数,它们似乎有相同的行

    const dateTimeStart = convertTimestampToDate(agent.parameters.date, agent.parameters.time);

您没有显示Schedule appointment - Date Time meetingPlaceor Schedule appointment - Date Time meetingPlace - yesIntent 配置本身,但听起来其中一个实际上没有dateortime参数。

履行的回应

您对没有生成哪些响应有点模糊,但是对于具有处理程序的两个 Intent,响应似乎设置正确。

UI 中的那些将在以下任何情况下使用:

  • 该 Intent 未启用 Fulfillment。
  • Fulfillment 没有明确添加回复。

每条消息有640 个字符限制,但我认为您的回复并没有达到这个限制,但我想在某些情况下可能是可能的。

动态参数未定义

我不确定您所说的“动态”是什么意思,但这听起来与date未定义time的问题相同。同样,我会检查以确保这些参数是在您认为的 Intent 中发送的。

从 Intent“流程”(我将在下面讨论),听起来您希望参数不断从 Intent 填充到 Intent。通常,参数是从当前Intent 提供的。

如果你设置了一个输出上下文,那么参数也可以在上下文中设置,这将延续到未来的意图。因此,您可能希望从上下文中获取它们,而不是从参数中获取它们。

根据下面 sid 的评论进行更新:如果您确实想要在当前 Intent 参数中使用过去的参数,则需要从上下文中显式设置 Intent 中的值,类似于#output-context.paramIntent 参数部分。

不显示在日历上

您使用的是相当旧的googleapis库版本,这可能是也可能不是问题。然而,较新的版本本身支持 Promises,这肯定会使您的代码更易于使用。

我不确定就是这样,但是您指定了一个名为的属性,该属性resource包含事件资源(根据规范)并且应该在请求的正文中。我不知道这是否与您使用的库有所不同,但在当前库中,应该调用此字段requestBody

上下文

处理上下文的最佳方法是使用直接访问器方法,例如agent.getContext()agent.setContext()agent.clearContext()

您可能已经看过一些较早的关于 Google 上的操作的文档,其中涉及conv.contexts,在这种情况下不适用。

带有连字符的实体

最简单的方法是将 Intent UI 中的参数名称更改为不带连字符的名称。您可以随心所欲地调用它们 - 您不必在实体类型之后命名它们。

如果您确实想保留连字符,请将它们索引到agent.properties对象(这似乎是您这样做的方式)。所以

agent.properties['what-ever']

意图流

您没有显示 Intent 配置,但听起来您将这些问题视为一系列后续 Intent。

这……并不总是一个好主意。对话并不总是线性的,用户可能会尝试在对话中后退(或前进),而后续意图处理不好。

更好的解决方案是将这些 Intent 中的大多数作为顶层,所有这些 Intent 都使用实现。Intent 负责将参数传递给实现,实现存储它们,然后确定哪些值仍然丢失并要求提供一个。如果你觉得你需要缩小你对用户的期望,你可以考虑使用上下文,但这并不像你想象的那么必要。(有关此问题的一些讨论,请参阅思考声音:设计对话而不是逻辑。)


推荐阅读