2
2

Self hosted AWS LambdaによるAlexaスキル開発

Last updated at Posted at 2023-11-30

はじめに

本記事は

の連載記事です。前段の項目が完了していることを前提として書かれております。
本記事では以下のアーキテクチャの作成を目指して解説をしていきます。
image.png

Alexaとのやりとりのイメージは以下のような形を目指します。
image.png

内容としては以下になりますので、部分的に活用することも可能です。

  • Alexa開発の基本
  • Self hosted LambdaによるAlexa開発
  • DynamoDBのデータスキャン

Alexa開発の基本

会話の流れと基本用語

Step1 呼び出し

Alexaの機能は"スキル"といった単位で定義されます。Alexaにスキルを認識させるには、呼び出しフレーズを伝える必要があります。呼び出しフレーズを参照し、Alexaは適切なスキルを呼び出します。
image.png

Step2 スキル内でユーザーからの発話受付

スキルが呼び出されると、基本的にはユーザーからの発話を受け付けます。
image.png

サンプル発話
ユーザからの発話を予測してあらかじめいくつかの例を設定しておきます。
基本的にサンプル発話:インテントは多:1の関係性であり、どの発話に引っかけるのかをうまく調整することが重要です。
サンプル発話の設定時には、どの部分が後述のスロットになるのかも併せて設定をしていくことになります。

スロット
ユーザーからの発話に対して、一部をスロットとして変数に格納します。
スロットはカスタムで設定することもできますが、Amazonが既に用意しているはスロットも数多くあり、これらを活用することで素早く設定をすることができます。

例えば地名を使いたい場合にはAMAZON.City、数値のみを取得したい場合にはAMAZON.NUMBERを使用することができます。

インテント
実際の処理を担います。Lambdaを使用して、実際に処理したいことを記載していきます。
デフォルトでもAmazonの組み込みのインテントもあり、それらを活用することも可能です。

※今回はイメージとしてインテントを分けていますが、サンプル発話やスロットもインテント設定の一部でもあります

これら上記の3セットは一つのコンポーネントとして定義され、これらを複数準備しておくことであらゆる発話に対して柔軟に処理を行うことができます。GUIから一つずつ設定することも可能であり、JSON形式で一括で定義することも可能です。

Step3 継続した会話

インテントでセッションを有効にすることで会話を継続することができます。また、セッション中にのみ使える変数としてセッションアトリビュートを使うことで、セッションで共通した変数を使用することが可能になります。

開発の進め方

基本的にはAlexaのDeveloperコンソールからスキルを作成していきます。

インテントについてはAWS Lambdaで処理を書いていきます。
Lambdaのプロビジョニングの方法には2種類あり、ユースケースに合わせて選択することができます。

       Alexa-hosted 独自のプロビジョニング
メリット
  • Alexa側でLambdaとDynamoDB1テーブルまで管理をしてくれる。
  • Alexa Developerコンソールからコードを修正することが可能
  • 一つのコードで完結する場合にお手軽
  • 独自のAWSアカウントのLambdaを使用することが可能
  • 自由な設定が可能
  • 他AWSサービスを使用したい場合に便利
デメリット
  • 使用量に制限がある
  • リージョンに制限がある
  • 他のAWSサービスを使用する場合には別途セキュリティやネットワーク設計を行う必要がある
  • AWSアカウントが必要
  • ユーザーがリソースを管理(lambdaであれば基本的にはAWS管理になるので、特に負荷はないです)

本アプリでは他AWSソリューションとの連携を行うことから、独自プロビジョニングを行います。

開発

Alexaスキルの新規作成

AlexaのDeveloperコンソールにアクセスをしてスキルを作成していきます。
image.png

名前・言語設定
必要な名前と言語を選択します。また、コンソール自体を日本語化したい場合には左下から言語選択も可能です。スキル作成後も変更可能です。
image.png

エクスペリエンス、モデル、ホスティングサービス設定
インテントの設定をしていきます。
エクスペリエンスを選択すると、既にAWSによって組み込まれたモデルを選択することが可能ですが、今回はカスタムモデルを使用するので、どれを選択しても問題ないです。
モデルをカスタムとし、ホスティングサービスとして独自のプロビジョニングを設定します。
image.png

テンプレート選択
今回はテンプレートを使用しないので、どれを選択しても問題ないです。
image.png

審査
設定の問題がないか確認をしてスキルを作成ボタンをクリックします。
数分経つとスキルが作成されます。

Alexaインテントの作成

GUIからでもインテントの設定は可能ですが、今回はJSONで記述をしていきます。
(GUIで設定したものもJSONに反映されます)

ナビゲーションバーからJSONエディターを開きます。
image.png

以下のJSONを配置します

Intent 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 (インテントの設定)
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関連の処理)
dynamo.py
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からの返答作成関連)
speech_text_util.py
# トピック一覧からタイトルを取得
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スキャン用ポリシー
dynamodb-scan
{
    "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側の設定

コンソールのトップから4.エンドポイントを選択します。
image.png

スキルIDはAlexaスキルの識別子です。こちらはLambda側の設定で使うので、値を控えておきます。
デフォルトの地域には作成したLambdaのARNを入力します。ARNはLambdaのAWSコンソールから確認可能です。
image.png

Lambda側の設定

トリガ設定
トリガ設定からAlexaを選択します。
image.png

AlexaのスキルIDを入力して保存します。
image.png

動作確認

コンソール上でのテスト

コンソールのテストタブからテストをすることが可能です。
音声でもテキストでも可能です。
その時に表示されるInput JSONはLambdaに送信されるJSONになるため、これを参考にLambda側でテストを行うことでスムーズな開発を行う都ができます。
image.png

限定公開で実際のAlexaから呼び出す

実際のアレクサで動作させるには、公開をする必要があります。
全ユーザーへの公開をする場合には、Amazonによる審査が必要になりますが、ベータテストしての限定公開であれば審査がなく実機でのテストをすることができます。

コンソールから公開を開き、必須項目を全て埋めます。画像も必要になりますが、コンソールから作成することもできます。
image.png

公開範囲の項目ではベータテストを選択します。ベータテストでは最大500人まで招待をすることが可能です。
以下のフォームに招待するユーザーのEmailを入力します。
image.png

なおベータテストは90日限定になるので、お気を付けください。

おわりに

以上で設定は終了です。
今回はAWSの最新ニュースを知らせてくれるスキルを開発しましたが、ご紹介したの機能を活用することで様々な用途のスキルを開発できるかと思います。ぜひご活用ください。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2