今回は QnA メーカーを使って作成したナレッジベースを既存のボットに追加します。
QnA メーカーサービス
QnA メーカーを使うと、既存の QA データから簡単にボットを作ることが出来ます。裏側では Azure Search や Web App などのサービスを組み合わせて実現しています。
1. Azure ポータル より「新しいリソース」をクリック。
2. QnA で検索し、「QnA Maker」を選択して作成。
3. 必要な設定を行い「作成」。テストのため全て無料版を利用。
4. 作成が完了したらリソースに移動。Keys より KEY をコピーして保存。
5. QnA Maker のサイト より「Create a knowledge base」をクリック。
6. STEP 1 の QnA メーカーのサービス作成は既に終わっているため、STEP 2 より作成したサービスを選択。
8. 任意の KB URL を指定。ここでは https://azure.microsoft.com/ja-jp/support/faq/ を利用。
10. 作成が完了したら「Save and train」をクリック。
11. Publish タブを選択して、「Publish」をクリック。
12. Publish 完了後、情報を確認。Curl や Postman で動作確認。URL に含まれる ID は KB ID で後ほど利用するためコピー。
QnA メーカーをボットに追加
次に作成した QnA メーカーをボットに追加しますが、そのためには 2 つの作業が必要です。
- QnA メーカーの情報を構成ファイル (.bot) に追加
- コードの追加
CLI を使った構成ファイルの更新
構成ファイルは手動でも更新できますが、CLI を使うとより簡単かつ確実に更新できます。
1. 以下コマンドで必要な CLI ツールをインストール。
npm install -g msbot qnamaker
2. Visual Studio Code の統合コンソールより以下のコマンドを実行。
- qnamaker get kb で KB の情報を取得
- kbId : QnA メーカーで作成された KB の ID
- subscriptionKey : Azure ポータルで作成した QnA メーカーリソースの Keys 情報
- --msbot オプションは出力結果を .bot ファイル用にフォーマット
- msbot connect qna で受け取った QnA ボットの情報を構成ファイルに追加
qnamaker get kb --kbId <KB ID> --subscriptionKey <QnA メーカーのキー> --msbot | msbot connect qna --stdin
3. コマンド実行が完了したら MyBot.bot ファイルの中身を確認。
コードの更新
次にコードを更新して QnA ボットを使えるようにします。
1. 以下コマンドを実行して、Microsoft.Bot.Builder.AI.QnA パッケージを追加。
dotnet add package Microsoft.Bot.Builder.AI.QnA
dotnet restore
2. Startup.cs に using を追加。
using Microsoft.Bot.Builder.AI.QnA;
3. Startup.cs の ConfigureServices メソッドに、以下コードを追加して QnaMaker を IoC コンテナに追加。
// 構成ファイルより QnAMakerService を取得
var qnaMakerService = (QnAMakerService)botConfig.Services.Where(x => x.Type == "qna").First();
var qnaEndpoint = new QnAMakerEndpoint()
{
KnowledgeBaseId = qnaMakerService.KbId,
EndpointKey = qnaMakerService.EndpointKey,
Host = qnaMakerService.Hostname,
};
var qnaMaker = new QnAMaker(qnaEndpoint);
services.AddSingleton(sp => qnaMaker);
4. Dialogs フォルダに QnADialog.cs を追加して、以下のコードに差し替え。
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Extensions.Localization;
using Microsoft.Bot.Builder.AI.QnA;
using CognitiveServices.Translator;
using CognitiveServices.Translator.Translate;
public class QnADialog : ComponentDialog
{
private MyStateAccessors accessors;
private QnAMaker qnaMaker;
private ITranslateClient translator;
private IStringLocalizer<QnADialog> localizer;
public QnADialog(MyStateAccessors accessors, QnAMaker qnaMaker, ITranslateClient translator, IStringLocalizer<QnADialog> localizer) : base(nameof(QnADialog))
{
this.accessors = accessors;
this.qnaMaker = qnaMaker;
this.translator = translator;
this.localizer = localizer;
// ウォーターフォールのステップを定義。処理順にメソッドを追加。
var waterfallSteps = new WaterfallStep[]
{
AskQuestionAsync,
ReplyAnswerAsync,
};
// ウォーターフォールダイアログと各種プロンプトを追加
AddDialog(new WaterfallDialog("qna", waterfallSteps));
AddDialog(new TextPrompt("question"));
}
private async Task<DialogTurnResult> AskQuestionAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// 戻り値は文字型のため、TextPrompt ダイアログを使って送信
return await stepContext.PromptAsync("question", new PromptOptions
{
Prompt = MessageFactory.Text(localizer["whatisquestion"])
},
cancellationToken: cancellationToken);
}
private async Task<DialogTurnResult> ReplyAnswerAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
// 日本語ではない場合、日本語に翻訳
if (userProfile.Language != "ja-JP")
{
var translateParams = new RequestParameter
{
From = userProfile.Language,
To = new[] { "ja" },
IncludeAlignment = true,
};
// LUIS に投げるために、元のインプットを変換したものに差し替え。
stepContext.Context.Activity.Text =
(await translator.TranslateAsync(
new RequestContent(stepContext.Context.Activity.Text), translateParams)
).First().Translations.First().Text;
}
var answer = await qnaMaker.GetAnswersAsync(stepContext.Context);
if (answer.FirstOrDefault() == null || answer.First().Score == 0)
await stepContext.Context.SendActivityAsync(localizer["noanswer"]);
else
{
var answerText = answer.First().Answer;
// 日本語以外の場合は元の言語に戻す
if (userProfile.Language != "ja-JP")
{
var translateParams = new RequestParameter
{
From = "ja-JP",
To = new[] { userProfile.Language },
IncludeAlignment = true,
};
// LUIS に投げるために、元のインプットを変換したものに差し替え。
answerText =
(await translator.TranslateAsync(
new RequestContent(answerText), translateParams)
).First().Translations.First().Text;
}
// 答えを返す
await stepContext.Context.SendActivityAsync(answerText);
}
return await stepContext.EndDialogAsync(true, cancellationToken);
}
}
5. 対応するリソースファイルを追加します。
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="whatisquestion" xml:space="preserve">
<value>質問を教えてください。</value>
</data>
<data name="noanswer" xml:space="preserve">
<value>適した答えが見つかりませんでした。</value>
</data>
</root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="whatisquestion" xml:space="preserve">
<value>What is your question?</value>
</data>
<data name="noanswer" xml:space="preserve">
<value>No good match found in KB.</value>
</data>
</root>
6. myfirstbot.csproj にリソースアイテムを追加。
<EmbeddedResource Update="Resources\QnADialog.en.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\QnADialog.ja.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
7. Startup.cs の ConfigureServices 内で QnADialog を IoC コンテナに追加。
services.AddScoped<QnADialog, QnADialog>();
8. MenuDialog.cs で QnADialog を使えるようにコードを変更。
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Choices;
using System.Linq;
using System;
using Microsoft.Extensions.Localization;
public class MenuDialog : ComponentDialog
{
static private Dictionary<string, string> menus;
static private IList<Choice> choices;
private IStringLocalizer<MenuDialog> localizer;
public MenuDialog(IServiceProvider serviceProvider, IStringLocalizer<MenuDialog> localizer) : base(nameof(MenuDialog))
{
this.localizer = localizer;
// ウォーターフォールのステップを定義。処理順にメソッドを追加。
var waterfallSteps = new WaterfallStep[]
{
InitializeAsync,
ShowMenuAsync,
ProcessInputAsync,
LoopMenu
};
// ウォーターフォールダイアログと各種プロンプトを追加
AddDialog(new WaterfallDialog("menu", waterfallSteps));
AddDialog(new ChoicePrompt("choice"));
AddDialog((WeatherDialog)serviceProvider.GetService(typeof(WeatherDialog)));
AddDialog((ScheduleDialog)serviceProvider.GetService(typeof(ScheduleDialog)));
AddDialog((QnADialog)serviceProvider.GetService(typeof(QnADialog)));
}
public async Task<DialogTurnResult> InitializeAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// メニューの作成。表示される文字と子ダイアログの名称をセットで登録
menus = new Dictionary<string, string>()
{
{ localizer["checkweather"], nameof(WeatherDialog) },
{ localizer["checkschedule"], nameof(ScheduleDialog) },
{ localizer["checkqa"], nameof(QnADialog) }
};
// ChoiceFactory で選択肢に設定する IList<Choice> を作成
choices = ChoiceFactory.ToChoices(menus.Select(x => x.Key).ToList());
return await stepContext.NextAsync();
}
public async Task<DialogTurnResult> ShowMenuAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Choice プロンプトでメニューを表示
return await stepContext.PromptAsync(
"choice",
new PromptOptions
{
Prompt = MessageFactory.Text(localizer["choicemenu"]),
Choices = choices,
},
cancellationToken);
}
private async Task<DialogTurnResult> ProcessInputAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// 答えを確認して次のダイアログ名を取得
var choice = (FoundChoice)stepContext.Result;
var dialogId = menus[choice.Value];
// 子ダイアログの実行
return await stepContext.BeginDialogAsync(dialogId, null, cancellationToken);
}
private async Task<DialogTurnResult> LoopMenu(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// スタックに乗せないように、Replace でメニューを再表示
return await stepContext.ReplaceDialogAsync("menu", null, cancellationToken);
}
}
9. MenuDialog のリソースに checkqa を追加。
<data name="checkqa" xml:space="preserve">
<value>KB を確認</value>
</data>
動作テスト
まとめ
QnA ボットは結構前から使えますが、CLI ツールにより統合が非常に簡単になりました。翻訳ではうまく表現されないものは、言語ごとに QA を用意するなど工夫は必要そうです。