パーソナライズとスキル内課金を実装した（おそらく）最小のスキルを作った話

えっと、、田中みそさんのtwitterを見かけて、つい勢いでalexaのadvent calenderに参加してみましたが、とくにネタを用意しないまま直前になってしまいちょっと慌てております。。

どうしようかなー、って考えたのですが、シンプルに最近作ったスキルの解説的な記事とさせてください。

# ターゲット

- パーソナライズの機能をさっくり使ってみたい
- スキル内課金のpythonでのソースが見たい
- askのcliでpython使いたい

だいたいこんな所でしょうか。

# 話す内容

- **「私のサンタクロース」** スキルについて
- ask-cli for python
- コードの抜粋と解説

# **「私のサンタクロース」** スキルについて

https://www.amazon.co.jp/dp/B081YJTLJS/

ちょっと前に「私のサンタクロース」というちょっとしたスキルをリリースしています。
このスキルの目的は・・

- ~~スキル内課金機能を実装して公開するとecho show5がもらえるキャンペーン参加のため~~
- ~~なんでもいいからパーソナライズ機能を実装してみたかった~~
- ~~ask-cli for python使ってみたかった~~
- サンタを信じるピュアなお子様やファミリーに幸せを届けたい

・・・こんな感じでした。シンプルですね。
ざっくりいうと、

1. サンタ役がスキルに登録する
2. プレゼントを欲しい側が願い事をする
3. サンタ役が願いごとを確認する

というスキルです。
ポイントとしてはサンタ役の識別にパーソナライズの機能を使っているところでしょうか。
願い事をする側も、パーソナライズが有効であれば活用しています。
また、課金機能はけっこう無理やり実装しており、課金すると願い事の上限が増えるようになっています。

# ask-cli for python

ask-cliは使ったことあるのですがいつもnodeでやっておりました。
一度pythonも使ってみようかなと思い、今回はpython使ってます。

## ask-cliとは

alexa skill kitをコマンドラインで使うものです。
これを使うと開発者コンソールを開かなくてもスキルを作る事が可能です。
`ask clone`で既存のスキルのコードを落としたり、チーム間でコードを共有したりする時はめちゃくちゃ便利です。
まあコンソールには何かと便利な事は多いし、申請の時なんかは結局コンソールを開く事になりますが。

使い方としては下記のチュートリアルがそのまま。

