ASK(Alexa Skills Kit)とAWSでアレクサのスキルを作ってみた


初めに

初めまして。SITCのLです。
この度SITCの卒業制作にて、アレクサを利用したスキル作成をチームで行いました。
チーム全員が他業種からの未経験入社でIT歴3か月目ですので、
その点を考慮して温かい目でご覧いただければと思います。



作成スキル紹介

SITCでは、毎朝「朝会」を実施しています。「朝会」では、決めた話題について話し合うことで、
コミュニケーション向上を図っています。話題は運営の方が毎日提案してくれています。
私たちはそのサポートするために、アレクサに話題を提案してもらうスキルを作成しました。
それでは、まずスキルの全体像をご紹介します。
 <構成図:アウトプット>
今回のスキル作成には、AWSサービスのLambdaとDynamoDBを、
アレクサのスキル作成のためのASK(Alexa Skills Kit)を採用しました。
ユーザーがアレクサに話題を求めたことを起点にLambdaが動作し、
DynamoDBに格納された話題データを取り出します。
取り出した話題をアレクサが教えてくれるという仕組みです。
内部の動きについて詳しく見ると、以下のようになります。

 <シーケンス図>
ユーザーの起動指示でアレクサが動き、それをトリガーにLambdaが起動します。
Lambdaは、それぞれのトリガーに反応し、適切な動きをします。
アレクサの動きも全てLambda関数の中でプログラミングしています。
AWS Lambdaがサポートする言語は多様(Node.js、Python、Ruby、Java、Go、.NET)ですが、
今回私たちはPythonを使用しました。
Pythonで作ったLambda関数にて、以下のようにアレクサのセリフも調整しました。

 <フロー図>
アレクサにスキルの起動を呼びかけると、ユーザーからの返事を待機するようになります。
その後に決めておいた言葉を認識させ、処理させます。
Lambda関数でこれらの処理をする前に、アレクサの設定について説明します。


Alexa Skills Kitの用語

「アレクサ、あさかい開いて」
この言葉でアレクサは、 スキル ’あさかい’を開きます。
’アレクサ’とは、ウェイクワードで、文字通りアレクサを起動するキーワードです。
続いて呼び出し名の’あさかい’と起動フレーズの’開いて’で指定したあさかいスキルを起動させます。

サンプル発話とは、ユーザーが話す単語やフレーズを意味しています。
あらかじめユーザーが話すであろう言葉を予想してアレクサに認識させておきます。
そうすることで、アレクサがユーザーの言葉を理解し、会話を続けることが出来るようになります。

アレクサは、ユーザーの言葉を登録したサンプル発話で理解します。それをインテントと言います。今回は「WadaiIntent」 を作成し、考えられるサンプル発話を登録しました。

<ASK設定画面:カスタムインテント1>
このように、インテントとはユーザーからアレクサに送られる処理のリクエストであり、サンプル発話とはインテントにマッピングされたユーザーが話す言葉です。 それでは、この{Theme}とはなんでしょうか。これはスロットといいます。スロットとは、単語の集まりだと理解してください。
<ASK設定画面:スロット>
私たちは、{Theme}スロットを作り、その中にファジェ(韓国語で”話題”です)、話題、トピック、テーマと入れました。このように、ユーザーが「話題」をどのような単語で発話するか予想し、事前に登録します。 インテントで定義したサンプル発話と組み合わせ、アレクサは次の言葉からWadaiIntentのリクエストを受けることができます。

例えば「話題出して・今日のトピックについて・ファジェください・テーマお願い」等になります。
スロットを使用しなかった場合、それぞれのサンプル発話を作らなければならない為、非効率的であり、人為的ミスで抜けてしまうこともあり得るでしょう。

