C#でAlexaスキル開発。同一セッション内でのデータの保持をやってみよう

（2018年11月8日追記：C#ソースコードにコメントを修正、追加しました。また、本来LuckyColorIntentHandlerメソッドでセッションを終了させる必要があったのに、続くようになっていました。同メソッドにおいて、Repromptを削除し、ShouldEndSession=trueを追加しました。）

#はじめに
これまでにVisual StudioとC#を用いたAlexaスキルの開発について書いてきました。
C#で記述する場合のAlexaスキルの基本的な構成がわかり、簡単なスキルであれば作ることができるようになったかと思います。
ここでいう**簡単なスキル**とは、ユーザーの発話に対して応答するスキルのことです。
例えば、

ユーザー「1たす1は？」
Alexa「2です」
ユーザー「じゃあ、2たす2は？」
Alexa「4です」

というような1往復ずつの単発の発話とその応答のスキルですね。

しかし、これでは複数往復のやりとりから最終的な応答をするようなスキルを作ることはできません。
例えば、

ユーザー「コーヒーください」
Alexa「いくつにしますか」
ユーザー「2つ」
Alexa「コーヒーを2つですね。少々お待ち下さい」

というような、注文を取るスキルであれば、複数回のやり取りの経過をすべて覚えている必要があります。この例の場合は「コーヒー」と「2」ですね。

ここで出てくるのが**セッション**というものです。
スキルが起動してから終了するまでが1つのセッションとなるわけですが、同一セッション内であれば任意のデータを記憶しておく仕組みが用意されています。（注：セッションが終わるとこの情報は消えてしまいます。）

これを使えばユーザーとの一連のやり取りに基づいて、よりバリエーション豊かな応答を行うことができるようになります。
一歩進んだ感じですね。

