Edited at

ポケモン言えるかなを作りながら覚えるAlexaSkill開発入門

More than 1 year has passed since last update.

こちらの資料は

【サポーターズ勉強会】Alexaスキル開発入門

で発表したものです。

qiitaのスライドモードを使ってスライドとしても見れるようになっているのでお好きな方で御覧ください。



はじめに

昨年末からGoogleHomeとAmazonEchoによる猛烈なAIスピーカーの波が押し寄せているなか何か自分でAIスピーカースキルを作ってみたいと思っている方も多いのではないでしょうか?

そこで今回は簡単なAlexaスキルの作成を通じてAlexaスキル開発基礎を身爆速で身に着けていただきたいと思います。



対象者


  • Alexaスキルを開発してみたい方

  • ざっくりAlexaスキルの概要を理解したい方

  • AmazonEchoを持っているプログラマー

(プログラミング初心者でもOK!!)

※ プログラミング初心者向けに説明しているためAWSの操作など冗長なところがあるかもしれません。慣れている方は適宜飛ばしながらやってください。

※ じっくり学びたい方はこちらで学ぶのが非常におすすめです。本投稿もこれを参考にさせていただいています。



作るスキルの概要

今回はポケモンの名前をどんどん言っていくだけという単純なゲームスキルを作成します。

以下のようにユーザーの発話がAlexaを通じてlambdaに渡されて、lambdaが処理した結果をAlexaに返して正解/不正解をスピーカーが返すという単純なものです。

単純ですがこれを通じてAlexaSkill開発の基本的な開発力を身につけられる題材となっています。



目次


  • 下準備

  • スキル作成

  • ポケモンいえるかなを実装



下準備



アカウント準備

今回のalexaスキル開発にはawsとamazon developerアカウントが必要になります。

以下より登録をお済ませください。

AWS

Amazon Developer

また、日本語スキルを実機でテストをする場合はAmazonDeveloper周りで1つ罠があるので最下部の実機でテストするためのアカウントについてのところをご覧ください。



IAMロールの作成


IAMとは?


  • IAM(AWS Identity and Access Management)とはAWS内のサービスへの権限制御をするための機構です。

今回はlambdaとdynamodbを使うのでそれらを実行できるロールを事前に作っておきます。


1.AWSトップページからIAMを選択します。


2.左メニューのロールを選択、ロールの作成をクリックします。


3.Lambdaを選択して次のステップをクリックします。


4.AWSLambdaFullAccessとAmazonDynamoDBFullAccessを選択して次のステップをクリックします。



  1. ロール名にlambda_and_dynamodb_full_accessを入力し、選択した権限が付与されていることを確認してロールの作成をクリック




スキルの作成


1.AmazonDeveloperにアクセスして、あなたのAlexaダッシュボードをクリック


2.ログインができたらAlexaをクリック


3.Alexaタブを選択してAlexaSkillsKitの始めるをクリック



  1. 右上の新しいスキルを作成するをクリック



5.以下にあるように必要な入力項目を入力し保存をクリック


  • スキルの種類: カスタム対話モデル

  • 言語: Japanese

  • スキル名: ポケモンいえるかな

  • 呼び出し名: ポケモンいえるかな


6.保存出来たら左上に表示されるアプリケーションIDをあとで使うのでコピーしておきます


7.次へをクリック


8.対話モデルについてはたくさん入力フォームがありますが以下を入力してください。

インテントスキーマ

{

"intents": [
{
"intent": "AMAZON.HelpIntent"
},
{
"slots": [
{
"name": "Pokemon",
"type": "ListOfPokemon"
}
],
"intent": "PokemonIntent"
}
]
}

カスタムスロットタイプ-タイプを入力

ListOfPokemon

カスタムスロットタイプ-値を入力して追加をクリック

ピカチュウ

カイリュー
ヤドラン
ピジョン
コダック

サンプル発話

PokemonIntent {Pokemon}

