himanago

C#とかAzureとかMS系の技術メモ中心に書きたいです。最近はLINE関連技術やVUIも多め。

Microsoft Open Tech Night #1 で LT をしました

少し間があいてしまいましたが、以下のイベントで LT しました。

msdevjp.connpass.com

タイトルは「Azure Application Gatewayでオンプレ DMZクラウドへ拡張する」。

勉強会のテーマにあわせ、いつもと違ってインフラ系での登壇。

ちょうど業務でやっている外部公開な Web アプリの Azure 移行についての話をしました。

内容

L7 ロードバランサの Application Gateway を使って、オンプレ DMZ にある複数のアプリを段階的に移行していく、という話です。

まず Application Gateway を立てて f:id:himanago:20191210233814p:plain

バックエンドプールに Web App 追加 f:id:himanago:20191210233852p:plain

アプリをひとつずつ移行 f:id:himanago:20191210233911p:plain

という流れで移行します。実際に業務でやっている内容なので、けっこう試行錯誤したところも多かったのですが、当日のツイートで

というのがあり、「おなじことやっている方がいたのかー!」とうれしかったのと同時に、「MVP になると答え合わせの対象として見られるのか…」とちょっとドキッとしました。

いままで以上に、下手なことは言えないな…と思ったので、これからもっと技術にきちんと向き合っていかなければ、とあらためて思いました。

資料

当日の資料はこちらです。

www.slideshare.net

雑記

今回のイベントは代官山に新しくできた「Azure Daikanyama Base」という施設で行われました。はじめての施設でしたが、相性が悪かったのか何なのか、なぜかどうやっても自分の PC が Wi-Fi につながらず。

Zoom での配信もあったため、登壇者のネット接続はマスト。しかたなく、おなじくメイン登壇のひとりだったちょまどさんの Surface Go を借りて登壇しました。

で、そこで問題が。発表者席は教壇的なものではなく、ふつうの椅子に座って使う机だったので、立ちでしゃべると Surface Go の画面が小さすぎて見えない!
そのため発表はだいぶ手間取り、ぎこちないかんじになってしまいました。。

立ちで発表するこだわりは捨てて、いっそ座っちゃえばよかったかも。ちょっと反省。

とりあえず LT はなんとかなったのでよかったんですが、この Azure Daikanyama Base、これから先ちょくちょく利用することになるはずのところ、今後ずっとネットにつながらないままなんだろうか。。

進化した LIFF v2 によるサイト導線強化について

最近、個人的に LIFF v2 が熱いと思っています。

LIFF は LINE Front-end Framework の略で、LINE 内に埋め込める Web アプリを作れるフレームワークです。
主に LINE Bot に Web のインターフェースを組み合わせる用途で使われてきました。

LINE Front-end Framework

LIFF v2

10月の v2 リリースを皮切りに、大きなアップデートが続いています。

リリースノート

いくつかのトピックがありますが、中でも Web サイトから LINE Bot 連携サービスへの導線強化に大きな意味があるものが目立ちます。今回はその観点で変更点を見ていきます。

外部ブラウザで動作するようになった

これまでは、LIFF で作った Web アプリは LINE アプリ内のみでの動作でしたが、PC・スマホ等の一般的な Web ブラウザで動かすことができるようになりました。

デバッグがしやすい

通常のブラウザで動かせるというのは、開発者にとってはデバッグがしやすくなるという利点があります。LINE アプリ内のブラウザには開発者ツールがないのでデバッグがつらかったですが、そこが解消されます。

LINE アプリ内でしか動かない API(メッセージ送信や QR コード読み取りなど)もあるので、そのあたりはこれまで通り alert デバッグ等でなんとかしないといけないのかなとは思いますが、初期化処理や CSS 操作での見た目の問題などベーシックな部分の開発を Web の世界だけで完結できるのはかなり効率が良くなりそう。

エンドポイント URL そのままで動く

LIFF は line://app/xxxxx というような URL を使って LINE 内で開いてきましたが、外部ブラウザで動作させる場合はエンドポイント URL にそのままアクセスすれば OK です。

f:id:himanago:20191208060506p:plain

これは、つまり 既存の Web サイト / Web アプリに LIFF の SDK を何も考えずそのまま導入できる ということを意味します。

すでに稼働している Web サイトを LIFF 対応したい場合のハードルが大きく下がる気がしています。

HTTPS のURLスキームと Deep Link

これが大きい!

外部ブラウザ動作用の URL としてはエンドポイント URL そのままを利用しますが、LIFF v2 では URL として、Deep Link(端末にインストールされているアプリへの遷移)をサポートするようになりました。

