話題の記事

[Amazon Lex] Lex-SDKでセッション情報や永続化情報が簡単に扱えるようになりました

2019.04.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

AIソリューション部の平内(SIN)です。

Amazon Lex(以下、Lex)では、 validation及び、FulfillmentでLambdaファンクションを呼び出すことが可能であり、きめ細かいスロットのバリデーションや、インテント完了時の処理を定義することができます。

Lexの仕組みは、インテント、スロット、ダイアログモードなど、その骨格はAmazon Alexa(以下、Alexa)に非常に似ていますが、Alexa SDKのような環境がないため、既に、Alexaの開発に慣れている方にも、Lex開発は、ちょっと戸惑うというのが現状かも知れません。

本記事で紹介する、Lex SDKは、この問題を解決するため、Alexa SDKと同じようなスタイルでLex開発することを目標としたSDKであり、JavaScript及び、TypeScriptで利用可能です。

https://github.com/furuya02/lex-sdk

今回は、昨日、このLex SDKに追加された、セッション情報や、永続化情報を簡単に扱うことができるAttributesManagerクラスの利用方法について紹介させて頂きます。

2 AttributesManagerクラス

AttributesManagerは、アトリビュートの管理のためのクラスであり、下記の3種類のデータを扱うことが出来ます。

  • リクエストアトリビュート(1リクエストのライフサイクルでのみ存続)
  • セッションアトリビュート(スキルセッションが継続している間に存続)
  • 永続アトリビュート(セッションのライフサイクルを終了しても存続)

利用可能なメソッドなメソッドは以下の7つです。

// リクエストアトリビュート用
getRequestAttributes() : {[key : string] : any};
setRequestAttributes(requestAttributes : {[key : string] : any}) : void;
// セッションアトリビュート用
getSessionAttributes() : {[key : string] : any};
setSessionAttributes(sessionAttributes : {[key : string] : any}) : void;
// 永続アトリビュート用
getPersistentAttributes() : Promise<{[key : string] : any};
setPersistentAttributes(persistentAttributes : {[key : string] : any}) : void;
savePersistentAttributes() : Promise<void>;

AttributesManagerは、Lex.HandlerInputのプロパティとして定義されており、必要な情報(アトリビュー)を取得(若しくは、保存)に利用することが出来ます。

下記は、SampleIntentのハンドラで、Lex.HandlerInput経由で取得したAttributesManager使用して、セッション情報を取得している例です。

