使用网络钩子创建 fulfillment

Dialogflow 中的网络钩子 fulfilment 使我们能够更好地控制代理流。在本教程中,您需要一个网络钩子来验证在“序列”意图中收集的字母数字序列。该网络钩子会反复轮询该意图,以便在更易于管理的迭代中收集长序列。

使用内嵌编辑器创建网络钩子

Dialogflow 在控制台中有一个内嵌编辑器,使您可以直接写入 NodeJS 代码,然后将其部署作为网络钩子在 Cloud Functions 上运行

如需使用 Dialogflow 的内嵌编辑器创建网络钩子,请按以下步骤操作:

  1. 点击导航栏上的 Fulfillment 标签页,转到“Fulfillment”页面。
  2. 将内嵌编辑器的按钮切换为“已启用”(ENABLED)。
  3. 删除内嵌编辑器的 package.json 标签页中的现有内容。
  4. 将以下 JSON 内容复制并粘贴到内嵌编辑器的 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": "10"
      },
      "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"
      }
    }
    
  5. 删除内嵌编辑器的 index.js 标签页中的现有代码。

  6. 将以下代码复制并粘贴到内嵌编辑器的 index.js 页标签中:

    /**
     * Copyright 2020 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');
    
    // TODO: set this to the minimum valid length for your sequence.
    // There's no logic in here to enforce this length, but once the
    // user has said this many digits, the slot-filling prompt will
    // also instruct the user to say "that's all" to end the slot-filling.
    const MIN_SEQUENCE_LENGTH = 10;
    
    exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
      let dfRequest = request.body;
      let action = dfRequest.queryResult.action;
      switch (action) {
        case 'handle-sequence':
          handleSequence(dfRequest, response);
          break;
        case 'validate-sequence':
          validateSequence(dfRequest, response);
          break;
        default:
          response.json({
            fulfillmentText: `Webhook for action "${action}" not implemented.`
          });
      }
    });
    
    ////
    // Helper functions
    
    /* Send an SSML response.
     * @param request: Dialogflow WebhookRequest JSON with camelCase keys.
     *     See https://cloud.google.com/dialogflow/es/docs/reference/common-types#webhookrequest
     * @param response: Express JS response object
     * @param ssml: SSML string.
     * @example: sendSSML(request, response, 'hello')
     *     Will call response.json() with SSML payload '<speak>hello</speak>'
     */
    function sendSSML(request, response, ssml) {
      ssml = `<speak>${ssml}</speak>`;
    
      if (request.originalDetectIntentRequest.source == 'GOOGLE_TELEPHONY') {
        // Dialogflow Phone Gateway Response
        // see https://cloud.google.com/dialogflow/es/docs/reference/rpc/google.cloud.dialogflow.v2beta1#google.cloud.dialogflow.v2beta1.Intent.Message.TelephonySynthesizeSpeech
        response.json({
          fulfillmentMessages: [{
            platform: 'TELEPHONY',
            telephonySynthesizeSpeech: {ssml: ssml}
          }]
        });
      }
      else {
        // Some CCAI telephony partners accept SSML in a plain text response.
        // Check your specific integration and customize the payload here.
        response.json({
          fulfillmentText: ssml
        });
      }
    }
    
    /* Extract an output context from the incoming WebhookRequest.
     * @param request: Dialogflow WebhookRequest JSON with camelCase keys.
     *     See https://cloud.google.com/dialogflow/es/docs/reference/common-types#webhookrequest
     * @param name: A string
     * @return: The context object if found, or undefined
     * @see: https://cloud.google.com/dialogflow/es/docs/reference/rpc/google.cloud.dialogflow.v2#google.cloud.dialogflow.v2.Context
     *     and note this webhook uses JSON camelCase instead of RPC snake_case.
     * @example:
     *     // Modify an existing output content
     *     let context = getOutputContext(request, 'some-context');
     *     context.lifespanCount = 5;
     *     context.parameters.some_parameter = 'new value';
     *     response.json({
     *       fulfillmentText: 'new value set',
     *       outputContexts: [context]
     *     });
     */
    function getOutputContext(request, name) {
      return request.queryResult.outputContexts.find(
          context => context.name.endsWith(`/contexts/${name}`)
      );
    }
    
    ////
    // Action handler functions
    
    /*
     * Fulfillment function for:
     *     actions: handle-sequence
     *     intents: "Sequence", "Sequence - Edit"
     * @param request: Dialogflow WebhookRequest JSON with camelCase keys.
     *     See https://cloud.google.com/dialogflow/es/docs/reference/common-types#webhookrequest
     * @param response: Express JS response object
     */
    function handleSequence(request, response) {
      let parameters = request.queryResult.parameters;
      let isSlotFilling = !request.queryResult.allRequiredParamsPresent;
      let isEditing = getOutputContext(request, 'editing-sequence');
      console.log(request.queryResult.action + ': ' + JSON.stringify(parameters));
    
      if (isSlotFilling) {
        // Prompt the user for the sequence
    
        let verbatim = `<prosody rate="slow"><say-as interpret-as="verbatim">${parameters.existing_sequence}</say-as></prosody>`;
    
        if (!parameters.existing_sequence && !parameters.new_sequence) {
          // Initial prompt
          response.json({
            fulfillmentText: "What is your sequence? Please pause after a few characters so I can confirm as we go."
          });
        }
        else if (!isEditing) {
          // Confirm what the system heard with the user. We customize the response
          // according to how many sequences we've heard to make the prompts less
          // verbose.
          if (!parameters.previous_sequence) {
            // after the first input
            sendSSML(request, response,
                `Say "no" to correct me at any time. Otherwise, what comes after ${verbatim}`);
          }
          else if (parameters.existing_sequence.length < MIN_SEQUENCE_LENGTH) {
            // we know there are more characters to go
            sendSSML(request, response,
                `${verbatim} What's next?`);
          }
          else {
            // we might have all we need
            sendSSML(request, response,
                `${verbatim} What's next? Or say "that's all".`);
          }
        }
        else {
          // User just said "no"
          sendSSML(request, response,
              `Let's try again. What comes after ${verbatim}`);
        }
      }
      else {
        // Slot filling is complete.
    
        // Construct the full sequence.
        let sequence = (parameters.existing_sequence || '') + (parameters.new_sequence || '');
    
        // Trigger the follow up event to get back into slot filling for the
        // next sequence.
        response.json({
          followupEventInput: {
            name: 'continue-sequence',
            parameters: {
              existing_sequence: sequence,
              previous_sequence: parameters.existing_sequence || ''
            }
          }
        });
    
        // TODO: CHALLENGE: consider validating the sequence here.
        // The user has already confirmed existing_sequence, so if you find a unique
        // record in your database with this existing_sequence prefix, you could send
        // a followUpEventInput like 'validated-sequence' to skip to the next part
        // of the flow. You could either create a new intent for this event, or
        // reuse the "Sequence - done" intent. If you reuse the "done" intent, you
        // could add another parameter "assumed_sequence" with value
        // "#validated-sequence.sequence", then modify the validateSequence function
        // below to customize the response for this case.
      }
    }
    
    /*
     * Fulfillment function for:
     *     action: validate-sequence
     *     intents: "Sequence - Done"
     * @param request: Dialogflow WebhookRequest JSON with camelCase keys.
     *     See https://cloud.google.com/dialogflow/es/docs/reference/common-types#webhookrequest
     * @param response: Express JS response object
     */
    function validateSequence(request, response) {
      let parameters = request.queryResult.parameters;
      // TODO: add logic to validate the sequence and customize your response
      let verbatim = `<say-as interpret-as="verbatim">${parameters.sequence}</say-as>`;
      sendSSML(request, response, `Thank you. Your sequence is ${verbatim}`);
    }
    
  7. 点击部署 (DEPLOY)。