スキームはドキュメントにありますが、これまで line://app/{liffId} であった LIFF へアクセスするための URL を、 https://liff.line.me/{liffId}/ とすることで以下の動きをする Deep Link 対応の URL となります。

  • LINE がインストールされている端末(主にスマホを想定)→LINE アプリが起動しその中で LIFF ページが開く
  • LINE がインストールされていない端末(主に PC を想定)→エンドポイント URL にリダイレクト

参考:LINE URLスキームを使う

line://~~ が非推奨になっていることからも、LINE Developers で LIFF 作成時に表示される LIFF URL も https のものに変更されるものと予想しています。

Deep Link の注意点としては、関係のないアカウントとのトーク画面を開いている状態で踏むと、その関係ないトーク画面上でそのまま LIFF が開くので、メッセージ送信の API などを使う場合は気を付けないといけません。
ユーザーの誤操作・誤爆を誘発してしまい予期せぬクレームにも発展しかねないので、LIFF からのメッセージ送信の設計は慎重に行う必要があると考えられます。

逆に Flex Message などを友だちとのトークで使えるような LIFF 便利アプリなんかも作ることができそうではあります。

ログイン強化

LINE ログインとの互換性向上

ユーザー情報の取得が簡単になり、また外部ブラウザでの動作時にも LINE ログインの機能が使えます。
これにより、Web アプリと LINE の連携がかなりしやすくなってきています。

LINE ログインのチャネルにしか作れないようになる

LIFF はこれまで Messaging APIBot)のチャネルの下に作ることが多かったですが、今後は「LINE ログイン」チャネルのににしか作れなくなるようです。

f:id:himanago:20191208061237p:plain

※ LIFF は何か別のチャネルの下にサブで追加するもの

LIFF はこれまで Bot の入力インターフェースの補完という印象が強かったですが、今後は LINE ログイン強化や Deep Link により、Web の世界から LINE へつなぐ役割を大きく担う存在になっていきそうです。

Bot の友だち追加もできる

LIFF の使用を開始するとき、ユーザーに連携する LINE 公式アカウント(Bot など)を友だち追加してもらうことができます。 このあたりの設定は LIFF の設定画面の「ボットリンク機能」から行え、ON にすると認証時に友だち追加をあわせて行えます(Normal と Aggressive はタイミングの違い)。

f:id:himanago:20191208064100p:plain

なので、LINE ログインチャネルにしか作れなくなっても、従来通りの Bot に対する補完としても LIFF は使っていくことができます。

ユーザーの Bot への導線強化に期待大

以上に見てきた機能強化から、LIFF v2 を使うとWeb やメール、他のアプリから LINE 公式アカウントへ誘導することが驚くほど簡単にできてしまいます。
なぜなら、https の LIFF URL にアクセスさせるだけでサービスへの LINE ログイン&Bot 友だち追加を行ってもらえるから です。

URL ひとつでこれだけできるのは、実はとんでもないことなのでは!?と思います。Web サイトと LINE 公式アカウント(Bot)を両方運営している/しようとしている方は、その連携を大きく強化できるので、LIFF v2 の導入を検討したほうが良いと思います。

まとめ

WebからLINE(Bot)への導線をしっかり確保できるので、きちんと設計すれば間違いなくかなりの武器になると思われます。

業務で複数の LINE Bot を開発していますが、LIFF v2 の進化は業務ニーズにこたえる正当な進化という印象で、今回の v2 はとてもうれしく思っています。

ほかにも機能追加など細かい部分でアップデートがあるので、開発や運営を進める中で感じたことなどあればまた書いていこうと思います。

C# によるスマートスピーカースキルのクロスプラットフォーム開発ライブラリ「XPlat.VUI」の紹介

本記事は、Qiita「スマートスピーカー Advent Calendar 2019」の3日目のエントリです。 qiita.com

この記事について

現在個人的に開発をして公開している、C#Googleアシスタント、Alexa、Clova に対応したクロスプラットフォームなスキルを効率よく開発できるようになるライブラリ「XPlat.VUI」を紹介します。

XPlat.VUI は、Googleアシスタント、Alexa、Clova に対応したスキル開発における処理の共通化が可能で、HTTPのリクエストをそのまま渡すだけで、各プラットフォームの起動およびインテントリクエストに対応したレスポンスを作って返せます。

背景

まずは簡単に、「XPlat.VUI」ライブラリを開発した背景について説明します。

C# で開発するメリット

