かずきのBlog@hatena

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

Google アシスタントアプリを開発する時に Fulfillment の先を C# で開発する方法(スマートスピーカーを遊びたおす会での LT 内容)

別に C# じゃなくても開発できるんですが個人的に一番好きな言語なので。 Fulfillment は決まった形の JSON でやり取りするだけの、ただの webhook なので POST を受け取る webhook が使えれば何でもいいです。

Azure のアカウントを作る

とりあえずそこそこの金額を無料で使えるので作りましょう。

azure.microsoft.com

あとは、このほかにもハンズオン系イベントとかでもバウチャーとか配られてるケースもあったり Visual Studio Subscription を持ってたりする人は特典として Microsoft Azure を開発目的で毎月数千円使える権利がついてたりします。 https://my.visualstudio.com あたりから有効化してしまいましょう。

ちなみに無料で使えると書いてある製品の中の Azure Functions を今回使います。

f:id:okazuki:20180627181504p:plain

1 月 1000000 回のリクエストってことは 30 日で割ると 1 日あたり 33,333 回のリクエストが OK ってことみたいです。 33,333 回も Google Home と 1 日に対話するか?と言われるとしないと思うので無料の範囲でお試しが出来そうな感じですね。

因みに開発ツールの Visual Studio 2017 は Community Edition が個人利用や小規模企業や学習目的では無料で使えたりします。

visualstudio.microsoft.com

あと、IDE みたいな重いのは好きじゃない人は Visual Studio Code でも普通に開発できます。

Azure Functions を作ろう

Dialogflow で Fulfillment に設定するための Azure Functions を作りましょう。 Azure ポータル上で右上の + ボタンを押して新しいリソース(Azure 上に作るものは全部リソースって言います)を作りましょう。 Functions で検索すると Functions App というのが出てくるので選択します。

f:id:okazuki:20180627182538p:plain

Functions App を選択すると必要事項の入力になります。アプリ名が世界で一意になる必要がある点が注意点なくらいです。大体以下のような意味になります。

  • アプリ名:URL に使われる
  • リソースグループ:フォルダみたいなものの名前。同じリソースグループに入れてるリソースはまとめて削除とかできる。
  • ホスティングプラン:従量課金プランがサーバーレスっぽく動いてる時間に対して課金される。AppService Planは自分でコンピューターを確保するイメージ。従量課金でよさそう。
  • 場所:日本もある
  • Storage:Function App が裏で使ってるストレージの名前。名前はかぶらないように自動生成されてるのでそのままで問題なし
  • Application Insights:90日間アプリログを保存してくれるサービス。あると便利だけど日本で提供されてないので最寄の Southeast Asia あたりが無難

作成を開始すると各リソースのデプロイが始まります。画面上部のベルのマークを押すと通知で進捗を確認できます。作ったリソースへは画面左のリソースグループを選択して、自分が作ったときに入力したリソースグループ名を選択すると行けます。

f:id:okazuki:20180628100856p:plain

そうすると雷マークのアイコンがあるはずなので、それが Functions App の本体です。

f:id:okazuki:20180628101451p:plain

一応簡単に説明すると、ここにできてるものは以下のようなものです

  • ストレージアカウント:Function App が裏で使ってるストレージ
  • Application Insights:ログ保管所。ログの検索やアラートの設定なんかもできる。
  • App Service:これが Function App。App Service には Function App の他にも Web App, Logic App, API App なんかがある
  • App Service プラン:App Service をホストしてるサーバーのイメージ。今回作ったのは従量課金のタイプのサーバーで使った時間で課金。他にも一定の性能を確保するタイプのものもある。

では、ここに Function App を作っていきましょう。LT ではブラウザでぽちぽちと作りましたが個人的には本格的に開発するのに、この画面で開発するのはつらいと思ってるので ローカルでやります。

