BEACHSIDE BLOG

Azure と GitHub と C# が好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

アダプティブダイアログ を使って Echo Bot を作ってみる (Bot Framework, C#)

先日 Cogbot でお話しした際にコーディングだけですましてしまったので、そのフォローアップとして話した内容やコードをまとめました。

はじめに

Azure Bot Service と Bot Framework の 2020 年5月の新機能では、アダプティブダイアログ (Adaptive Dialogs) を中心にとしたアップデートが多かったなーと感じてます。

個人的な感覚として、Bot Framework は各種のダイアログを中心にコードを書いていくものといっても過言ではないと思いますが、アダプティブダイアログはその中心を取って代わるくらい大きい要素になるものなのかなーと "現時点" では感じてます。といっても Bot Framework は仕様の Braking Changes が多いので今後のことはわかりませんがね。

今まで C# で書いてたダイアログだと複雑度が高くになるにつれて辛くなってきたので、JSON フォーマットで書く アダプティブダイアログ にすることで GUI を使って可視化できるものにしようって流れに進むのかなと感じてます。
その GUI の位置づけが Bot Framework Composer ですね。

とゆーことで今回は、Bot Framework (SDKv4, .NET Core 3.1) のテンプレートの Echo bot をアダプティブダイアログを使った Echo bot に書き換えて動かしてみることで、アダプティブダイアログの雰囲気を理解しつつ、その周辺技術である 言語生成: Language Generationアダプティブ式: Adaptive Expressions に触れてみます。

ちなみに今回使った Bot Framework のバージョンは現時点で最新の v4.9.3 です。

Echo Bot の作成

Visual Studio 2019 で Bot Framework (SDKv4, .NET Core3.1) での開発をするには、多少の事前準備が必要です。
以下のドキュメントの前提条件にある「C# 用 Bot Framework SDK v4 テンプレート」「.NET Core 3.1」「Bot Framework Emulator」のリンクを見ながら準備するとよいでしょう。

Bot Framework SDK for .NET を使用したボットの作成 - Bot Service - Bot Service | Microsoft Docs

事前準備が終わったらそのままドキュメント通りに進めると、Echo bot を作ってデバッグ実行し、Emulator からアクセスするところまで行けます。

Echo bot が動く状態になったところで、本題に取り掛かります。

EchoBot の Adaptive dialog を書いてみる

root.dialog の追加

Adaptive Daialogs を作ってみましょう。

Visual Studio のソリューションエクスプローラーで、Dialogs ってフォルダを作ってその中に root.dialog ってファイルを追加します。

f:id:beachside:20200716004933p:plain:w300

中のコードをこうします。

アダプティブダイアログは、ざっくりいうとトリガーとそれに紐づくアクションを定義することでボットの動作を制御するものです。

今回だと 6行目のトリガーの定義が Microsoft.OnUnknownIntent で、その際の動作が9-10行目に書かれているものです。
初めてだと「は?」って感じだと思いますが、ここでは雰囲気だけつかんでおけばいいと思います。Microsoft.OnUnknownIntent のトリガーは、(正確にはちょっと違いますが)雑に言うと他のトリガーが検知しなかったときって感じです。

やりたいこと・実装したいことができて詳しく知りたくなったら以下のドキュメントをみるとよいです。

ちなみに2行目に書かれてる schema のファイルは存在しませんが、なければ無視されるだけなので気にしなくて大丈夫です。

余談ですが、VS Code の拡張機能である Bot Framework Adaptive Toolがリリースされたら、インテリセンスが聞いていい感じに json がかけたりデバッグができるようになるようです。

出力ディレクトリに常にコピーする設定

.dailog っていうファイルを作ったわけですが、.cs とか特定のファイルじゃない限りはビルド時に出力ディレクトリにコピーされません。
そのため、ファイルを右クリックしてプロパティを開いて、出力ディレクトリにコピー の設定をしてあげる必要があります。
ただ、今回のようなケースではこんなファイルがどんどん増えていくのでファイルごとに設定しているとそのうち忘れてへぼーんとなります。

ということでデフォルトでに出力ディレクトリにコピーする制御を csproj でしましょう。

ソリューションエクスプローラーでプロジェクトを右クリック > プロジェクトファイルの編集 をクリックします。

f:id:beachside:20200717001521p:plain:w480

csproj ファイルが表示されます。appsetting.json の設定がされている ItemGroup を以下のようにします。

拡張子が .dialog のファイルを常に出力ディレクトリにコピーするようにしました。このブログの後半で登場する .lg にも同様の設定をしておきます。

Bot Framework の動作のおさらい

ここから C# のコードをいじっていくわけですが、C# の Bot のプログラムは ASPNET Core の Web アプリなのでその動きがわかってないと辛いです。チャットボットがメッセージを受け取ったさいの動作をざっくりおさらいしましょう。

  1. Web アプリの起動時 Startup.cs で DI などシステムの構成が行われる。
  2. チャットボットへのアクセスするためのエンドポイントは BotController.csPostAsync
  3. 2 の PostAsync メソッドでは、Adapter という概念のオブジェクト (今回の EchoBot だと AdapterWithErrorHandler.cs) の ProcessAsync メソッドが呼ばれるように実装されている。
  4. 3 で実行される ProcessAsync メソッドの内部の実装で、引数に渡された IBot インターフェースを実装したオブジェクト (今回のプログラムだと EchoBot.cs) の OnTurnAsync メソッドが実行される。

ということで、ここら辺を見ながらアダプティブダイアログが呼ばれるようにしていけばよいだけです。簡単ですね。
今回は、4→3→2→1とさかのぼりながら実装していきましょう。

EchoBot.cs を更新する

まずは dialog を読み込むために必要な以下3つの NuGet パッケージをインストールします。

  • Microsoft.Bot.Builder.Dialogs
  • Microsoft.Bot.Builder.Dialogs.Adaptive
  • Microsoft.Bot.Builder.Dialogs.Declarative

インストールしたら、EchoBot.cs の中身を以下のように全面変更です。

  • アダプティブダイアログや言語生成のファイルを読み込むために Bot Framework が用意してる class が ResourceExplorer です。16行目の class のコンストラクターでもらいます(つまりあとで DI を構成します)。
  • 30-36行目のメソッドでダイアログを管理する DialogManager を初期化しています。ここら辺はお決まりの書き方なのでガチで使わない限りは気にしなくて大丈夫です。 - 38-41行目OnTurnAsync メソッドが呼ばれることで、_dialogManager.OnTurnAsync が呼ばれることで root.dialog が動きます。

Adapter を更新する

次はおさらいで説明した 3 の部分です。

BotController.cs で呼ばれてる Adapter を見ていきます。BotController.csでは IBotFrameworkHttpAdapter のインスタンスが使われていますが、Startup.cs で DI の構成を見てわかるとおり実態は AdapterWithErrorHandler class です。

AdapterWithErrorHandler.cs を開いて以下のように編集しましょう。

13行目以降はいじってません。編集しているのは、コンストラクターの引数を追加して、MiddlewareSet として登録していることです。雑に説明すると Adapter の実装する際にその super class となる BotAdapter class で用意されてる Use() 拡張メソッドの UseBotState() を使って、Middleware を登録しているだけです。

これもほぼボイラーテンプレート的な書きかたなのでサンプルで動かす程度ならそんなに気にしなくてよいでしょう。

using の部分は省略してますのでよしなに追加する必要があります)

