Alexaスキルの開発 0から公開まで

初めて **Alexaスキル** の開発を始めてから公開するまでの流れについて。

# フローチャートを作成する
事前に Alexaにどう発話したらどう分岐するかをまとめたフローチャートを作っておくとフローが整理できて開発が捗るので以下のような感じで作成します。(黒塗り多くてすみません)

![alexa-flowchart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/eb926020-0439-8afd-7548-05d19c892f89.png)


# alexa developer console への登録

[amazon alexa](https://developer.amazon.com/ja-JP/alexa)にアクセスし、アカウントを作成してログインします。
「スキルの作成」ボタンを押し、好きなスキル名を入力して作成。

スキルを作成した後、ビルドタブの左メニューから「エンドポイント」を選択し、表示される **スキルID** を覚えておきましょう。
![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/86e393c8-0799-0ba7-dd13-9cd10641d090.png)

# AWS Lambdaの利用
### 作成
AWSにログインしてLambdaにアクセスし、**関数の作成**ボタンを押します。
関数名を入力し、ランタイムは今回は「Node.js」を選択し、作成。
(自分が開発していた当時は東京リージョンだと必要な機能が揃っておらず、オレゴンリージョンを利用しました)

### Alexa Skill Kit の追加
次にLambdaの作成した関数の画面にて、「トリガーを追加」で「Alexa Skill Kit」を選択し、スキルIDに先程 alexa deevloper console でコピーした**スキルID**を貼り付けて追加します。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/1c7d1c17-72d1-086c-4b96-a61d82e9b8e2.png)


### ARNをメモ
画面右上の **ARN** 値を覚えておきます。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/fdffcb7d-4cdf-13eb-822f-a23a9a8bcba0.png)

# 再び alexa developer consoleに戻ってスキル設定
### alexa developer console と Lambdaの紐付け
alexa developer console にて、スキルを選択したあとのビルドタブの左メニューから「エンドポイント」を選択し、
「Aws LambdaのARN」を選択して、「デフォルトの地域」に先程メモした ARN を貼り付けて「エンドポイントを保存」。

### 呼び出し名の設定
ビルドタブの「呼び出し名」をクリックし、なんという呼びかけでスキルが起動するかを設定します。

### インテントの追加
ビルドタブのインテントの追加をクリックし、今回のスキルでAlexaが受け付ける可能性のある全発話をインテントとして作成していきます。例えば、

| インテント名 | 発話例 |
| --- | --- |
| RecommendIntent | 「おすすめを教えて」「おすすめ見せて」「おすすめ開いて」|
| SelectNumberIntent | 「{number}で」「{number}でお願い」「{number}がいい」 |
| SelectCategoryIntent | 「{category}にする」「{category}が見たい」「{category}を見せて」|

といった感じ。編集が終わったらモデルのビルドをすることで反映されます。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/31172a56-ce5c-e330-3b84-99c4dc644d33.png)

* 各インテントにて、できる限り発話の揺らぎを網羅するようにします。足りていないとAmazonからのレビューで指摘されます。
* 上記 `{number}`や`{category}` のように、一部を変数化して、複数の発話を受け付けることも可能。変数のパターンは別途 **スロットタイプ** に登録します。{number}など一部の変数は既に用意されています。
* 肯定/否定の返事をするインテントや「前へ」「次へ」などのインテントは既にビルトインとして用意されているものがあるので、独自に追加せずにそれをビルトインインテントとして追加します。
* CancelIntent (取り消し)、HelpIntent (ヘルプ)、StopIntent (中止)などいくつかのインテントはルール上、必ず実装しないといけない模様です。

# Cloud9と連携する
LambdaとCloud9を連携させることで、Cloud9の高度なIDE上でAlexaスキル用の関数をコーディングすることができます。Cloud9を使わずにLambda内で書くことも可能ですが、Cloud9の方が使いやすくておすすめ。

### 環境の作成
AWSからCloud9を選択し、「Create Environment」を押して、名前などを適当に入力して環境を作成します。
Lambdaと同じリージョンで作成していれば、少し前に作成したLambda関数が右ペインの `AWS Resources` の `Remote Functions` に表示されているので、それをインポートすることでコーディングできます。
コーディングが終わったら `Local Functions`を Deployすれば Lambdaに反映可能。

