Amazon PersonalizeをAlexaから呼び出してみる

AWS Personalizeのチュートリアルをやってみたついでに、作成したキャンペーンをAlexaから呼び出してみました。

チュートリアルでは、ユーザIDを指定すると映画のレコメンドを返すキャンペーンを作成しますが
これを呼び出すAlexaスキルを作成します。

# Amazon Personalizeとは

アプリケーションを使用するユーザー向けにレコメンデーションを簡単に追加できるサービスです。
https://aws.amazon.com/jp/personalize/

# 必要なもの

- AWSアカウント
- Alexa開発者アカウント

# 手順
1. Personalizeのチュートリアルを実施する
2. DynamoDBにメタデータを登録する
3. Lambdaを作成する
    2. サンプルアプリケーションをデプロイする
    3. ロールを修正する
    4. boto3のLayerを作成・追加する
    5. サンプルアプリケーションのコードを修正する
4. Alexaスキルを作成する
5. 完成


# 1. Personalizeのチュートリアルを実施する

以下を用意します。
https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/getting-started.html#gs-prerequisites

- S3 Bucketの作成とBucket Policyの設定
- IAM Roleの作成
- CSVファイルのダウンロード・加工・アップロード

バケットポリシーは以下の手順で適用する必要があります。

1. Block public accessを一旦OFFにする
2. バケットポリシーを適用する
3. Block public accessをONに戻す

あとはマニュアルの手順通りに作成します。
https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/getting-started-console.html

こんな感じでレコメンデーションが取得できるようになります。
小一時間もあればできると思います。

<img width="645" alt="1_recommend.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/9d032543-8bd1-c3a6-ba00-af1f5f3932e1.png">

# 2. DynamoDBにメタデータを登録する

次に、Alexaのレスポンスが映画のIDだけでは寂しいので、Alexaから映画のタイトルとジャンルをレスポンスできるようにDynamoDBに登録します。

DynamoDBに以下のテーブルを作成後、チュートリアルでダウンロードしたzipファイル中にあるmovies.csvをDynamoDBに登録します。

- table name: movie
- key: movieId

```dynamodb_import.py
import boto3
import csv
import time

dynamodb = boto3.client('dynamodb')


with open('movies.csv') as f:
    moviereader = csv.reader(f, delimiter=',', quotechar='"')
    next(moviereader, None)
    for line in moviereader:
        movieId, title, genres = line
        dynamodb.put_item (
            TableName='movie',
            Item={
                'movieId': {
                    'S': movieId
                },
                'title': {
                    'S': title
                },
                'genres': {
                    'S': genres
                }
            }
        )
        time.sleep(0.2)
```

# 3. Lambdaを作成する
## 3-1. サンプルアプリケーションをデプロイする
まずは、Serverless Application Repositoryからalexa-skills-kit-python36-factskillをデプロイしてLambdaを作成します。

## 3-2. ロールを修正する
LambdaからPersonalizeとDynamoDBを呼び出すので、上記で指定したIAM RoleにPersonalizeとDynamoDBのポリシーを付与します。

#### ポリシーの追加

<img width="500" alt="3_2_1role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/b1e96e85-214f-ea82-9658-472f5deccc01.png">


## 3-3. boto3のLayerを作成・追加する
Lambdaに標準でインストールされているboto3はPersonalizeに対応していないため(2019-08-31時点)、最新のboto3のLayerを作成してLambdaに追加します。

#### アップロード用のLayerパッケージの作成

```bash
mkdir -p boto3/python
cd boto3
echo "boto3==1.9.220" > requrements.txt
pip install -r requrements.txt -t python
zip -r ../boto3.zip .
```

#### Layerの作成

<img width="619" alt="3_3_1boto3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/06ba08a5-9d5b-cac7-6dea-0c5728db64ad.png">

#### Layerの追加

<img width="1207" alt="3_3_2layer.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/eb25573c-b0aa-fc44-d666-5ac9fd8e7027.png">


## 3-4. コードを修正する

get_recommendationsのレスポンスからitemId(映画ID)を取得して、DymamoDBからitemIdをキーにtitleとgenres(ジャンル)を取得しています。

※campaignArnは書き換えてください。
※チュートリアルを流用している関係でレコメンドとAlexaのユーザIDはマッチ出来ないので、レコメンド取得時のユーザIDはランダムで渡しています。