最後で、アレクサの動きについて説明します。
アレクサはリクエストをベースに動きます。
その動き方は大きく3種類あり、以下のようになっています。
アレクサの動きの説明、リクエスト
<出典:Alexa公式動画シリーズ「Alexa道場」S2-EP8、https://youtu.be/hdctJafMlZw
LaunchRequestは、スキルの呼び出し名を受けただけでサンプル発話がない場合のセッションです。
概念図での「アレクサ、あさかい開いて」というユーザーのセリフを受け取った場合に該当します。

IntentRequestは、スキル名に加えてサンプル発話にマッチするインテントがあった場合のセッションです。
「アレクサ、あさかい開いてテーマ出して」と話しかければ、 アレクサはスキル「あさかい」を開き、
スロットやサンプル発話からユーザーが話題を求めていると認識しWadaiIntentだと判断します。

SessionEndRequestは、アレクサの終了を意味します。
理由は、ユーザーの応答がない場合やコード内エラーが発生した場合などがあります。


Lambda関数

まずは全体的なアレクサの動きとデータを取り出すLambda関数の一部をお見せします。Python3.6を使用しました。
公式のサンプルコードから改変しています。アレクサの公式のサンプルコードはこちらからとることができます。
アレクサのそれぞれのリクエストでの動きをlambda_handlerで定義します。

import boto3
#boto3をインポートすることでAWSのリソースを使えるようになります。
import random

def lambda_handler(event, context):

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']}, event['session'])
        
    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
#リクエストベースで動きを定義
"""中略"""
def on_session_started(session_started_request, session):
    print("requestId=" + session_started_request['requestId'] + ", sessionId=" + session['sessionId'])
"""後略"""
次に、IntentRequestセッションの内部の動きを定義します。
"""前略"""
def on_intent(intent_request, session):
     intent = intent_request['intent']
     intent_name = intent_request['intent']['name']

    if intent_name == "WadaiIntent":                            
        return wadai_in_session(intent, session)
    elif intent_name == "AMAZON.FallbackIntent":                
        return fallback_from_session(intent, session)
    elif intent_name == "huraretaIntent":                       
        return denied_by_user_session(intent,session)
    elif intent_name == "AMAZON.YesIntent" or intent_name == "AMAZON.HelpIntent" or intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")
"""後略"""
インテントをコードに書き込むためにはASKコンソールでも設定しておく必要があります。
<ASK設置画面:カスタムインテント2>
アレクサで用意されているインテントをビルドインインテント
開発者が作成したインテントをカスタムインテントと言います。

最初の話題の問い合わせに答えるためのWadaiIntentと、ユーザーが提示された話題を気に入らなかった時に備えhuraretaIntentをカスタムインテントとして作りました。

DynamoDBからのデータの取り出しは以下のように作りました。

def get_theme_from_ddb():
    dynamodb = boto3.resource('dynamodb')
#リソースとしてDynamoDBをboto3から呼び出す
    table = dynamodb.Table('sequence')
    n = total_number_of_wadais()        
    table = dynamodb.Table('Theme')
    ran = range(1, n, 1)
    response = table.get_item(
        Key={
            'seq': random.choice(ran)
        }
    )
    item = response['Item']['wadai']
    return item
ランダムを回すにあたって、話題データの総数を数える関数をsequenceテーブルで行い、実際のデータのピックアップはThemeテーブルから行います。 戻り値を設定するためのKeyはプライマリキーである必要があります。Themeテーブルではseqをプライマリキーとして使いましたので、それを入れました。

総数の取り出しの関数は以下のように定義しました。

def total_number_of_wadais():
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('sequence')
    response = table.get_item(
        Key={
            'tablename': 'Theme'
        }
    )
    Total_number_of_Table_Theme = response['Item']
    return(int(Total_number_of_Table_Theme.get('seq')))
こちらの戻り値ですが、Total_number_of_Table_Theme = response([‘Item’][‘seq’])にしたら
フォーマットがDecimalでSyntax Errorがでたので、強制的にInt化しました。
最後にIntentRequestセッションでの動きの中でWadaiIntentでの動きを紹介します。

