Alexaスキル のリテンション率向上のために、リマインダーAPIの導入を検討した話、およびサンプルコード


[Alexa Advent Calendar 2018](https://adventar.org/calendars/2975)の12/8担当のItoです。
長くなりすぎて1日遅れてしまいました。すいませんー！(^o^)
コード解説と見せかけつつ、思いの丈をぶちまけていきたいと思います！

私は1年半ほど趣味でAlexaスキルやClovaスキルの開発をしている、アマチュアVUI開発者です。

また、「温泉♨️BBA」というユニットで、[@vui_rie](https://twitter.com/vui_rie?lang=ja)さんと、「[スマートスピーカーのアプリの作り方を書いた同人誌](https://booth.pm/ja/items/1032329)」を書いたり、LINE Boot Awards2018のファイナルに選んでいただいたり、
先日は[アマゾン様の会議室をお借りして、VUIの勉強会をやったりしました。](https://connpass.com/event/110193/)

いつかVUIを仕事にしたいなーとおもいつつ、なかなか現業とのすり合わせ地点が見つからず、試行錯誤しています。



# お小遣い欲しい！

### Alexaスキル「夜の体操」の通算ユーザー数がやっと1000人を超えました！が...
6月にリリースしたAlexaスキル「夜の体操」の通算ユーザー数が1000人を超えました。わーいわーい。
こいつは、スクワットとプランクをするだけという簡単なスキルで、[Amazon スキルストアで配信中です。](https://www.amazon.co.jp/voicehackerjp-%E5%A4%9C%E3%81%AE%E4%BD%93%E6%93%8D/dp/B07F2ZNJ86/)
もちろん無料。
名前はいかがわしいですが、たいそう健全なスキルです(^o^)


|![スクリーンショット 2018-12-09 11.04.46.png](https://qiita-image-store.s3.amazonaws.com/0/173392/110689c6-2fe0-db6b-0f8b-5862e5632556.png)|
|:-:|


[利用者が増えたらリワードがもらえる](https://developer.amazon.com/ja/alexa-skills-kit/rewards)という噂ですが、Amazonさんから連絡はこず、夢のまた夢。
しかし改善をしたらいつの日か狙えるようになるかもしれないので、
**お小遣い欲しい(⌯¤̴̶̷̀ω¤̴̶̷́)✧！**の一念で地味な調査をしました。


### スキル評価ガイダンスのランダム挿入が始まっていた
最近、スキルの再生が終わった後に、スキルの評価を★いくつにするか？という質問が
挿入されてることに気づき、驚愕しました。遭遇したのは1回だけで、その後は一度もありませんが、
おそらくこれのおかげで、評価者数が「71」にまで増えたのだと思います。
**なんとあの「ピカチュウトーク」パイセンより評価者数が多い結果となっています。**
ピカチュウトークの場合、そういったガイダンスを挿入すると世界観とずれてしまうので、できないのでせう。

![スクリーンショット 2018-12-09 11.04.46.png](https://qiita-image-store.s3.amazonaws.com/0/173392/96fdb4fc-0bd5-9f8e-0724-974af898d11c.png)


![picca.png](https://qiita-image-store.s3.amazonaws.com/0/173392/d1be9011-83d0-045c-bc02-6b1818530011.png)|
|:-:|


また、★評価数が急に増えると「急上昇人気スキル」に入るようです。
以下は11/30のスキルストアのトップページのスクショですが、左から３番目に入っています。
このランキングは流動的で、「夜の体操」が入ったり出たりします。
心が折れそうな時など、時々眺めるのが励みになっています(⌯¤̴̶̷̀ω¤̴̶̷́)✧

|![ranking.png](https://qiita-image-store.s3.amazonaws.com/0/173392/598fc9f4-1402-dfc7-b2e9-c1fe88c8ae23.png)|
|:-:|

## 改善点を探す

### まず、[Alexa developer console](https://developer.amazon.com/)でKPIを見てみる


１.  30日間の新規ユーザー＝「スキルの有効化」:987 

* 前月比+354.84%
* このスキルは6月のTシャツ目当てで作ったものですが、その後1ヶ月で100ユーザーを超えてEcho Dotをいただきました。たぶん名前がいかがわしいので、ウケたのだと思います(^o^)  その後は地道に推移してましたが、ここへきて急にユーザー爆増。
* おそらく「急上昇人気スキル」に入ったことによる、新規流入だと思われます。
* ありがてえ！！！

|![gaiyou.png](https://qiita-image-store.s3.amazonaws.com/0/173392/420ea468-09fe-4e67-8a29-87726d4b2fbb.png)|
|:-:|




２.  新規登録した方の1週間後のリテンション率は20%、2週間後は13%

* 1週間後のリテンションは、6月のSIN時から一貫して20%程度を維持してます。
* ですが、2週間後は大きく下がってしまうので、**維持が課題です。**
* とくに、このスキルは「1日置きにやる」というものなので、**忘れがち。**


![retention.png](https://qiita-image-store.s3.amazonaws.com/0/173392/539a5b34-ea5c-64a7-8b0d-7b210734654e.png)





３.  対話パスの解析

どのIntentを何人通ったかを可視化してくれるというスゴイ可視化ツール。
はじめてこのフローを見た時、総毛立ったのを覚えています。
**これ改善にめっちゃ使えるやん。。**

複雑な会話をするスキルの場合は、袋小路に入ってしまうことも多いでしょう。
そういうケースがどれくらいの割合で起きているかが、如実にわかるようになっています。
私のスキルは単純なのでそれほど迷わずに最後までいけてるようです。
**よかった！ここは取りこぼしポイントではなさそう。**

クリックすると大きい画像が出ます。

|![スクリーンショット 2018-12-09 2.05.59.png](https://qiita-image-store.s3.amazonaws.com/0/173392/bb3bd383-6db3-f8df-c83b-57cd20a84700.png)|
|:-:|

少し詳しく説明すると、「夜の体操」では、ユーザーの選択したコースをDynamoDBに記録していて、
起動時に、前回の利用時のコースを読み出して「同じコースにしますか？」とたずねる仕組みになってますので、初回と2回目以降は、会話の流れが異なります。

図の中のオレンジの枠内が初回起動、赤い枠内が２回目以降の起動です。
この1ヶ月の対話パスの統計ですと、
初回起動93　：　2回目以降：120
となっており、2回目以降の起動が、新規より多いことがわかります。



### 課題はどこか
新規がある程度流入しているというありがたい状態であるものの、
既存ユーザーのほうが新規より多く、改善対象は、2の「リテンション率」だと思われます。
ここが上がれば、DAUが増えるコトは確実。
いつの日か**リワードがもらえるかも。。**
今のように、全部オーガニックで質の高い新規流入がいる間に、手を打たないといけないなぁ。
リテンション率あげるいい方法何か無いかしら。
うーん。

* 2週間続けたらインセンティブ
* あなたの運動で消費したカロリーは通算●●、と教えてくれる

こういうところが思い浮かびますが、そもそもユーザーさんの選択コースって、永久にとっとくようには考えてなくて、時々消し込みする運用設計なんですよね。
DynamoDBの無料枠におさめたいので。
クッ、ここでも金か。。


### 11月にリリースされた「リマインダーAPI」はどうか？

リマインダーAPIは、当該ユーザーのAlexaアプリに対してリマインダーを登録してくれるもの。
たとえば「次回は○月○日に、夜の体操をリマインダー登録しますか？」というリマインダー登録をして、
実際にその日その時になったらAmazon Echoが、自動的にそのリマインダーを読み上げてくれるので、
忘れずに起動することができます。


ですが、後述しますが、ユーザーパーミションを増やすコトに対する葛藤があり、まだ出していません。
既存ユーザーに変更を求めるのはどうかなと。。
しかしほっとくと忘れてしまうので、技術的に調べてみた内容を書いておこうと思います。
それで新しいスキルに組みこんで実験してみようと思うのです。

### 参考記事　その１

Amazon公式 リマインダーAPI リファレンス
https://developer.amazon.com/ja/docs/smapi/alexa-reminders-api-reference.html#reminder-events-in-json-format

検証中に盛大に行き詰まっているとき、岡本さんの素晴らしい記事を拝見しました。参考にさせていただきました。ありがとうございます！(^o^)

「ついに登場！AlexaのリマインダーAPIをカスタムスキルへ組み込む」
https://wp-kyoto.net/getting-started-alexa-reminder-api-2

クラメソさんの解説記事
https://dev.classmethod.jp/cloud/alexa-audio-skill-reminders/

Garrett Vargasさんの記事
https://hackernoon.com/adding-reminders-to-your-custom-alexa-skill-2ff2bd33101f

また、ここまで行ってないですが、リマインダーのVUI設計時のベストプラクティスも参考になります。
企画の人は先にこっちを見るといいかも。
https://developer.amazon.com/ja/docs/smapi/alexa-reminders-guidelines-for-usage.html


# 実装

## AlexaスキルへのリマインダーAPIの取り込み

### 環境

* Node.js 
* AWS Lambdaにホスティング
* ランタイムは、Node.js v8.10
* 申請は出していないが、Amazon Echo Dotでベータテストが動くことを確認済み

### 進め方

実機テストまでの進め方は以下のようになります。

* コードやskill.json、ja-JP.jsonなど一式を書く
* AWS-CLIでアップする
* alexa developer consoleで設定状況の確認
* シミュレーターでテスト
* ベータテスト登録
* Alexa アプリで、スキルを有効にする
* Echo Dotでテスト

詳しいコトを全部かくと長くなってしまうので、ここから先の説明は、かいつまんでサンプルを書いていきます。


### スキルのコード

```index.js

"use strict";

const Alexa = require('ask-sdk-core');
var ex = require('./ex.js');
const rp = require('request-promise');
const moment = require('moment');
const dt = require('date-utils');

// スキル起動時。本来はworkoutIntentとかでやるんだけど、テスト用にここでリマインダーへの遷移を開始。
const HelpHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'LaunchRequest'
      || (request.type === 'IntentRequest'
        && request.intent.name === 'AMAZON.HelpIntent');
  },
  handle(handlerInput) {
    var speechOutput = 'このスキルでは、朝、起き抜けにやると体によいヨガを、音声でガイダンスします。画面付きデバイスだと、説明画像が見られます。';
    speechOutput = speechOutput + 'では、始めます。';
    speechOutput = speechOutput + '<break time="850ms"/>ワニのポーズをします。';
    speechOutput = speechOutput + 'リマインダーに登録しますか？'; // リマインダーへの遷移
    var reprompt = '続ける場合は、続けて、と言ってください。終了する場合は、そのままお待ちください。';    

    //レスポンスの生成
    return handlerInput.responseBuilder
      .speak(speechOutput)
      .reprompt(speechOutput)
      .getResponse();
  },
};

// リマインダーセット
  const reminderSetIntentHandler = {
    canHandle(handlerInput) {
      const request = handlerInput.requestEnvelope.request;
      return request.type === 'IntentRequest'
        && request.intent.name === 'reminderSetIntent';
    },
    async handle(handlerInput) {
      return setReminder(handlerInput)
      .then((result) => {
        if (result === 'OK') {
          const speech = 'リマインダーにセットしました';
          return handlerInput.responseBuilder
            .speak(speech)
            .withShouldEndSession(true)
            .getResponse();
        } else if (result === 'UNAUTHORIZED') {
          // Get their permission to show a reminder
          return handlerInput.responseBuilder
            .speak('リマインダーをセットする権限が無いようです。権限のご確認をお願いします。')
            .withAskForPermissionsConsentCard(['alexa::alerts:reminders:skill:readwrite'])
            .withShouldEndSession(true)
            .getResponse();
        } else {
          // Some other problem not auth-related
          return handlerInput.responseBuilder
            .speak('リマインダーの設定に問題が発生しました。次こそは')
            .withShouldEndSession(true)
            .getResponse();
        }
      });

      var speechOutput = 'リマインダーをセットしました。おつかれさまでした。';
      const reprompt = '続けますか？'; 
        
      //レスポンスの生成
      return handlerInput.responseBuilder
        .speak(speechOutput + reprompt)
        .reprompt(reprompt) 
        .getResponse();
    }
  };


const skillBuilder = Alexa.SkillBuilders.custom();

// Lambda関数のメイン処理
exports.handler = skillBuilder
  .addRequestHandlers(
    reminderSetIntentHandler,
    HelpHandler
  )
  .lambda();

function setReminder(handlerInput) {
  const alert = {};
  const event = handlerInput.requestEnvelope;
  const timezone = 'Asia/Tokyo';
  
  let start = "2018-12-08T04:00:00.000"; //ToDOここを動的にする。翌日の同じ時間とか。

  moment.locale('ja');
  alert.requestTime = start;
  alert.trigger = {
    type: 'SCHEDULED_ABSOLUTE',
    scheduledTime: start,
    timeZoneId: timezone,
    recurrence: {
      freq: 'WEEKLY', //TODO これは週１回、日曜、という内容なので変える。
      byDay: [
        "SU"
      ],
    },
  };
  alert.alertInfo = {
    spokenInfo: {
      content: [{
        locale: event.request.locale,
        text: '夜の体操の時間だよ',
      }],
    },
  };
  alert.pushNotification = {
    // status: 'ENABLED',
    status: 'DISABLED',
  };
  const params = {
    url: event.context.System.apiEndpoint + '/v1/alerts/reminders',
    method: 'POST',
    headers: {
      'Authorization': 'bearer ' + event.context.System.apiAccessToken,
    },
    json: alert,
  };

  console.log('debug params: ' + JSON.stringify(params));

  // Post the reminder
  return rp(params).then((body) => {
    // Reminder was set OK
    return 'OK';
  })
  .catch((err) => {
    console.log('SetReminder error ' + err.error.code);
    console.log('SetReminder alert: ' + JSON.stringify(alert));
    return err.error.code;
  });
};

```


### AWS-CLIでデプロイするためのskill.json

自分は、Mac上でAWS-CLIを叩いて、Alexa developer consoleとAWS Lambdaに一発アップロードしています。
2〜3分、時間かかりますけど、便利ですよね。

自分はずっとUSアカウントで開発しており、自宅のAmazon Echo Dotでテストするには、
βテスト化する必要があります。その際、リリース申請を出すのと同じ状態にしなければならないとのことで、
以下の追加が必要でした。

skill.json、該当箇所は2点です。
* permissions
* privacyAndCompliance

他の定義ファイルはとくに影響ないです。


```skill.json
{
  "manifest": {
    "publishingInformation": {
      "locales": {
        "ja-JP": {
          "summary": "朝のヨガの説明です",
          "examplePhrases": [
            "アレクサ、朝のヨガをはじめて",
            "便秘解消コース",
            "気分爆上げコース"
          ],
          "smallIconUri": "https://s3-xxxxxx/yoga108.png",
          "largeIconUri": "https://s3-xxxxxx/yoga512.png",          
          "name": "朝のヨガ",
          "description": "朝、ねむくておふとんから出づらい時、ヨガをしましょう。目覚めスッキリコース、便秘解消コース、気分爆上げコースの3種類があります。"
        }
      },
      "isAvailableWorldwide": true,
      "testingInstructions": "ヨガのコースは全部で3つとなります。",
      "category": "HEALTH_AND_FITNESS",
      "distributionCountries": []
    },
    "apis": {
      "custom": {
        "endpoint": {
          "sourceDir": "lambda/custom",
          "uri": "ask-xxxxxxxxxxx"
        }
      }
    },    
    "manifestVersion": "1.0",
    "permissions": [
      {
        "name": "alexa::alerts:reminders:skill:readwrite"
      }
    ],
    "privacyAndCompliance": {
      "allowsPurchases": false,
      "usesPersonalInfo": false,
      "isChildDirected": false,
      "isExportCompliant": true,
      "containsAds": false,
      "locales": {
        "ja-JP": {
          "privacyPolicyUrl": "http://www.hogehoge.jp/xxxxxxxx"
        }
      }
    }    
  }
}


```



### Alexa Developer Consoleで設定状況確認

AWS-CLIでアップロード後に、Alexa Developer Consoleで、以下の点を確認します。

１. アクセス権限＞リマインダー　がONになってるコトを確認。

|![access_permission.png](https://qiita-image-store.s3.amazonaws.com/0/173392/5f805874-9360-c1fe-aac6-fd8adb08fe7c.png)|
|:-:|


スクショはありませんが、同じくAlexa Developer Consoleで、以下も確認。

２. 公開＞スキルのプレビュー＞「日本語(日本)」で、「プライバシーポリシーのURL」に、skill.jsonに書いたURLが入ってること
３. 公開＞「プライバシーとコンプライアンス」＞「このAlexaスキルはユーザーの個人情報を収集しますか？」が「いいえ」
４. 公開＞「プライバシーとコンプライアンス」＞「このスキルは13歳未満のお子様を対象としていますか？」が「いいえ」


### Alexa アプリでスキルを有効にする

スマホのAlexaアプリでスキルを有効にする時、権限についての確認画面が表示されます。
リマインダーをONにするかどうか。

<img width="375" alt="IMG_7720.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/173392/e645aaac-ce58-f9a5-36b4-b692552fb70a.png">

ONにした上で、スキルを起動して、「リマインダーを設定しますか？」の問いに「はい」と答えると、
即座に、以下のようにリマインダーが登録されます。Alexaアプリの「リマインダー・アラーム」で確認できます。
<img width="375" alt="IMG_7722.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/173392/6db65ec5-c01d-7155-7ee1-3174ee0441b6.png">
詳細を開くと、以下のようになっています。手入力での変更も可能です。
<img width="375" alt="IMG_7731.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/173392/e0478a6a-51e4-4ae2-9f29-e3de6ead2404.png">


### トラブルシュート

何箇所かでエラーが出て、丸一日ハマりました。それでこのアドベントカレンダー書くのが遅くなっちゃった次第です。


1. UNAUTHORIZED

「リマインダーをセットする権限がない」と言われる。

実機でテストしていてエラーが出たので、
シミュレーターでスキルI/Oのレスポンスをみたら、カードが表示されて止まっちゃってた。
原因は、skill.jsonの記述もれで、permissionsを正しく記述してなかったため。
Alexa developer consoleで見ても、アクセス権限＞リマインダーがオフになっていた。
なんで早く気づかなかったのか。
書き方を試行錯誤して、上に書いたskill.jsonのように直して解決。


```
{
	"body": {
		"version": "1.0",
		"response": {
			"outputSpeech": {
				"type": "SSML",
				"ssml": "<speak>リマインダーをセットする権限が無いようです。権限のご確認をお願いします。</speak>"
			},
			"card": {
				"type": "AskForPermissionsConsent",
				"permissions": [
					"alexa::alerts:reminders:skill:readwrite"
				]
			},
			"shouldEndSession": true
		},
		"sessionAttributes": {},
		"userAgent": "ask-node/2.3.0 Node/v8.10.0"
	}
}
```

２. INVALID_BEARER_TOKEN

apiAccessTokenがNULLだった。ハンドラの渡しミス。
"event.context.System.apiAccessToken"の記述でOKだった。

あと最初のころ、シミュレーターで採取したapiAccessTokenのハッシュをハードコーディングして渡していたが、煮詰まって一晩ねたら、翌日は有効期限が切れてエラーが出た。（apiAccessTokenの有効期限は1時間。）
この時も、EXPIRED_BEARER_TOKENではなくINVALID_BEARER_TOKENが出た気がする。はっきりと覚えていないけど。

３. INVALID_TRIGGER_SCHEDULED_TIME_FORMAT

時間のフォーマットが違うというエラー。
startのケツにZがついてた。
Lambdaの実行ログをCloudWatchで丹念に見て発覚。
Garrett Vargasさんのサンプルでは、ちゃんとZを取り除くコードになっているのに、デバッグ中に日時をハードコーディングしたのが過ちでした。完全に私のせい。(^^;)


4. DEVICE_NOT_SUPPORTED

(2019/2/17 NISHIZONOさんから指摘をいただき、追記。ありがとうございます！(^o^)）

```
・テストシミュレータまたはスマホAlexaアプリからだと DEVICE_NOT_SUPPORTED となり、
　リマインダー登録できない。（たぶん）
・Echo Spotだとリマインダー登録できた。
```
自分のほうでも再現しました。リマインダー登録がされることを確認するためには、実機でテストする必要がありますね。


### 参考記事 その2

skill.jsonの書き方
https://developer.amazon.com/ja/docs/smapi/skill-manifest.html#custom

reminderのパーミション 
https://developer.amazon.com/de/docs/smapi/skill-manifest.html

エラーメッセージ、公式のリファレンス
https://developer.amazon.com/ja/docs/smapi/alexa-reminders-api-reference.html#error-messages



# 大事なこと

## 葛藤しています

ここまで技術検証をしたのですが、「夜の体操」にはまだ組み込めないでいます。
その理由は、すでにリリースしているスキルのユーザーに、
ある日いきなり、パーミションの要求が増えたら、どうなってしまうのか？
という問題です。

おそらく、有効にしなくてもスキル自体は使えるんですが、スキル側では一律
「リマインダー登録しますか？」と聞いてしまうので、
「はい」と答えたユーザーさんには、こういう画面が出てしまうはず。

<img width="375" alt="IMG_7717.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/173392/af3b1223-7166-6413-f4d6-b001de5b7b12.png">

これはまずいです。
うーん。

というわけで、新しく作る「朝のヨガ」というスキルのほうに組み込むことにしました。
そっちで実験してみます。



# 感想

上のコードも画像も、「夜の体操」から「朝のヨガ」に作り変えてる途中にとったので、わかりにくくてすみません(^^;) コードと画像は一致してます。スキルをクロスで起動したいわけじゃないんす。フハハ

* 広告配信とかで、できるといいですけどね
* つか、広告枠を作ってください！アマゾンさん！(^o^)(^o^)
* なんでスキルを広告枠にしないんですか〜?! 時期尚早なんですか〜？！
* 広告APIをつくってくれたら、私のスキルに今すぐ入れますよ！！！人柱になります！今すぐナウで！葛藤タイムゼロで！
* なんだったら、自動で広告入れてくれてもいいです、リワードくれるなら！！


### 今後の調査事項

リマインダーが発動した時に何かを起こす、というコトができるらしいので、調べてみようと思います。
https://developer.amazon.com/ja/docs/smapi/alexa-reminders-api-reference.html#subscribe-to-reminder-events


また、にわかには信じがたいのですが、スキル外からでもリマインダーの操作ができるようなコトも書かれているので、調べてみようと思います。
https://developer.amazon.com/ja/docs/smapi/configure-an-application-or-service-to-send-messages-to-your-skill.html
後者は、実は、以前[こういうもの](https://www.slideshare.net/SayakaIto2/apialexa-88231549)を調べていたこともあり、公式が対応してくれたとすると、とても嬉しいです。

どなたかやってみたら、どんなもんだったか教えてつかわさーい！


だいぶ長くなってしまいましたが、以上っす！(^o^)