DialogflowのWebhookをAPI Gateway + Lambdaで作成し、Alexa-SDK(V2)みたいに書けるようにしてみた(TypeScriptで)

2018.06.10

1 はじめに

Googleアシスタントでオリジナルの動作を実装するには、Dialogflow を使用します。

Dialogflowでは、通常、バックエンドの処理に、Cloud Functions for Firebase が使われますが、今回は、ここをAWS Lambda(以下、Lambda)及び、Amazon API Wategay(以下、API Wategay)で実装してみました。

最初に動作している様子です。

2 Alexa SDK風の実装

Dialogflow用のSDKであるActions on Google Client Library(actions-on-google) は、そのままではLambdaで使用することはできません。

actions-on-googleLambdaで実行させるために actions-on-lambda というブリッジ実装もありましたが、 「すっかり慣れてきた、Alexa SDKと同じように書きたい!」 という身勝手で、ここはラッパーを自作することにしました。

次のコードは、自作したラッパーを使用して作成したコードです。既に Alexa SDK(V2) を使ったことがある方なら、ほとんど同じであることが感じて頂けるのでは無いでしょうか・・・

index.ts

import { GoogleCloudDialogflowV2WebhookRequest } from 'actions-on-google';
import { DialogFlow, HandlerInput, RequestHandler } from './DialogFlow'; // 自作ラッパー

const CalcIntentHandler: RequestHandler = {
  canHandle(handlerInput: HandlerInput) {
    return handlerInput.requestEnvelope.intentName == 'CalcIntent';// CalcIntentを処理する
  },
  handle(handlerInput: HandlerInput) {
    let params = handlerInput.requestEnvelope.parameters;

    const birthday = new Date(params.year, params.month - 1, params.day); // 誕生日
    const today = new Date(); //今日
      const ms = today.getTime() - birthday.getTime(); // 日付差(ミリ秒)
    const days = Math.floor(ms / (1000 * 60 * 60 *24)); // ミリ秒から日付へ変換

    const speechText = '今日は、あなたが生まれてから、' + (days + 1) + '日目です';

    return handlerInput.responseBuilder
        .speak(speechText)
        .withShouldEndSession(false)
        .getResponse();
  }
};

export async function handle(event: GoogleCloudDialogflowV2WebhookRequest, context: any) {
  const dialogFlow = new DialogFlow()
    .addRequestHandlers(
      CalcIntentHandler)
    .create();
  return dialogFlow.invoke(event, context);
}

3 DialogFlow.ts

DialogFlow.tsは、ちょうどAlexa SDKの部分を代替えするようなラッパーとなっています。

(1) RequestとResponse

actions-on-googleは、TypeScriptで作成されており、Dialogflowからのリクエストとレスポンスが型定義されいます。今回は、その型情報をそのまま利用させて頂きました。

Dialogflowは、最近V2となりましたが、V1とV2では、JSON形式が結構変わっているようです。しかし、この辺はSDKの型情報をそのまま使うことで安全に実装できそうです。

import { GoogleCloudDialogflowV2WebhookRequest } from 'actions-on-google';
import { GoogleCloudDialogflowV2WebhookResponse } from 'actions-on-google';

(2) ハンドラ

処理のメインとなるハンドラのインターフェースは、Alexa SDK(V2)と同じとしました。canHandler()が処理対象となるかどうかの判定で、handle()が実装の本体です。

ただし、レスポンスについては、actions-on-googleで定義されているGoogleCloudDialogflowV2WebhookResponseとしています。

export interface RequestHandler {
    canHandle(handlerInput: HandlerInput): Promise<boolean> | boolean;
    handle(handlerInput: HandlerInput): Promise<GoogleCloudDialogflowV2WebhookResponse> | GoogleCloudDialogflowV2WebhookResponse;
}

(3) レスポンスビルダー

レスポンスビルダーは、現状、メソッドが下記の3つだけですが、使い方は、Alexa SDK(V2)と同じです。

speak(speechOutput: string): this
withShouldEndSession(val: boolean): this
getResponse(): GoogleCloudDialogflowV2WebhookResponse
export class ResponseBuilder {
    private response: GoogleCloudDialogflowV2WebhookResponse;
    constructor(){
        this.response = {
            payload: {
                google: {
                    expectUserResponse: true,
                    richResponse: {
                        items: [
                            {
                                simpleResponse: {
                                    textToSpeech: ''
                                }
                            }
                        ]
                    }
                }
            }
        }
    }
    speak(speechOutput: string): this {
        if (this.response.payload && this.response.payload.google) {
            let google = this.response.payload.google
            if (google.richResponse && google.richResponse.items ) {
                let items = google.richResponse.items;
                if (items.length > 0) {
                    if (items[0].simpleResponse) {
                        items[0].simpleResponse.textToSpeech = speechOutput;
                    }
                }
            }
        }
        return this;
    }

    withShouldEndSession(val: boolean): this {
        if( this.response.payload ) {
            if(this.response.payload.google ) {
                if(this.response.payload.google.expectUserResponse ) {
                    this.response.payload.google.expectUserResponse = val;
                }
            }
        }
        return this;
    }

    getResponse(): GoogleCloudDialogflowV2WebhookResponse{
        return this.response;
    }
}

(4) リクエストの処理

Dialogflowからのリクエストは、RequestEnvelopeで処理されます。レクエストについては、ちょっとAlexaと差異が大きいので、インテント名やパラメータをコンストラクタでパースしてしまいました。

export class RequestEnvelope {
    intentName: string;
    query: string;
    parameters: ApiClientObjectMap<any>;
    userId: string;

    constructor(request: GoogleCloudDialogflowV2WebhookRequest) {
        if (request.queryResult){
            if( request.queryResult.intent && request.queryResult.intent.displayName ) {
                this.intentName = request.queryResult.intent.displayName;
            }
            if( request.queryResult.queryText ) {
                this.query = request.queryResult.queryText;
            }
            if( request.queryResult.parameters ) {
                this.parameters = request.queryResult.parameters;
            }
        } 
        if (request.responseId) {
            this.userId = request.responseId;
        }
        // console.log('IntentName: ' + this.intentName);
        // console.log('Query: ' + this.query);
        // console.log('Parameters: ' + JSON.stringify(this.parameters));
        // console.log('UserId: ' + this.userId);
    }
}

実装時は、下記のように、RequestEnvelopeから簡単に取り出せます。

let intentName = handlerInput.requestEnvelope.intentName;
let params = handlerInput.requestEnvelope.parameters;

4 Dialogflowの定義

最後になりましたが、Dialogflowでの定義を簡単に紹介させて頂きます。

  • Default Welcome Intentは、最初のメッセージを変更しただけです。

  • インテントの定義は、CalcIntentだけで、生年月日を取得するようになっています。

  • Fullfilmentでは、WebhookAPI Gateway のEndpointを指定しています。

  • Action on Googleでは、呼び出し名を「生まれて何日」とし、Actionsで先Dialogflowに繋いでいます。

5 最後に

今回は、DialogflowのWebhookをLambdaで実装し、Alexaのスキル風に書けるようにしてみました。

音声認識アプリの実装は、各社結構似たところがあるように思います。各社の技術を似た実装で乗り入れる仕組みを用意しておくと、もしかすると工数削減に有効かもしれません。

今回実装したコードは、下記に置きました。参考になれば幸いです。 ※簡単に真似ただけですので、Alexa SDKを置き換えるようなものではないことを予めご了承ください。
github [GitHub] https://github.com/furuya02/DialogflowOnLambda/