Amazon Lexで簡単なボットを作成してみた

# はじめに
この記事はLexの勉強用として試したことをまとめたものです。
書籍「[AWSでつくる-AIプログラミング入門](https://www.amazon.co.jp/AWS%E3%81%A7%E3%81%A4%E3%81%8F%E3%82%8B-AI%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%85%A5%E9%96%80-%E6%9D%BE%E6%B5%A6-%E5%81%A5%E4%B8%80%E9%83%8E/dp/4798059056)」を参考にして対話ボットを作成してみました。

# Amazon Lexについて
* 対話型インターフェイスを作成することができるAIサービスである。
* 会話ボットを簡単に構築することができる。
* Amazon Alexaで使われている深層学習技術と同じ技術を使用している。
* 現時点で日本語未対応である。

# IAMロールを作成
* Lexに権限を与えるためのロール「AWSServiceRoleForLexBots」を作成する。
* https://docs.aws.amazon.com/ja_jp/lex/latest/dg/howitworks-service-permissions.html

# ボットを作成
* アイスクリームを注文を処理するボットを作成する。
* https://docs.aws.amazon.com/ja_jp/lex/latest/dg/gs2-create-bot-create.html

# インテントを作成
* インテントとはユーザがLexに対して要求するための機能である。（会話のメニューみたいなもの）
* ここでは「アイスクリームを注文する」というのがインテントになる。他にも「明日の天気は」や「雨がふっていますか」などがインテント。
* https://docs.aws.amazon.com/ja_jp/lex/latest/dg/gs2-create-bot-configure-intent.html

# スロットタイプの作成
* スロットとはユーザから受け取るためのパラメーターを指す。（例えば、日時や場所などの情報）
* ここではアイスクリームの注文を想定しているため、容器と味の2つのスロットを作成する。
* https://docs.aws.amazon.com/ja_jp/lex/latest/dg/howitworks-custom-slots.html

# ボットを構築
* 「Build」ボタンを押下して、ボットを作成する。
* 以下のように設定してみた。
<img width="640" alt="sampleボット.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466400/6769ee90-f668-0f7c-8b9a-4f064f17fa30.png">

# Lambdaを作成
* 以下スクリプトを作成する。

```python
import json
import datetime
import time
import os
import dateutil.parser
import logging

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

# --- Main ---
def lambda_handler(event, context):
    print(event)
    return dispatch(event)

# --- Input Check ---
def try_ex(func):
    try:
        return func()
    except KeyError:
        return None
        
# --- Intent Check ---
def dispatch(intent_request):

    logger.debug('dispatch userId={}, intentName={}'.format(intent_request['userId'], intent_request['currentIntent']['name']))

    intent_name = intent_request['currentIntent']['name']

    if intent_name == 'OrderIntent':
        return ice_cream_order(intent_request)

# --- Intent Request ---
def ice_cream_order(intent_request):

    Container = try_ex(lambda: intent_request['currentIntent']['slots']['Container'])
    Flavor = try_ex(lambda: intent_request['currentIntent']['slots']['Flavor'])
    session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
    print(session_attributes)

    if intent_request['invocationSource'] == 'DialogCodeHook':
        # Validate any slots which have been specified.  If any are invalid, re-elicit for their value
        validation_result = validate_order(intent_request['currentIntent']['slots'])
        if not validation_result['isValid']:
            slots = intent_request['currentIntent']['slots']
            slots[validation_result['violatedSlot']] = None
            
            return elicit_slot(
                session_attributes,
                intent_request['currentIntent']['name'],
                slots,
                validation_result['violatedSlot'],
                validation_result['message']
            )

    print(Container)
    print(Flavor)
    msg = "OK, {} ice cream in {}".format(Flavor, Container)

    print("json : " + str(msg))
    return close(
        session_attributes,
        'Fulfilled',
        {
            'contentType': 'PlainText',
            'content': msg
        }
    )
```

* Lexへ返すための関数を定義する。
    * ElicitSlot・・・ユーザーがレスポンスでスロット値を提供するよう予期されていることをLexに伝える。
    * Close・・・ユーザーからのレスポンスを予期しないようにLexに伝える。レスポンス不要である。

```python
# --- Validate Check ---
def validate_order(slots):
    Container = try_ex(lambda: slots['Container'])
    Flavor = try_ex(lambda: slots['Flavor'])

    if Container is None:
        return build_validation_result(
            False,
            'Container',
            'corn or cup ?'
        )

    if Flavor is None:
        return build_validation_result(
            False,
            'Flavor',
            'vanilla, chocolate or strawberry ?'
        )

    return {'isValid': True}
    
# --- Validate Result ---
def build_validation_result(isvalid, violated_slot, message_content):
    return {
        'isValid': isvalid,
        'violatedSlot': violated_slot,
        'message': {'contentType': 'PlainText', 'content': message_content}
    }
    
# --- Responses Build ---
def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message):
    return {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'ElicitSlot',
            'intentName': intent_name,
            'slots': slots,
            'slotToElicit': slot_to_elicit,
            'message': message
        }
    }

# --- Responses Close ---
def close(session_attributes, fulfillment_state, message):
    response = {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'Close',
            'fulfillmentState': fulfillment_state,
            'message': message
        }
    }
    print("Res : " + str(response))
    return response
```

# 会話のテスト
* コンソール画面からテストができるので試してみる。
* 「ice cream order」とLexを呼び出してみると会話が始まる。
<img width="316" alt="sample会話_OK.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466400/71204f94-f919-3f71-f992-15efacfd5ec8.png">

* 登録されていないインテントでLexを呼び出してみると「Please try again.」と返ってくる。
（Error handlingでインテントに入らなかった時にどう返すか定義しているため。）
<img width="313" alt="sample会話_NG.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466400/2bbfaabf-0379-57eb-ada4-6dd06afac9ef.png">

# まとめ
* Lexへ返すための形式に苦労したが一度作成してしまえば色々と応用できそう。
* 日本語対応が待ち遠しい。

# 参考
* https://dev.classmethod.jp/cloud/amazon-lex-error-handler/