himanago

Azure・C#などのMS系技術やLINE関連技術など、好きな技術について書くブログ

スマートスピーカーを遊びたおす会 vol.8 で少しだけしゃべりました

半月以上たってしまいますが、下記イベントにて、少しだけ話しました。

kotodama.connpass.com

以前、XPlat.VUI という C# でAlexa / Google Assistant / Clova スキルをクロスプラットフォームで開発できる共通化ライブラリを作って公開しましたが、それをちょまどさんのセッションの中で紹介いただけるということで、せっかくならとセッション内で 1~2 分しゃべる機会をいただいたかたちです。

話したこと

MVP になったよ、ということと「XPlat.VUI」ライブラリの紹介です。

すごく短い時間だったので、あまり深いところまで話せませんでしたが、C# で直感的に3プラットフォームのスキル(アクション)の共通のレスポンスが作れるということを説明しました。

github.com

C# 製で、特に Azure Functions でスキルを作るときに便利なのでその環境でのスキル開発にぜひ使ってほしいなと思います。

当日参加された方のツイートで「使いたい」などのコメントもいただけたので、実用レベルで使えるようアップデートしていきたいなという気持ちになりました。紹介していくことは大事ですね。

そのほか

今回はちょまどさんのセッションのなかでちょっとだけ前に出てしゃべったのですが、ちょまどさんの作った空気のなかでバトンを受け取るのはなかなか大変だということがわかりました(笑)

話がとてもおもしろく、会場も非常に盛り上がっていたので、その空気に負けないようにと頑張った結果、クロスプラットフォームは人類の夢!と少し熱く語ってしまいました。
その空気に食らいつくのは大変だったのですが、自分が作れない空気の中でしゃべるというのは予想以上に楽しかったです。

いちおう、MVP受賞後にはじめて人前に立ってしゃべった…ということですが、これはまぁ登壇としてはノーカンとしたいので、MVPとしての初登壇は次の機会ということにしておきたいと思います。

遊びたおす会でも、いつかちゃんと登壇できたらな、と思います。

ちなみに、XPlat.VUI について現在機能追加をしていて、12/3 に スマートスピーカー Advent Calendar 2019 を担当するので、そこで少し紹介する予定です(記事自体はこのブログに書くつもり)。

Microsoft MVP を受賞しました

f:id:himanago:20191117013811j:plain

11/1 に、Microsoft MVP を受賞しました。カテゴリは Microsoft Azure です。
受賞から少し時間がたってしまいましたが、賞状と盾(写真参照)が手元に届いたので、このタイミングでご報告です。

Microsoft MVP とは

一言でいうと、Microsoft に関連する技術を登壇や執筆でコミュニティに広めていく活動に対する表彰です。
Visual Studio の最上位エディションや Azure のクレジットなどの特典もたくさんいただけます。

受賞してから 1年間、MVP を名乗って活動することができるようになります(ただ今回は更新タイミングの関係で、今回の受賞の有効期限はちょっと長い 2021 年 6 月まで)。

詳細は検索するといろいろ出てくるのですが、

あたりの記事や、日本のMVP 事務局を担当されている森口さんのスライドを見るとわかりやすいです。

www.slideshare.net

現在日本の MVP は 200 名弱で、Azure カテゴリでは 40 名ちょっと。この中に選ばれたこと、とても光栄です。
(ちなみに世界では現在 2700 名ちょっとの模様。一時期よりだいぶ減っている印象です)

光栄なのと同時に、MVP として恥ずかしくない活動をしていかなければ、というプレッシャーも少なからずあります。
うまくそれを力に変えて、自信と誇りをもって今後の活動に励んでいきたいと思います。

MVP になったのは

Microsoft の技術に興味をもって調べていくうちに、JAZUG(Japan Azure User Group)や JXUG(Japan Xamarin User Group)など、Microsoft 関連のコミュニティで頻繁に勉強会が開催されていることを知りました。

コミュニティには、自分たちが好きな技術についてとても楽しそうに、かつ高度なレベルで登壇や議論を重ね高めあっているたくさんの MVP の方々がいました。

その姿にとても憧れがあり、自分もそうなりたいと強く思うようになりました。

Microsoft 内外の技術者の交流を通して、それが Microsoft や業界への貢献になっているというのが、Microsoft の昨今のオープンな方向性とも相まって、とてもいいサイクルだと感じています。

そのいまの Microsoft を作ったサティア・ナデラ CEO の名前が賞状に入っているのが、なんだかとてもうれしいですね。