Visual Studio 2017 でも Visual Studio Code + CLI ツール でも出来ます。Azure Functions には 1.x 系と 2.x 系があるのですが 2.x はプレビューなので現時点では触らない方がいいと思います。バグ踏んでつまったりするとメンドクサイので入門には向かない。

とりあえず今回は Visual Studio Code でしてみたいと思います。

ここを見て 1.x の Azure Functions Core Tools を入れます。(Windows限定なので Mac とかでやる場合は必然的に 2.x になってしまいます)

docs.microsoft.com

Node がいるので入れたら npm でインストールです。

npm install -g azure-functions-core-tools

そして、Visual Studio Code の Azure Functions の拡張機能を入れます。

marketplace.visualstudio.com

入れたら以下の場所をクリックしてプロジェクトを作ります。

f:id:okazuki:20180628102707p:plain

ウィザードがはじまるので、フォルダと言語(C#)を選びます。テンプレートのインストールを求められたら 1.x を選びます。

作成が終わったら、またAzureアイコンをクリックしてFUNCTIONSのところにある雷マークを選択します。これで関数(今回は HTTP に対応して呼び出される関数)を作ります。

f:id:okazuki:20180628103516p:plain

HTTP Trigger を選んで関数名を入れます。とりあえず名前は DialofflowWebhook とかにしておきました。次に名前空間です。これは適当でいいですけど仕事でやるときは「会社名.プロダクト名」(例:Microsoft.Hogehoge)とかにしたりします。Authorization level は認証です。デフォルトの Function でいいでしょう。Function にしてると関数の URL のパラメーターに指定したキーを渡さないと呼べない状態になります。

インターネットに無防備でさらすわけじゃないので少し安心。 これでコードが出来上がります。

あとは、以下のドキュメントにあるような JSON をやり取りすれば OK っぽいです。

Dialogflow Webhook Format  |  Actions on Google  |  Google Developers

C# で JSON をデシリアライズしたらシリアライズする一般的な方法は JSON.NET (Newtonsorf.Json)というライブラリを使ってやることです。このライブラリは Microsoft の各種プロダクトでも使われてるのと、そもそも Functions App のプロジェクトを作ったら最初から入ってたりします。

その際にスクリプト言語みたいに、型は指定しないでカジュアルに解析する方法と、JSON を格納するためのクラスをあらかじめ定義しておいて、そこに流し込んだり、それを元に JSON を吐くといったことが出来ます。 コード補間の恩恵を受けたいので今回はクラスを定義しておこうと思います。

クラスの定義は、JSONがあればさくっとできます。私のおすすめは https://quicktype.io を使うことです。

このページを開いて右上のボタンから実際の画面に行くと JSON を張り付けることができる場所が出てきます。ここに Dialogflow のドキュメントのサンプルの JSON を張り付けます。

f:id:okazuki:20180628105010p:plain

張り付けるとコードが生成されるので名前空間とかクラス名を適切なものに変更してからコピーしておきます。私はリクエストの名前空間を DialogflowWebhook.Requests にしてクラス名を DialogflowRequest にしました。レスポンスは名前空間を DialogflowWebhook.Responses にしてクラス名を DialogflowResponse にしました。

リクエストとレスポンスに対してC#のコードが生成できたらプロジェクトフォルダに拡張子 cs のファイルをリクエスト用とレスポンス用に作成して、そこに張り付けます。

あとは関数本体を作っていきます。

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using DialogflowWebhook.Requests;
using DialogflowWebhook.Responses;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

using ResponsePayload = DialogflowWebhook.Responses.Payload;

namespace DialogflowSample.Functions
{
    public static class DialogflowWebhook
    {
        [FunctionName("DialogflowWebhook")]
        public static async Task<DialogflowResponse> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]DialogflowRequest req,
            TraceWriter log)
        {
            log.Info(req.QueryResult.Intent.DisplayName);
            if (req.QueryResult.Intent.DisplayName == "now")
            {
                var tst = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
                var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tst);
                return CreateResponse($"こんにちは!今は{now.Hour}時だよ!");
            }
            
            return CreateResponse("こんにちは!よくわからなかったよ!");
        }

        private static DialogflowResponse CreateResponse(string text)
        {
            return new DialogflowResponse
            {
                Payload = new ResponsePayload
                {
                    Google = new Google
                    {
                        ExpectUserResponse = true,
                        RichResponse = new RichResponse
                        {
                            Items = new[]
                            {
                                new Item
                                {
                                    SimpleResponse = new SimpleResponse
                                    {
                                        TextToSpeech = text,
                                    }
                                }
                            }
                        }
                    }
                },
            };
        }
    }
}

