のべ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