C#スマートスピーカースキルを開発するメリットとして、

  • LINQ、async / await、非同期ストリームなど C# 言語そのものの強み
  • Visual StudioIDE)による強力な開発支援機能
  • 豊富な NuGet パッケージ
  • ASP.NET Core や Xamarin など C# を用いた他のプロダクトとのコード共有
  • Microsoft Azure との親和性

などが挙げられます。

特に Azure では、Azure Functions(Azure におけるサーバーレスなコード実行基盤サービス)を使用することが多く、スキルのバックエンドを効率よく実装できます。

また、スキル開発に C# / Azure Functions を使用する理由の一つに、Durable Functions があります。
Durable Functions は サーバーレスでありあがらステートフルな処理をコードのみで実現する Azure Functions の拡張機能です。
これを用いるとスマートスピーカースキルに多く存在する制約を超えていくことができます(今回は本題からずれるので詳細は割愛)。

クロスプラットフォーム対応の苦しみ

Alexa, Googleアシスタント, Clova では、それぞれカスタムスキル(アクション)を実行するときにやりとりされる JSONスキーマが異なります。
また、それぞれの SDK のしくみや使い方も違います。C# SDK においても、それは同様です。

そのため、3プラットフォームに同じ内容のスキルをリリースしたいと思った場合、ロジック部分はある程度共通化できても、それぞれで異なるライブラリを使用し異なるデータのやりとりを実装しなければなりません。

スマートスピーカースキルは使用感の軽いものが多く、スキル内容のコーディングそのものは簡単に済んでしまうケースが多いはずですが、3つのプラットフォームに対応させようとすると、その差異への対応のほうに多く時間を取られる結果となってしまいます。そのため、単一のプラットフォームでの提供のみで終わってしまうケースも多くあると思われます。

XPlat.VUI とは

そこで、XPlat.VUI を開発しました。

NuGet: www.nuget.org

ソースコードgithub.com

XPlat.VUIは、C# を使って Alexa, Googleアシスタント, Clova それぞれのプラットフォームに対応したスキルを共通の実装で開発できるようにしたライブラリです。
これを使えばそれぞれのプラットフォームでの差異を意識することなく、スキルの処理本体の実装に注力することができます。

基本的な使い方(Azure Functions の例)

Azure Functions でのスキル実装の例を見ていきます。

準備

HTTP トリガーの関数を作成

Visual Studio で Azure Functions のプロジェクトを作成し、HTTPトリガーの関数を作ります。言語は C# です(詳細は省略)。
NuGet より XPlat.VUI のパッケージをプロジェクトに追加しておきます。

AssistantBase 抽象クラスを継承したクラスを用意

以下のように、XPlat.VUI では AssistantBase を継承したクラスを作ります。
このクラス内で、スキルがいつ・どんな応答をするかを組み立てていきます。

public class MyAssistant : AssistantBase
{
}

ロガーなど処理の中で使用したいものをプロパティとして持たせたい場合は、IAssistant インターフェースを使って拡張します。

public interface ILoggableAssistant : IAssistant
{
    ILogger Logger { get; set; }
}

public class MyAssistant : AssistantBase, ILoggableAssistant
{
    public ILogger Logger { get; set; }
}

DI の設定

AssistantBase を継承したクラスをスキルのエンドポイントで使用するのですが、単純に

var Assistant = new MyAssistant();

とするよりも DI(Dependency Injection)によって DI コンテナからもらってくるのがおすすめです。
スキルの実装がエンドポイントと依存せず、実装・テストがしやすくなり、他のサービスクラスとの連携もしやすくなります。

なお Azure Functions で DI するには

が必要なので NuGet で追加・更新しておきます。

Azure Functions での DI は、以下のような Startup クラスを定義します。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using XPlat.VUI;

[assembly: FunctionsStartup(typeof(Sample.Startup))]
namespace Sample
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddAssistant<ILoggableAssistant, MyAssistant>();
        }
    }
}

エンドポイント関数の実装

以下のようにエンドポイントとなる関数を実装します。
Alexa, Googleアシスタント, Clova それぞれで異なるエンドポイントを用意するため、関数メソッドは3つ用意しています。
また、DI を使用するので クラス、メソッド双方から static を外し、コンストラクタで AssistantBase 継承クラスを受け取っています。

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 Sample
{
    public class Endpoints
    {
        private ILoggableAssistant Assistant { get; }

        public Endpoints(ILoggableAssistant assistant)
        {
            Assistant = assistant;
        }

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

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

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

    }
}