今の時間を返しているだけです。 HttpTrigger の関数の特徴として引数に特定のクラスを指定していると自動的に body のリクエストの JSON をデシリアライズして入れてくれたり、レスポンスは関数の戻り値を自動で返してくれたりします。便利。

もっと細かく制御したいときは HttpRequestMessage や HttpResponseMessage クラスを使うことで色々設定できます。今回は規定の動作でいいのでこんな感じで。

発行して動作確認

クラウドに発行しましょう。Visual Studio Code の左側の Azure のアイコンを選択してさっき作った Azure Functions を右クリックして Deploy Function App を選択すると配備対象プロジェクトを選ぶように言われるので選んだらデプロイが始まります。

f:id:okazuki:20180628134051p:plain

デプロイが終わると Azure ポータル上で以下のように関数が表示されます。関数の URL の取得を押すと関数を呼ぶ駄目の URL が表示されるのでコピーしておきます。

f:id:okazuki:20180628134246p:plain

Dialogflow での作業

Dialogflow で適当にアプリを作って Fulfillment の先の URL に先ほど入手した Azure Functions で作成した関数の URL を入れます。

f:id:okazuki:20180628134757p:plain

あとは適当に Dialogflow でインテントを作りましょう。今回は Fulfillment に設定した webhook の中でインテントの now があることを期待してるので now インテントを作りました。

f:id:okazuki:20180628134529p:plain

このインテントでは Fulfillment を呼ぶように指定します。

f:id:okazuki:20180628134618p:plain

保存してテストしてみましょう。こんな感じで動いてくれるはずです。

f:id:okazuki:20180628140029p:plain

ローカルテストの方法

因みにクラウドにあげなくてもローカルでもテストできます。

Visual Studio Code で F5 を押すとローカルで実行します。あとは ngrok を使ってインターネット越しにアクセスできるようにして、その URL を Dialogflow の Fulfillment の URL に設定するだけです。

注意点は Azure Functions のエミュレーターがローカルからのアクセスしか認めてないので ngrok で起動するときに以下のように -host-header オプションを指定する必要がある点です。(下の例はポート番号 7071 ですが、実際には自分が起動した Azure Functions のエミュレーターのポート番号にあわせてください)

ngrok http 7071 -host-header="localhost:7071"

クラウドでのデバッグ

Visual Studio 2017 を使ってるとクラウドエクスプローラーというビューから Functions App のリストが見れるのですが右クリックしてデバッガをアタッチってやるとクラウドで動いてるものに対してデバッガがアタッチできます。これは結構強いと思う。 デバッグビルドでアップロードしてないとダメだったかな?

Application Insights

ログですね。Function App 作るときに一緒に作られた Application Insights を開くと色々見れます。 Application Insights を開いてとりあえず検索を選択して出てきた画面で検索するのが一番とっつきやすいと思います。関数の引数にわたって来てる TraceWriter に対して Info とかのメソッドでログをはいてると、ここに表示されます。

f:id:okazuki:20180628141128p:plain

その他にはライブメトリックスストリームとかも面白いです。リアルタイムでアクセスがあるかとか出力されてるログが確認できるので。

f:id:okazuki:20180628141304p:plain