補足として、なぜ 9-11行目で Bot の State を登録するかというと、EchoBot.cs で DialogManager class を使っていますよね。こいつが State の管理をしているため UserState や ConversationState を管理する設定が必須のためです。

今回のように興味本位でこんなことをしたくなったときに、Bot Framework は Open Source なのでコードをみて使い方を知れるのはよいですね♪

Controller を..

変更するところはありません♪

DI

最後に DI を構成しましょう。Startup.cs を開いて以下のようにします。

今回はデバッグ実行しかするつもりがないので、34行目で State の保存先はインメモリーにしています。変えたければここを Azure Blob や Cosmos DB に変更するだけで、Adapter 側のコードは変更する必要はありません。

38-41行目のようにちょっと癖がある部分もありますが、ここもほぼお決まりの書き方です。後ほど Language Generation をいじるので先に追加してしまいました。

デバッグ実行してみる

Bot Emulator を起動して、Open Bot をクリックします。

f:id:beachside:20200717002801p:plain:w480

私の環境だとデバッグの実行が https://localhost:3979 だったので (https ですよ)、Bot URL には https://localhost:3979/api/messages になります。入力して Connect をクリックしましょう。

f:id:beachside:20200717014327p:plain

無事にボットと繋がったら、適当に文字を打ってみましょう。エコーしてくれたら OK です♪