コミュニティで登壇したりするの楽しそう、MVP になりたい、なんてことを考えだしたのが2017年の春ごろ。このブログを作ったのもその時期。

そこから2年半ほどかかりましたが、念願の初受賞。目標がひとつ、達成できました。

やったこと(1年間の振り返り)

活動は Qiita やブログへの投稿や勉強会での登壇などが中心です。

MVP 審査は1年間の活動に対して行われ、今回は 2018年10月から2019年9月までが対象でした。
昨年の10月といえば、ちょうどスマートスピーカーに興味を持ちその界隈での活動をし始めた時期だったので、そこからの1年は Azure × スマートスピーカーという内容が多めです。

評価いただいた活動は MVP ページ にまとまっています。

これからのこと

コミュニティで出会った方や Microsoft の方に本当に、本当によくしていただき、ついに今回の受賞に至りました。

受賞後に参加した勉強会でお会いした方からもたくさんの温かいお言葉や祝福をいただきました。本当にありがとうございます。

Twitter でのこんな簡易な報告にも、たくさんのリプライやいいねをいただきました。

新規のフォロワーさんにも受賞をきっかけにたくさんフォローいただいて、大変ありがたいです。

ここが、新しいスタートラインです。

特に、まだまだ技術面・知識面では諸先輩方に及ばない点が多々あります。
Microsoft MVP は技術力に対する評価ではなく、あくまでも情報発信等によるコミュニティへの貢献に対する評価に過ぎません。
今後はもっと、より多くの正しい情報を伝えられるよう技術向上に努めていきます。

コミュニティに対する貢献では、これまではスマートスピーカー × Azure という文脈での登壇や記事が多かったのですが、今後はもっと幅を広げていけたらと思います。

また、所属企業でのメインの業務が教育担当で研修講師ということもあり、そういった部分を活かしての登壇やハンズオンの実施などたくさんやっていきたいです。
今回の MVP 受賞が、活動の幅を広げるきっかけになったらなと思います。

コミュニティのみなさま、Microsoft のみなさま、これからどうぞよろしくお願いします。

クロスプラットフォームスマートスピーカースキル開発ライブラリ「XPlat.VUI」をリリースしました

はじめに

技術書典7 で本を出しました。この振り返りはまたのちほどしますが…
techbookfest.org

※電子版はここに置いてます:

Microsoft AzureでつくるクロスプラットフォームAIアシスタントスキル - himanago - BOOTH

これのなかで、C#Googleアシスタント、Alexa、Clova に対応したクロスプラットフォームなスキルを効率よく開発できるようになるライブラリを作る話を書きました。
そのライブラリを少し修正し、NuGet公開してみました。

XPlat.VUI

NuGetはこちら。

www.nuget.org

ソースコードはここ。

github.com

READMEにあるようなかんじでシンプルに実装できます。

できること

Googleアシスタント、Alexa、Clova に対応したスキル開発における処理の共通化です。
HTTPのリクエストをそのまま渡すだけで、各プラットフォームの起動およびインテントリクエストに対応したレスポンスを作って返せます。
インテントに設定されたスロット(エンティティ)も読み取れ、それに応じた処理を行うことも可能です。
また、レスポンスにはテキストとmp3などの音源(効果音など)を使えます。
GoogleアシスタントとAlexaは内部でSSMLに変換している)

実装例

おそらくもっとも有名な3プラットフォーム対応のC#+Azure Functions製スキルであろう、以下のスキルを XPlat.VUI で再実装してみました。

元ネタ

ちょまどさん(@chomado)のデモアプリです。
Tech Summit 2018 や LINE DEVELOPER DAY 2018 で披露された「最新のブログ記事」スキル。 github.com

XPlat.VUI 版

これを Fork して改造してみました。動きは同じです。

github.com

commit差分

コード内容

この「XPlat.VUI」ライブラリは、

  • AssistantBase を継承したクラスを作ってそこにスキルの処理を実装
  • それをDIしてエンドポイントの関数で呼ぶ

という流れで実装します。

なお Azure Functions で DI するには

が必要なので NuGet で入れておきましょう。

BlogAssistant

AssistantBase の継承クラスです。

この「最新のブログ記事」スキルの場合、ブログ記事の RSS を見に行く ChomadoBlogService というサービスクラスがあって、これをスキルの本体が使ってます。

また、プラットフォームが Clova の場合のみ、結果を連携するLINE Bot にも送るということをやっています。そこで使うのは LineMessagingClient
せっかくなので、これらも DI したい。コンストラクタインジェクションされるようにしておきます。

