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つのため、あまり困りませんでしたがステート数が増えてきたり、入れ子のステートにも対応しようとすると、もう少しコード側の設計に工夫がいるかもしれません。


See also