はじめに
本記事は
- Alexa×BedrockでAWS最新ニュースキャッチアップアプリ コンセプト・設計
- AWS Lambdaで自作RSSリーダー開発
- Self hosted AWS LambdaによるAlexaスキル開発 (本記事)
- AlexaでBedrockに質問をするスキル開発
の連載記事です。前段の項目が完了していることを前提として書かれております。
本記事では以下のアーキテクチャの作成を目指して解説をしていきます。
Alexaとのやりとりのイメージは以下のような形を目指します。
内容としては以下になりますので、部分的に活用することも可能です。
- Alexa開発の基本
- Self hosted LambdaによるAlexa開発
- DynamoDBのデータスキャン
Alexa開発の基本
会話の流れと基本用語
Step1 呼び出し
Alexaの機能は"スキル"といった単位で定義されます。Alexaにスキルを認識させるには、呼び出しフレーズを伝える必要があります。呼び出しフレーズを参照し、Alexaは適切なスキルを呼び出します。
Step2 スキル内でユーザーからの発話受付
スキルが呼び出されると、基本的にはユーザーからの発話を受け付けます。
サンプル発話
ユーザからの発話を予測してあらかじめいくつかの例を設定しておきます。
基本的にサンプル発話:インテントは多:1の関係性であり、どの発話に引っかけるのかをうまく調整することが重要です。
サンプル発話の設定時には、どの部分が後述のスロットになるのかも併せて設定をしていくことになります。
スロット
ユーザーからの発話に対して、一部をスロットとして変数に格納します。
スロットはカスタムで設定することもできますが、Amazonが既に用意しているはスロットも数多くあり、これらを活用することで素早く設定をすることができます。
例えば地名を使いたい場合にはAMAZON.City
、数値のみを取得したい場合にはAMAZON.NUMBER
を使用することができます。
インテント
実際の処理を担います。Lambdaを使用して、実際に処理したいことを記載していきます。
デフォルトでもAmazonの組み込みのインテントもあり、それらを活用することも可能です。
※今回はイメージとしてインテントを分けていますが、サンプル発話やスロットもインテント設定の一部でもあります
これら上記の3セットは一つのコンポーネントとして定義され、これらを複数準備しておくことであらゆる発話に対して柔軟に処理を行うことができます。GUIから一つずつ設定することも可能であり、JSON形式で一括で定義することも可能です。
Step3 継続した会話
インテントでセッションを有効にすることで会話を継続することができます。また、セッション中にのみ使える変数としてセッションアトリビュートを使うことで、セッションで共通した変数を使用することが可能になります。
開発の進め方
基本的にはAlexaのDeveloperコンソールからスキルを作成していきます。
インテントについてはAWS Lambdaで処理を書いていきます。
Lambdaのプロビジョニングの方法には2種類あり、ユースケースに合わせて選択することができます。
Alexa-hosted | 独自のプロビジョニング | |
---|---|---|
メリット |
|
|
デメリット |
|
|
本アプリでは他AWSソリューションとの連携を行うことから、独自プロビジョニングを行います。
開発
Alexaスキルの新規作成
AlexaのDeveloperコンソールにアクセスをしてスキルを作成していきます。
名前・言語設定
必要な名前と言語を選択します。また、コンソール自体を日本語化したい場合には左下から言語選択も可能です。スキル作成後も変更可能です。
エクスペリエンス、モデル、ホスティングサービス設定
インテントの設定をしていきます。
エクスペリエンスを選択すると、既にAWSによって組み込まれたモデルを選択することが可能ですが、今回はカスタムモデルを使用するので、どれを選択しても問題ないです。
モデルをカスタムとし、ホスティングサービスとして独自のプロビジョニングを設定します。
テンプレート選択
今回はテンプレートを使用しないので、どれを選択しても問題ないです。
審査
設定の問題がないか確認をしてスキルを作成ボタンをクリックします。
数分経つとスキルが作成されます。
Alexaインテントの作成
GUIからでもインテントの設定は可能ですが、今回はJSONで記述をしていきます。
(GUIで設定したものもJSONに反映されます)
以下のJSONを配置します
{
"interactionModel": {
"languageModel": {
"invocationName": "レポートくん",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "NewsTopicIntent",
"slots": [
{
"name": "topic_number",
"type": "AMAZON.NUMBER"
}
],
"samples": [
"{topic_number} 番",
"{topic_number} 番目",
"{topic_number} こ目",
"{topic_number}"
]
}
],
"types": []
}
}
}
インテントの解説
invocationName
invocationName
は呼び出し名になります。スキルを呼び出すときの名前をここで設定可能です。ここの値を修正すると、スキル構築時に指定したスキル名も変化します
追加したインテント
追加したインテントはNewsTopicIntent
です。その他はAlexaにデフォルトで備わているものです。
スロットとしてAMAZON.NUMBER
を受け付けています。AMAZON.NUMBER
はAlexaに備わるマネージドのスロットです。発話の中の数字を含む項目に対して作動し、数値のみを取得してインテントに渡すことができます。
今回はユーザーから「○○番目!」や「○○こ目!」という返事を想定しているので、サンプルにそれらの発話を登録をしています。
これらの設定により、ユーザーからの「○○番目!」という発話に対してNewsTopicIntent
を起動することができ、数値の部分のみをパラメータとしてインテントに渡すことが可能になります。
AWS Lambdaの構築
関数の作成
通常のLambdaと同様に関数を作成していきます。
SDKの都合上ランタイムはPythonとNode.jsから選択していきます。本記事ではPython3.10を使用しています。
Lambdaレイヤー
- ask_sdk_core
- Alexa SDK for Python
- urllib3<2
- botocoreがurllib3 v2.0以降に対応していないためバージョンを抑えたurllib3の導入が必要
- 参考
Lambdaコードのデプロイ
それぞれ機能によってファイルを分離して、以下の3ファイルを使用しました。
lambda_function.py (インテントの設定)
# myfile
import dynamo
import speech_text_util
# library
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.utils import get_slot_value, is_intent_name, is_request_type
from ask_sdk_model import Response
from ask_sdk_model.ui import SimpleCard
from ask_sdk_model.dialog import ElicitSlotDirective
from ask_sdk_model import (Intent , IntentConfirmationStatus, Slot, SlotConfirmationStatus)
sb = SkillBuilder()
@sb.request_handler(can_handle_func=is_request_type("LaunchRequest"))
def launch_request_handler(handler_input):
# type: (HandlerInput) -> Response
speech_text = "本日の最新ニュースをお知らせします。"
topics = dynamo.get_topics()
count = len(topics)
if count == 0:
speech_text = "本日のトピックはありませんでした。"
else:
# セッションスコープとして値を格納
attr = handler_input.attributes_manager.session_attributes
attr["topics"] = topics
# 返答の生成
topic_titles = speech_text_util.get_topic_titles(topics)
speech_text = speech_text + "本日は" + str(count) + "件のニュースがあります。\n" + topic_titles + "何番目のトピックを知りたいですか。"
handler_input.response_builder.speak(speech_text).set_card(
SimpleCard("AWS News", speech_text)).set_should_end_session(
False)
return handler_input.response_builder.response
@sb.request_handler(can_handle_func=is_intent_name("NewsTopicIntent"))
def news_topic_intent_handler(handler_input):
# ユーザーから得た指定番号を取得
topic_number = int(get_slot_value(handler_input=handler_input, slot_name="topic_number"))
# セッションスコープにユーザーからの指定番号を格納
attr = handler_input.attributes_manager.session_attributes
topics_count = len(attr["topics"])
if topic_number <= topics_count:
attr["topic_number"] = topic_number
# 指定番号の"description"を取得
description = speech_text_util.get_description(topic_number, attr["topics"])
# 返答の生成
speech_text = str(topic_number) + "番ですね。" + description
end_session = True
handler_input.response_builder.speak(speech_text).add_directive(directive)
else:
speech_text = str(topic_number) + "番は範囲外です。" + str(topics_count) + "以下の番号で指定してください。"
handler_input.response_builder.speak(speech_text)
return handler_input.response_builder.response
@sb.request_handler(can_handle_func=is_intent_name("AMAZON.HelpIntent"))
def help_intent_handler(handler_input):
# type: (HandlerInput) -> Response
speech_text = "こんにちは。と言ってみてください。"
handler_input.response_builder.speak(speech_text).ask(speech_text).set_card(
SimpleCard("Fortune Telling", speech_text))
return handler_input.response_builder.response
@sb.request_handler(
can_handle_func=lambda handler_input:
is_intent_name("AMAZON.CancelIntent")(handler_input) or
is_intent_name("AMAZON.StopIntent")(handler_input))
def cancel_and_stop_intent_handler(handler_input):
# type: (HandlerInput) -> Response
speech_text = "またいつでも聞いてください"
end_session = True
handler_input.response_builder.speak(speech_text).set_card(
SimpleCard("レポートくん", speech_text)).set_should_end_session(end_session)
return handler_input.response_builder.response
@sb.request_handler(can_handle_func=is_request_type("SessionEndedRequest"))
def session_ended_request_handler(handler_input):
# type: (HandlerInput) -> Response
return handler_input.response_builder.response
@sb.exception_handler(can_handle_func=lambda i, e: True)
def all_exception_handler(handler_input, exception):
# type: (HandlerInput, Exception) -> Response
print(exception)
speech = "すみません、わかりませんでした。もう一度言ってください。"
handler_input.response_builder.speak(speech).ask(speech)
return handler_input.response_builder.response
lambda_handler = sb.lambda_handler()
dynamo.py (DynamoDB関連の処理)
import boto3
import json
import datetime
from dateutil import tz
# date
timezone = tz.gettz('Asia/Tokyo')
today = datetime.datetime.now(timezone).replace(hour=0,minute=0,second=0,microsecond=0)
yesterday = today + datetime.timedelta(days=-1)
# DynamoDb
dynamodb = boto3.client('dynamodb')
TABLE_NAME = 'rss_feeds'
COLUMS = ["link", "ja_title", "ja_description"]
def get_topics():
print("Get topics from : " + yesterday.isoformat())
options = {
'TableName': TABLE_NAME,
'ScanFilter': {
'published':{
'AttributeValueList': [
{'S': yesterday.isoformat()}
],
'ComparisonOperator': 'GE'
}
}
}
# スキャン
response = dynamodb.scan(**options)
items = response['Items']
print("Items Count : " + str(len(items)))
topics = []
for item in items:
topic = {}
for column in COLUMS:
topic[column] = item[column]["S"]
topics.append(topic)
return topics
speech_text_util.py (Alexaからの返答作成関連)
# トピック一覧からタイトルを取得
def get_topic_titles(topics):
text = ""
for i, topic in enumerate(topics):
text = text + str(i+1) + ". " + topic["ja_title"] + "。\n"
return text
# トピック番号から要約を取得
def get_description(number, topics):
index = number - 1
description = topics[index]['ja_description']
return description
コード解説
実装スタイルについて
インテントの処理を記述する方法は2通りあります。本記事ではデコーダータイプを使用して記述をしています。
LaunchRequest
LaunchRequestインテントはスキルが呼び出された時に最初に必ず呼び出されるインテントです。
記事冒頭のAlexaやり取りイメージにある通り、このLaunchRequestが呼び出された時に、最新の記事のタイトルを返答するように設定します。したがって、ここの記述でDynamoDBからデータを取得します。
今回対象となるテーブルrss_feeds
のパーティションキーをlink
にしているため、dynamoDBのスキャンを使用して日本時間の前日00:00以降のデータを取得しています。
※スキャンの場合、全件操作になるのでクエリに比べるとキャパシティを多く消費します。そのため、TTLの設定によって古い情報を削除することでキャパシティ消費を抑えた設計にしています。
https://bbh.bz/2019/07/21/dynamodb-scanapi-queryapi/
また、1回のスキル呼び出しで1回のスキャンに抑えるため、セッションアトリビュートを活用しています。これを使用することでセッション中(スキルが開始から終了するまで)に使用可能な変数となり、別のインテントでも変数を使用することができるようになります。
# セッションアトリビュートして値を格納
attr = handler_input.attributes_manager.session_attributes
attr["topics"] = topics
NewsTopicIntent
NewsTopicIntentではユーザーの発話から取得した数値を取得し、その数値と一致するトピックについてRSSから取得した要約文を返します。トピックはセッションアトリビュートに格納された値を参照し、再度DynamoDBをスキャンすることはありません。
タイムアウト設定
- Lambdaのデフォルトは3秒であり、今回の処理は3秒を上回る可能性があるため1分程度にする。
ロール設定
以下のポリシーを設定する。
- Lambda作成時にデフォルトで作成されるポリシー
- DynamoDBスキャン用ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "dynamodb:Scan",
"Resource": "arn:aws:dynamodb:<YOUR REGION>:<ACCOUNT ID>:table/rss_feeds"
}
]
}
Alexa - Self hosted Lambdaの接続設定
Alexa側の設定
スキルIDはAlexaスキルの識別子です。こちらはLambda側の設定で使うので、値を控えておきます。
デフォルトの地域には作成したLambdaのARNを入力します。ARNはLambdaのAWSコンソールから確認可能です。
Lambda側の設定
動作確認
コンソール上でのテスト
コンソールのテストタブからテストをすることが可能です。
音声でもテキストでも可能です。
その時に表示されるInput JSONはLambdaに送信されるJSONになるため、これを参考にLambda側でテストを行うことでスムーズな開発を行う都ができます。
限定公開で実際のAlexaから呼び出す
実際のアレクサで動作させるには、公開をする必要があります。
全ユーザーへの公開をする場合には、Amazonによる審査が必要になりますが、ベータテストしての限定公開であれば審査がなく実機でのテストをすることができます。
コンソールから公開を開き、必須項目を全て埋めます。画像も必要になりますが、コンソールから作成することもできます。
公開範囲の項目ではベータテストを選択します。ベータテストでは最大500人まで招待をすることが可能です。
以下のフォームに招待するユーザーのEmailを入力します。
なおベータテストは90日限定になるので、お気を付けください。
おわりに
以上で設定は終了です。
今回はAWSの最新ニュースを知らせてくれるスキルを開発しましたが、ご紹介したの機能を活用することで様々な用途のスキルを開発できるかと思います。ぜひご活用ください。