PokemonIntent {Pokemon} はポケモン
PokemonIntent {Pokemon} イズポケモン

と入力して次へをクリックします。



Alexaスキルの仕組み

さてここで「何入力してるんや?」という感じになったと思うので少し解説を入れます。

Alexaはユーザーの入力をインテントとスロットで受け取ります。



インテントとは?

例えば

「眠たい」「布団に入りたい」「横になりたい」と言った言葉は全て眠りたいという意思(intent)であり、Alexaは聞き取った言葉がどの意思(intent)になるかを判別して処理を分岐させます。

よく使われる一般的なintentはAMAZONがBuilt-in intentとして定義してくれているのでそれを使う事ができます。(詳しくはこちら)

インテントスキーマで入力したAMAZON.HelpIntentはその一例で使い方を尋ねるインテントです。



スロットとは?

スロットとはユーザ−の発話内容から動的に変わるものを変数として抽出出来る機能です。

上記の例でいれば眠りたい場所というのは動的に色々変わり得ます。Alexaは発話された内容からインテントを判定した上でその中からスロットを抽出してサーバーに渡します。

インテントと同様よく使うスロットはBuilt-inが用意されています。AMAZON.DATEやAMAZON.CITYなどがあります。

詳細はこちらを御覧ください



スロットについて注意

ここで重要なことはスロットに設定されていないものは取れないということです。例えば「お風呂で眠りたい」と言ったところでスロットのリストの中に「お風呂」が設定されていなければお風呂という値は入ってきません。

つまり好きな食べ物はなんですか?と聞いて答え取るというようなユーザーが何を発話するかわからない自由発話を取るのは難しいのです。

実はAMAZON.LITERALというスロットで取得することが出来るのですが。現状日本語では利用することができません。英語圏でも一回deprecateになってまた復活したようなので、日本でもそのうち使えるようになるかとは思いますが。



カスタムインテント、スロットの定義の方法

では実際どのようにカスタムインテントスロットを定義すれば良いのでしょうか?

今回のポケモンの例ではまずインテントスキーマに以下を追加しています。

これによってPokemonIntentとその中でPokemonという名前でListOfPokemonというスロットを使いますよ−と宣言しています。

{

"slots": [
{
"name": "Pokemon",
"type": "ListOfPokemon"
}
],
"intent": "PokemonIntent"
}

続いてこのListOfPokemonに何が入るのかということをカスタムスロットタイプで設定しています。今回であればポケモン5種類です。

さらにサンプル発話のところでどういう発話をPokemonIntentとするかを設定します。この{Pokemon}にListOfPokemonの中からどのPokemonが発話されたかがスロット値として渡されます。

一番上がややわかりづらいですがポケモンの名前のみの場合はスロットであると同時にそれをPokemonIntentとして処理しています。

PokemonIntent {Pokemon}

PokemonIntent {Pokemon} はポケモン
PokemonIntent {Pokemon} イズポケモン



lambda関数の作成


ここまででalexa skillの方は一旦置いておいてawsでlambda関数を作成していきます。

1.AWSのトップページからlambdaを選択


2.関数の作成を選択


2.「設計図」を選択しalexaで検索して、「alexa-skill-kit-sdk-factskill」を選択して設定をクリック


3.名前に「can_you_say_pokemon_all_lambda」、既存のロールを先程作成した「lambda_and_dynamodb_full_access」ロールを選択して関数の作成をクリック


4.左のトリガーを追加の中からAlexaSkillsKitを追加


5.画面下部に移動し先程取得したアプリケーションIDを入力して追加をクリック


6.画面上部に戻り保存をクリック


7.画面最上部にあるARNをコピーします


8.alexaのスキル画面に戻って「AWS Lambda の ARN (Amazonリソースネーム)」を選択しコピーしたARNを貼り付けて次へをクリック

長かったですがこれでやっとalexa skillを開発する準備が整いました!!



ポケモンいえるかなを実装する



とりあえず動かしてみる