### 必要なパッケージのインストール
必要なパッケージをCloud9上のbashコンソールでインストールしておきます。

```bash
cd package.json が入ったディレクトリ
npm install ask-sdk --save
```

### まずは、返事をするだけのスキルを作ってみる
Cloud9上に index.js を作成します。
どのディレクトリに置くかは、Cloud9でどの範囲をインポートしたかによるので一概に言えませんが、
もし package.json があればそれと同じ階層に作成します。

```javascript
const Alexa = require('ask-sdk');

/**
 * あいさつ
 */
const GreetingHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    // 初回起動時
    return request.type === 'LaunchRequest';
  },

  async handle(handlerInput) {    
    return handlerInput.responseBuilder
      .speak('今日もおはようございます。')
      .withShouldEndSession(true)
      .getResponse();
  },
};

/**
 * 全てマッチしなかった場合
 */
const FallbackHandler = {
  canHandle() {
    return true;
  },
  
  handle(handlerInput) {
    return handlerInput.responseBuilder
      .speak('申し訳ありません。もう一度お話しください。')
      .reprompt('もう一度お話しください。')
      .getResponse();
  },
};

const ErrorHandler = {
  canHandle() {
    return true;
  },
  
  handle(handlerInput, error) {
    return handlerInput.responseBuilder
      .speak('エラーが発生しました。もう一度お話しください。')
      .reprompt('もう一度お話しください。')
      .getResponse();
  },
};

exports.handler = Alexa.SkillBuilders.standard()
  .addRequestHandlers(
    GreetingHandler,
    FallbackHandler
  )
  .addErrorHandlers(ErrorHandler)
  .lambda();
```

とりあえずこんな感じ。GreetingHandlerが今回作ったメインのハンドラー。
開発は主に、各イベントや発話ごとのハンドラーを作成し、それをexports.handlerに登録していくことになります。
Handlerで必須なのは、`canHandle()` と `handle()` で、前者はどのようなイベントや発話を受け取ったときにそのハンドラーを起動するかを設定し、`handle()`ではユーザーに画面や発話などを返したり裏で行う処理などをコーディングしていきます。

ハンドラーが複数あった場合、`addRequestHandlers()`内で指定した順に `canHandle()` での判定が行われ、最初にマッチしたハンドラーが起動します。
上記例の様に、どのハンドラーの条件にもマッチしなかった場合に必ず受け取るFallbackHandlerを作っておくと良さげ。(`switch` 文の `default` みたいなイメージ)

### デプロイ
コーディングが終わったら、Cloud9の右ペインの `AWS Resources` で作成した `Local Functions` を選択して `↑`(Deploy)ボタンを押せば、Lambdaに反映されます。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/c01b964b-9fb0-ac6a-a14d-8d7bd47a775f.png)

# 作ったスキルのテスト

### エミュレーター上でテスト

alexa developer consoleの「テスト」タブに移動し、入力欄に**呼び出し名** (少し前の項目で設定した名前) を入力するとスキルが起動して、「今日もおはようございます」と返ってくることを確認できました。
今はまだ作り込んでいないので、これですぐにスキルが終了してしまいますが、この後の作り込みでもうちょっとやり取りができるスキルを作っていきます。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/79fa2b3e-6b79-830c-b5fd-a12b13749aeb.png)

### 実端末でテスト

