とりあえず、スタート地点は Bot Builder SDK v4 のテンプレートの中の Echo ボットを使います。
テンプレートを Visual Studio に追加するにはこの拡張機能を入れます。
Echo のテンプレートを使って作成したら、とりあえず EchoWithCounterBot.cs
を MyQABot.cs
とかにリネームして、その時に出てくるダイアログでクラス名も変更するようにします。
そして https://qnamaker.aiにアクセスして適当なナレッジベースを作ります。 出来たらトレーニングとかさせて公開しておきましょう。
ここらへんのドキュメントが参考になります。
BotConfiguration.bot に対して QnA Maker 向けの設定を追加します。 msbot コマンドでこんな感じで出来ます。
msbot コマンドは node.js を入れて以下のコマンドで入ります。
npm i -g msbot
msbot connect qna --name "QnA" --kbId <your kbid> --subscriptionKey <your subscription key> --endpointKey <your endpoint key> --hostname <hostname>
--subscriptionKey
が Azure ポータルに作成された QnA Maker のリソースの Keys からとれる値で、--kbid
は PUBLISH したあとに出てくる URL の中の /knowledgebases/<ここ>/generateAnswer
の値で、--endpointKey
が PUBLISH した後の Authorization: EndpointKey <ここ>
になります。--hostname
は Azure ポータルに作成されている QnA Maker のデプロイ先の Web App の概要ページにある URL の値です。
コマンドを BotConfiguration.bot のあるフォルダで打つと QnA Maker の定義が追加されます。
じゃぁ QnA Maker を呼びます。NuGet で Microsoft.Bot.Builder.AI.QnA
を追加します。追加の際に Bot Builder SDK のバージョンが古いと言われたら NuGet パッケージマネージャーからさくっと Bot Builder SDK 関連のものをアップデートしましょう。
次に、QnAMaker の設定を読み込んでアプリから使えるようにします。Bot Builder SDK のサンプルでは、BotServices クラスを定義してそこに各種サービスを突っ込むのがセオリーっぽいです。ここでは QnAMaker だけしか使わないのと他の煩わしい部分を省くために BotServices クラスを省いて QnAMaker クラスを直接使います。
まず、Startup.cs に以下のメソッドを追加します。
private QnAMaker InitQnAMaker(BotConfiguration config) { var qna = (QnAMakerService)config.Services.Single(x => x.Type == ServiceTypes.QnA); returnnew QnAMaker(new QnAMakerEndpoint { KnowledgeBaseId = qna.KbId, EndpointKey = qna.EndpointKey, Host = qna.Hostname, }); }
そして、Startup.cs の ConfigureServices メソッドの AddBot メソッドの呼び出しの後ろらへんに以下のコードを追加して QnAMaker クラスも services に登録しておきます。QnAMaker を作るには BotConfiguration が必要なので、それの生成を AddBot から ConfigureServices の先頭に移動します。
var secretKey = Configuration.GetSection("botFileSecret")?.Value; var botFilePath = Configuration.GetSection("botFilePath")?.Value; var botConfig = BotConfiguration.Load(botFilePath ?? @".\BotConfiguration.bot", secretKey);
そして AddBot の後当たりに以下の行を追加します。
services.AddSingleton(sp => { var qna = (QnAMakerService)botConfig.Services.Single(x => x.Type == ServiceTypes.QnA); returnnew QnAMaker(new QnAMakerEndpoint { KnowledgeBaseId = qna.KbId, EndpointKey = qna.EndpointKey, Host = qna.Hostname, }); });
MyQnABot のコンストラクタで、この QnAMaker クラスも受け取るようにします。
publicclass MyQnABot : IBot { privatereadonly EchoBotAccessors _accessors; privatereadonly QnAMaker _qnaMaker; // 追加privatereadonly ILogger _logger; /// <summary>/// Initializes a new instance of the <seecref="MyQnABot"/> class./// </summary>/// <paramname="accessors">A class containing <seecref="IStatePropertyAccessor{T}"/> used to manage state.</param>/// <paramname="loggerFactory">A <seecref="ILoggerFactory"/> that is hooked to the Azure App Service provider.</param>/// <seealsocref="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1#windows-eventlog-provider"/>public MyQnABot(EchoBotAccessors accessors, QnAMaker qnaMaker, ILoggerFactory loggerFactory) // 引数追加 { if (loggerFactory == null) { thrownew System.ArgumentNullException(nameof(loggerFactory)); } _logger = loggerFactory.CreateLogger<MyQnABot>(); _logger.LogTrace("EchoBot turn start."); _accessors = accessors ?? thrownew System.ArgumentNullException(nameof(accessors)); _qnaMaker = qnaMaker; // 追加 }
あとは OnTurnAsync で呼ぶだけ。
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { if (turnContext.Activity.Type == ActivityTypes.Message) { var answers = await _qnaMaker.GetAnswersAsync(turnContext); if (answers?.Any() ?? false) { // 答えがあった await turnContext.SendActivityAsync(answers[0].Answer, cancellationToken: cancellationToken); } else { // 答えがなかった await turnContext.SendActivityAsync("ごめんね…もっと勉強するわ…", cancellationToken: cancellationToken); } } else { await turnContext.SendActivityAsync($"{turnContext.Activity.Type} event detected"); } }
動かしてみましょう。
元にした QA が 一般法人向け Office 365 とは | よく寄せられる質問なので、それっぽくなってます。
おまけ
わからない QA に対して何かしてみましょう。例えばわからない質問がきたら Microsoft Teams に飛ばすとか。MS Teams の投稿先にしたいチャネルでコネクタを追加します。Incoming Webhook があるので追加します。作成すると URL が貰えるのでコピーします。
本番では appsettings.json とか Azure にデプロイする場合はアプリケーション設定とかに URL を追加してそこから読むのがいいのですが、今回はさぼってハードコードします。マネしないでね。
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { if (turnContext.Activity.Type == ActivityTypes.Message) { var answers = await _qnaMaker.GetAnswersAsync(turnContext); if (answers?.Any() ?? false) { // 答えがあった await turnContext.SendActivityAsync(answers[0].Answer, cancellationToken: cancellationToken); } else { // 答えがなかった await turnContext.SendActivityAsync("ごめんね…もっと勉強するわ…", cancellationToken: cancellationToken); using (var c = new HttpClient()) { await c.PostAsync("さっきゲットした URL をここに", new StringContent(JsonConvert.SerializeObject(MessageFactory.Text($"{turnContext.Activity.From.Name} さんからわからない質問がありました。質問内容「{turnContext.Activity.Text}」")), Encoding.UTF8, "application/json")); } } } else { await turnContext.SendActivityAsync($"{turnContext.Activity.Type} event detected"); } }
ちゃんとわからない質問がきたら Teams にメッセージが出るようになりました。ばっちり。