コードを書く前にとりあえず動かしてみます。

lambdaの画面に戻ってDesigner内のlambdaを選択するとコードエディタが出てくるので以下のコードをコピペして保存します。

"use strict";

const Alexa = require('alexa-sdk');

exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.registerHandlers(handlers); // handlerを登録
alexa.execute();
};

// ここに各intentに対する処理を書いていく
var handlers = {
'LaunchRequest': function () { // 起動時
var message = 'ポケモン言えるかなへようこそ。ポケモンを言ってください。'
this.emit(':ask', message);
},
'AMAZON.HelpIntent': function () { // 使い方を聞かれたとき
var message = 'ポケモンを言っていくゲームです。'
this.emit(':ask', message);
},
   'Unhandled': function() {
this.emit('LaunchRequest');
}
};



動作確認

保存できたらalexaの画面に戻ってテストタブを選択し、サービスシュミレーターに「ポケモンいえるかなを開いて」と入力して、呼び出しを押してみましょう。lambdaからレスポンスが返ってくることが確認出来ると思います。


また、「使い方を教えて」と入力すると使い方の説明が返答されてHelpIntentとして処理されていることがわかります。

また実行時にエラーになってしまった場合は補足のデバッグを参照してください。



実装のポイント

ここでの処理のポイントはhandlersです。

handlers内で各intentが来た時にどういう処理をするかを書き

// ここに各intentに対する処理を書いていく

var handlers = {
'LaunchRequest': function () { // 起動時
var message = 'ポケモン言えるかなへようこそ。ポケモンを言ってください。'
this.emit(':tell', message);
},
'AMAZON.HelpIntent': function () { // 使い方を聞かれたとき
var message = 'ポケモンを言っていくゲームです。'
this.emit(':ask', message);
},
};

そのhandlerをalexaに登録しています。

こうすることで来たインテントに応じてどういう処理をするかを振り分ける事ができます。

alexa.registerHandlers(handlers); // handlerを登録

また、メッセージを返答する際には


  • tellの場合はセッションを継続しない(返答した後alexaの電気が消える)

  • askの場合はセッションを継続します(次の返答を待ってalexaの電気は点いたまま)

となります。

this.emit(':tell', message) // 返答してsession終了

this.emit(':ask', message) // 返答してsession継続



ポケモンいえるかなを追加する


簡単な動作確認が出来たところでポケモン言えるかな機能を追加しましょう!!

以下のコードをコピペしてlambaに貼り付けて保存します。

"use strict";

const Alexa = require('alexa-sdk');

// ステートの定義
const states = {
PLAYING_MODE: '_PLAYING_MODE'
};

// Pokemons スロットに登録したものと同じものを配列で代入
const pokemons = [
'ピカチュウ',
'カイリュー',
'ヤドラン',
'ピジョン',
'コダック'
]

exports.handler = function(event, context, callback) {
const alexa = Alexa.handler(event, context);
// 既存のハンドラに加えてpokemonHandlers(下で定義を登録)
alexa.registerHandlers(handlers, pokemonHandlers);
alexa.execute();
};
const handlers = {
'LaunchRequest': function () {
this.handler.state = states.PLAYING_MODE; // stateにPLAYING_MODEをセット
const message = 'ポケモン言えるかなへようこそ。ポケモンを言ってください。'
this.emit(':ask', message);
},
'AMAZON.HelpIntent': function () {
this.handler.state = states.PLAYING_MODE; // stateにPLAYING_MODEをセット
this.handler.state = states.PLAYING_MODE;
const message = 'ポケモンを言っていくゲームです。ポケモンを言ってください。'
this.emit(':ask', message);
}
};

