プロキシ環境下でBot Framework から QnA Makerを呼び出せない問題への対処


TL;DR

環境

  • Visual Studio 2019 16.4.3
  • .NET Core 2.1
  • Microsoft.Bot.Builder.AI.QnA 4.7.1
  • Microsoft.Bot.Builder.Integration.AspNet.Core 4.7.1

事象

Visual StudioでBot Serviceを起動し、Bot Framework EmulatorでQnA Makerを利用したBotをテストした場合、以下のようなメッセージが表示されサービスに接続できない

コード

EchoBot.cs
private async Task AccessQnAMaker(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
   var results = await EchoBotQnA.GetAnswersAsync(turnContext);
   if (results.Any())
   {
      await turnContext.SendActivityAsync(MessageFactory.Text("QnA Maker Returned: " + results.First().Answer), cancellationToken);
   }
   else
   {
      await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, could not find an answer in the Q and A system."), cancellationToken);
   }
}

エラー

var results = await EchoBotQnA.GetAnswersAsync(turnContext);の行で以下のエラーが発生する

Bot Framework Emulator

The bot encounted an error or bug.
To continue to run this bot, please fix the bot source code.

AdapterWithErrorHandler.csでの例外(Exception)

StacTrace
   at System.Net.Http.ConnectHelper.<ConnectAsync>d__2.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnectionPool.<CreateConnectionAsync>d__44.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnectionPool.<WaitForCreatedConnectionAsync>d__49.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnectionPool.<SendWithRetryAsync>d__39.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Net.Http.RedirectHandler.<SendAsync>d__4.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__62.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.HttpRequestUtils.<ExecuteHttpRequestAsync>d__4.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.GenerateAnswerUtils.<QueryQnaServiceAsync>d__13.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.GenerateAnswerUtils.<GetAnswersRawAsync>d__9.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.QnAMaker.<GetAnswersRawAsync>d__22.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.QnAMaker.<GetAnswersAsync>d__21.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at EchoBot3QnA.Bots.EchoBot.<AccessQnAMaker>d__10.MoveNext() in C:\Users\User1\~
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at EchoBot3QnA.Bots.EchoBot.<OnMessageActivityAsync>d__8.MoveNext() in C:\Users\User1\~
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.BotFrameworkAdapter.TenantIdWorkaroundForTeamsMiddleware.<OnTurnAsync>d__0.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.MiddlewareSet.<ReceiveActivityWithStatusAsync>d__3.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.BotAdapter.<RunPipelineAsync>d__15.MoveNext()
InnerException
NativeErrorCode = 10060 System.Exception {System.Net.Sockets.SocketException}
Message
"接続済みの呼び出し先が一定の時間を過ぎても正しく応答しなかったため、接続できませんでした。または接続済みのホストが応答しなかったため、確立された接続は失敗しました。"    string

原因

ここにヒントがあるが、QnA Maker接続用のクラスMicrosoft.Bot.Builder.AI.QnA.QnAMaker内部で使用しているHttpClientでプロキシが使用されていないため。

ここによれば、HttpClientHandlerを一度初期化すれば、以降はどのようにHttpClientでコネクションしても接続できるとある。しかし、.NET Core 2.1の問題なのか仕様が変更されたのか、QnAMakerモジュールの仕様かは不明だが、HttpClientHandlerを一度初期化するだけでは解決しない。

.NET Core 3.1で改善している可能性もあるが、検証できていない。

対策

Microsoft.Bot.Builder.AI.QnA.QnAMakerのインスタンスを作成する際に、プロキシを設定した HttpClientを渡すように変更する。

また、環境でプロキシのアドレスが異なる場合も考えられるため、設定ファイルにプロキシのアドレスを持たせる。

  • .NET Core 3.1ではHttpClient.DefaultProxyを使用できる可能性もあるが、検証できていないため、設定ファイルに持たせる形としている
  • 開発時の環境の相違を考慮し、appsetting.jsonの「新しい場合はコピーする」プロパティを新しい場合はコピーするとしている

変更前

appsetting.json
{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "ScmType": "None",
  "QnAKnowledgebaseId": "knowledge-base-id",
  "QnAAuthKey": "qna-maker-resource-key",
  "QnAEndpointHostName": "your-hostname" // This is a URL ending in /qnamaker
}
EchoBot.cs
public QnAMaker EchoBotQnA { get; private set; }
public EchoBot(QnAMakerEndpoint endpoint)
{
   // connects to QnA Maker endpoint for each turn
   EchoBotQnA = new QnAMaker(endpoint);
}

変更後

appsetting.json
{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "ScmType": "None",
  "ProxyAddress": "http://(プロキシのアドレス):(プロキシのポート)",
  "QnAKnowledgebaseId": "knowledge-base-id",
  "QnAAuthKey": "qna-maker-resource-key",
  "QnAEndpointHostName": "your-hostname" // This is a URL ending in /qnamaker
}
EchoBot.cs
public QnAMaker EchoBotQnA { get; private set; }
public EchoBot(IConfiguration configuration, QnAMakerEndpoint endpoint)
{
    Configuration = configuration;

    // connects to QnA Maker endpoint for each turn
    var proxyAddress = Configuration.GetValue<string>($"ProxyAddress");
    if (string.IsNullOrEmpty(proxyAddress))
    {
        EchoBotQnA = new QnAMaker(endpoint);
    }
    else
    {
        var httpClientHandler = new HttpClientHandler
        {
            // .NET Core 2.1では(HttpClient.DefaultProxy)がない(https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient.defaultproxy?view=netcore-3.1)
            Proxy = new WebProxy(proxyAddress, true),
            UseProxy = true
        };
        var client = new HttpClient(httpClientHandler);
        EchoBotQnA = new QnAMaker(endpoint, null, client);
    }
}