Amazon Lexで日本語チャットボットを作ろう

## はじめに
Amazon Lex はチャットボット等の会話インターフェースを作成するため AI サービスです。
昨年より東京リージョンで利用可能になっていたものの、サービスの対応言語としては長らく日本語に対応していませんでしたが、ついに日本語を正式にサポートしました！

https://aws.amazon.com/jp/blogs/news/amazon-lex-tokyo/

簡単なボットを作成し、Slack に連携させるまでをやってみたいと思います。

## Lex V1 と V2 の違い
Amazon Lex は 2021年1月に V2 API がリリースされています。コンソールのデザインが変わっただけではなく、新しい機能を含む新規の API としてのリリースです。1つのボットで複数言語に対応させることができるなど、生産性が向上するアップデートも多く含まれています。

https://aws.amazon.com/jp/blogs/aws/amazon-lex-enhanced-console-experience/

V1 で作成済みのボットは V2 では利用できないのでご注意ください。
2021/7/14 に Migration Tool がリリースされており、V1 → V2 の移行が可能になりました。

https://aws.amazon.com/jp/about-aws/whats-new/2021/07/amazon-lex-announces-tooling-to-migrate-bots-from-lex-v1-console-to-lex-v2-console/

詳細は以下のドキュメントを参照ください。

https://docs.aws.amazon.com/lex/latest/dg/migrate.html

また現時点で V2 API で作成されたボットは ~~Amazon Connect のコンタクトフロー統合を利用できないという制限があります。~~
→Amazon Connect との統合も 2021/8/12 のアップデートでサポートされるようになりました。

https://aws.amazon.com/jp/about-aws/whats-new/2021/08/amazon-connect-amazon-lex-v2-console-apis/

## ボットの作成
せっかくなので、今後主流となっていくであろう V2 API を使用します。
内容としては以下の aws-samples で公開されている架空の携帯電話会社のカスタマーサービスチャットボットを日本語で作成してみます。

https://github.com/aws-samples/amazon-lex-customerservice-workshop

このチャットボットでは顧客がビジネスや休暇のために海外旅行をするときに、既存の電話アカウントに国際電話プランを簡単に追加できるようにします。

以下の URL から直接 V2 コンソールを開くことができます。
または V1 コンソールのメニューに切り替えボタンが表示されているかと思います。
https://ap-northeast-1.console.aws.amazon.com/lexv2/home?region=ap-northeast-1

Create bot から新規のボットを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/18848777-d282-747a-649c-dd07b1583048.png)

Creation method では Create を選択し、ボットの名前を指定します。
ここでは `InternationalPlan` としました。IAM ロールは ` Create a role with basic Amazon Lex permissions.` を選択して新規のサービスロールを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/cf82e5b9-8085-18ad-3f50-0d5107d046c0.png)


今回はサンプルのアプリケーションで 13歳未満の個人情報を収集することはないため、COPPA に関する選択は No とします。その他もデフォルト設定のまま次に進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/05a2555e-7511-9c74-c9a6-419e85de3b49.png)

Add Language to bot で Japaneses (JP) を選択し、ボットの作成を完了します。ボットの日本語音声は Amazon Polly でも提供されている Mizuki と Takumi から選択できますが、今回はテキストベースでしか検証しないため `None.~` を選択しました。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/e0fa6596-a33c-d131-c4b8-7b49704a8eb1.png)

## 最初のインテントを追加する
ボットを作成すると Intent editor へ遷移するので、Intent name に`ListInternationalPlans` を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/0f4ecf9a-ac77-ec36-7c36-8df5f9e2c49e.png)

次にユーザーからの入力をパラメーターとして受け取るために、スロットを定義します。ユーザーは国際電話プランを追加するために旅行先の国名を指定する必要があるため、Add slot から組み込みスロットである `AMAZON.Country` を選択します。スロット名は `Country` とします。Prompts にはユーザーに入力を確認する際のメッセージを指定できます。
`どこの国を旅行しますか？`と入力しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/fa9317dc-a59f-0e31-bc8e-b68f63957a78.png)

スロットを設定したら、 Sample utterances (サンプル発話) を入力していきます。サンプル発話を複数のパターンで定義することで、Amazon Lex がユーザーの入力から適切なインテントを選択できるように学習させることができます。