中身としては起動リクエストとインテントリクエストに対応したオーバーライドメソッドを作るだけ。
これはエンドポイント関数を指定通りにつくれば自動で呼ばれるようになります。

using Line.Messaging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TechSummit2018.ServerlessSmartSpeaker.Services;
using XPlat.VUI;
using XPlat.VUI.Models;

namespace SmartSpeakerGetLatestArticle
{
    public class BlogAssistant : AssistantBase
    {
        private static string IntroductionMessage { get; } = "こんにちは、LINEデベロッパー・デイのデモアプリです。最新記事を教えてと聞いてください。";
        private static string HelloMessage { get; } = "こんにちは、ちょまどさん!";
        private static string ErrorMessage { get; } = "すみません、わかりませんでした!";

        private ChomadoBlogService Service { get; }
        private ILineMessagingClient MessagingClient { get; }

        public BlogAssistant(ChomadoBlogService service, ILineMessagingClient messagingClient)
        {
            Service = service;
            MessagingClient = messagingClient;
        }

        protected override Task OnLaunchRequestAsync(Dictionary<string, object> session, CancellationToken cancellationToken)
        {
            Response
                .Speak(IntroductionMessage)
                .KeepListening();
            return Task.CompletedTask;
        }

        protected override  async Task OnIntentRequestAsync(
            string intent, Dictionary<string, object> slots, Dictionary<string, object> session, CancellationToken cancellationToken)
        {
            switch (intent)
            {
                case "HelloIntent":
                    Response.Speak(HelloMessage);
                    break;

                case "AskLatestBlogTitleIntent":
                    var blog = await Service.GetLatestBlogAsync();

                    if (blog != null)
                    {
                        Response.Speak($"ちょまどさんのブログの最新記事は {blog.Title} です。");

                        // Clova の場合は LINE にプッシュ通知する
                        if (Request.CurrentPlatform == Platform.Clova)
                        {
                            _ = MessagingClient.PushMessageAsync(
                                to: Request.UserId,
                                messages: new List<ISendMessage>
                                {
                                    new TextMessage($"ちょまどさんの最新記事はこちら!"),
                                    new TextMessage($@"タイトル『{blog.Title}』
{blog.Url}"),
                                });
                        }
                    }
                    else
                    {
                        Response.Speak("ちょまどさんのブログの最新記事は、わかりませんでした。");
                    }
                    break;

                default:
                    Response.Speak(ErrorMessage);
                    break;
            }
        }
    }
}

Clova のときだけ LINE にメッセージを送る、というのは
if (Request.CurrentPlatform == Platform.Clova) という条件で書けます。
こんなかんじでプラットフォーム固有の処理も書きやすく工夫してます。

DI(Startup)

Startup で DI します。

using Line.Messaging;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using System;
using TechSummit2018.ServerlessSmartSpeaker.Services;
using XPlat.VUI;

[assembly: FunctionsStartup(typeof(SmartSpeakerGetLatestArticle.Startup))]
namespace SmartSpeakerGetLatestArticle
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services
                .AddSingleton<ILineMessagingClient, LineMessagingClient>(_ =>
                     new LineMessagingClient(Environment.GetEnvironmentVariable("LineMessagingApiSecret")))
                .AddSingleton<ChomadoBlogService>()
                .AddAssistant<IAssistant, BlogAssistant>();
        }
    }
}

LineMessagingClientChomadoBlogServiceBlogAssistant へ注入され、BlogAssistant はエンドポイント関数のクラスへ注入されます。

ちなみに、AddAssistant メソッドは XPlat.VUI による拡張メソッド。

実は本を書いたときは AddAssistant の 2番目の型引数(BlogAssistantにあたる部分)にnew制約が付いていて、この拡張メソッド内で引数なしコンストラクタが呼ばれる前提でした。
つまり、このクラスへコンストラクタインジェクションできない(=引数ありコンストラクタが呼ばれない)状態だったということ。

今回みたいに LineMessagingClientChomadoBlogService を DI できたほうが絶対楽なので、new制約はないほうがいい。

この「最新のブログ記事」スキルの XPlat.VUI版を作って、かずきさん(@okazuki)にレビューいただいた1「new制約のせいで拡張性がない」ことをいまさらながら実感。
たしかにnew制約ついてたらだめだわ。CEK.CSharp も直さないと2

エンドポイント関数

ここがおそろしくシンプルになります。
DIをする場合は static じゃなくするのがポイント。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using XPlat.VUI;
using XPlat.VUI.Models;

namespace TechSummit2018.ServerlessSmartSpeaker
{
    public class SmartSpeakerEndpoints
    {
        private IAssistant Assistant { get; }