现在,您应该可以通过调用代理来测试集成了。如果您尚未执行此操作,建议您立即设置我们的合作伙伴提供的一键式电话集成,或设置 Dialogflow Phone Gateway 来通过电话测试代理。

了解代码

作为网络钩子的入口点,每次触发网络钩子时,都会调用此处的 dialogflowFirebaseFulfillment 函数。对于每个请求,Dialogflow 都会发送您在 Dialogflow 控制台中为意图指定的“操作”名称。代码会使用此操作名称来确定要调用的网络钩子函数(handleSequencevalidateSequence)。

处理序列

handleSequence 是本教程的核心功能。它负责序列槽位填充的所有方面,包括:

  • 当会话首次输入意图时读出初始说明。
  • 先重复该序列,然后再提示下一组。
  • 告知最终用户如何更正聊天机器人。
  • 识别有效序列何时具有足够的数位,并告知最终用户如何最后确定输入(请参阅代码中的“MIN_SEQUENCE_LENGTH”)。
  • 循环槽位填充,以收集多个部分序列。
  • 将部分序列连接成一个长序列。

验证序列

使用 validateSequence 时,您需要添加与数据存储区的连接,以验证最终序列并根据该数据返回自定义消息。例如,如果您要构建订单查询代理,可以在此处自定义响应,例如:

Thank you. Your order ${verbatim} will arrive on ${lookup.date} and will ${lookup.require_signature ? '' : 'not'} require a signature.

其中,lookup 是您在数据存储区中找到的与此订单相关的某个对象。

辅助函数

此示例不使用任何特定于 Dialogflow 的依赖项。请改为遵循 WebhookRequest 参考文档了解 request.body 中的预期内容,并遵循 WebhookResponse 参考文档来了解要使用 response.json({...}) 响应的内容。

此代码包含两个辅助函数,更易于:

  • 通过将字符串传递给 sendSSML,针对当前平台发送正确的响应 JSON。
  • 通过将上下文名称传递给 getOutputContext,从请求中查找有效的 Dialogflow 上下文。

进一步改进

这可以帮助您开始使用网络钩子来实现高级使用场景。您设计了一个代理,该代理可在最终用户说出其序列时循环显示序列提示,从而确保虚拟代理能够正确地听到它们。

以下是一些进一步改进体验的方法:

  • 更改一些网络钩子响应,以与您的品牌相符。例如,您可以修改代码,指定“您的订单号是什么?”,而不是宽泛的“您的序列是什么?”您可以改为在...”上查找。
  • 考虑向“序列 - 完成”意图添加其他输出上下文,然后在该输入上下文下创建一些新意图,以便用户可以就其订单继续提问。
  • 如果您想深入了解此使用场景,请查看上面示例代码中的 TODO: CHALLENGE,了解如何进一步改进用户体验。