使用 Webhook 建立執行要求

Dialogflow 中的 Webhook 執行要求可讓我們充分控制服務機器人的流程。在本教學課程中,您需要使用 webhook 驗證在「序列」意圖中收集到的英數字元序列。Webhook 會重複執行該意圖,以便在更易管理的疊代中收集長序列。

使用內嵌編輯器建立 Webhook

Dialogflow 主控台內建內嵌編輯器,可讓您直接編寫 NodeJS 程式碼,然後部署至 Cloud Functions 上,以 Webhook 的形式執行。

如要使用 Dialogflow 的內嵌編輯器建立 webhook,請按照下列步驟操作:

  1. 按一下導覽列中的「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. 按一下「部署」

您現在應該可以透過呼叫代理程式來測試整合功能。如果您尚未設定,現在正是時候設定合作夥伴提供的一鍵電話整合功能,或是設定 Dialogflow 電話閘道,透過電話測試您的服務機器人。

瞭解程式碼

由於 dialogflowFirebaseFulfillment 函式是 webhook 的進入點,因此每次觸發 webhook 時,系統都會呼叫這個函式。每次要求時,Dialogflow 都會傳送您在 Dialogflow 主控台為意圖指定的「action」名稱。程式碼會使用這個動作名稱,判斷要呼叫哪個 Webhook 函式 (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 背景資訊。

進一步改善

這應該能讓您開始使用 webhook 進行進階用途。您設計的代理程式可在使用者說出序列時重複播放序列提示,並在過程中確保虛擬服務專員正確聽到使用者說的內容。

以下提供一些想法,協助你進一步改善使用體驗:

  • 變更部分 webhook 回應,以符合品牌風格。舉例來說,您可以編輯程式碼,改為使用「What is your order number? 你可以在 ..." 中找到它。
  • 建議您在「序列 - 完成」意圖中新增其他輸出背景資訊,然後在該輸入背景資訊下建立一些新意圖,讓使用者針對訂單提出後續問題。
  • 如果您想進一步瞭解這個用途,請查看上述範例程式碼中的 TODO: CHALLENGE,瞭解如何進一步改善使用者體驗。