俺がわかるように書いたAlexaスキル入門５（アトリビュート）

## Alexaの基本的なスキル作成５（アトリビュート）
###### 2021/6

### ■いや
書くつもりはなかったんだけど、あまりに公式ドキュメントが雑なので書いとく。
道場もNode.jsだけだし、おまじないだなんだでのなし崩し感があったので。

ついでにAudioPlayerで止めた次の日でもresumeできるのが作れそうだったのもある。
<br>

---
### ■Attribute？
スキルが何かを覚えておく機構を称してAttributeという単語が使われています。

3種類ありますが、実質2種類。

|名前|性質|
|--|--|
|Request|リクエストを得て、返すまでの間記憶してくれるらしいが、何の役に立つのか今のところ分からない。can_handleからhandleに渡すなにか？|
|Session|スキルを起こして、一連の会話が終了するまでをセッションといい、その間記憶してくれる。|
|persistent|DBに実際に保存するので、消すまで記憶してくれる。|

で、どこから使うのかですが、おなじみのhandler_inputにattributes_managerというのが入ってます。
これのメソッドからアクセスします。
<br>

#### session
こんな感じで、attributes_managerのsession_attributesというのにdictを放り込めば保持してくれます。

```python
attr = handler_input.attributes_manager.session_attributes
if type(attr) is dict:
	attr['key'] = value
else:
	handler_input.attributes_manager.session_attributes({'key':value})
```

読み出しも同様に引っ張るだけで入れてたら入ってる。

```python
attr = handler_input.attributes_manager.session_attributes
if type(attr) is dict and 'key' in attr:
	return attr['key']
return None
```
sessionはAudioPlayerのtokenのようにサーバーへのresponseと次のrequest（Intent）に乗っかり続けるって感じなので、特に何かしなくても使えます。
（responseに乗っけるのはattributes_managerが勝手にやってくれると思ってください）

<br>

#### persistent

で、persistentってのがDBを使うためのアダプターってのを用意しないと使えません。

ただ、実際の保存、読み出しの手順は名前が違うだけで、sessionとほぼ一緒です。
<br>

---
### ■persistentを使うための仕組み
attributes_managerを初期化する際の引数で、persistence_adapterというのが設定されているとattributes_manager.persistent_attributes関連のメソッドが使えるようになります。（setter,getter,save,delete）

「おいおい、なんだ初期化する際って？」

当然ですがそのタイミングでなにか手を出せるわけではないのであらかじめこれを使ってねというのを、さらに上位の者に設定してなくてはいけないということです。

そこでお呼びがかかるのがSkillBuilderなんですが、こいつにadapterを追加するメソッドがあったり、初期化時に引数に追加とかではなく「CustomSkillBuilderというSkillBuilderの派生クラスを代わりに使って、初期化時に渡せ」という荒技を持ち出してきます。

<br>

---
### ■アダプター
ask_sdk入れた時に一緒に入るDynamoDBとかいうDBを利用したアダプターですが、また新しいのをいろいろやるよりもう一つあるS3Adapterってのが、なんかAudioPlayerで目にしたS3に近いので、そっちでできないか考えます。

そっちはオフラインでは別途インストールしないといけないです。

```
pip install ask-sdk-s3-persistence-adapter
```
S3Adapterというのが目的となるアダプターです。

ask_sdk_s3/adapter.pyにあります。

```python:ask_sdk_s3/adapter.py
def __init__(self, bucket_name, path_prefix=None, s3_client=None, object_keygen=user_id_keygen):
```
ふむ。全然わからん。
特に必須ぽいbucketがわからん。
適当な文字列入れても動かない。

と、ここでS3のURLを作ってくれてたutils.pyを見るわけです。
s3_client、bucket_nameとかあるじゃん。
path_prefix、object_keygenは特に必要なければ設定しなくていいっぽいので、utils.pyで使ってるのと同じやつをいれて関数を作っちまいましょう。