        public SmartSpeakerEndpoints(IAssistant assistant)
        {
            Assistant = assistant;
        }

        [FunctionName("Line")]
        public async Task<IActionResult> Line(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            var response = await Assistant.RespondAsync(req, Platform.Clova);
            return new OkObjectResult(response.ToClovaResponse());
        }

        [FunctionName("GoogleHome")]
        public async Task<IActionResult> GoogleHome(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            var response = await Assistant.RespondAsync(req, Platform.GoogleAssistant);
            return new OkObjectResult(response.ToGoogleAssistantResponse());
        }

        [FunctionName("Alexa")]
        public async Task<IActionResult> Alexa(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            var response = await Assistant.RespondAsync(req, Platform.Alexa);
            return new OkObjectResult(response.ToAlexaResponse());
        }
    }
}

基本的にこれだけになります。

実装のまとめ

DIとエンドポイントはほぼ定型でいけるので、テンプレート化したらもっと楽できるだろうなと思っています。

なので、テンプレートから枠を作ったあとに開発者がやることは AssistantBase を継承したクラスにおける起動・インテントリクエストをさばく処理の実装だけ。
(プラットフォームごとのリクエスト・レスポンス形式の差異などは XPlat.VUI で吸収してくれるからロジック部分を書くだけでよい)

残りの課題

セッションデータの保持が実装できていないのでそこを実装したいなと思ってます。

その他サンプル

書籍執筆時に作ったスキル例。
いま見返すとちょっとうまくないな…という部分もあるのであとで直すかもです。

サイコロをいくつか転がして合計を教えてもらうスキル(スロットを使った例)

github.com

数を裏で数えるだけのスキル(Durable Functionsを使った例)

github.com

おわりに

これを使ったハンズオンなんかやってみたいなーと思い始めました。
XPlat.VUI は思い切って(最大ではない)公約数でライブラリ化したので、シンプルなスキルなら一瞬でできるようになるはず。
簡単なスキルを作りたいときの選択肢として C# + Azure Functions を挙げてもらえるようにしたいな…と思うので、もう少し育てて何かイベント企画できたらなと思います。


  1. 入稿後に見ていただいたので、書籍のおまけとして「かずきさんレビュー」を付けました。なんて自虐的なコンテンツ。でもこれがかなりウケた。

  2. もともと CEK.CSharp のほうで同じく DI させるための拡張メソッドである AddClova の型引数にnew制約をつけていた名残。これも実装方法がうまくなかった(し、まずさに気づけなかった)。

CEK.CSharp がアップデートで便利になりました

はじめに

だいぶ前になりますが、@kenakamu108さんに誘われて、LINE Developer Community の C# SDK 開発にかかわらせてもらっています。
自分は、いつも使っている Clovaスキル開発用のSDKである CEK.CSharp のアップデートをやらせてもらえることになり、少し前にリニューアル版をリリースしました(0.2.1 まではちょっとバグがあるので、0.2.2 が出たらアップデート推奨です)。

www.nuget.org

CEK.CSharpのリニューアル

今回のリニューアル(0.2.x 以降)では、他のLINE APISDKと合わせて名前空間LineDC.XXX に統一されるようになったり、推奨の使い方が変化したりとわりかし大きな変更になっています。

アップデートは、4人いるC# SDKチームのメンバーで意見を出し合いながら作ってきました。
#便利だなーと思う部分はだいたいもらった意見がもとになっているので、自分の功績はいったいどこなのか…(笑)

これまでのCEK.CSharp では CEK のリクエスト・レスポンスの薄いラッパーというかんじで、そのままの構成でオブジェクトを作っていかないといけなかった(オリジナルのリクエスト、レスポンスのスキーマ構造を理解していないといけない)ので、それが結構面倒でした。

新しい CEK.CSharp ではそこを軽減し、開発者がやることを実装そのものにできるよう、考え方が変わっています。

以下、新しくなったSDKの使い方を簡単に紹介します。

使い方

抽象クラス ClovaBase を継承したクラスを作る

まず ClovaBase という抽象クラスを継承します。これがスキルのロジック部分本体になります。
いままでになかったものですが、これがいちばん重要かつ大きな変更点です。

public class MyClova : ClovaBase
{
}

何か外部から受け取ったりするための機能を持たせたいときは、(このあと出てくるDIのために)拡張用のインターフェースを作ります。

public interface ILoggableClova : IClova
{
    ILogger Logger { get; set; }
}

public class MyClova : ClovaBase, ILoggableClova
{
    public ILogger Logger { get; set; }
}

