かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Azure Functions で LINE Clova の簡単な Extension を作成するを C# でやってみた

LINE Clova の スキルが開発できるようになりましたね!ということでドキュメントを見ると node.js でやってみようぜ!ってノリで紹介されてます。

clova-developers.line.me

今回は、ここの node.js で作ってるサーバーサイドを Azure Functions + C# の組み合わせでやってみようと思います。

インテントってどんなデータが飛んでくるの?

カスタムインテントとビルドインインテントに対応する必要があるみたいです。 ドキュメントに以下のように記載があります。

Clovaは、ユーザーの音声入力を解析した結果をExtensionサーバーに送信します。サーバーは、受け取ったリクエストに対して適切に応答するように実装する必要があります。
解析された結果が、開発者によって登録されたインテント(カスタムインテント)に該当するの場合、
ClovaはExtensionの実行、登録されたインテントの実行、Extensionの終了など3つのタイプから、1つのリクエストメッセージを送信します。サーバーは受け取ったメッセージに応じて、Extensionの起動、指定されたインテントの処理、Extensionの終了を処理し、結果を返す必要があります。
解析された結果が、Clovaでデフォルトで提供されているインテント(ビルトインインテント)に該当する場合、
Clovaはそれに応じてヘルプの案内、肯定、否定、実行のキャンセルなどのリクエストメッセージを送信し、サーバーはそれに応じて一般的な応答を返す必要があります。

ふむふむ。そして、飛んでくるインテントは JSON 形式なのでそれを見て処理をして返すって流れですね。 ではやってみましょう。

JSON を格納するクラスを作ろう

どんな種類のメッセージがあるのかを見てみると以下のような感じでした。

# リクエストタイプ 
リクエストメッセージは、次の4つのタイプがあります。リクエストのタイプによって、リクエストメッセージのrequestオブジェクトのフィールドの構成が異なります。
- EventRequest
- IntentRequest
- LaunchRequest
- SessionEndedRequest

ふむふむ。とりあえずこの中の InetntRequest が自分が定義したインテントで、 LaunchRequest, SessionEndedRequest あたりが開始終了っぽいですね。EventRequest はよくわかりませんが簡単なスキルの時には使わないっぽい?

とりあえず JSON サンプルの JSON をドキュメントから拝借して https://quicktype.io で C# のクラスに変換して request プロパティはリクエストタイプによって変わるみたいなので、そこは JObject に変更したりという軽い修正を加えて入れ物を作りました。

コードは長いので以下のリンクから飛んでください。

Clova.CSharp/ClovaRequest.cs at master · runceel/Clova.CSharp · GitHub

そして、リクエストタイプごとに request の中身が変わるみたいなので、それ用のクラスも追加します。LauncheRequest と SessionEndedRequest は基本的に空なので EventRequest と IntentRequest に対応します。 作成方法は基本的にドキュメントにサンプルの JSON があるので、それを quicktype.io で変換して微調整という形で生成します。

Clova.CSharp/EventRequest.cs at master · runceel/Clova.CSharp · GitHub

Clova.CSharp/IntentRequest.cs at master · runceel/Clova.CSharp · GitHub

おまけで、思った通りにパース出来るかというのを確認するためのテストコードも追加して完璧!

Clova.CSharp/ClovaRequestTest.cs at master · runceel/Clova.CSharp · GitHub

レスポンスは…?

レスポンスも JSON なので同じ要領でクラスを定義します。

Clova.CSharp/ClovaResponse.cs at master · runceel/Clova.CSharp · GitHub

こっちはちょっと色々出来すぎるので音楽再生の部分とかまではちゃんと見れてない気がする…。

作ってみよう!

対話モデル

対話モデルを作成します。ここに従って。

clova-developers.line.me

ThrowDiceIntent を 1 つ作るだけなので簡単ですね。

サーバーサイド

Azure Functions で行きます。基本的に HttpTrigger でいけそうですね。 ということでさくっと実装。

using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;
using Clova.CSharp.Models;
using Microsoft.Extensions.Logging;
using System;

namespace Clova.DiceSkill
{
    public static class Clova
    {
        private static Random Random { get; } = new Random();

        [FunctionName("clova")]
        public static IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest httpRequest, ILogger log)
        {
            var s = new StreamReader(httpRequest.Body);
            var json = s.ReadToEnd();
            log.LogInformation(json);
            var req = ClovaRequest.FromJson(json);
            if (req.RequestType == RequestType.IntentRequest)
            {
                var intentRequest = req.Request.ToObject<IntentRequest>();
                switch (intentRequest.Intent.Name)
                {
                    case "ThrowDiceIntent":
                        return CreateSimpleTextResponse($"サイコロを1個投げます。結果は{Random.Next(1, 7)}です。", true);
                    default:
                        return CreateSimpleTextResponse("すいません。よくわかりません。サイコロを振ってと話しかけてください。");
                }
            }

            if (req.RequestType == RequestType.LaunchRequest)
            {
                return CreateSimpleTextResponse("こんにちは。サイコロを振ってと話しかけてください。");
            }

            if (req.RequestType == RequestType.SessionEndedRequest)
            {
                return CreateSimpleTextResponse("さようなら。");
            }

            if (req.RequestType == RequestType.EventRequest)
            {
                return CreateSimpleTextResponse("すいません。イベントには対応していません。");
            }

            throw new InvalidOperationException();
        }

        private static JsonResult CreateSimpleTextResponse(string text, bool shouldEndSession = false)
        {
            return new JsonResult(new ClovaResponse
            {
                Response = new Response
                {
                    OutputSpeech = new OutputSpeech
                    {
                        Type = "SimpleSpeech",
                        Values = new Values
                        {
                            Type = "PlainText",
                            Lang = "ja",
                            Value = text,
                        },
                    },
                    ShouldEndSession = shouldEndSession,
                }
            });
        }
    }
}

これで、起動時に「こんにちは。サイコロを振ってと話しかけてください。」というアナウンスが流れて「サイコロを振って」と話しかけると適当にサイコロの目を返してスキルは終了します。

Clova でのスキルの起動のしかた

「ねぇClova、〇〇を開いて」でした。〇〇のところにスキル設定で指定した呼び出し名(メイン)と呼び出し名(サブ)で指定した名前が入ります。

まとめ

世の中のスマートスピーカーの大半がフロントでインテントの解析をして Webhook を呼び出すという流れになってますね。 Webhook の先の実装プラットフォームなどは特に縛られないっぽいので素敵です。