[Alexa](https://alexa.amazon.co.jp/spa/index.html) にアクセスし、アカウントを作って実端末と紐付けを行います。
ウィザードにしたがって進んでいくことで紐付けは完了します。

[有効なスキル - 開発スキル](https://alexa.amazon.co.jp/spa/index.html#skills/your-skills/) に行くと、今開発中のスキルが表示されるので、ここでAlexaに開発中のスキルをインストールできます。(ここは記憶が定かでないので間違っていたらすみません)
あとは、実端末で「アレクサ、○○」と話しかければスキルが起動します。

### ログを確認

スキルがうまく動作しなかった場合は、AWSの**CloudWatch**でログを確認できます。
CloudWatchのロググループをクリックすると作ったスキルが表示されるので、そこをクリックすることでログを確認できます。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/c880bd01-4a03-7935-4a77-e2cd72cc06c7.png)

# スキルの公開
さて、まだ現時点では公開できる内容のスキルではありませんが、全体の流れを追うため、一旦スキルの作り込みは後回しにし、スキルの作り込みが終わった前提でスキルの公開までの流れを書いていきます。

alexa developer consoleの「公開タブ」にアクセスし、公開設定をおこなっていきます。
質問項目が多岐に渡りますが、最低限、必須項目だけでも頑張って埋めていきます。
全て入力が終わり、何か問題があれば指摘を受けますので修正します。
問題なければ「実行」ボタンを押すことで、自動テストが実行されます。
問題なければ、Amazonスタッフに検証依頼を出すことになります。
その後数日以内にフィードバックがありますので、修正等のやり取りを行い、最終的に問題なければ無事公開されます。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/78f3b6f8-297f-3287-66c9-800d4ad441fe.png)

# バージョン管理を行う
無事公開はできましたが、このままだとスキルを編集して反映すると即本番のスキルに影響が出てしまいます。
それだと問題があるので、Lambdaのエイリアス機能を使って、本番用と開発用を別々に管理していきます。

### エイリアスの作成
Lambdaにアクセスし、「アクション」→「新しいバージョンを発行」で、今の最新版に対してバージョン名をつけます。名前は「1.0.0」など好きな名前をつけます。
次に「エイリアスの作成」で本番用のエイリアスを作成します。名前は「prod」など適当につけ、先程作ったバージョン番号を選択します。

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/08292cf3-55ec-fc3f-d9fb-1d8aa36212be.png)

### スキルの向き先を変える

次に今作ったエイリアスの**ARN**をコピーし、alexa developer consoleの「ビルド」タブの「エンドポイント」の「デフォルトの地域」の値を上書きします。
これで、今開発中のスキルは本番のエイリアスを向いていることになります。

### 再度、本番公開

この状態で、本番公開を進めれば、スキルは本番用のエイリアスを向いた状態でリリースされます。
その後またエンドポイントを`$LATEST`のARNに戻すことで、開発中のスキルは最新の状態を向きますが、本番スキルはエイリアスを向いたままとなり、プログラムを更新しても本番は影響を受けずにすみます。

### その後の運用

スキルを更新する場合は、再度新しいバージョンを作ってエイリアスをそのバージョンに向け直せば、本番のスキルを更新できます。インテントの修正が無ければ再度Amazonに依頼を出す必要はありません。(インテントの修正がある場合は毎回依頼が必要)

少し面倒な点として、再度Amazonに依頼を出す際、開発中のスキルを本番用エイリアスに向けた状態で依頼を出す訳にもいきませんが、かといって何らかのエイリアスに向けておかないと本番公開されたときにエイリアスを向かなくなってしまい困ってしまいます。
したがって、もう一つ本番用のエイリアス (prod2) を作成し、そこに向けた状態で依頼を出します。
そうすることで、本番公開後、prod2に向いた状態でスキルが公開されます。
今後は、prodとprod2を交互に切り替えて依頼していくことになります。(もっと良い方法があれば…)


# もうちょっと作り込みしてみる

### 問いかける

handle()の最後で以下のようなレスポンスを返すことで、ユーザーに問いかけを発信して、応答を待つことができます。

```javascript
handle(handlerInput) {
	// ～略～ 色々な処理
	// 問いかけ
	return handlerInput.responseBuilder
		.speak('あなたは男性ですか？女性ですか？')
		.reprompt('あなたは男性ですか？女性ですか？')
		.getResponse();
}
```

なお、文章をAlexaが正しく読んでくれないときは、

```javascript
'<phoneme alphabet="x-amazon-ja-jp" ph="オオダ\'シ">大田市</phoneme>'
```

という感じの文をspeak()に入れることで正しく読んでくれます。「`\'`」は日本語アクセントが低音に移る点を指定。

### 問いかけ後、返事を受け取る

canHandle()で受け取りたいインテントをキャッチすれば、そのハンドラーのhandle()が起動します。
以下の例は、肯定の応答を受けたときに起動する設定。