// PLAYING_MODEの場合はこのhandlerがintentを処理する
const pokemonHandlers = Alexa.CreateStateHandler(states.PLAYING_MODE, {
'PokemonIntent': function() {
   // 言ったポケモンをスロットから取得する
const pokemon = this.event.request.intent.slots.Pokemon.value;
if (pokemons.indexOf(pokemon) > -1) {
const message = '正解です!!次のポケモンを言ってください'
const reprompt = 'ポケモンを言ってください〜'
this.emit(':ask', message, reprompt); // 正解の場合は継続
} else {
const message = '残念。そんなポケモンはいません。'
this.handler.state = '';
this.emit(':tell', message); // 不正解の場合は終了
}
}
});



動作確認

ポケモンいえるかなを開いた上でピカチュウと言うと正解ですのメッセージが返り、スロットに設定した5種類のポケモン以外の言葉を言うと不正解となればOKです!!

正解

不正解



実装のポイント

ここでのポイントはstate、pokemonHandlers、スロットからのポケモンの取得の3つです。

stateとはその名の通り状態を表す変数で以下のようにstateに値を代入すると会話を跨いで値が保存されます。

今回は起動時にstateをPLAYING_MODEに切り替えています。

this.handler.state = states.PLAYING_MODE;

さらにpokemonHandlersという新しいハンドラーをPLAYING_MODEのとき専用のhandlerとして定義したうえでalexaオブジェクトに登録します。

こうすることでstateがPLAING_MODEの場合はpokemonHandlerがインテントをハンドルします。

// PLAYING_MODEの場合はこのhandlerがintentを処理する

const pokemonHandlers = Alexa.CreateStateHandler(states.PLAYING_MODE, {...})

// 既存のハンドラに加えてpokemonHandlers(下で定義を登録)

alexa.registerHandlers(handlers, pokemonHandlers);

また、pokemonHandlerの中では渡されたポケモンをスロットから取得しています。

const pokemon = this.event.request.intent.slots.Pokemon.value;



今何匹言ったかをカウントする(SessionAttributeの保存)


今の状態ではただ言ったポケモンがいるかいないかを返すだけで、ゲームが終わりません。

そこで言ったポケモンを保存しておいて後何匹かを返答する機能を追加します。

以下のコードをlambdaにコピーし保存してください。

"use strict";

const Alexa = require('alexa-sdk');

// Pokemons
const pokemons = [
'ピカチュウ',
'カイリュー',
'ヤドラン',
'ピジョン',
'コダック'
]

// ステートの定義
const states = {
PLAYING_MODE: '_PLAYING_MODE'
};

exports.handler = function(event, context, callback) {
const alexa = Alexa.handler(event, context);
// alexa.appId = process.env.APP_ID;
alexa.registerHandlers(handlers, pokemonHandlers); // 既存のハンドラに加えてpokemonHandlers(下で定義を登録)
alexa.execute();
};
const handlers = {
'LaunchRequest': function () {
this.emit('AMAZON.HelpIntent');
},
'AMAZON.HelpIntent': function () {
this.handler.state = states.PLAYING_MODE; // stateにPLAYING_MODEをセット
this.attributes['said_pokemons'] = [];
const message = 'ポケモンを言っていくゲームです。ポケモンを言ってください。'
this.emit(':ask', message);
},
'Unhandled': function() {
this.emit('LaunchRequest');
}
};

// PLAYING_MODEの場合はこのhandlerがintentを処理する
const pokemonHandlers = Alexa.CreateStateHandler(states.PLAYING_MODE, {
'PokemonIntent': function() {
const pokemon = this.event.request.intent.slots.Pokemon.value;
if (pokemons.indexOf(pokemon) > -1) {

// 言ったポケモンをsession attributeに保存する
this.attributes['said_pokemons'].push(pokemon);
const notSaidPokemons = getArrayDiff(pokemons, this.attributes['said_pokemons'])
console.log(notSaidPokemons);
// まだ残っている場合はあと何匹か言う
if(notSaidPokemons.length > 0) {
const message = '正解です!!あと' + notSaidPokemons.length.toString() + '匹です。次のポケモンを言ってください'
const reprompt = 'ポケモンを言ってください〜'
this.emit(':ask', message, reprompt); // しばらく何も回答しなかった場合にrepromptが呼ばれる
} else {
// 全て終わった場合は終了する
const message = 'おめでとうございます全てのポケモンが言えました!!'
this.emit(':tell', message);
}

} else {
const message = '残念。そんなポケモンはいません。'
this.handler.state = '';
this.emit(':tell', message);
}
},
'Unhandled': function() {
const message = '残念。そんなポケモンはいません。'
this.handler.state = '';
this.emit(':tell', message);
}
});