DIを使ってインスタンスを取得

HTTPリクエストをさばく部分(Function Appの関数メソッドだったり、ASP.NET Coreのアクションメソッドだったり)で ClovaBase 継承クラスを使うために、DI します。
DI用に AddClova という拡張メソッドを用意しています。
最近 Azure FunctionsでもDIできるようになったのでうれしいですね(以下は ASP.NET Core の例)。

public void ConfigureServices(IServiceCollection services)
{
    services.AddClova<IClova, MyClova>();
    services.AddMvc();
}

RespondAsyncメソッドの呼び出し

HTTPリクエストをさばく処理は以下のようになります。
clova はDIで受け取ったオブジェクトの RespondAsync を呼んでその戻り値をHTTPレスポンスに入れるだけ。

var response = await clova.RespondAsync(Request.Headers["SignatureCEK"], Request.Body);
return new OkObjectResult(response);

リクエストごとのオーバーライドメソッド

リニューアルの一番のポイントはここです。
従来、switch とかで LaunchRequest やら IntentRequest ごとに分岐を作らなきゃいけなかった部分ですが、分岐はSDK内に隠蔽して、事前に各リクエストに対応するメソッドが用意されていて、RespondAsync を呼べばそれぞれのタイミングで動くようになってます。
ちょうど、Bot Framework の OnTurnAsync みたいなイメージで、開発者は独自の処理を行いたいタイミングのメソッドをオーバーライドしてコードを書くことになります。

public class MyClova : ClovaBase
{
    protected override async Task OnLaunchRequestAsync(
        Session session, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

オーバーライドできるメソッドはこれだけあります。
イベントリクエストをイベントごとに分けて用意したのはちょっとしたこだわりポイントです。

メソッド 引数
OnLaunchRequestAsync Session session, CancellationToken cancellationToken
OnIntentRequestAsync Intent intent, Session session, CancellationToken cancellationToken
OnEventRequestAsync Event ev, Session session, CancellationToken cancellationToken
OnSkillEnabledEventAsync Event ev, Session session, CancellationToken cancellationToken
OnSkillDisabledEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayFinishedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayPausedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayResumedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayStartedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayStoppedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnProgressReportDelayPassedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnProgressReportIntervalPassedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnProgressReportPositionPassedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnStreamRequestedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnSessionEndedRequestAsync Session session, CancellationToken cancellationToken
OnUnrecognizedRequestAsync CEKRequest request, CancellationToken cancellationToken

レスポンスの作り方

各オーバーライドメソッド内でやる、レスポンス内容の作成はメソッドチェーンで書けます。
メソッドチェーンのアイディアはチームの打ち合わせの中で出たものなんですが、とてもすっきり書けるのでとても気に入っています。

Response
    .AddText("こんにちは!");
    .AddUrl("https://dummy.domain/myaudio.mp3");
    .AddText("Hi!", Lang.En);
    .AddUrl("https://dummy.domain/myaudio.mp3", Lang.En);

ShouldEndSession も隠蔽していて、KeepListening というメソッドとセッションが継続されます。

Response
    .AddText("What do you want?", Lang.En)
    .KeepListening();

AudioPlayer

個人的によく使う AudioPlayer もシンプルなメソッドを用意しました。

メソッド 引数
PlayAudio Source source, AudioItem audioItem, AudioPlayBehavior playBehavior
EnqueueAudio Source source, params AudioItem[] audioItems
PauseAudio -
ResumeAudio -
StopAudio -

サンプル

Before / After がわかりやすいように、旧SDKでできたものを新SDKに変えて見比べられるようにしてみました。 題材は、少し前の スマートスピーカーを遊びたおす会 で話題になったちょまどさんの滑舌スキルです。

FunctionsのDI

スキルの本体である KatsuzetsuClova をDIします。

using LineDC.CEK;
using Line.Messaging;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using KatsuzetsuApp.Settings;

[assembly: FunctionsStartup(typeof(KatsuzetsuApp.Startup))]
namespace KatsuzetsuApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services
                .AddClova<ILineMessageableClova, KatsuzetsuClova>()
                .AddSingleton<ILineMessagingClient, LineMessagingClient>(_ => new LineMessagingClient(Keys.token));
        }
    }
}

関数本体

KatsuzetsuClova に、同じくDI で受け取った LineMessagingClient を 引き渡します。
こうやって引き渡せるようインターフェースの拡張が必要というわけです。

