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