Help us understand the problem. What is going on with this article?

Bot Builder v4 でボット開発 : QnA Maker で簡単に QA ボットを作る

今回は QnA メーカーを使って作成したナレッジベースを既存のボットに追加します。

QnA メーカーサービス

QnA メーカーを使うと、既存の QA データから簡単にボットを作ることが出来ます。裏側では Azure Search や Web App などのサービスを組み合わせて実現しています。

1. Azure ポータル より「新しいリソース」をクリック。
image.png

2. QnA で検索し、「QnA Maker」を選択して作成。
image.png

3. 必要な設定を行い「作成」。テストのため全て無料版を利用。
image.png

4. 作成が完了したらリソースに移動。Keys より KEY をコピーして保存。
image.png

5. QnA Maker のサイト より「Create a knowledge base」をクリック。
image.png

6. STEP 1 の QnA メーカーのサービス作成は既に終わっているため、STEP 2 より作成したサービスを選択。
image.png

7. 名前を設定。
image.png

8. 任意の KB URL を指定。ここでは https://azure.microsoft.com/ja-jp/support/faq/ を利用。
image.png

9. 「Create your KB」をクリックして作成。
image.png

10. 作成が完了したら「Save and train」をクリック。
image.png

11. Publish タブを選択して、「Publish」をクリック。
image.png

12. Publish 完了後、情報を確認。Curl や Postman で動作確認。URL に含まれる ID は KB ID で後ほど利用するためコピー。
image.png

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 ファイルの中身を確認。
image.png

コードの更新

次にコードを更新して 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. 対応するリソースファイルを追加します。

QnADialog.ja.resx
<?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>
QnADialog.en.resx
<?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>

動作テスト

1. 英語でまず動作を確認。回答も翻訳されている。
image.png

2. 日本語でも確認。
image.png

まとめ

QnA ボットは結構前から使えますが、CLI ツールにより統合が非常に簡単になりました。翻訳ではうまく表現されないものは、言語ごとに QA を用意するなど工夫は必要そうです。

次の記事へ
目次へ戻る

この記事のサンプルコード

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away