using System;
using Line.Messaging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace KatsuzetsuApp
{
    public class ClovaEndpoint
    {
        private readonly ILineMessageableClova _clova;

        public ClovaEndpoint(ILineMessageableClova clova, ILineMessagingClient lineMessagingClient)
        {
            _clova = clova;
            _clova.LineMessagingClient = lineMessagingClient;
        }

        [FunctionName("clova")]
        public async Task<IActionResult> Clova(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var response = await _clova.RespondAsync(req.Headers["SignatureCEK"], req.Body);
            return new OkObjectResult(response);
        }
    }
}

ロジック部分

スキルの本体。処理を行いたいリクエストのメソッドをオーバーライドして実装します。

using KatsuzetsuApp.Settings;
using LineDC.CEK;
using LineDC.CEK.Models;
using Line.Messaging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace KatsuzetsuApp
{
    public class KatsuzetsuClova : ClovaBase, ILineMessageableClova
    {
        public ILineMessagingClient LineMessagingClient { get; set; }

        protected override Task OnLaunchRequestAsync(Session session, CancellationToken cancellationToken)
        {
            Response
                .AddText(Messages.WelcomeMessage)
                .KeepListening();

            return Task.CompletedTask;
        }

        protected override async Task OnIntentRequestAsync(Intent intent, Session session, CancellationToken cancellationToken)
        {
            switch (intent.Name)
            {
                case IntentNames.DefaultIntent:
                    {
                        // 滑舌オーケー
                        if (intent.Slots.TryGetValue(SlotNames.NamamugiNamagomeNamatamago, out var slot) ||
                            intent.Slots.TryGetValue(SlotNames.SyusyaSentaku, out slot))
                        {
                            // LINE messaging に投げる
                            await LineMessagingClient.PushMessageAsync(
                                to: session.User.UserId,
                                messages: new List<ISendMessage>
                                {
                                    new TextMessage(string.Format(Messages.LineCongratsMessage, slot.Value)),
                                }
                            );
                            // Clova に喋らせる
                            Response
                                .AddText(Messages.GenerateCongratsMessage(slot.Value))
                                .KeepListening();
                        }
                        // 滑舌ダメです
                        else
                        {
                            await LineMessagingClient.PushMessageAsync(
                                to: session.User.UserId,
                                messages: new List<ISendMessage>
                                {
                                    new TextMessage(Messages.LineWrongMessage),
                                }
                            );

                            Response
                                .AddText(Messages.WrongPronunciationMessage)
                                .KeepListening();
                        }

                        break;
                    }
                default:
                    // noop
                    break;
            }
        }

        protected override Task OnSessionEndedRequestAsync(Session session, CancellationToken cancellationToken)
        {
            Response.AddText(Messages.GoodbayMessage);
            return Task.CompletedTask;
        }
    }
}

ロジック部分(ClovaBase継承クラス)がシンプルにわかりやすく作れるところがポイントですが、これくらいの規模のスキルだと拡張したインターフェースを用意して関数にDIするところが少し面倒かも(テンプレート化したりすれば軽減できそうではある)。

もちろんDIなしでも使えるので、ClovaBase継承クラスだけ作って関数上でnewしてもOK。てっとりばやく作りたければこれでいいかもです。

おわりに

SDKOSS の開発をがっつりやったのは初めてだったのでとても、勉強になりました。
今後もメンテナンスなど継続していきます。

そして、この新SDKを使った話を入れた書籍を技術書典で頒布します。

自サークルの『AzureでつくるクロスプラットフォームAIアシスタントスキル』
techbookfest.org

と、お隣の

『LINE API HANDBOOK』
techbookfest.org

にも Clova+C# で参加してます!

それぞれの本に書いた内容、リンクというか、ちょっと似てるけど違う…という箇所があるのでぜひ見比べていただけると!
(手抜きやコピペとか言わないでくださいね…)

日付上はもう明日。
雨の予報ですが、【す22D】と【す23D】でお待ちしてますm(_ _)m

Googleアシスタントでも ShouldEndSession = true; したい

はじめに

ちょっと間が空いてしまいましたが、技術書典7に向けて執筆していた中で見つけたスマートスピーカースキル開発まわりの小ネタを書きます。
今回もGoogleアシスタントC#です。

ShouldEndSession

タイトルに書いた通りなんですが、Alexa と Clova はレスポンスで ShouldEndSession = true; としてあげると、その返答でスキルのセッションを終了させることができます。