const SampleIntentHandler: Lex.RequestHandler = {
    canHandle(h: Lex.HandlerInput) {
        return (h.intentName == 'SampleIntent')
    },
    async handle(h: Lex.HandlerInput) {
        // セッション情報の取得
        let attributes = await h.attributesManager.getPersistentAttributes();

3 動作確認用ボット

特に何もないテスト用のボットで動作確認していきます。テスト用のボットは、以下のようになっています。

  • インテントは、SmpleIntentのみ
  • サンプル発話は「Hello」のみ
  • スロットなし
  • validation及び、FulfillmentにLambda(LexSmaple)を設定

4 リクエストアトリビュート

最初に、1回のリクエストのライフサイクルでのみ存続するデータであるリクエストデータを使うサンプルです。

リクエストデータは、getRequestAttributes()で取得し、setRequestAttributes()で保存できます。

ハンドラ間でデータのやり取りができていることを確認するために、すべてのハンドラの前に処理を挿入できるaddRequestInterceptors()で追加したハンドラ(requestInspectorHandler)でデータを設定し、それを別のハンドラ(SampleIntentHandler)で使用しています。

import * as Lex from 'lex-sdk';

let bot: Lex.Bot;
exports.handler = async function (event: Lex.IntentRequest, context: any) {
    console.log(JSON.stringify(event));
    if (!bot) {
        bot = Lex.BotBuilder()
            .addRequestHandlers(SampleIntentHandler)
            .addRequestInterceptors(requestInspectorHandler)
            .create();
    }
    return bot.invoke(event, context);
}

const requestInspectorHandler: Lex.RequestInterceptor = {
    process(h: Lex.HandlerInput) {
        // リクエスト情報の設定
        let attributes = h.attributesManager.getRequestAttributes();
        attributes.speak = 'test'; // リクエストデータとして、speakを格納する
        h.attributesManager.setRequestAttributes(attributes);
    }
}

const SampleIntentHandler: Lex.RequestHandler = {
    canHandle(h: Lex.HandlerInput) {
        return (h.intentName == 'SampleIntent')
    },
    async handle(h: Lex.HandlerInput) {

        // リクエスト情報の取得
        const attributes = h.attributesManager.getRequestAttributes();
        const speak = attributes.speak; // リクエストデータのspeakを取り出す

        if (h.source === Lex.InvocationSource.DialogCodeHook) {
            return h.responseBuilder
                .getDelegateResponse(h.slots)

        } else {  // FulfillmentCodeHook
            const message =  { contentType: Lex.ContentType.PlainText, content: speak};
            return h.responseBuilder
            .getCloseResponse(
                Lex.FulfillmentState.Fulfilled,
                message)
        }
    }
}

動作は、以下のようになっています。

5 セッションアトリビュート

リクエストアトリビュートは、あまり使う場面が無いかも知れませんが、セッションデータは、比較的利用頻度が高いのではないでしょうか。

リクエストデータは、getSessionAttributes()で取得し、setSessionAttributes()で保存できます。

setSessionAttributes()で設定されたアトリビュートは、ResponseBuilderがレスポンスを生成する際に、自動的にSttributeに追加されます。

import * as Lex from 'lex-sdk';

let bot: Lex.Bot;
exports.handler = async function (event: Lex.IntentRequest, context: any) {
    console.log(JSON.stringify(event));
    if (!bot) {
        bot = Lex.BotBuilder()
            .addRequestHandlers(
                SampleIntentHandler)
            .create();
    }
    return bot.invoke(event, context);
}

const SampleIntentHandler: Lex.RequestHandler = {
    canHandle(h: Lex.HandlerInput) {
        return (h.intentName == 'SampleIntent')
    },
    async handle(h: Lex.HandlerInput) {
        
        if (h.source === Lex.InvocationSource.DialogCodeHook) {
            return h.responseBuilder
                .getDelegateResponse(h.slots)

        } else {  // FulfillmentCodeHook

            // リクエスト情報の取得
            const attributes = h.attributesManager.getSessionAttributes();
            if(attributes.counter == undefined) {
                attributes.counter = 0;
            }
            attributes.counter = Number(attributes.counter) + 1;
            h.attributesManager.setSessionAttributes(attributes);

            let speak = `カウンターは、${attributes.counter}です。`

            const message =  { contentType: Lex.ContentType.PlainText, content: speak};
            return h.responseBuilder
            .getCloseResponse(
                Lex.FulfillmentState.Fulfilled,
                message)
        }
    }
}

上記のコードを実行した結果です。コンソール上でのテストでは、Clear chat historyを押すまでは、同一セッションとして動作するため、カウンターがインクリメントされている様子が確認できます。

6 永続アトリビュート

最後に永続アトリビュートです。セッションに関係なく、ボットとして永続的にデータを保持したい場合に利用できます。

永続データは、getPersistentAttributes()で取得し、setPersistentAttributes()で保存でき、savePersistentAttributes()で永続化(DB上書き)されます。setPersistentAttributes()では、まだ、DBへ書き込まれていないことにご注意下さい。

(1) テーブル定義

なお、永続アトリビュートを使用する場合、.withTableName()による、DynamoDBのテーブルの定義が必要です。.withTableName()の第1パラメータは、テーブル名であり、第2引数は、主キーに格納するデータです。

Alexa SDKの場合、主キーに格納する値は、デフォルトでUserIdが利用され、各ユーザーごとのデータが保持される仕組みとなっていますが、Lexでは、ユーザーを識別するという概念自体が存在しないため、ここでは、値を明示的に指定して、ボットに固有のデータとして保存する仕様としています。

※ 実は、LexにのUserIdがあるのですが、この値は、クライアント側アプリが自由に設定できるため、ユーザーの識別に利用できる保証はありあせん。

exports.handler = async function (event: Lex.IntentRequest, context: any) {
    console.log(JSON.stringify(event));
    if (!bot) {
        bot = Lex.BotBuilder()
            .addRequestHandlers(SampleIntentHandler)
            .withTableName("tableName", "TestBot") // テーブル名指定
            .withAutoCreateTable(true) //テーブル作成もスキルから行う
            .create();
    }
    return bot.invoke(event, context);
}

(2) 利用例

永続データを保存・取得しているサンプルです。

import * as Lex from 'lex-sdk';

let bot: Lex.Bot;
exports.handler = async function (event: Lex.IntentRequest, context: any) {
    console.log(JSON.stringify(event));
    if (!bot) {
        bot = Lex.BotBuilder()
            .addRequestHandlers(
                SampleIntentHandler)
                .withTableName("SampleBotTable", "SampleBot") // テーブル名指定
                .withAutoCreateTable(true) //テーブル作成もスキルから行う
            .create();
    }
    return bot.invoke(event, context);
}

const SampleIntentHandler: Lex.RequestHandler = {
    canHandle(h: Lex.HandlerInput) {
        return (h.intentName == 'SampleIntent')
    },
    async handle(h: Lex.HandlerInput) {
        
        if (h.source === Lex.InvocationSource.DialogCodeHook) {
            return h.responseBuilder
                .getDelegateResponse(h.slots)

        } else {  // FulfillmentCodeHook

           // 永続情報の取得
            const attributes = await h.attributesManager.getPersistentAttributes();
            if(attributes.counter == undefined) {
                attributes.counter = 0;
            }
            attributes.counter = Number(attributes.counter) + 1;
           // 永続情報の保存
            h.attributesManager.setPersistentAttributes(attributes);
            await h.attributesManager.savePersistentAttributes();

            let speak = `カウンターは、${attributes.counter}です。`

            const message =  { contentType: Lex.ContentType.PlainText, content: speak};
            return h.responseBuilder
            .getCloseResponse(
                Lex.FulfillmentState.Fulfilled,
                message)
        }
    }
}

実行すると、下記のようになります。 Clear chat historyを押して、セッションを新たに開始しても、カウンターは初期化されません。

DynamoDBでは、永続データが保存されていることを確認できます。

(3) DynamoDBの定義

Lsx SDKでDynamoDBを使用すると、デフォルトで、Lambdaの配置されたリージョンのDynamoDBが利用されます。 これを、変更したい場合や、ローカルの試験中に別のパーミッションで動作させたい場合などは、DynamoDBオブジェクトを自前で定義することも可能です。

import * as Lex from './lex-sdk';
const AWS = require('aws-sdk');
AWS.config.credentials = new AWS.SharedIniFileCredentials({profile: 'my-profile'});

let bot: Lex.Bot;
exports.handler = async function (event: Lex.IntentRequest, context: any) {
    console.log(JSON.stringify(event));
    if (!bot) {
        bot = Lex.BotBuilder()
            .addRequestHandlers(SampleIntentHandler)
            .withTableName("SampleBotTable", "SampleBot") // テーブル名指定
            .withAutoCreateTable(true) //テーブル作成もスキルから行う
            .withDynamoDbClient(
                new AWS.DynamoDB({ apiVersion: "latest", region: "ap-northeast-1" })
            )
            .addErrorHandlers(ErrorHandler)
            .create();
    }
    return bot.invoke(event, context);
}

7 最後に

今回は、セッション情報や、永続化情報を簡単に扱うことができるAttributesManagerクラスの利用方法について紹介させて頂きました。

Lex SDKは、まだまだ、雑な実装ですが、逐次進化できるように頑張りたいと思います。


弊社ではAmazon Connectのキャンペーンを行なっております。

4月に引き続き、5月も「無料Amazon Connectハンズオンセミナー」を開催予定です。導入を検討されておられる方は、是非、お申し込み下さい。

また音声を中心とした各種ソリューションの開発支援も行なっております。