```javascript
canHandle(handlerInput) {
	const request = handlerInput.requestEnvelope.request;	
	// 「はい」と応答されたとき
	if (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.YesIntent') {
		return true;
	}
	return false;
}

handle(handlerInput) {
	// 「はい」と応答されたときの処理
}
```

### 会話の進捗によって処理を変える

例えば、「はい」「いいえ」で答える質問を2回に分けて投げかける場合、1つ前の方法だとどっちの質問か区別せずに反応してしまいます。そこで、現在の会話の進捗をセッション変数に保持しておき、その進捗に応じて処理を行います。

```javascript
// 最初の質問
const FirstQuestionHandler = {
	canHandle(handlerInput) {
		const request = handlerInput.requestEnvelope.request;
		// 初回起動時
		return request.type === 'LaunchRequest';
	}

	handle(handlerInput) {
		// セッション変数に現在の質問の位置を記憶
		const attributesManager = handlerInput.attributesManager;
		const attributes        = attributesManager.getSessionAttributes();
		attributes.state        = 'first';
		return handlerInput.responseBuilder.speak('あなたは男性ですか？').reprompt('あなたは男性ですか？').getResponse();
	}
}

// 2つ目の質問
const SecondQuestionHandler = {
	canHandle(handlerInput) {
		const request = handlerInput.requestEnvelope.request
		if (request.type === 'IntentRequest') {
			const attributesManager = handlerInput.attributesManager;
			const attributes        = attributesManager.getSessionAttributes();
			// 1つ目の質問の後のとき
			if (attributes.state && attributes.state === 'first') {
				// 「はい」か「いいえ」で答えたとき
				if (['AMAZON.YesIntent', 'AMAZON.NoIntent'].includes(request.intent.name)) {
					return true;
				}
			}
		}
		return false;	
	}

	handle(handlerInput) {
		// セッション変数に現在の質問の位置を記憶
		const attributesManager = handlerInput.attributesManager;
		const attributes        = attributesManager.getSessionAttributes();
		attributes.state        = 'second';
		return handlerInput.responseBuilder.speak('あなたは20歳以上ですか？').reprompt('あなたは20歳以上ですか？').getResponse();
	}
}

// 結果
const ResultHandler = {
	canHandle(handlerInput) {
		const request = handlerInput.requestEnvelope.request
		if (request.type === 'IntentRequest') {
			const attributesManager = handlerInput.attributesManager;
			const attributes        = attributesManager.getSessionAttributes();
			// 2つ目の質問の後のとき
			if (attributes.state && attributes.state === 'second') {
				// 「はい」か「いいえ」で答えたとき
				if (['AMAZON.YesIntent', 'AMAZON.NoIntent'].includes(request.intent.name)) {
					return true;
				}
			}
		}
		return false;
	}

	handle(handlerInput) {
		// 処理
	}
}
```

### 問いかけ時に画面に情報を表示する

handle()のレスポンスでテンプレート機能を使うことで画面に情報を表示させることができます。
テンプレートはいくつか種類があるようです。
また、画面タッチにも対応させることができます。
ただし画面表示に対応していない端末を考慮して処理を分岐させるといいでしょう。

```javascript
	handle(handlerInput) {
		// 画面表示に対応している場合
		if (handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Display) {
			const viewport = handlerInput.requestEnvelope.context.Viewport;
			// 画面が丸い端末の場合
			const isRound  = viewport && viewport.shape === 'ROUND';
			handlerInput.responseBuilder.addRenderTemplateDirective({
				type: 'BodyTemplate2', // 今回は背景画像とその上に文字を表示するテンプレートを選択
				token: 'token',
				backButton: 'HIDDEN',
				image: isRound ? new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage() : null,
				backgroundImage: new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage(),
				title: 'タイトル',
				textContent: new Alexa.RichTextContentHelper().withPrimaryText(
					'文章文章文章<br /><action token="detail">説明を聞く</action>'
				).getTextContent(),
			});
		}
	}
```

上記で設置した「説明を聞く」ボタンのタッチは以下のようにcanHandle()内で検知することができます。

```javascript
if (request.type === 'Display.ElementSelected' && request.token === 'detail') {
```

