のべ4000人以上が来場した日本マイクロソフト公式技術カンファレンス de:code 2019 で登壇しました。
コード
私の書いたデモアプリのコードはこちらです。
https://github.com/chomado/MeetingResponseServer
↑ GitHub のレポジトリ
Office 368 (Outlook) に入れた私の予定をスマートスピーカーから取得できるスキルを、3プラットフォーム向けに開発しました。
質問があったのでここに書きます。
Google Assistant 向けの対話モデル作成に関しては、Google Dialogflow だけでなく Microsoft LUIS も使えるのですが、
共同登壇の方のセッションで田中さんがそちらをカバーしてくださっているので、私は Dialogflow でいっています。
LINE のエンドポイント
この辺の nuget パッケージ使ってます
using CEK.CSharp; using CEK.CSharp.Models; using Line.Messaging;
エンドポイント記述 (Azure Functions, C#)
[FunctionName("Line")] public static async Task<IActionResult> Line( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, ExecutionContext context, ILogger log) { var client = new ClovaClient(); var cekRequest = await client.GetRequest(req.Headers["SignatureCEK"], req.Body); var cekResponse = new CEKResponse(); switch (cekRequest.Request.Type) { // 起動時に飛んでくる intent case RequestType.LaunchRequest: cekResponse.AddText(IntroductionMessage[0]); cekResponse.ShouldEndSession = false; break; // ユーザ定義の intent case RequestType.IntentRequest: { // slot を抜き出す CEK.CSharp.Models.Slot when = null; cekRequest.Request.Intent.Slots?.TryGetValue(key: "when", value: out when); // intent ごとに処理を振り分け var texts = await HandleIntentAsync(cekRequest.Request.Intent.Name, when?.Value ?? "今日", Platforms.Clova); if (texts.Any()) { foreach (var text in texts) { cekResponse.AddText(text); } } else { cekResponse.AddText("予定はありません。"); } // 予定があったら LINE にプッシュ通知する if (texts.Any()) { var config = Models.AuthenticationConfigModel.ReadFromJsonFile("appsettings.json"); var secret = config.LineMessagingApiSecret; var textMessages = new List<ISendMessage>(); textMessages.Add(new TextMessage("ちょまどさんの予定はこちら!")); foreach (var text in texts) { textMessages.Add(new TextMessage(text)); } var messagingClient = new LineMessagingClient(secret); await messagingClient.PushMessageAsync( //to: cekRequest.Session.User.UserId, to: config.MessagingUserId, messages: textMessages ); } } break; } return new OkObjectResult(cekResponse); }
Alexa
using Alexa.NET.Request; using Alexa.NET.Request.Type; using Alexa.NET.Response;
[FunctionName("Alexa")] public static async Task<IActionResult> Alexa( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, ILogger log) { var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(await new StreamReader(req.Body).ReadToEndAsync()); var skillResponse = new SkillResponse { Version = "1.0", Response = new ResponseBody(), }; switch (skillRequest.Request) { case LaunchRequest lr: skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech { Text = IntroductionMessage[0], }; break; case IntentRequest ir: { var texts = await HandleIntentAsync(ir.Intent.Name, ir.Intent.Slots["when"].Value, Platforms.Alexa); if (texts.Any()) { var plainTextOutputSpeech = "ちょまどさんの予定をお知らせします。\n"; foreach (var text in texts) { plainTextOutputSpeech += $"{text}\n"; } skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech { Text = plainTextOutputSpeech }; } else { skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech { Text = "予定はありません。" }; } } break; default: skillResponse.Response.OutputSpeech = new PlainTextOutputSpeech { Text = "すいません。わかりません。", }; break; } return new OkObjectResult(skillResponse); }
Google Assistant
using Google.Protobuf; using Google.Cloud.Dialogflow.V2;
[FunctionName("GoogleHome")] public static async Task<IActionResult> GoogleHome( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, ILogger log) { var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true)); var webhookRequest = parser.Parse<WebhookRequest>(await req.ReadAsStringAsync()); var entities = webhookRequest.QueryResult.Parameters.Fields["when"].StringValue; var webhookResponse = new WebhookResponse(); log.LogInformation(webhookRequest.QueryResult.Intent.DisplayName); switch (webhookRequest.QueryResult.Intent.DisplayName) { case "Default Welcome Intent": webhookResponse.FulfillmentText = IntroductionMessage[0]; break; default: { var texts = await HandleIntentAsync(webhookRequest.QueryResult.Intent.DisplayName, entities, Platforms.GoogleAssistant); if (texts.Any()) { var fulfillmentText = "ちょまどさんの予定をお知らせします。\n"; foreach (var text in texts) { fulfillmentText += $"{text}\n"; } webhookResponse.FulfillmentText = fulfillmentText; } else { webhookResponse.FulfillmentText = "予定はありません。"; } } break; } return new ProtcolBufJsonResult(webhookResponse, JsonFormatter.Default); }
ビジネスロジック
3プラットフォーム間で共通化してる部分です
private static async Task<IEnumerable<string>> HandleIntentAsync(string intent, object meetingDay, Platforms platform) { switch (intent) { case "HelloIntent": return HelloMessage; // 明日の予定を教えて case "AskScheduleIntent": { var start = ParseMeetingDay(meetingDay, platform); var response = await Services.MeetingInfoService.GetMeeting(startTime: start, endTime: start.AddDays(1)); return ScheduleMessage(response.Value); } default: return ErrorMessage; } } private static DateTimeOffset ParseMeetingDay(object meetingDay, Platforms platform) { if (platform == Platforms.Clova || platform == Platforms.Alexa) { return ConvertJname2Datetime((string)meetingDay); } else { // GoogleAssistant // 2019-05-27T12:00:00+09:00 var d = DateTimeOffset.Parse((string)meetingDay); return new DateTimeOffset(d.Date, d.Offset); } } private static IEnumerable<string> ScheduleMessage(Models.Value[] meeting) { return meeting .Select(x => $"{x.Start.DateTime.ToJst().Hour}時{x.Start.DateTime.ToJst().Minute}分から{x.Subject}があります。場所は{x.Location.DisplayName}です。"); } // "今日" -> DateTime private static DateTimeOffset ConvertJname2Datetime(string when) { var temp = DateTimeOffset.UtcNow.ToJst(); var utc = new DateTimeOffset(temp.Date, temp.Offset); switch (when) { case "今日": break; case "明日": utc = utc.AddDays(1); break; } return utc; } // UTC -> JST private static DateTimeOffset ToJst(this DateTimeOffset utc) { var jstZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time"); return utc.ToOffset(jstZoneInfo.BaseUtcOffset); } private static DateTime ToJst(this DateTime utc) { var jstZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time"); return TimeZoneInfo.ConvertTimeFromUtc(utc, jstZoneInfo); }
Microsoft Graph API 叩いてる部分(私の予定を取ってくる)
using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Client; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; namespace MeetingResponseServer.Services { public static class MeetingInfoService { public static async Task<Models.MeetingModel> GetMeeting(DateTimeOffset startTime, DateTimeOffset endTime) { var config = Models.AuthenticationConfigModel.ReadFromJsonFile("appsettings.json"); IConfidentialClientApplication app; app = ConfidentialClientApplicationBuilder.Create(config.ClientId) .WithClientSecret(config.ClientSecret) .WithAuthority(new Uri(config.Authority)) .Build(); string[] scopes = new string[] { "https://graph.microsoft.com/.default" }; AuthenticationResult result = null; result = await app.AcquireTokenForClient(scopes) .ExecuteAsync(); var httpClient = new HttpClient(); var apiCaller = new ProtectedApiCallHelper(httpClient); var requestUrl = $"https://graph.microsoft.com/v1.0/users/{config.MyUserId}/calendarview?startdatetime={startTime.ToUniversalTime().DateTime}&enddatetime={endTime.ToUniversalTime().DateTime}"; var response = await apiCaller.CallWebApiAndProcessResultAsync<Models.MeetingModel>(requestUrl, result.AccessToken); return response; } } }
デモアプリについて
今回のデモアプリでは「ちょまどさんの予定を教えて」と私の予定決め打ちでしたが
もし「ログインユーザーの予定を教えて」としたい場合は
ログイン周りの実装をすることになります。
参考:Azure Active Directory のドキュメント
スライド
湊川あいさんがグラフィックレコーディングしてくれました!
ちょまどさん @chomado のセッションを絵にしました!#decode19 #CM13 #Azure #AzureFunctions#湊川あいグラレコ pic.twitter.com/eXLDhpGvmK
— 湊川あい🌱 #わかばちゃんと学ぶ シリーズ発売中 (@llminatoll) May 29, 2019
ちょまどさんのセッション行って来ました!
途中笑いもあり説明わかりやすかったです
うちでecho買ったけど導入まだっぽいのでやらせてもらおうかな最後のパーティーでステッカー頂きました
生ちょまどさん可愛かったですが早口でなんか安心しましたw#decode19 #CM13 pic.twitter.com/lyvpQjQc17— コルネ (@koruneko32767) May 29, 2019
参考リンク
- Azure Funcitons クイックスタートガイド(公式ドキュメント) : Azure Portal 上(Web 画面ポチポチ)で完結するシナリオ
- Azure Funcitons クイックスタートガイド(公式ドキュメント)Visual Studio を使用して初めてのAzure Functionsを作成する: こっちは VS 上からやるやつ。私はこっちの方が好き
- Azure Active Directory のドキュメント : 今回のデモアプリは「ちょまどの予定」決め打ちなのですが、もし「ログインユーザーの予定」を取得したいとなった場合は、このログインまわりの実装についてもやることになります
Intent や Entity についての説明。#decode19 #CM13 pic.twitter.com/1jLz2AhYUF
— you (@youtoy) May 29, 2019