エラーが起きてしまっていたら、デバッグ実行しているコンソールにエラーが吐かれていますのでいますので確認しましょう。もしくは、AdapterWithErrorHandler class の OnTurnError の中でブレークポイントをはっておけばより細かく確認できます。

f:id:beachside:20200717002733p:plain

ちなみに微妙に動作が不安定な気がするのですが、それはこのコードの問題なのか Framework 側の問題なのかわかってないです。さっきまで普通に動いてたのにいきなり動かなくなる症状に何度か遭遇してますが、私の場合は bin/obj のフォルダを消してビルドしなおすと 100% 解消しています。

挨拶をリッチにしてみる

ここで、アダプティブダイアログと一緒に使われることがおおいであろう 言語生成 (...これ日本語だとイマイチな気がするんですが...)、Language Generation の機能を使ってみます。

ついでにアダプティブ式: Adaptive Expressions も使ってみます。

Language Generation を使う

まずは言語生成..Language Generation...略して LG を作ります。何なのかの説明は使ってみるのわかりやすいので省きます。

今回はとりあえず Dialogs フォルダーの中に common.lg ってファイルを追加します。

f:id:beachside:20200717004819p:plain:w300

中身はこう書きました。Markdown に近いフォーマットですね。フォーマットの詳細は動かした後で興味がわいたらドキュメントを見てみるとよいと思います。

5行目でさらに関数っぽいのを呼んでます。これが Adaptive Expressions で、ここではPrebuilt で用意されてるものを使いました。

root.dialog の更新

root.dialog にトリガーを追加して以下のように更新しました。

  • 4行目で先ほど作った LG のファイルを指定しています。
  • 15行目Microsoft.OnConversationUpdateActivity というトリガーを追加しています。
  • そのアクションは、19行目です。このよくありそうな ${ } って書き方の中で Greeting() ってメソッドを呼んでいます。こう書くだけでさきほどの LG の中の Greeting を認識してくれます。

デバッグ実行

Emulator で再度アクセスしてみましょう。上部の Restart... をクリックすると新しい会話をリスタートすることができます。リスタートするたびにGreeting() の中で定義しているものをランダムに返していることがわかります。

f:id:beachside:20200717010322p:plain

(同じことさっきも書きましたが)
ちなみに微妙に動作が不安定な気がするのですが、それはこのコードの問題なのか Framework 側の問題なのかわかってないです。さっきまで普通に動いてたのにいきなり動かなくなる症状に何度か遭遇してますが、私の場合は bin/obj のフォルダを消してビルドしなおすと 100% 解消しています。

コードの完成版

若干動きが不安定な気がしてますが、Adaptive Dialogs の雰囲気つかむようのものっていちづけで最終的なコードは以下に置いておきました。

GitHub - beachside-project/adaptive-dialogs-echo-bot

エラーを見るたびに「は?何言ってんだ? おめー (bin/obj) の中おかしんじゃね?」って感じなので bin/obj フォルダを消してビルドしなおすと正常に動くのを何度か味わいました。

まとめ

これで個人的には Adaptive Dialog の雰囲気がつかめました。実際はこんなことせずに Bot Framework Composer を使うんでしょうかね?

また、LG を使うことで容易に会話の幅を増やすことができ、Adaptive Expressions も使ってさらに拡張の幅を増やせそうですね。

今までの Dialog 同様に、Adaptive Dialogs でも別の Adaptive Dialogs を呼ぶことで会話をマルチターンで行ったり複雑なやり取りをすることができます。そして JSON で構成されているので、GUI で表現しやすいことも特徴の一つです。

GUI でダイアログを構成できるようになったことで、より多くの人がチャットボットを容易に作れる世界が近づいてる感じがします。

おしまい。

参考ドキュメント