このセッションについては[公式のチュートリアル第3回](https://developer.amazon.com/ja/blogs/alexa/post/cddf2434-42ee-422f-886d-5be9c36dba14/jp-training-chapter3)が詳しく、このチュートリアルをとおしてその基本的な事柄を理解することができるかと思います。

その上で、ここではもちろんC#でこの[公式チュートリアル第3回](https://developer.amazon.com/ja/blogs/alexa/post/cddf2434-42ee-422f-886d-5be9c36dba14/jp-training-chapter3)を実装することで、C#での同一セッション内でのデータの保持のやり方を説明したいと思います。

#どうやってデータを保持するの？
今回説明する同一セッション内でのデータの保持ですが、どこにどうやって保持するかというと、変数でも、Alexaサービスでも、Alexa端末でもなく、Alexaサービスとエンドポイントの間でやり取りされるリクエストとレスポンスのJSONデータの中に持たせる、という仕組みになっています。

JSONの中の`sessionAttributes`がそれにあたります。

例えば[公式チュートリアル第3回](https://developer.amazon.com/ja/blogs/alexa/post/cddf2434-42ee-422f-886d-5be9c36dba14/jp-training-chapter3)に従って作成したスキルの応答は以下のようになります。
（チュートリアルをやっておくとわかりやすいです）
これはユーザーが「ふたご座の運勢を占って」と発話したことに対する応答です。

```json
{
	"body": {
		"version": "1.0",
		"response": {
			"outputSpeech": {
				"type": "SSML",
				"ssml": "<speak>今日のふたご座の運勢は星ひとつでイマイチでしょう。他にラッキーカラーが占えます。ラッキーカラーを聞きますか？</speak>"
			},
			"card": {
				"type": "Simple",
				"title": "ふたご座の運勢：",
				"content": "今日のふたご座の運勢は星ひとつでイマイチでしょう。"
			},
			"reprompt": {
				"outputSpeech": {
					"type": "SSML",
					"ssml": "<speak>他にラッキーカラーが占えます。ラッキーカラーを聞きますか？</speak>"
				}
			},
			"shouldEndSession": false
		},
		"sessionAttributes": {
			"sign": "ふたご座"
		},
		"userAgent": "ask-node/2.2.0 Node/v8.10.0"
	}
}
```

注目してもらいたいのはこのJSONの中の`sessionAttributes`という項目です。
ここに任意のデータを入れて、セッションの間中保持することができます。
サーバーレスなLambda側ではDynamoDBのような外部記憶を使わない限り、変数などにデータを保持しておくことはできません。

```json
        "sessionAttributes": {
            "sign": "ふたご座"
        },
```

では、次に同一セッション内でユーザーが「ラッキーカラーを占って」と発話すると、Lambda側に送られるJSONはどうなっているかというと、以下抜粋ですが、こんな感じでやはりセッション情報が入っていることが確認できます。`attributes`という項目ですね。

```json
{
	"version": "1.0",
	"session": {
		"new": false,
		"sessionId": "amzn1.echo-a...",
		"application": {
			"applicationId": "amzn1.ask..."
		},
		"attributes": {
			"sign": "ふたご座"
		},
		"user": {
			"userId": "amzn1.as..."
		}
	},

    /////////略/////////
}
```


このように、セッションアトリビュートとして任意の値を登録し、登録したデータはその後のリクエストとレスポンスのJSONの中に入れてやり取りすることになります。

#ではC#でやってみましょう
##まずは下準備
以下の準備を行ってください。

1. Visual StudioでAWS Lambdaのプロジェクトを作成。
2. Alexa.NETをNugetからインストール。
3. C#でのスキルの最小構成をコピペ。([こちら](https://qiita.com/shuhey/items/5a50e1f583daa6865a2e)から)
4. ビルドが通ることを確認


##公式チュートリアル第3回のNode.jsをC#で書き直す
ソースコードはチュートリアルのページに載っているので、それを見ながらC#へ翻訳した結果がこれです。

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using Alexa.NET.Request;
using Alexa.NET.Request.Type;
using Alexa.NET.Response;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace HoroscopeSkill_CSharp
{
    public class Function
    {
        private class FortuneScore
        {
            public string Score { get; }
            public string Description { get; }

            public FortuneScore(string score, string description)
            {
                this.Score = score;
                this.Description = description;
            }
        }

        private readonly List<FortuneScore> _fortunes = new List<FortuneScore>
        {
            new FortuneScore("good","星みっつで良いでしょう"),
            new FortuneScore("normal","星ふたつで普通でしょう"),
            new FortuneScore("bad","星ひとつでイマイチでしょう"),
        };

        private readonly string[] _luckyColors =
        {
            "赤",
            "ピンク",
            "オレンジ",
            "ブルー",
            "水色",
            "紺色",
            "紫",
            "黒",
            "グリーン",
            "レモンイエロー",
            "ホワイト",
            "チャコールグレー"
        };

        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="skillRequest"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public SkillResponse FunctionHandler(SkillRequest skillRequest, ILambdaContext context)
        {
            SkillResponse skillResponse = null;

            try
            {
                //型スイッチの利用
                switch (skillRequest.Request)
                {
                    case LaunchRequest launchRequest:
                        skillResponse = HelpIntentHandler(skillRequest);
                        break;
                    case IntentRequest intentRequest:
                        switch (intentRequest.Intent.Name)
                        {
                            case "HoroscopeIntent":
                                skillResponse = HoroscopeIntentHandler(skillRequest);
                                break;
                            case "LuckyColorIntent":
                                skillResponse = LuckyColorIntentHandler(skillRequest);
                                break;
                            case "AMAZON.HelpIntent":
                                skillResponse = HelpIntentHandler(skillRequest);
                                break;
                            case "AMAZON.CancelIntent":
                                skillResponse = CancelAndStopIntentHandler(skillRequest);
                                break;
                            case "AMAZON.StopIntent":
                                skillResponse = CancelAndStopIntentHandler(skillRequest);
                                break;
                            default:
                                //skillResponse = ErrorHandler(skillRequest);
                                break;
                        }

                        break;
                    case SessionEndedRequest sessionEndedRequest:
                        skillResponse = SessionEndedRequestHandler(skillRequest);
                        break;
                    default:
                        //skillResponse = ErrorHandler(skillRequest);
                        break;
                }
            }
            catch (Exception ex)
            {
                skillResponse = ErrorHandler(skillRequest);
            }

            return skillResponse;
        }




        #region 各インテント、リクエストに対応する処理を担当するメソッドたち

        private SkillResponse HoroscopeIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            //StarSignスロットから値を取り出します。
            var sign = intentRequest.Intent.Slots["StarSign"].Value;

            //占い結果をランダムに取り出す
            var random = new Random();
            int fortuneIdx = random.Next(3);
            var fortune = _fortunes[fortuneIdx];

            speechText = $"今日の{sign}の運勢は{fortune.Description}。";
            var repromptText = "他にラッキーカラーが占えます。ラッキーカラーを聞きますか？";

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText + repromptText
            };
            skillResponse.Response.Card = new SimpleCard
            {
                Title = "サンプル星占い",
                Content = speechText
            };
            skillResponse.Response.Reprompt = new Reprompt
            {
                OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = repromptText
                }
            };
            //セッションオブジェクトを取得
            var attributes = skillResponse.SessionAttributes;

            //nullだったらインスタンスを生成
            if (attributes == null)
            {
                attributes = new Dictionary<string, object>();
            }
            //「sign」をキーにしてユーザーの星座を格納
            attributes["sign"] = sign;
            //レスポンスに格納
            skillResponse.SessionAttributes = attributes;

            return skillResponse;
        }

        private SkillResponse LuckyColorIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };


            SkillResponse ComposeReturnToAskFortuneResponse()
            {
                speechText = "そういえばまだ運勢を占っていませんでしたね。";
                speechText += "今日の運勢を占います。" +
                              "たとえば、ふたご座の運勢を教えてと聞いてください";

                skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = speechText
                };
                skillResponse.Response.Reprompt = new Reprompt
                {
                    OutputSpeech = new PlainTextOutputSpeech
                    {
                        Text = speechText
                    }
                };

                return skillResponse;
            }

            //保存された情報がない場合(sessionAttributesがない)
            if (skillRequest.Session.Attributes == null)
            {
                return ComposeReturnToAskFortuneResponse();
            }

            //セッションオブジェクトを取り出す
            Dictionary<string, object> attributes = skillRequest.Session.Attributes;

            //もう一つ、Attributesの中に目的のキーがない
            if (!attributes.ContainsKey("sign"))
            {
                return ComposeReturnToAskFortuneResponse();
            }



            var random = new Random();
            int luckyColorIdx = random.Next(3);
            var luckyColor = _luckyColors[luckyColorIdx];

            speechText = $"今日の{attributes["sign"]}のラッキーカラーは" +
                $"{luckyColor}です。素敵ないちにちを。";

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Card = new SimpleCard
            {
                Title = "サンプル星占い",
                Content = speechText
            };
            skillResponse.Response.ShouldEndSession = true;//セッション終了を指定

            return skillResponse;
        }

        private SkillResponse HelpIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "今日の運勢を占います。" +
                "例えば、ふたご座の運勢を教えてと聞いてください。";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Reprompt = new Reprompt
            {
                OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = speechText
                }
            };

            return skillResponse;
        }


        private SkillResponse CancelAndStopIntentHandler(SkillRequest skillRequest)
        {
            var intentRequest = skillRequest.Request as IntentRequest;

            var speechText = "";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Card = new SimpleCard
            {
                Title = "サンプル星占い",
                Content = speechText
            };
            skillResponse.Response.ShouldEndSession = true;

            return skillResponse;
        }


        private SkillResponse SessionEndedRequestHandler(SkillRequest skillRequest)
        {
            var sessionEndedRequest = skillRequest.Request as SessionEndedRequest;

            return new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };
        }


        private SkillResponse ErrorHandler(SkillRequest skillRequest)
        {
            var speechText = "すみません。聞き取れませんでした。";

            var skillResponse = new SkillResponse
            {
                Version = "1.0",
                Response = new ResponseBody()
            };

            skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech
            {
                Text = speechText
            };
            skillResponse.Response.Reprompt = new Reprompt
            {
                OutputSpeech = new PlainTextOutputSpeech
                {
                    Text = speechText
                }
            };

            return skillResponse;
        }

        #endregion
        //テスト
    }
}
```

#テスト
これでテストすると、セッションアトリビュートに保持されたデータを使って`LuckyColorIntentHandler`における応答が組み立てられていることがわかります。

- リクエスト側
![image.png](https://qiita-image-store.s3.amazonaws.com/0/245438/c59ef716-8fa7-f9e1-9493-59d41dea0f26.png)

- レスポンス側
![image.png](https://qiita-image-store.s3.amazonaws.com/0/245438/767f4afd-e97d-47f3-9aa8-4bf235fe7094.png)


#注意点
レスポンスに仕込んだセッションアトリビュートのデータは、もしそのレスポンスに対してユーザからリクエストが返ってきた場合にはその中に**自動的に**入れられます。

しかし、リクエストからレスポンスには当然**自動的には入りません**ので、何往復かするやり取りの場合は毎回、リクエスト内のセッションアトリビュートをレスポンスのセッションアトリビュートへ渡す必要があります。

- レスポンス→リクエスト：レスポンス内の`sessionAttributes`がリクエストに入れられる。
- リクエスト→レスポンス：リクエスト内の`attributes`から明示的に`sessionAttributes`へ移動させる必要がある。


#おわりに
今回の同一セッション内でデータを保持する方法によって、より複雑で柔軟なスキルの開発が可能になります。
作ることのできるスキルの幅も大きく広がるものと思います。

今回で公式チュートリアル第3回までやったことになるので、今度は第4回のDynamoDBを使った永続的なデータの保持について書きたいと思います。
セッションをまたいでデータを保持することができるようになります。

さて、DynamoDBを触るAPIってありますよね？きっと。