1
votes

I am currently using the Enterprise Bot Template with Luis to route the intent to the correct QnAMaker knowledge base. The QnAMaker knowledge base endpoints are stored in the bot file. I have a request to enable multiprompt: https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/qnamaker-prompting/csharp_dotnetcore

In the example provided, the QnAMaker knowledge base endpoint is in the appsettings.json and I can't figure out how to either add additional knowledge base endpoints to the appsettings and have the intent get sent to the appropriate knowledge base or to have the multiprompt dialog pull the endpoint information from the bot file instead.

In the example, the knowledge base is set at start up

BotBuilder-Samples-master\BotBuilder-Samples-master\experimental\qnamaker-prompting\csharp_dotnetcore\Helpers\QnAService.cs:

private static (QnAMakerOptions options, QnAMakerEndpoint endpoint) InitQnAService(IConfiguration configuration)
    {
        var options = new QnAMakerOptions
        {
            Top = 3
        };

        var hostname = configuration["QnAEndpointHostName"];
        if (!hostname.StartsWith("https://"))
        {
            hostname = string.Concat("https://", hostname);
        }

        if (!hostname.EndsWith("/qnamaker"))
        {
            hostname = string.Concat(hostname, "/qnamaker");
        }

        var endpoint = new QnAMakerEndpoint
        {
            KnowledgeBaseId = configuration["QnAKnowledgebaseId"],
            EndpointKey = configuration["QnAEndpointKey"],
            Host = hostname
        };

        return (options, endpoint);
    }

BotBuilder-Samples-master\BotBuilder-Samples-master\experimental\qnamaker-prompting\csharp_dotnetcore\Startup.cs:

// Helper code that makes the actual HTTP calls to QnA Maker. It is injectable for local unit testing.
        services.AddHttpClient<IQnAService, QnAService>();

Here is my setup without any modifications to the example code:

From the main dialog:

else if (intent == Dispatch.Intent.q_RJH_Test_multiprompt)
        {
            _services.QnAServices.TryGetValue("RJH_Test_multiprompt", out var qnaService);      

            if (qnaService == null)
            {
                throw new Exception("The specified QnA Maker Service could not be found in your Bot Services configuration.");
            }
            else
            {
                QnABotState newState = null;
                var query = dc.Context.Activity.Text;
                var qnaResult = await _qnaService.QueryQnAServiceAsync(query, newState);

                if (qnaResult != null && qnaResult.Count() > 0)
                {
                    // start QnAPrompt dialog
                    await dc.BeginDialogAsync(nameof(QnADialog));
                }
                else
                {
                    await _responder.ReplyWith(dc.Context, MainResponses.ResponseIds.Confused);
                }
            }
        }

The dialog launches correctly and the multiprompt works if the endpoint is provided in the InitQnAService method - but I have not been able to figure out how to use multiple knowledge bases with this setup.

1
What's stopping you from putting "QnAKnowledgebaseId1", etc, in your appsettings and building a new QnAMakerEndpoint with them under a new variable name?JJ_Wailes
@JJ_Wailes - when adding additional variables to handle multiple QnAMakerEndpoints, I can't figure out how to switch between the endpoint variables . So say an intent hits QnAKnowledgebaseId2, when the QueryQnAServiceAsync method is called, the requestUrl is hardcoded based on only one of the endpoint variables. I'm struggling passing the identification of the endpoint from the MainDialog to the QnADialog in the BeginDialogAsync method call.Steven Griffiths

1 Answers

0
votes

Read the whole answer before starting as I changed my thinking halfway through.

How about instead of QnADialog taking an IQnAService, you pass it something a modified version of IBotServices - the implementation of which is here.

You could remove the LuisRecognizer property as it isn't required, and make the QnAMaker property a collection, then read all of your QnA keys from appsettings to create QnAMaker object which you then insert into the collection. You'd then need to have something inside your QnADialog to iterate over and query the various KBs, then handling for if no answer was found.

A rough outline is:

  • Create IBotServices interface which has a single property (something like List<QnAMakerService> QnAMakerKbs)
  • Create BotServices class which implements the IBotServices interface.
  • In the constructor of the BotServices class you should:
    • Create QnaMakerService objects for each of your KBs using the keys from your appSettings.json file.
    • Add the QnAMakerService objects to the QnAMakerKbs property.
  • In Startup.cs replace:
    • services.AddHttpClient<IQnAService, QnAService>();
    • with
    • services.AddHttpClient<IBotService, BotService>();
  • In the constructor for QnABot replace:
    • IQnAService qnaService
    • with
    • IBotService botService,
    • Replace the first parameter of the QnADialog inside the : base call with botService.
  • In QnADialog.cs replace the use of IQnAService with IBotService and rename variables appropriately.
  • Update the logic in the ProcessAsync method to be something like:
var query = inputActivity.Text;
var index = 0;

foreach (var qnaKb in _botServices.QnAMakerKbs)
{
  // Passing in the state won't be supported
  var qnaResult = await qnaKb.GetAnswersAsync(query, (QnABotState)oldState);
  var qnaAnswer = qnaResult[0].Answer;

  // If no answer was found in the KB then go to the next KB,
  // unless it is the last KB in the collection, then we will
  // fall through to the default else handling below
  if (qnaAnswer == "Default no answer found string" && index < _botServices.QnAMakerKbs.Length - 1)
  {
    continue;
  }

  var prompts = qnaResult[0].Context?.Prompts;

  if (prompts == null || prompts.Length < 1)
  {
      outputActivity = MessageFactory.Text(qnaAnswer);
  }
  else
  {
      // Set bot state only if prompts are found in QnA result
      newState = new QnABotState
      {
          PreviousQnaId = qnaResult[0].Id,
          PreviousUserQuery = query
      };

      outputActivity = CardHelper.GetHeroCard(qnaAnswer, prompts);
  }

  index++;
}

An exercise for the OP will be to add the logic for passing the state through to the request so that the prompt state is maintained. My code is based off this sample which doesn't use the prompts. In the sample that you are using a custom QnAService is used which manually builds the request as per this code and passed in the QnABotState to keep track of prompt progress.

This should get you on the right path.

Edit

Using the knowledge I have provided above you could probably just modify the QnAService class (and the IQnaService interface) to change the QnAMakerEndpoint property into a collection and handle the looping inside QueryQnAServiceAsync.