[クイックスタート： Alexa Skills Kitコマンドラインインターフェース（ASK CLI）](https://developer.amazon.com/ja-JP/docs/alexa/smapi/quick-start-alexa-skills-kit-command-line-interface.html)

1. ask init
2. ask new
3. ask deploy

ってやるだけ。`ask new`でサンプルスキルのコードをcloneできるので、手っ取り早くなんか作る事もできる。

## ask-cli for python

`ask new`の時にpython3を選択するとpythonでalexaスキルを作れます。

```
$ ask new
? Please select the runtime Python3
? List of templates you can choose Hello World (using Classes)
? Please type in your skill name:  skill-sample-python-helloworld-classes
Skill "skill-sample-python-helloworld-classes" has been created based on the chosen template
```

(↑なお19/12/18時点の話ですが、`.ask/config`の`python3.6`を`python3.7`にしないとdeployでエラーになった。)

ちなみに`$ ask create-hosted-skill --runtime python3.7 --skill-name ExampleSkill --auto-clone true`みたいにやるとhosted スキルが作れます。
要はalexa側でリソース(lambdaとかDynamoDbとか）をホストしてくれるスキル。ちょっとしたスキルならこれで十分。
本来はブラウザ使ってコード書かないといけないhostedスキルだけど、cli使えばローカルで開発できてしまうことになるし超便利。


# コードの抜粋と解説

私のサンタクロースのコードです。
（コードのお話なのにやっつけなコードですいません。）

https://github.com/ikegam1/alexa-myhomesanta-ask-python37

- lambda/py/lambda_function.py スキルのバックエンド処理全般
- lambda/py/vendor/alexa/data.py　スキルの発話部分のワード
- isps/subscription/my-home-santa.json　課金アイテムの設定
- models/ja-JP.json　スキルのフロント部分のインテントなど
- skill.json　

説明するのは`lambda_function.py`の部分です。

## (説明名の)流れ

こんな感じの流れに沿って解説します

1. 初回起動時の流れ
2. 願い事登録時の流れ
3. 願い事確認の流れ
4. 課金関連の流れ

## 関数等一覧

クラスや関数の一覧。ヘルプとか普通の処理は省いてます

|項番| 名称      |内容 |
|:---:|:-----------------|:------------------|
|1| class LaunchRequestHandler(AbstractRequestHandler)| ローンチインテント。スキル起動時に処理される|
|2| class WishAddInIntentHandler(AbstractRequestHandler) |願い事を登録する時のインテント。この後に確認の処理に遷移|
|3| class WishDeleteIntentHandler(AbstractRequestHandler)|願い事を削除するインテント。パーソナライズ必須。自身が登録したものは削除できる|
|4| class WishListIntentHandler(AbstractRequestHandler)|願い事を確認するインテント。パーソナライズ必須。自身が登録したものは確認できる|
|5| class AnswerClassIntentHandler(AbstractRequestHandler)|サンタとしてユーザーを登録するためのインテント|
|6| class PremiumInfoIntentHandler(AbstractRequestHandler)|課金アイテムの紹介|
|7| class YesIntentHandler(AbstractRequestHandler)|願い事を登録時の確認が主な処理|
|8| class ShoppingIntentHandler(AbstractRequestHandler) |課金アイテムを購入しているかどうか|
|9| class BuyIntentHandler(AbstractRequestHandler) |課金アイテムを購入する際のインテント|
|10| class CancelSubscriptionIntentHandler(AbstractRequestHandler) |課金アイテムをキャンセルする際のインテント|
|11| class CancelResponseHandler(AbstractRequestHandler) |課金アイテムキャンセル処理時のレスポンスを拾う|
|12| class BuyResponseHandler(AbstractRequestHandler) |課金アイテム購入処理時のレスポンスを拾う|
|13| def is_santa(santa, person_id) |自身がサンタかどうかを判別|
|14| def is_skill_product(handler_input) |課金アイテムか有効かどうかを判別|
|15| def in_skill_product_response(handler_input) |有効か課金アイテム情報をalexa側のapiを通じて取得する|
  

## 初回起動時の流れ

### 1.LaunchIntent
パラメータはDynamoDBに永続化されています。
`persistence_attr['santa']`が空だと初回起動と見なします。

パーソナライズが有効かどうかチェック。チェックは`request_envelope.context.system.person`にpersonIdがあるかどうかで判断できる。
If パーソナライズ無効 -> 「有効にしてね」で終了
If パーソナライズ有効 -> 「サンタとして登録しますか？」の流れ。「サンタです」と言わせて、`AnswerClassIntent`へ導く

### 5. AnswerClassIntent
まずパーソナライズが有効かどうかチェック。

If パーソナライズ有効 ->　`persistence_attr['santa']['id']`にこのユーザーのIDを登録する。以後、このpersonIdはサンタ扱い。

## 願い事登録の流れ
### 1.LaunchIntent
`persistence_attr['santa']['id']`にperson_idが登録されていて、それが本人の場合はサンタと判断。
そうじゃない場合は **願い事をする一般ユーザー**。
なおチェックは`13. def is_santa(santa, person_id)`で判別する。

If not サンタ -> 「願い事をする」と言わせて`WishAddInIntent`に導く

### 2.WishAddInIntent
願い事を拾うインテント。
願い事はDialogで拾うので、このインテントで処理されるタイミングでは**願い事**は拾えている。
ただ、確認はしたいのでリピートし、「はい」か「いいえ」を求める。「はい」であれば`YesIntent`へ。
また、願い事は無課金時には3件がMaxとなる。`persistence_attr['msg']`をチェックしすでに3件登録されていたら、課金アイテムをリコメンドして終了。

### 7.YesIntent
願い事登録時のconfirmの戻りで入ってくる。
Dialogを使いたかったが複雑になりそうなのでBuildinIntentを使った。
`persistence_attr['session']['msg']`に値があり、登録から1分以内であれば、WishAddInIntentからの遷移とみなす。

メッセージはpersistence_attr['msg']に登録するが、もしパーソナライズが有効だった場合には、`persistence_attr['msg'][idx]['person_id']`にpersonIdを登録し、本人確認に用いる。
本人確認ができた場合には自身の願い事の削除が可能

## 願い事確認の流れ

### 4. WishListIntentHandler

「願い事を確認」
サンタかそうでない場合に分岐があります。

If サンタ -> 登録されている**願い事**を順番に話します。また「○番目の願い事を削除」で願い事の削除につながる
If 一般ユーザー -> 確認するにはパーソナライズが有効である必要があり、有効であれば自身が登録した願い事を確認できる。また「削除するには四桁のパスワード1234を言ってね」みたいに削除に繋げる

### 3. WishDeleteIntent

If サンタ -> message_numberスロットを確認。該当の**願い事を削除する**
If 一般ユーザー ->passwordスロットを確認。パスワードが一致した場合、自身の願い事を削除

## 課金の流れ

「私のサンタクロースのプレミアム機能」というサブスク商品が設定されています。

### 9.BuyIntentHandler
「プレミアム機能を購入」から遷移。

この商品のproductIdをキーとして、alexa側に処理をぶん投げてるだけです。

```
            return handler_input.response_builder.add_directive(
                SendRequestDirective(
                    name="Buy",
                    payload={
                        "InSkillProduct": {
                            "productId": product[0].product_id
                        }
                    },
                    token="correlationToken")
            ).response
```

これを下記のインテントが受け取る

### 12. BuyResponseHandler

通常のIntentではなく、`Connections.Response`というのが返ってきます。

```
        return (ask_utils.is_request_type("Connections.Response")(handler_input) and
                handler_input.request_envelope.request.name == "Buy")
```

購入後の場合は"Buy"というパラメータ名で返ってきますが、ステータスとして、`PurchaseResult.ACCEPTED`、`PurchaseResult.DECLINED`、`PurchaseResult.ALREADY_PURCHASED` というような購入したか、してないか、もともと購入済みだったか、みたいな事も返ってくるのでこれに応じた発話を返しています。
キャンセルの場合の処理もBuyがCancelに変わるだけでだいたいおんなじ。

# おしまい

そんなわけで以上です。
ちょっとしたチュートリアルくらいのボリュームじゃないかなと個人的には思ってます。

あと、正直、パーソナライズは使い所が難しく、これが必須となった時点で利用ユーザーは激減かなって思います。**無効でも使えるけど、有効になっているとなお便利**といった程度の利用がベストプラクティスかなぁと。今後に期待。
スキル内課金も・・日本のスキルだとまだだいぶ厳しそうですよね。生きるのはかなりニッチな領域かと思っています。