Visual Studio+C#でAlexaカスタムスキル2 Sessionを使って会話のキャッチボールをする
はじめに
この記事はVisual Studio+C#でAlexaカスタムスキル1 Twitterにあいさつしてみたの続編です。 前回の記事の内容がベースになっています。
lambdaの記述方法としてはマイナー(と勝手に思っている)なC#を使ってカスタムスキルを作成します。
概要
前回の実装では以下のように、1往復の会話で処理が完了していました。
そこで今回は少し改良して、以下のように会話を続けてみます。 またユーザーの返答によって処理を分岐してみます。
実装
今回の実装は前回からほぼlambdaのみを変更します。 (対話モデルも少しだけ変更します) Alexaと会話を続けるにはセッションを利用する必要があるので、その辺りの実装をしました。セッション管理の利用方法はAlexaスキル開発トレーニングシリーズ 第3回 音声ユーザーインターフェースの設計が参考になります。 今回はここのNode.jsのサンプルをもとにC#のコードを作成しました。
lambdaのプロジェクトはこちらに上げてあります。
対話モデル
インテントスキーマのみ少し変更しました。 ※Yes, Noを受け付けるためにAMAZON.NoIntentとAMAZON.YesIntentを追加しています。
カスタムスロットタイプ、サンプル発話は前回と同じです。
{ "intents": [ { "slots": [ { "name": "Word", "type": "GREETING_WORD" } ], "intent": "TwitterIntent" }, { "intent": "AMAZON.NoIntent" }, { "intent": "AMAZON.YesIntent" }, { "intent": "AMAZON.HelpIntent" }, { "intent": "AMAZON.StopIntent" } ] }
lambda
前回と同様AWS Toolkit for Visual Studioを導入済みの環境でコーディングしていきます。
NuGetから以下の2つを追加しました。 - Alexa.Net - CoreTweet
初期化
前回同様Twitterのアクセス情報を環境変数から取得しています。 あとは今回はステート管理を行い、ステート毎にコールされる関数を分けています。 ifで分岐しても良いのですが、今後ステート数が増えることも考えてDelagateをキャッシュしています。
private readonly string APIKey; private readonly string APISecret; private readonly string AccessToken; private readonly string AccessTokenSecret; private Dictionary<EConversationState, Func<IntentRequest, Session, SkillResponse>> FunctionMap = new Dictionary<EConversationState, Func<IntentRequest, Session, SkillResponse>>(); public Function() { APIKey = Environment.GetEnvironmentVariable("API_KEY"); APISecret = Environment.GetEnvironmentVariable("API_KEY_SECRET"); AccessToken = Environment.GetEnvironmentVariable("ACCESS_TOKEN"); AccessTokenSecret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET"); // ステートに応じた関数をキャッシュしておく FunctionMap[EConversationState.StartState] = FunctionHandler_StartState; FunctionMap[EConversationState.ConfirmState] = FunctionHandler_ConfirmState; }
FunctionHandler
ユーザーがAlexaに話しかけると必ず呼ばれる関数です。
今回ステート毎にコールされる関数を変更する仕組みにしていますが、
ステートはセッションアトリビュートに格納しています。
(以下のソースのinput.Session.Attributes["STATE"]
)
この関数内ではセッションアトリビュートからステートを読み取り、 キャッシュしてあるデリゲートを取り出してステート毎の処理を行います。
public SkillResponse FunctionHandler(SkillRequest input, ILambdaContext context) { // リクエストタイプを取得 var requestType = input.GetRequestType(); // インテントリクエスト以外は無視 if (requestType != typeof(IntentRequest)) return null; // ステートの読み取り EConversationState State = EConversationState.StartState; if (input.Session?.Attributes?.ContainsKey("STATE") == true) { Enum.TryParse(input.Session.Attributes["STATE"] as string, out State); } // ステートに応じたFunctionを呼び出し return FunctionMap[State](input.Request as IntentRequest, input.Session); }
FunctionHandler_StartState
StartState時の処理です。 ユーザーから「△△△とつぶやいて」と言われる想定ですので、 まず△△△を取得しています。 取得した内容は記憶する必要があるため、セッションアトリビュートに「Word」というKeyで登録しています。 あとはAlexaから応答をさせるのですが、この時TellではなくAskを使用しています。 Askを使うとセッションが続き、Alexaはすぐに次の発話を待ち受ける状態になります。
private SkillResponse FunctionHandler_StartState(IntentRequest intentRequest, Session Session) { // TwitterIntent以外は無視 if (intentRequest.Intent.Name.Equals("TwitterIntent") == false) return ResponseBuilder.Tell("予期しないリクエストです。中止します"); // Wordスロットの値を取得 var wordSlotValue = intentRequest.Intent.Slots["Word"].Value; // Axexaから応答 Reprompt rep = new Reprompt(); rep.OutputSpeech = new PlainTextOutputSpeech() { Text = "つぶやいていいですか?" }; Session.Attributes = new Dictionary<string, object>(); // つぶやく文言を記憶する Session.Attributes["Word"] = wordSlotValue; // ステートをConfirmStateに変更 Session.Attributes["STATE"] = EConversationState.ConfirmState.ToString(); return ResponseBuilder.Ask($"{wordSlotValue}とつぶやいていいですか?", rep, Session); }
FunctionHandler_ConfirmState
ConfirmState時の処理です。 ユーザーが「はい」or「いいえ」を言ってくる想定なので、 言われた結果に応じて処理を変えています。
なお「はい」「いいえ」はBuilt-In Intentを利用しています。 当たり前ですが、自分で作成したIntentよりもBuilt-Inの方が認識精度が良いようです。
はいの場合
セッションアトリビュートから記憶しておいて文言を取得してTwitterに投稿する
いいえの場合
投稿をキャンセルする
また、「はい」でも「いいえ」でも一旦セッションは終了させたいので、 Alexaからの応答はTellを使用しています。 これでセッションは終了し、再度機能を使用する場合は最初からとなります。
private SkillResponse FunctionHandler_ConfirmState(IntentRequest intentRequest, Session Session) { // NOが返ってきたらやめる if (intentRequest.Intent.Name.Equals("AMAZON.NoIntent")) { return ResponseBuilder.Tell("はい、やめます"); } // YES以外は想定外なのでやめる if (intentRequest.Intent.Name.Equals("AMAZON.YesIntent") == false) { return ResponseBuilder.Tell("予期しない返答です。中止します"); } // 記憶しておいた文言を取得 var wordSlotValue = Session.Attributes["Word"] as string; // Twitter APIの必要情報を生成 var tokens = CoreTweet.Tokens.Create($"{APIKey}", $"{APISecret}", $"{AccessToken}", $"{AccessTokenSecret}"); // つぶやき実施 tokens.Statuses.UpdateAsync(new { status = wordSlotValue }).Wait(); // 結果を報告 return ResponseBuilder.Tell($"{wordSlotValue}とつぶやきました"); }
実機での確認
実はすぐには上手くいかず、何度か手直しをしましたが、 最終的には思った動作をするようになりました。 1往復の会話で処理をさせると、Alexaが誤認識したときに困りますが、一度ユーザーの確認をはさむことで精度が良くなりました。 ただいつも確認をいれるようだと、使い勝手が悪くなる側面もあるので実際のスキル開発では、シーンに応じた設計が要求されると思います。
まとめ
Visual StudioでC#を使ったスキル作成でセッションを利用してAlexaと会話のキャッチボールをすることに成功しました。 これにより実現できる機能の幅が広がったと思います。
ただし今回はステート数が2つのため、あまり困りませんでしたがステート数が増えてきたり、入れ子のステートにも対応しようとすると、もう少しコード側の設計に工夫がいるかもしれません。