Googleアシスタントでは、会話の終了は Dialogflow のインテントの設定で行うのが一般的なようで、バックエンド側の指示でセッションを終了させるようなプロパティは(C#の)SDKでは見当たりませんでした。

f:id:himanago:20190919233530p:plain

3プラットフォームでなるべく同じような開発方法でできたらな、とあきらめきれず探していたら、以下のように書けば Dialogflow 側で設定しなくてもバックエンドの指示だけでセッションを終了できるということがわかりました。

var webhookResponse = new WebhookResponse
{                
    FulfillmentText = "もうこの話は終わりね。",
    Payload = new Struct
    {
        Fields =
        {
            {
                "google", Value.ForStruct(new Struct
                {
                    Fields =
                    {
                        { "expectUserResponse", Value.ForBool(false) },
                    }
                })
            },
        }
    }
};

Payload の中で、expectUserResponsefalse にすればOK。
これで3プラットフォームで同じような作り方ができます!

目的は共通化

実はこれを調べていた目的は、3プラットフォームの「共通化」でした。

3つのプラットフォームに向けたスキル開発をする際、コードをいかに共通化できるかは、設定できる項目をどれだけ共通化できるかがカギです。
3プラットフォームの最大公約数だけでスキル開発が完結できるのであれば、共通化ライブラリが作れるからです。
(なので ShouldEndSession 的な機能をGoogleアシスタントで見つけることは非常に大きな意義があった)

おわりに

そんなこんなで、技術書典で頒布する本、入稿できました(ほんとぎりぎりでどうなることかと…)。

実はこの本の中で、C#クロスプラットフォーム開発できるライブラリを作る話を紹介しています。
そして、実はそのライブラリを、すでに NuGet で公開していたりします。
…ただ、ちょっとまだ粗削りな部分があるので、もう少し詰めたらここにも記事を書いて、少しずつアピールしていきたいな、と。。

目指すのはスマートスピーカースキルにおける Xamarin.Forms みたいなもの。
最大公約数な機能を共通のコードで実現しつつ、必要に応じてプラットフォーム固有の実装ができるようなものにしたいと思っています。

ということで技術書典も今週末…。。
『AzureでつくるクロスプラットフォームAIアシスタントスキル』、ぜひいらしてください!m(_ _)m

techbookfest.org

Googleアシスタント対応バックエンドを C#+Azure Functions で「簡単に」作る(ProtcolBufJsonResult不要論)

はじめに

今日もスマートスピーカースキル開発まわりの小ネタを。
今日はGoogleアシスタントでいきます。

ProtcolBufJsonResult 的なクラス

Googleアシスタント用のバックエンドを C# + Azure Functionsで作るときの話です。

Azure Functionsでバックエンドを作るのはとてもお手軽なんですが、毎回こんなクラスを用意していました(地味に面倒)。

JSONシリアライズをするための、IActionResult の実装です。

public class ProtcolBufJsonResult : IActionResult
{
    private readonly object _obj;
    private readonly JsonFormatter _formatter;

    public ProtcolBufJsonResult(object obj, JsonFormatter formatter)
    {
        _obj = obj;
        _formatter = formatter;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json; charset=utf-8"));
        var stringWriter = new StringWriter();
        _formatter.WriteValue(stringWriter, _obj);
        await context.HttpContext.Response.WriteAsync(stringWriter.ToString());
    }
}

C# で開発をされている諸先輩方が使っていたので必須なのかなぁとまねしてたのですが…。

実はいらなかった

作らなくてもよかったみたいです。

return new OkObjectResult(webhookResponse.ToString());

というようにレスポンスのオブジェクトを ToString() するだけでシンプルに返却できました。

関数コード全体だとこんなかんじです。

※もはや会話でもなんでもないですが…

using Google.Cloud.Dialogflow.V2;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace Sample
{
    public static class GoogleEndpoint
    {
        [FunctionName(nameof(GoogleEndpoint))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "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 webhookResponse = new WebhookResponse();

            switch (webhookRequest.QueryResult.Intent.DisplayName)
            {
                case "Default Welcome Intent":
                    webhookResponse.FulfillmentText = "こんにちは。";
                    break;

                case "CustomIntent":
                    webhookResponse.FulfillmentText = "そうですね。";
                    break;

                case "Default Fallback Intent":
                default:
                    webhookResponse.FulfillmentText = "特に何もしません。";
                    break;
            }

            return new OkObjectResult(webhookResponse.ToString());
        }
    }
}

おわりに

意外と簡単にできるなぁという気づきでした。

技術書典で頒布する本では上記の書き方で書いてます。

頒布するのは Azure && (C# || ノンコーディング) でクロスプラットフォームスマートスピーカースキル開発する本で、順番にやっていくとAzureのPaaSとスマートスピーカースキル開発両方をマスターできるように構成してます!

『AzureでつくるクロスプラットフォームAIアシスタントスキル』、興味のある方はぜひm(_ _)m

サークルチェックもお願いします!

techbookfest.org

Alexa.NET を使うなら ResponseBuilder がおすすめ

前置き

技術書典7 で本を出します!
Azure && (C# || ノンコーディング) でクロスプラットフォームスマートスピーカースキル開発する本です。

techbookfest.org

いままで試してきたことをまとめたいなぁと思って書いていたのですが、書いているといろいろ気づくこととか新たに知ることも多かったりします。

そこで、技術書典までにちょこちょこそういった小ネタを少しずつブログのほうにも載せていこうと思います。

C# で Alexa開発

今回は Alexaスキルの開発の話です。
普段 Alexa.NET という NuGetパッケージを使ってC#+Azure Functions でスキル開発しています。

www.nuget.org

で、このSDKなんですが、よくよく見てみると便利な機能がありました。

今回紹介するのは、そのひとつ ResponseBuilder です。

ResponseBuilder とは

Alexa に返すレスポンスオブジェクトを簡単に作れる便利クラスです。

代表的な機能は TellAsk の2つです。

このメソッドの戻り値をそのままHTTPレスポンスで返してしまえばOKという、非常にシンプルで使い勝手のいい機能です。

Alexa のレスポンス

Alexa のレスポンスは shouldEndSessiontrue にするか false にするかで対話を継続するかどうかを切り替えることができます。

shouldEndSessionfalse にして返すとAlexa がしゃべったあとユーザーの返答を待つためにリスニング状態でスキルのセッションが維持されます。

ResponseBuilder.Tell

Tell メソッドは、単純なテキストのメッセージの読み上げをセッションを継続せずに行います(shouldEndSession: trueでレスポンスを作ってくれる)。

Tell は「伝える」という意味であり、質問ではありません。なのでユーザーの返答が不要なときに使うことになります。

ResponseBuilder.Ask

対して Ask メソッドは、単純なテキストのメッセージの読み上げをセッションを継続して行います(shouldEndSession: falseでレスポンスを作ってくれる)。

Ask は「尋ねる」という意味で、質問です。だからユーザーの返答が必要。わかりやすい!

中学生でも知っているような明確な単語で動きをシンプルに定義していてとてもよいです。こういうSDKの作り方、見習いたい。

サンプルコード

Azure Functions でのコードです(なんてことない「はい」「いいえ」だけの会話)。

いちいち response.ShouldEndSession = false; とか書かなくていいのですっきりです。

あと地味にビルトインインテントを定数で用意してくれてるのもポイント高い。

using Alexa.NET;
using Alexa.NET.Request;
using Alexa.NET.Request.Type;
using Alexa.NET.Response;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace Sample
{
    public static class AlexaEndpoint
    {
        [FunctionName(nameof(AlexaEndpoint))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var request = JsonConvert.DeserializeObject<SkillRequest>(await req.ReadAsStringAsync());

            // レスポンス用の変数を宣言
            SkillResponse response = null;

            switch (request.Request)
            {
                case LaunchRequest lr:
                    response = ResponseBuilder.Ask(
                        "こんにちは。お元気ですか?", new Reprompt("元気ですか?"));
                    break;

                case IntentRequest ir:
                    switch (ir.Intent.Name)
                    {
                        case BuiltInIntent.Yes:    // 「はい」と答えたとき
                            response = ResponseBuilder.Ask(
                                "それはよかったです。まだお話ししますか?", new Reprompt("まだお話しますか?"));
                            break;

                        case BuiltInIntent.No:     // 「いいえ」と答えたとき
                            response = ResponseBuilder.Tell("そうですか。それではまた元気なときにお話ししましょう。");
                            break;

                        default:
                            response = ResponseBuilder.Tell("またお話ししましょう。");
                            break;
                    }
                    break;
            }

            return new OkObjectResult(response);
        }
    }
}

C# 8.0 の正式リリースが来たら、switch式でもっとすっきりしそうな可能性を秘めてる気がする。

おわりに

ということで、あまり記事として紹介されていない話かなと思ったので紹介してみました(GitHubのREADMEにはちゃんと書いてある)。

技術書典で頒布する本にはこういった話もたくさん入れつつ体系的にまとめようとしています(まだできてないw)。

『AzureでつくるクロスプラットフォームAIアシスタントスキル』、興味のある方はぜひm(_ _)m

サークルチェックもお願いします! techbookfest.org