AWS Personalizeのチュートリアルをやってみたついでに、作成したキャンペーンをAlexaから呼び出してみました。
チュートリアルでは、ユーザIDを指定すると映画のレコメンドを返すキャンペーンを作成しますが
これを呼び出すAlexaスキルを作成します。
Amazon Personalizeとは
アプリケーションを使用するユーザー向けにレコメンデーションを簡単に追加できるサービスです。
https://aws.amazon.com/jp/personalize/
必要なもの
- AWSアカウント
- Alexa開発者アカウント
手順
- Personalizeのチュートリアルを実施する
- DynamoDBにメタデータを登録する
- Lambdaを作成する
2. サンプルアプリケーションをデプロイする
3. ロールを修正する
4. boto3のLayerを作成・追加する
5. サンプルアプリケーションのコードを修正する - Alexaスキルを作成する
- 完成
1. Personalizeのチュートリアルを実施する
以下を用意します。
https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/getting-started.html#gs-prerequisites
- S3 Bucketの作成とBucket Policyの設定
- IAM Roleの作成
- CSVファイルのダウンロード・加工・アップロード
バケットポリシーは以下の手順で適用する必要があります。
- Block public accessを一旦OFFにする
- バケットポリシーを適用する
- Block public accessをONに戻す
あとはマニュアルの手順通りに作成します。
https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/getting-started-console.html
こんな感じでレコメンデーションが取得できるようになります。
小一時間もあればできると思います。

2. DynamoDBにメタデータを登録する
次に、Alexaのレスポンスが映画のIDだけでは寂しいので、Alexaから映画のタイトルとジャンルをレスポンスできるようにDynamoDBに登録します。
DynamoDBに以下のテーブルを作成後、チュートリアルでダウンロードしたzipファイル中にあるmovies.csvをDynamoDBに登録します。
- table name: movie
- key: movieId
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のポリシーを付与します。
ポリシーの追加

3-3. boto3のLayerを作成・追加する
Lambdaに標準でインストールされているboto3はPersonalizeに対応していないため(2019-08-31時点)、最新のboto3のLayerを作成してLambdaに追加します。
アップロード用のLayerパッケージの作成
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の作成

Layerの追加

3-4. コードを修正する
get_recommendationsのレスポンスからitemId(映画ID)を取得して、DymamoDBからitemIdをキーにtitleとgenres(ジャンル)を取得しています。
※campaignArnは書き換えてください。
※チュートリアルを流用している関係でレコメンドとAlexaのユーザIDはマッチ出来ないので、レコメンド取得時のユーザIDはランダムで渡しています。
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)
呼び出し名の指定

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

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

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

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

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