himanago

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

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