以下のようにサンプル発話を入力しました。国名が入る箇所には先ほど追加した {Country} スロットを指定するのがポイントです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/c0d4f063-1154-8036-b43c-5570b2026887.png)

この状態でボットをビルドし、コンソールからテストを実行することができます。
画面下部の Save intent でインテントを保存してから、Build を実行します。
ビルドが成功したら、Test をクリックします。

私はアメリカに旅行します。 と入力するとスロットの値を判別し適切にインテントを検知できていることがわかります。次に どのようなプランがありますか と入力するとユーザーの入力には国名が含まれていません。この場合、スロットの設定で Prompts に入力したメッセージを出力し、ユーザーに入力を促します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/7220dc64-aaf1-b926-8667-7af8b6e3f15d.png)

本来であれば指定された国にどのような国際電話プランがあるかを検索するバックエンドロジックを AWS Lambda で実装する必要がありますが、今回は Lex 日本語対応の動作イメージを確認することが目的であるため、割愛します。ここでは固定のレスポンスを返すため、Closing responses に以下を入力して再度 Save しておきます。

```
2つのプランがあります。ベーシックプランは 1000 円で 16GB のデータ通信量と、200分の通話枠があります。プレミアムプランは5,000 円で無制限で利用できます。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/da3a1113-9e77-4110-9b08-58e144ddd667.png)

## カスタムスロットを利用する
次にほしいのはユーザーが選択した国際電話プランを購入するためのインテントです。先ほどの固定レスポンスで返した内容にある、ベーシックプランとプレミアムプランをユーザーが選択できるようにカスタムスロットを追加していきます。

Slot types の Add slot type から Add blank slot type を選択します。
Slot type name には `TravelPlan` を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/373389c5-24ba-53b0-374f-6b55e090b251.png)

Slot value resolution では `Restrict to slot values` を選択します。
Slot type values には 以下のように `premium` と `basic` を追加します。また日本語のシノニムとしてそれぞれに `プレミアム` と `ベーシック` を追加しました。シノニムにはスロット値に対応する同義語を設定しておくことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/c4e87d7b-91c2-378d-8205-b5d8cb570376.png)

スロットを保存したら Intents の Add intents → Add empty intent から次のインテントを追加します。Intent name は `ApplyTravelPlan` とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/69542ab5-3fd9-9010-38d8-334b2cf7621f.png)

1つめのインテントを追加したときと同じ手順で `Country`, `planName`, `startDate`, `numOfWeeks` というスロットを追加していきます。`planName` で先ほど作成したカスタムインテントを指定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/038a59a1-8768-521d-7267-8743a800f235.png)

サンプル発話は以下のように入力しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/7b199660-9b58-1880-2787-c3d887ee0c39.png)

国際電話プランの追加は顧客のアカウントに影響があるリクエストであるため、`Confirmation prompts and decline responses` で確認メッセージを設定します。
Confirmation prompts には出力する確認メッセージを、Decline responses にはキャンセルされた場合のメッセージを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/9c696ad3-e636-f8b4-dc5c-7c40184e7478.png)

やはりここでも実際の登録処理は省略するため、Closing reponses に `正常に追加されました` と固定メッセージ入力して、インテントを保存＆ボットをビルドします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/87abc977-3a02-8510-25cb-93a5ad237c96.png)

ビルドが成功したらボットをテストします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/9b2a3c8e-ba67-82ad-1f3d-344a042ab99d.png)


ListInternationalPlans インテントでプランを確認したあとにプレミアムプランを追加するように伝えてみました。planName のスロット値は正常に入力されましたが、先ほど入力したはずの国名を再度聞かれてしまっています。

本来であれば、ListInternationalPlans インテントから ApplpyTravelPlan インテントが起動された際、ボットが国名を維持している方がユーザーとしては自然な体験となります。今回は残念ながらそのような実装になっていません。これは [セッション属性](https://docs.aws.amazon.com/lexv2/latest/dg/context-mgmt-cross-intent.html) という機能を使用して実現できるのですが、Lambda 関数で実装する必要があるため、今回は割愛させていただきます。

残りの部分は想定どおり応答を返すことができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/6b8356a9-58f9-9a17-278e-d59d336f46a3.png)

## Slack と連携する
Slack や Facebook メッセンジャー、Twilioなどの外部ツールから Lex で作成したボットを使用することができます。
~~実際に Slack 連携を試してみたのですが、結論から言うと日本語のボットはメッセージが文字化けしてしまいました。英語のボットでは正常に出力されるので連携の手順自体に問題はなさそうなのですが、原因がわかり次第追記します。せっかくなので手順だけは残します。~~

### 2021/5/5 追記
上述のとおり、当初日本語ボットの Slack 連携は正常に動作していなかったのですが、2021/5/5 時点で正常に動作するようになっていました！追記おわり。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/11217866-047c-c664-84e8-1c5de603c8dd.png)

### Slack アプリの作成
https://api.slack.com/apps?new_app=1 にアクセスして新しい Slack アプリを作成します。アプリ名とデプロイ先のワークスペース名を設定する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/71fdc280-bf27-c95a-bed0-bc7c42251419.png)

アプリを作成したら Interactivity & Shortcuts に移動し、Request URL に任意の有効な URL を設定します。ここでは `https://slack.com` として変更を保存します。後ほど Lex 側の設定が完了した際に正しい値に入力しなおす必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/712079d8-2a7d-7f03-52f4-58a53031ab75.png)