def wadai_in_session(intent, session): #WadaiIntentの時
    card_title = intent['name']
    session_attributes = {}
    should_end_session = False
    t_or_f = intent['slots']['Theme']['resolutions']['resolutionsPerAuthority'][0]['status']['code']
    spoken_speech = intent['slots']['Theme']['value']
    #聞かれた単語で返事する
    #’テーマ出して’からはテーマで返事するなど    
    if t_or_f == 'ER_SUCCESS_MATCH':
        got_theme = get_theme_from_ddb()
        speech_output = "本日の" + \
                        spoken_speech + \
                        "は " + \
                        got_theme + \
                        "です。これにしますか? "
        reprompt_text = speech_output#8秒間待ってもユーザーからの答えがない時
        session_attributes =  {"got_theme": got_theme}
    else:
        speech_output = "テーマ、話題、トピックと言ってください。 " 
        reprompt_text = speech_output
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))
t_or_fですが、アレクサはスキルのI/OをJSON形式で受け取るので、
If文を作るためのリストはJSONで書かれている値を入れます。
<ASK設定画面:テスト画面の一部>


データのインプット

データのインプットと構成についてお話しします。 S3より話題についてのデータをインプット後、DynamoDBへ格納しました。 データインプットは、研修で学んだS3の静的ページを使ったDynamoDBへのインプットの知識を生かしました。
該当記事(https://lab.m-field.co.jp/2020/01/10/s3webhost-lambda/
<構成図:インプット>
S3から静的ページをホスティングし、そこで話題をインプットするとデータとしてThemeテーブルに入ります。
データが記録されるとその都度カウントされsequenceテーブルの総数が増える、という仕組みです。
DynamoDBに作成した2つのテーブルの詳細は以下のとおりです。

1.Themeテーブル
  実際に話題データを入れています。
NoPK項目名概要データ型
1seq話題データの入力順を示す番号INT
2wadai話題データSTR
2.sequenceテーブル
  データの総数をカウントしています。
NoPK項目名概要データ型
1Tablename話題データが格納されているテーブル名 STR
2seq入力されたデータの総数INT
実際のコードはこちらです。

"""前略"""
#総数を増やす関数定義
#静的ページでインプットすると総数+1
def next_seq(table, tablename):
    response = table.update_item(
        Key={
            'tablename' : tablename
        },
        UpdateExpression="set seq = seq + :val",
        ExpressionAttributeValues= {
            ':val' : 1
        },
        ReturnValues='UPDATED_NEW'
    )
    return response['Attributes']['seq']
"""中略"""
def lambda_handler(event, context):
    param = urllib.parse.parse_qs(event['body'])
    #入力された話題の値をwadaiとして受け取る
    wadai = param['theme'][0]
    seqtable = dynamodb.Table('sequence')
    #総数のアップデート
    nextseq = next_seq(seqtable, 'Theme')
    # 受け取ったデータ値であるwadaiをThemeテーブルに登録
    Topictable = dynamodb.Table("Theme")
    Topictable.put_item(
        Item={
            'seq' : nextseq,
            'wadai' : wadai
        }
    )
"""後略"""
HTMLフォームに入力された内容をAPI Gatewayへホストし、Lambdaを介してDynamoDBへ記録されます。結果の判定には’statusCode’ : 200を使いました。


まとめ

冒頭にも申し上げました通り、経験の浅いメンバーであり、作成前はチーム全員がアレクサという名前やざっくりとこんなことができるもの等と知る程度でした。そのため、この話をいただいた時、皆が口を揃えて「えっ⁉アレクサ??」と完成することが想像できませんでした。しかし、見事完成することができ、大変嬉しく思っています。
2週間の作成過程においては、一つひとつ模索しながら、試行錯誤を繰り返し行うことが欠かせませんでした。しかしこの制作で得た経験は、これからも大事になっていくと感じております。 早い段階で経験することができたことをありがたく存じます。 この制作にあたり関わって下さった皆様へ、心より御礼申し上げます。

投稿者: HYEONCHEOL LEE