function getArrayDiff(arr1, arr2) {
let arr = arr1.concat(arr2);
return arr.filter((v, i)=> {
return !(arr1.indexOf(v) !== -1 && arr2.indexOf(v) !== -1);
});
}



動作確認

ではまた動作を確認してみましょう!!!

ポケモンを言えば言うほど残りが減っていき全て言うとおめでとう!!となることが確認出来るかと思います。

会話をまたいで言ったポケモンが保存されていっているのがわかります。



実装のポイント

会話を跨いでデータを保存したい場合session attributesという機能を使います。

使い方は簡単で

this.attributes['awsome_key'] = 'awsome_value'

のようにthis.attributesに保存した値はsession attributeとして会話をまたいで保存されます。

ここでは発話したポケモンが正解の場合said_pokemonsというkeyでsession attributesに値を保存しています。

if (pokemons.indexOf(pokemon) > -1) {

    // 言ったポケモンをsession attributeに保存する
this.attributes['said_pokemons'].push(pokemon);
...
}



最高記録を保存する(DynamoDBを使ったデータの永続化)


さてここまでで基本的な機能は実装できました。最後に最高記録を保存できる機能を実装してみましょう。

最高記録を保存するためにはsessionが終わってもデータを残しておく必要があるためデータの永続化が必要です。

といっても実はこれはとても簡単に出来てしまいます。

例のごとくこちらのコードをコピペして貼り付けて保存します。

"use strict";

const Alexa = require('alexa-sdk');

// ステートの定義
const states = {
PLAYING_MODE: '_PLAYING_MODE'
};

// Pokemons
const pokemons = [
'ピカチュウ',
'カイリュー',
'ヤドラン',
'ピジョン',
'コダック'
]

exports.handler = function(event, context, callback) {
const alexa = Alexa.handler(event, context);
// POINT1: dynamoDBTableをセット
alexa.dynamoDBTableName = 'PokemonSkillTable';
// alexa.appId = process.env.APP_ID;
alexa.registerHandlers(handlers, pokemonHandlers); // 既存のハンドラに加えてステートハンドラ(後半で定義)も登録
alexa.execute();
};