メニューから Basic Information に移動し、画面中央の App Credentials から次の値をメモしておきます。

* Client ID
* Client Secret
* Verification Token

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/d1e29f5a-deec-4f72-9bdc-f43c1f2154cd.png)

### Lex ボットと Slack の連携設定
Lex V2 コンソールを開き、Channel integrations から Add channel を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/0a3bfceb-891c-ffc0-ef4d-c9444c5e32fa.png)

Platform の設定で Slack を選択し、設定情報を安全に保管するための KMS キーを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/b028bf02-8eb3-4d83-c8bc-0efe902dcaaf.png)

連携設定の名前を入力し、ボットのエイリアスと言語を指定します。エイリアスを使用してボットの特定のバージョンを指定することができます。今回の手順ではエイリアスを発行していないため、デフォルトで作成済みの `TestBotAlias` を選択します。Addtional configuration に先ほど Slack アプリから取得した各値を設定し、Create します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/28839856-db1c-15e8-5049-aa4e55628538.png)

作成した連携設定を開き、Callback URL に表示されている Endpoint および OAuth endopoint を Slack アプリに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/11f0ba23-af8d-f2b7-bf75-73f24ac1fca3.png)

Slack アプリのメニューから OAuth & Permissions を開きます。
Redirect URLs に Lex コンソールからコピーした OAuth endopoint を入力して保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/e190a453-78c5-c720-3462-5c6a79dd868a.png)

さらに画面中央の Bot Token Scopes で `chat:write` および `team:read` を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/609d7bf6-7402-fb61-a516-c9f618121c1e.png)

メニューから Interactivity & Shortcuts 戻り、Request URL の値を Lex コンソールでコピーした Endpoint の URL に書き換えて変更を保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/4c64a554-865c-e183-4146-d4aea5a4659f.png)

メニューから Event Subscriptions を開き、Enable Events を ON に変更します。
Request URL には Lex コンソールで取得した Endpoint を設定します。更に Subscribe to bot events で `message.im` を Bot User Event として追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/2b731e62-c749-fc04-a2bd-390672cf7d18.png)

### Slack での動作確認
メニューの Manage Distribution から Add to Slack をクリックしてワークスペースにアプリケーションをインストールします。アプリ名と接続先のワークスペースに問題が無いことを確認して、接続を許可します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/70e9ee73-d772-b64a-1331-3ed8de1d686f.png)

Slack のワークスペースにリダイレクトされるので、AmazonLexJPBot に DM を送ることでテストを実施できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/f8f24753-7642-06b2-8797-73b99e6bdead.png)


## 参考: 公式ドキュメント

https://docs.aws.amazon.com/lexv2/latest/dg/what-is.html

以上です。
なんとなくでも V2 コンソールにおける日本語チャットボットの作成の雰囲気が伝われば幸いです。