### スワイプして一覧の中から選べるようにする
スワイプして複数の商品から選択する、といったテンプレートもあります。

```javascript
const template = {
	type: 'ListTemplate2',
	token: 'string',
	title: '選択してください。',
	backButton: 'HIDDEN',
	listItems: []
};

template.listItems.push({
	token: 'detail_1',
	image: new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage(),
	textContent: new Alexa.RichTextContentHelper().withPrimaryText('<font size="2">商品1</font>').getTextContent()
	});
}
template.listItems.push({
	token: 'detail_2',
	image: new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage(),
	textContent: new Alexa.RichTextContentHelper().withPrimaryText('<font size="2">商品2</font>').getTextContent()
	});
}
handlerInput.responseBuilder.addRenderTemplateDirective(template);

return handlerInput.responseBuilder
	.speak('商品一覧を表示しています。画面を右へスワイプすると、すべての商品を見ることができます。商品の詳細を聞くには、画面にタッチするか、番号をおっしゃってください。')
	.reprompt('商品を選択してください。')
	.getResponse();
```

### 発話時に一緒にカードでメッセージを通知する。

```javascript
return handlerInput.responseBuilder
	.speak('ありがとうございました。詳しくはお送りしたカードを参照してください。')
	.withSimpleCard('購入した商品: XXX')
	.withShouldEndSession(true) // スキルを終了する
	.getResponse();
```

### スロットタイプの値を受け取る

前述のインテント作成の項目のような形でスロットタイプ型のインテントを作成した場合、スロットタイプのどの値が発話されたか、受け取ることができます。
例えば、`{number}番目にする` と発話された場合、`number`の値を受け取ることができますので、その値に応じて処理を分岐できます。

```javascript
// スロット値が渡された場合
if ((request.intent.slots.category.resolutions && request.intent.slots.number.resolutions.resolutionsPerAuthority[0].values) {
	const number = request.intent.slots.number.resolutions.resolutionsPerAuthority[0].values[0].value.id;
}
```

### 次回起動時に前回の状態に応じた処理を行う
次回起動時に前回の続きから処理を行ったり、前回どう応答したかに応じて処理を変えたい場合は、`DynamoDB` を利用した、状態を永続的に記憶する機能を使います。

```javascript
exports.handler = Alexa.SkillBuilders.standard().addRequestHandlers(
	// 略
	FallbackHandler
  )
  .addErrorHandlers(ErrorHandler)
  .withTableName('テーブル名')
  .withAutoCreateTable(true)
  .lambda();
```

こんな感じで機能を有効にしつつ、

```javascript
async handle(handlerInput) {
	const attributesManager = handlerInput.attributesManager;
	const persistentAttrs = await attributesManager.getPersistentAttributes();
	persistentAttrs.hogehoge = '変数に入れる値';
	attributesManager.setPersistentAttributes(persistentAttrs);
	await attributesManager.savePersistentAttributes();
}
```

で状態を記録します。 `await` しないといけないので、メソッドに `async` をつける必要があります。

```javascript
const persistentAttrs	 = await attributesManager.getPersistentAttributes();
if (typeof persistentAttrs.hogehoge !== 'undefined' && hogehoge === 'X') {
	return true;
}
```

あとは、こんな感じで判定できます。前回終了直前に、今の状態を記憶しておいて、次回起動時、状態が記憶されていれば再開用のハンドラーを呼び出せばいいでしょう。
通常、起動優先度を上げるために、`addRequestHandlers` の上位のハンドラーで判定する必要があるでしょう。

### APIと通信してその結果に応じた処理をする

普通に Axios などを利用すれば良いです。以下、一例。handle()に `async`をつけるのを忘れないようにする必要があります。

```javascript
const axios = require('axios');

// 略

const response = await axios.post('https://APIのURL', parameters, {
	headers: {
		Authorization: 'Bearer ' + handlerInput.requestEnvelope.context.System.user.accessToken
	},
	validateStatus: function (status) {
		return true;
	}
});
		
if (response.status !== 200) {
```

### アカウントリンク機能を利用する

今回は省略し、必要に応じて別記事で書きたいと思います。

### Amazon Payでの課金機能を利用する

今回は省略し、必要に応じて別記事で書きたいと思います。