const handlers = {
'LaunchRequest': function () {
this.emit('AMAZON.HelpIntent');
},
'AMAZON.HelpIntent': function () {
this.handler.state = states.PLAYING_MODE;
// reset
this.attributes['said_pokemons'] = [];
this.attributes['score'] = 0;
let message = 'ポケモン言えるかなへようこそ。ポケモンを言ってください。'
const bestScore = this.attributes['bestScore']
if(bestScore){
message = message + 'これまでのベストスコアは' + bestScore.toString() + 'です。';
} else {
this.attributes['bestScore'] = 0;
}
this.emit(':ask', message);
},
'SessionEndedRequest': function () {
this.emit(':saveState', true);
},
'Unhandled': function() {
this.emit('LaunchRequest');
}
};
// ステートハンドラの定義
const pokemonHandlers = Alexa.CreateStateHandler(states.PLAYING_MODE, {
'PokemonIntent': function() {
const pokemon = this.event.request.intent.slots.Pokemon.value;
if (pokemons.indexOf(pokemon) > -1) {

// 言ったポケモンをsession attributeに保存する
this.attributes['said_pokemons'].push(pokemon);
this.attributes['score'] = this.attributes['said_pokemons'].length;
const notSaidPokemons = exports.getArrayDiff(pokemons, this.attributes['said_pokemons'])
console.log(notSaidPokemons);
// まだ残っている場合はあと何匹か言う
if(notSaidPokemons.length > 0) {
const message = '正解です!!あと' + notSaidPokemons.length.toString() + '匹です。次のポケモンを言ってください'
const reprompt = 'ポケモンを言ってください〜'
this.emit(':ask', message, reprompt);
} else {
// 全て終わった場合は終了する
const message = 'おめでとうございます全てのポケモンが言えました!!'
this.emit(':tell', message);
}

} else {
const score = this.attributes['said_pokemons'].length
let message = '残念。そんなポケモンはいません。結果は' + score.toString() + '匹でした。'
if(this.attributes['bestScore'] < score){
this.attributes['bestScore'] = this.attributes['said_pokemons'].length;
message += "ベストスコアを更新しました!"
}

// STATEをリセット
this.attributes['STATE'] = undefined;
this.handler.state = '';
this.emit(':tell', message);
}
},
'SessionEndedRequest': function () {
this.attributes['STATE'] = undefined;
this.handler.state = '';
console.log("session ended in playing")
this.emit(':saveState', true);
},
'Unhandled': function() {
this.emit('PokemonIntent');
}
});

exports.getArrayDiff = function(arr1, arr2) {
let arr = arr1.concat(arr2);
return arr.filter((v, i)=> {
return !(arr1.indexOf(v) !== -1 && arr2.indexOf(v) !== -1);
});
}



動作確認

動作確認を行いましょう。

1度プレイした後に2度目の起動をすると以下のように前回のスコアを保存出来ているのが確認できますね。



実装のポイント

今回のポイントは

alexa.dynamoDBTableName = 'PokemonSkillTable'; 

でなんとここでdynamoDBTableNameにテーブル名を代入しておくだけで初回起動時に自動でテーブルを作った上に、会話session終了時にsession_attributesをmapAttrと言うかラムでdynamoDBに自動で保存してくれるのです!すごい!

またstateは'STATE'というkeyに保存されるので

this.attributes['STATE'] = undefined;

としてstateをリセットしておきます。


dynamoDBはどうなっているかも確認してみます。

1.awsからdynamoDBを選択


2.PokemonSkillTableが作成されているのでクリックします


3.session atttributeが保存出来ているのが確認できますね。


お疲れ様でしたこれで全て終了です。

今回のハンズオンでAlexaSkillの基本がわかったのではないでしょうか?

是非これを機に自分のオリジナルスキルを開発してみてください



補足



実機でテストするためのアカウントについて

日本語で実機テストをしようとするとdevスキル一覧にスキルが表示されないという問題が発生します。

こちらはアカウント登録の問題で解決法としては


  • AmazonDeveloperのメルアドを変更する

  • amazon.comのパスワードを変更する

のいずれかを行う必要があります。

詳しくはこちらをご覧ください

AmazonDeveloperのメルアドを変更する方法

https://developer.amazon.com/ja/blogs/alexa/post/9f852a38-3a44-48bd-b78f-22050269d7c7/hamaridokoro

amazon.comのパスワードを変更する方法

https://dev.classmethod.jp/voice-assistant/solution-of-a-problem-amazon-com-account-conflict/



デバッグ

スキルを作っていてテストをおこなうと以下のようなエラーになることがあります。

これだけ見ても何のエラーなのか全くわからないのでlambda側でデバッグをします。

(本格的な開発ではserverless環境を用意するなどしたほうが良いです)


1.エラーが出たときのサービスリクエストをコピーします


2.lambdaの画面にいってテストイベントの設定を選択します


3.新しいイベントを作成してイベントテンプレートとイベント名は適当に入力し、1でコピーしたサービスリクエストを貼り付けて作成を押します。


4.lambdaの画面に戻り作成したテストを選択してテストを押します


5.エラーのスタックトレースが画面上部に表示されます