웹훅을 사용하여 fulfillment 만들기

Dialogflow의 웹훅 fulfillment를 사용하면 에이전트 흐름을 확실하게 제어할 수 있습니다. 이 가이드에서는 '시퀀스' 인텐트에서 수집된 영숫자 시퀀스를 검증하기 위한 웹훅이 필요합니다. 웹훅은 긴 시퀀스를 좀 더 관리가 쉬운 반복으로 수집하기 위해 인텐트를 거듭하여 반복합니다.

인라인 편집기로 웹훅 만들기

Dialogflow 콘솔에는 Cloud Functions에서 웹훅으로 실행되도록 배포할 수 있는 NodeJS 코드를 직접 작성할 수 있는 인라인 편집기가 있습니다.

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. 배포를 클릭합니다.

이제 에이전트를 호출하여 통합을 테스트할 수 있습니다. 아직 설정하지 않았다면 Google 파트너가 제공하는 클릭 한 번이면 끝나는 전화 통합을 설정하거나 Dialogflow Phone Gateway를 설정하여 전화를 통해 에이전트를 테스트합니다.

코드 이해하기

웹훅의 진입점인 dialogflowFirebaseFulfillment 함수는 웹훅이 트리거될 때마다 호출됩니다. 요청마다 Dialogflow가 Dialogflow 콘솔에서 지정한 '작업' 이름을 인텐트로 전송합니다. 코드에서는 작업 이름을 사용하여 호출할 웹훅 함수(handleSequence 또는 validateSequence)를 결정합니다.

시퀀스 처리

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 관련 종속 항목을 사용하지 않습니다. 대신 request.body에 필요한 작업은 WebhookRequest 참조를, response.json({...})에 대한 응답은 WebhookResponse 참조를 따르세요.

이 코드에는 다음 작업에 도움이 되는 2가지 도우미 함수가 포함되어 있습니다.

  • 문자열을 sendSSML에 전달하여 현재 플랫폼에 적절한 응답 JSON을 전송합니다.
  • 컨텍스트 이름을 getOutputContext에 전달하여 요청에서 활성 Dialogflow 컨텍스트를 찾습니다.

추가 개선사항

이를 통해 고급 사용 사례에 웹훅을 사용할 수 있습니다. 가상 에이전트가 시퀀스를 제대로 인식하도록 최종 사용자가 시퀀스를 말하는 동안 시퀀스 프롬프트를 반복할 수 있는 에이전트를 설계했다고 가정하겠습니다.

이 환경을 더 개선할 수 있는 몇 가지 아이디어를 소개합니다.

  • 브랜드에 맞게 웹훅 응답 중 일부를 변경하세요. 예를 들면 일반적인 'What is your sequence?...' 프롬프트 대신 'What is your order number? You can find it on ...'으로 코드를 수정할 수 있습니다.
  • 'Sequence - Done' 인텐트에 다른 출력 컨텍스트를 추가한 후 사용자가 주문에 대한 추가 질문을 할 수 있도록 해당 입력 컨텍스트 아래에 새 인텐트를 만들어 보세요.
  • 이 사용 사례에 관한 자세한 내용은 위 샘플 코드의 TODO: CHALLENGE를 참조하세요. 사용자 환경을 더욱 개선할 수 있는 방법을 알려드립니다.