```python
import random
import json
import boto3

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import (
    AbstractRequestHandler, AbstractExceptionHandler,
    AbstractRequestInterceptor, AbstractResponseInterceptor)
from ask_sdk_core.utils import is_request_type, is_intent_name
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_model import Response

personalizeRt = boto3.client('personalize-runtime')
dynamodb = boto3.client('dynamodb')
campaignArn = 'arn:aws:personalize:us-east-1:XXXXXXXXXXXX:campaign/ratings-campaign'
sb = SkillBuilder()


def get_recommendations(userId):
    try:
        res = personalizeRt.get_recommendations(
            campaignArn=campaignArn,
            userId=userId
        )
        itemId = res['itemList'][0]['itemId']
        res = dynamodb.get_item(
            TableName='movie',
            Key={
                'movieId': {
                    'S': itemId
                }
            }
        )
        genres = res['Item']['genres']['S'].split('|')
        title = res['Item']['title']['S']
        return (title, genres)
    except Exception as e:
        print(e)
        raise e


class GetMovieHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return (is_request_type("LaunchRequest")(handler_input) or
                is_intent_name("GetMovieIntent")(handler_input))

    def handle(self, handler_input):
        userId = str(random.randrange(611))
        title, genres = get_recommendations(userId)

        speech = 'おすすめの映画は' \
             + title \
             + 'です。' \
             + 'ジャンルは' \
             + 'と'.join(genres) \
             + 'です。'

        handler_input.response_builder.speak(speech).set_should_end_session(
            False)
        return handler_input.response_builder.response


class HelpIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_intent_name("AMAZON.HelpIntent")(handler_input)

    def handle(self, handler_input):
        message = '何かお困りですか。'
        handler_input.response_builder.speak(message).ask(message)
        return handler_input.response_builder.response


class CancelOrStopIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return (is_intent_name("AMAZON.CancelIntent")(handler_input) or
                is_intent_name("AMAZON.StopIntent")(handler_input))

    def handle(self, handler_input):
        handler_input.response_builder.speak('停止します。')
        return handler_input.response_builder.response


class FallbackIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_intent_name("AMAZON.FallbackIntent")(handler_input)

    def handle(self, handler_input):
        handler_input.response_builder.speak('わかりません。')
        return handler_input.response_builder.response


class SessionEndedRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_request_type("SessionEndedRequest")(handler_input)

    def handle(self, handler_input):
        return handler_input.response_builder.response


class CatchAllExceptionHandler(AbstractExceptionHandler):
    def can_handle(self, handler_input, exception):
        return True

    def handle(self, handler_input, exception):
        handler_input.response_builder.speak('システムエラーが発生しました。')
        return handler_input.response_builder.response


sb.add_request_handler(GetMovieHandler())
sb.add_request_handler(HelpIntentHandler())
sb.add_request_handler(CancelOrStopIntentHandler())
sb.add_request_handler(FallbackIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())
sb.add_exception_handler(CatchAllExceptionHandler())

lambda_handler = sb.lambda_handler()
```

# 4. Alexaスキルを作成する

スキルを作成後、以下を指定・追加します。

- 呼び出し名
- カスタムインテント
- エンドポイント(lambdaのARN)

#### 呼び出し名の指定

<img width="656" alt="4_1_invokename.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/b384f2af-438c-67a0-3f27-309ae53e048b.png">

#### カスタムインテントの追加

<img width="557" alt="4_2_intent.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/fa3918e7-7f35-3b1c-a0ec-2ea7cb54700f.png">

#### エンドポイント(lambdaのARN)の指定

<img width="1197" alt="4_3_endpoint.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/ac19cc18-d913-29f6-e950-74834fd7ad2b.png">

#### lambdaのトリガーの修正

エンドポイントの画面に表示されているスキルIDをコピーして、lambdaのトリガーに指定します。
こうすると特定のスキルIDからのみ受け付けるようになります。
トリガーは画面から修正できないので、Alexa Skills Kitトリガーを追加してから古いトリガーを削除します。

<img width="612" alt="4_4_trigger.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/24edea1f-1d8c-8bac-9723-1e42740c11f6.png">

# 完成

シミュレータはこんな感じになります。

<img width="426" alt="5_1_fin.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/fdad704f-cf30-cf3c-b429-54c57fe6850d.png">

# 感想

レコメンドがサクッと実装できるって、いい世の中だなぁ。