AmazonLexへのインテント設定とLambda関数構築

AmazonLexでチャットボットを作成しようとしてインテントの概念理解につまずいたのでメモしておきます。

# インテントとは？
そもそもインテントって何？ってところからよく理解できないまま、AWSのドキュメントを探しても見つからず右往左往しました。

色々試して分かったのは、

:::note warn
インテント＝ユーザがある種の質問をしたときに、それが解決するまでの全体の流れ
:::

という感じでしょうか。

Amazon Lex上では、その一連の流れをインテントとして作成して設定できる画面があります。

![スクリーンショット 2022-06-06 11.35.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/4af3bbbb-8400-37ae-6dff-0004e8d43874.png)

#　会話のフローを整理する
インテント作成にはそもそもユーザとの会話の流れをイメージして設計しておく必要があります。
今回はQAチャットボットを作りたかったので以下のようにフローチャートを作成して整理ました。

![LexFlow.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/eeec8f38-75c2-050e-34c0-ed6cc833d4e3.png)

こうすると以下の3つのインテントを作成する必要があることがわかります。
- QAカテゴリ選択：QAのカテゴリを表示して質問の内容を選択してもらう
- 質問回答：質問に対して回答を生成して表示し、最後に解決したかどうかを問う
- 電話番号表示：疑問が解決しなかったときに有人で対応するための電話番号を表示する

# インテントの設定
インテントの設定は基本的にマネジメントコンソールにログインしてAmazon Lexをひらけば設定できます。

## サンプル発話
インテント開始条件になるユーザの会話内容を何種類か追加します。想定でいくつか入れておくのがポイントです。

![スクリーンショット 2022-06-06 11.41.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/233e8cb5-eddd-b3ec-7232-48edfa5a1187.png)

## スロット
インテントの中で収集したい情報があれば、ここにどんどん追加していきます。
![スクリーンショット 2022-06-06 11.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/b0e81fa0-e1bb-d77a-61b4-718532e6a63e.png)

これにより、たとえば住所、氏名、Eメールなどの情報を収集して最後にLambda関数でデータベースに書き込んで、何らかの注文をチャットボットでとる、というような流れを作成することもできます。

スロットタイプというのはテキスト入力とか日付入力とかいくつかのタイプがデフォルトでありますが、独自の選択項目などを作りたい場合は、新規に「スロットタイプ」を作成しておく必要があります。


## 確認プロンプト、フルフィルメント、応答を閉じる
ある条件を満たすとメッセージを送信したいときにこれらの設定をします。
![スクリーンショット 2022-06-06 11.45.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/f138fb92-9144-79da-e981-8c808e69b380.png)

- 確認プロンプト：スロットに設定した情報を全て収集した際に送信するメッセージ
- フルフィルメント：最終処理完了後に表示するメッセージ、Lambda関数として設定することもできます
- 応答を閉じる：全ての処理が完了したときに「ありがとうございました」などと表示してインテントを終了させます

## コードフック
ユーザからメッセージを受け取ったときに、全てLambda関数に処理を任せたい場合、こちらにチェックを入れてください。

![スクリーンショット 2022-06-06 11.50.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/527185a5-1d6d-7afc-a78d-e4f81425b963.png)

:::note warn
Lambda関数に処理を任せた場合、AmazonLexのインテント設定画面で設定したメッセージは利用されません。
Lambda関数のレスポンスにメッセージを設定する必要があります。
:::

フックさせるLambda関数の指定は「ボット」→「エイリアス」→「言語」から設定します。
なぜ「言語」？というのがちょっと疑問ですが。。。

![スクリーンショット 2022-06-06 11.54.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1926542/a1249cc9-8e5f-0b41-0261-93253b8f487c.png)

# Lambda関数の作成
今回はQAカテゴリを選択した時点でフルフィルメントが実行され、その際にのみLambda関数を呼ぶよう設定しました。
実際のLambda関数は以下のような感じです。

```python
import logging
import os
import json
import boto3

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
client = boto3.client('lambda') 

def elicit_intent(message,slots):
    return {
        "sessionState":{
            'dialogAction': {
                'type': 'ElicitIntent'
            },
            "intent":{
                "slots":slots
            }
        },
        'messages':[{
            'contentType':'PlainText',
            'content':message
        }]
    }

def close(message,slots):
    return {
        "sessionState":{
            'dialogAction': {
                'type': 'Close'
            },
            "intent": {
                "confirmationState": "Confirmed",
                "name":"QAlistIntent",
                "state":"Fulfilled",
                "slots":slots,
            }
        },
        'messages':[
            {
                'contentType':'PlainText',
                'content':message
            },
            {
                "contentType": "ImageResponseCard",
                "imageResponseCard": {
                    "title": "疑問点は解決しましたか？",
                    "buttons": [
                        {
                            "text": "はい、解決しました",
                            "value": "はい、解決しました"
                        },
                        {
                            "text": "いいえ、解決していません",
                            "value": "いいえ、解決していません"
                        },
                        {
                            "text":"他に質問があります",
                            "value":"他に質問があります"
                        }
                    ]
                }
            }
        ]
    }

def build_validation_result(is_valid, violated_slot, message_content):
    if message_content is None:
        return {
            "isValid": is_valid,
            "violatedSlot": violated_slot,
        }

    return {
        'isValid': is_valid,
        'violatedSlot': violated_slot,
        'message': {'contentType': 'PlainText', 'content': message_content}
    }
def validate_question(question_type):
    question_types = ['price', 'place']
    if question_type is not None and question_type.lower() not in question_types:
        return build_validation_result(False,
                                       'QuestionType',
                                       'よくある質問には回答がありませんでした。もう一度お知りになりたい内容を教えてください。')
    return build_validation_result(True, None, None)

def answer_question(intent_request):

    question_type = intent_request['sessionState']['intent']['slots']['QA_list']['value']['interpretedValue']
    slots = intent_request['sessionState']['intent']['slots']
    validation_result = validate_question(question_type)
    if not validation_result['isValid']:
        result = elicit_intent(validation_result['message'],slots)

    elif question_type == 'price':
        result = close('金額についてはこちらを参照してください　https://xxxx.co.jp/',slots)
    elif question_type == 'place':
        result = close('場所はこちらです https://xxx.com/',slots)

    return result

def dispatch(intent_request):
    intent_name = intent_request['sessionState']['intent']['name']

    # Dispatch to your bot's intent handlers
    if intent_name == 'QAlistIntent':
        return answer_question(intent_request)

    raise Exception('Intent with name ' + intent_name + ' not supported')


def lambda_handler(event, context):
    return dispatch(event)
```

:::note warn
今回はLexV２を使いましたが、まだあまりリソースが公開されてないようで仕様理解に随分てこづりました。
Lexから渡されるリクエストの形とLexに返すべきレスポンスの形をしっかり理解する必要があります。
:::


https://docs.aws.amazon.com/ja_jp/lexv2/latest/dg/lambda.html#lambda-response-format

# まとめ
Lambda関数を使いこなせば、色々な応答設定をできそうです。
たとえば今回は会話ログを残していませんが、そういったこともLambda関数とRDSなどを使えばできるかもしれません。