はじめに
だいぶ前になりますが、@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 API用SDKと合わせて名前空間が 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; }
}
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))
{
await LineMessagingClient.PushMessageAsync(
to: session.User.UserId,
messages: new List<ISendMessage>
{
new TextMessage(string.Format(Messages.LineCongratsMessage, slot.Value)),
}
);
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:
break;
}
}
protected override Task OnSessionEndedRequestAsync(Session session, CancellationToken cancellationToken)
{
Response.AddText(Messages.GoodbayMessage);
return Task.CompletedTask;
}
}
}
ロジック部分(ClovaBase
継承クラス)がシンプルにわかりやすく作れるところがポイントですが、これくらいの規模のスキルだと拡張したインターフェースを用意して関数にDIするところが少し面倒かも(テンプレート化したりすれば軽減できそうではある)。
もちろんDIなしでも使えるので、ClovaBase継承クラスだけ作って関数上でnewしてもOK。てっとりばやく作りたければこれでいいかもです。
おわりに
SDK や OSS の開発をがっつりやったのは初めてだったのでとても、勉強になりました。
今後もメンテナンスなど継続していきます。
そして、この新SDKを使った話を入れた書籍を技術書典で頒布します。
自サークルの『AzureでつくるクロスプラットフォームAIアシスタントスキル』
techbookfest.org
と、お隣の
『LINE API HANDBOOK』
techbookfest.org
にも Clova+C# で参加してます!
それぞれの本に書いた内容、リンクというか、ちょっと似てるけど違う…という箇所があるのでぜひ見比べていただけると!
(手抜きやコピペとか言わないでくださいね…)
日付上はもう明日。
雨の予報ですが、【す22D】と【す23D】でお待ちしてますm(_ _)m