```python:utils.py
from ask_sdk_s3.adapter import S3Adapter

def create_persistence_adapter():
    s3_client = boto3.client('s3',
                            region_name=os.environ.get('S3_PERSISTENCE_REGION'),
                            config=boto3.session.Config(signature_version='s3v4',s3={'addressing_style': 'path'}))
    bucket_name = os.environ.get('S3_PERSISTENCE_BUCKET')
    return S3Adapter(bucket_name=bucket_name, path_prefix="", s3_client=s3_client)
```
こんな感じでまるパクリ。
例外はデバッグ時に外で拾いたかったのでここで隠蔽しませんでしたが、まあそこらへんは好きにやってください。


<br>

---
### ■実装
アダプターに目途がついたので実際に使ってみます。

```python:lambda_function.py
from ask_sdk_core.skill_builder import SkillBuilder
from utils import create_presigned_url

sb = SkillBuilder()
```
もともとはこんな感じになってるのを

こういう感じに変更です。

```python:lambda_function.py
from ask_sdk_core.skill_builder import CustomSkillBuilder
from utils import create_presigned_url, create_persistence_adapter

sb = CustomSkillBuilder(create_persistence_adapter())
```

あと、S3Adapterをデプロイ先で利用するためにrequirements.txtへの追記が必要になります。


```text:requirements.txt
boto3==1.9.216
ask-sdk-core==1.11.0
```
これだけだったところに、

こう追加です。

```text:requirements.txt
boto3==1.9.216
ask-sdk-core==1.11.0
ask-sdk-s3-persistence-adapter==1.0.0
```
バージョンわかんねーよが普通だと思います。

pipでインストールされたフォルダ（ask_sdk_s3_persistence_adapter-1.0.0.dist-info）にあるMETADATAに

```text:requirements.txt
Name: ask-sdk-s3-persistence-adapter
Version: 1.0.0
```
と書いてあるので、こうなのでしょう。まあフォルダ名もそうなんで、フォルダ見てって感じでも問題ないかと。
<br>

---
### ■実験
sessionと名前が違うだけですね。

```python
attr = handler_input.attributes_manager.persistent_attributes
if type(attr) is dict:
	attr['key'] = value
else:
	handler_input.attributes_manager.persistent_attributes({'key':value})
```

読み出しも同様に引っ張るだけで入れてたら入ってる。

```python
attr = handler_input.attributes_manager.persistent_attributes
if type(attr) is dict and 'key' in attr:
	return attr['key']
return None
```
で、ちょっと違うのがsave操作をしないとsessionみたいに消えちゃうことです。
DBアクセスがあるので、本来saveは最低限にとどめておきたい。

なので、スキルが閉じるときだけ保存するってのが良いです。

```python:lambda_function.py
# 完了しました
class SessionEndedRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return is_request_type("SessionEndedRequest")(handler_input)

    def handle(self, handler_input):
        handler_input.attributes_manager.save_persistent_attributes()
        return handler_input.response_builder.response
```
こいつです。
スキル作ったら出来てるSessionEndedRequestHandlerというやつ。
SessionEnded・・・Sessionじゃねぇか・・・。

まあ、さらにケチるなら通常はSessionに放り込んでおいてこのhandleでSession内の保存すべきやつを移動して保存するのが実装的には簡単でいいかもですね。

とはいえ、AudioPlayerはPause,ResumeがあってなかなかSessionが終わらないこともあるのでAudioPlayerでは設定してすぐに保存しちゃってもいいかもです。

まあ、その辺も好きにやってください。
<br>
で、sessionを終了させて、再度起動したときに読んでみると保存されてるのがわかると思います。

とりあえずうまくいった。
<br>

### ■まとめ
少し長いので、なんか複雑感出ましたが、大雑把にはこれだけですかね。

- utils.pyにアダプターを作る関数を作る。
- SkillBuilderをCustomSkillBuilderに変更して、アダプターをセット。
- requirements.txtでデプロイ先でアダプターを使えるようにしてくれと頼む。
- 使う。
<br>

---
### ■マジ終わり
S3ストレージを見に行くとMediaの上位フォルダに長ったらしい名前のファイルができてる。
フォルダを作って、path_prefixにそのフォルダ名を入れておけばそっちにまとまるのかな。

bucketってのはこの割り当てられたMediaの上位フォルダ以下の領域のことかな。

というあたりで、終わり。