それぞれのエンドポイントで、

  • ロガーの受け渡し(任意)
  • RespondAsync メソッドのコール(プラットフォーム種別を指定)
  • 各プラットフォームに合わせたレスポンス変換・返却

を行っています。この部分は基本的に定型なので、毎回同じようなコードになるはずです。

スキル処理本体の実装

続いて、スキルの応答の中身を作っていきます。この部分がスキル構築の核になります。

リクエスト/イベントベースの処理

AssistantBase 継承クラスでのスキル実装は、起動やインテントリクエストなど、タイミングごとに用意された仮想メソッド(RespondAsync 内で実行される)をオーバーライドして行います。

例えば、起動時とインテントリクエストで何か処理を行いたい場合は以下のように OnLaunchRequestAsyncOnIntentRequestAsync をオーバーライドし、それぞれ処理を実装します。

public class MyAssistant : AssistantBase
{
    protected override Task OnLaunchRequestAsync(
        Dictionary<string, object> session, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    protected override Task OnIntentRequestAsync(
        string intent, Dictionary<string, object> slots, Dictionary<string, object> session,
        CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

OnIntentRequestAsync では インテント名やスロット(エンティティ)の値が引数から受け取れるので、処理の中で使用することができます。

XPlat.VUI では、3つのプラットフォームそれぞれで異なるリクエスト種別、イベントのタイミングをなるべく共通化して差異を吸収し、以下のメソッドを用意しています。

メソッド タイミング 備考
OnLaunchRequestAsync 起動時
OnIntentRequestAsync インテントリクエス Googleアシスタントは起動時以外
OnAudioPlayStartedAsync AudioPlayer での再生開始時 Alexa, Clova のみ
OnAudioPlayPausedOrStoppedAsync AudioPlayer での一時停止または停止時 Alexa, Clova のみ
OnAudioPlayNearlyFinishedAsync AudioPlayer での再生終了直前 Alexa のみ
OnAudioPlayFinishedAsync AudioPlayer での再生終了時 Alexa, Clova のみ

レスポンスの実装

オーバーライドメソッドの中では、スキルの応答内容を定義します。応答内容は Response プロパティに対して追加していきます。応答内容の追加に使用できるメソッドには、以下のものがあります。

Speak

スマートスピーカーが読み上げる、通常のテキストのレスポンスを定義します。

Response.Speak("こんにちは");
Play

mp3 オーディオファイルを再生します。効果音などでの使用を想定しています。

Response.Play("https://dummy.domain/myaudio.mp3");
Break

レスポンスの読み上げ・再生の間の休止(無音の空白)を「秒」で指定します。

Response
    .Speak("こんにちは")
    .Break(3)
    .Speak("サンプルスキルへようこそ");

Speak, Play, Break は、Clova を除き内部的に SSML に変換してそれぞれのレスポンスを作っていますが、Clova は SSML に対応していません。

そのため、特に break については標準機能に存在しないため、Clova の場合は指定秒数無音 mp3 を流すレスポンスを返すことで疑似的に SSML の break と同様の機能を実現しています。
ここで使用している無音 mp3 ファイルは silent-mp3リポジトリGitHub Pages として公開したものを使用しているので、スキルのコードやバックエンドサーバーに配置する必要がありません。

KeepListening

スキルの応答後、さらにユーザーによる返答を待つように指定します。
Alexa, Clova では ShouldEndSession の切り替えをし、Googleアシスタントでもレスポンスの expectUserResponse を切り替えています。

引数で Reprompt のメッセージを指定することができます。

Response
    .Speak("お元気ですか?")
    .KeepListening("元気かどうか教えてください。");
メソッドチェーン

これらは以下のようにメソッドチェーンを用いてワンライナーで書くことができます。

Response
    .Speak("お元気ですか?")                     // Speek simple text
    .Break(3)                                   // Pause for 3 seconds.
    .Play("https://dummy.domain/myaudio.mp3");  // Play mp3 audio.

ユーザーID/デバイスID

Request.UserId でユーザーID を、Request.DeviceId でデバイスID を取得できます。

Googleアシスタントでは、ユーザーID は XPlat.VUI で生成した文字列を疑似的なユーザーID として使用しています。また、Googleアシスタントでは現在デバイスID の取得には対応していません。

応用的な機能

そのほか、スキル開発の幅を広げる応用的な機能も持ちます。

プラットフォームごとに異なる処理を行う

3プラットフォームでコードを共有し処理を共通化しても、どうしても各プラットフォームで異なることをしたいことがあります。

たとえば、プラットフォームごとに異なるレスポンスを返したいことがあります。しゃべらせたいセリフを変えたり、流す効果音を変えたりすることが考えられます。
サウンドライブラリーなどはプラットフォームごとに異なるものが用意されていますし、しゃべり方もプラットフォームごとに違うため、言い回しを変えたりすることが考えられます。

そこで、Speak, Play, Break, PlayWithAudioPlayer では引数に Platform.GoogleAssistant などを渡すことで、指定したプラットフォームのみに含まれるレスポンスを定義することができます。
これも通常のレスポンス同様メソッドチェーンで書くことができるので、プラットフォームで異なるレスポンスを直観的に定義できます。

Response
    .Speak("私はグーグルです。", Platform.GoogleAssistant)
    .Speak("私はアレクサです。", Platform.Alexa)
    .Speak("私はクローバです。", Platform.Clova);

また、それ以外にロジックそのものを動作しているプラットフォームで切り替えたい場合は、Request.CurrentPlatform で分岐させることができます。
以下は、Clova の場合だけ LINE Bot(Messaging API)で Push メッセージを送信する処理を実装する方法です。

if (Request.CurrentPlatform == Platform.Clova)
{
    await lineMessagingClient.PushMessageAsync(Request.UserId, "Hello!");        
}

オーディオ再生(AudioPlayer / Media responses)

PlayWithAudioPlayer を使うと、AudioPlayer(Alexa, Clova)や Media responses(Googleアシスタント)を使ってオーディオコンテンツの再生が可能です。
通常レスポンスの Play でも mp3 再生は可能ですが、通常レスポンスでは再生時間の制限があるため専用のオーディオ再生機能がそれぞれのプラットフォームで用意されています。この機能はそれを共通化したものです(Googleアシスタントの Media responses でのオーディオ再生レスポンスの実装が特に辛かった…)。

Response.PlayWithAudioPlayer(
    "sample-id",                          // audio item id
    "https://dummy.domain/myaudio.mp3",   // audio url
    "Sample Title", "Sample Subtitle").   // タイトルなど

この部分は特に3プラットフォームでの差異が激しいので、複雑なイベント制御によるスキル構築をコード共有することは現状難しいです。
そもそも Googleアシスタントでは再生イベントのハンドリングは XPlat.VUI でも対応していないので、個別にインテントを作って処理する必要があります。

今後の機能追加予定

エンドポイントの共通化

現在3プラットフォームごとにエンドポイントとなる関数を別々に定義する必要がありますが、これを共通して1つで済ませられるようにできたらと考えています。

使用イメージとしては以下のようになります(RespondAsync内で送信元のプラットフォームを判別し、適切なレスポンスを返却する)。

namespace Sample
{
    public class Endpoints
    {
        private ILoggableAssistant Assistant { get; }

        public Endpoints(ILoggableAssistant assistant)
        {
            Assistant = assistant;
        }

        [FunctionName(nameof(CommonEndpoint))]
        public async Task<IActionResult> CommonEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            return await Assistant.RespondAsync(req);
        }
    }
}

プロジェクトテンプレート

DI 部分とエンドポイントの実装をあらかじめ済ませた状態のテンプレートを作っておくことで、スキル開発者が 100% スキルの処理の実装に集中できるようにしたいと考えています。
まずは Azure Functions のテンプレートから作ろうと思いますが、折を見て ASP.NET Core や、AWS Lambda 用のものなども作れたらいいなと思います。

画面対応

将来的には Echo Show や Nest Hub, Clova Desk などのスマートディスプレイ対応も共通化できたらおもしろいなと思っています。Clova はまだ SDK が公開されていなかったり、どのような実装になるかは全く見当もつきませんが、いつかやってみたいと考えています。

まとめ

XPlat.VUI を使えば、簡単に3プラットフォーム対応のスキルを開発できます。

また、C# と Azure を用いることで、言語やクラウドの便利なところはもちろん、Visual Studio による恩恵によってもとても快適な開発が可能です(もちろん VS Code でもOK)。Durable Functions 等による応用的なスキルも開発できます。

スキル開発者は、作りたいものを形にすることに注力し、3プラットフォームに展開することでより多くのユーザーにスキルを使ってもらうことができます。

ぜひ XPlat.VUI を用いて様々なスキル開発に挑戦してほしいなと思います。不具合や要望、プルリクなども受け付けておりますので、ぜひ C# でのスキル開発に興味がある方は使ってみていただけるとうれしいです。

スマートスピーカーを遊びたおす会 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