장기 실행 작업 관리

적용 대상: SDK v4

장기 실행 작업의 적절한 처리는 강력한 봇의 중요한 측면입니다. Azure AI Bot Service 채널에서 봇에 활동을 보내면 봇은 작업을 신속하게 처리해야 합니다. 봇이 채널에 따라 10~15초 이내에 작업을 완료하지 않으면 Azure AI Bot Service 시간 초과되고 봇 작동 방식에 설명된 대로 클라이언트504:GatewayTimeout에 다시 보고합니다.

이 문서에서는 외부 서비스를 사용하여 작업을 실행하고 작업이 완료되면 봇에 알리는 방법을 설명합니다.

사전 요구 사항

이 샘플 정보

이 문서는 다중 턴 프롬프트 샘플 봇으로 시작하고 장기 실행 작업을 수행하기 위한 코드를 추가합니다. 또한 작업이 완료된 후 사용자에게 응답하는 방법을 보여 줍니다. 업데이트된 샘플에서 다음을 수행합니다.

  • 봇은 사용자에게 수행할 장기 실행 작업을 요청합니다.
  • 봇은 사용자로부터 활동을 수신하고 수행할 작업을 결정합니다.
  • 봇은 사용자에게 작업이 다소 시간이 걸릴 것임을 알리고 작업을 C# 함수로 보냅니다.
    • 봇은 상태를 저장하여 진행 중인 작업이 있음을 나타냅니다.
    • 작업이 실행되는 동안 봇은 사용자의 메시지에 응답하여 작업이 아직 진행 중임을 알립니다.
    • Azure Functions 장기 실행 작업을 관리하고 작업을 봇에 보내 event 작업이 완료되었음을 알립니다.
  • 봇은 대화를 다시 시작하고 사용자에게 작업이 완료되었음을 알리는 사전 대응 메시지를 보냅니다. 그런 다음, 봇은 앞에서 언급한 작업 상태를 지웁니다.

이 예제에서는 LongOperationPrompt 추상 ActivityPrompt 클래스에서 파생된 클래스를 정의합니다. 는 LongOperationPrompt 처리할 작업을 큐에 대기할 때 활동의 속성 내에서 사용자의 선택을 포함합니다. 그런 다음 이 작업은 Direct Line 클라이언트를 사용하여 봇으로 다시 전송되기 전에 Azure Functions 사용, 수정 및 다른 event 활동으로 래핑됩니다. 봇 내에서 이벤트 활동은 어댑터의 계속 대화 메서드를 호출하여 대화를 다시 시작하는 데 사용됩니다. 그런 다음 대화 상자 스택이 로드되고 가 LongOperationPrompt 완료됩니다.

이 문서에서는 다양한 기술을 다룹니다. 관련 문서에 대한 링크는 추가 정보 섹션을 참조하세요.

Azure Storage 계정 만들기

Azure Storage 계정을 만들고 연결 문자열을 검색합니다. 연결 문자열을 봇의 구성 파일에 추가해야 합니다.

자세한 내용은 스토리지 계정 만들기Azure Portal 자격 증명 복사를 참조하세요.

봇 리소스 만들기

  1. ngrok를 설치하고 로컬 디버깅 중에 봇의 메시징 엔드포인트 로 사용할 URL을 검색합니다. 메시징 엔드포인트는 추가된 HTTPS 전달 URL입니다 /api/messages/ . 새 봇의 기본 포트는 3978입니다.

    자세한 내용은 ngrok를 사용하여 봇을 디버그하는 방법을 참조하세요.

  2. Azure Portal 또는 Azure CLI를 사용하여 Azure Bot 리소스를 만듭니다. 봇의 메시징 엔드포인트를 ngrok로 만든 엔드포인트로 설정합니다. 봇 리소스를 만든 후 봇의 Microsoft 앱 ID 및 암호를 가져옵니다. Direct Line 채널을 사용하도록 설정하고 Direct Line 비밀을 검색합니다. 이를 봇 코드 및 C# 함수에 추가합니다.

    자세한 내용은 봇을 관리하는 방법 및 봇을Direct Line 연결하는 방법을 참조하세요.

C# 함수 만들기

  1. .NET Core 런타임 스택을 기반으로 Azure Functions 앱을 만듭니다.

    자세한 내용은 함수 앱 및Azure Functions C# 스크립트 참조를 만드는 방법을 참조하세요.

  2. 함수 앱에 DirectLineSecret 애플리케이션 설정을 추가합니다.

    자세한 내용은 함수 앱을 관리하는 방법을 참조하세요.

  3. 함수 앱 내에서 Azure Queue Storage 템플릿을 기반으로 함수를 추가합니다.

    원하는 큐 이름을 설정하고 이전 단계에서 만든 을 선택합니다 Azure Storage Account . 이 큐 이름은 봇의 appsettings.json 파일에도 배치됩니다.

  4. function.proj 파일을 함수에 추가합니다.

    <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
            <TargetFramework>netstandard2.0</TargetFramework>
        </PropertyGroup>
    
        <ItemGroup>
            <PackageReference Include="Microsoft.Bot.Connector.DirectLine" Version="3.0.2" />
            <PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.4" />
        </ItemGroup>
    </Project>
    
  5. 다음 코드로 run.csx 를 업데이트합니다.

    #r "Newtonsoft.Json"
    
    using System;
    using System.Net.Http;
    using System.Text;
    using Newtonsoft.Json;
    using Microsoft.Bot.Connector.DirectLine;
    using System.Threading;
    
    public static async Task Run(string queueItem, ILogger log)
    {
        log.LogInformation($"C# Queue trigger function processing");
    
        JsonSerializerSettings jsonSettings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore };
        var originalActivity =  JsonConvert.DeserializeObject<Activity>(queueItem, jsonSettings);
        // Perform long operation here....
        System.Threading.Thread.Sleep(TimeSpan.FromSeconds(15));
    
        if(originalActivity.Value.ToString().Equals("option 1", StringComparison.OrdinalIgnoreCase))
        {
            originalActivity.Value = " (Result for long operation one!)";
        }
        else if(originalActivity.Value.ToString().Equals("option 2", StringComparison.OrdinalIgnoreCase))
        {
            originalActivity.Value = " (A different result for operation two!)";
        }
    
        originalActivity.Value = "LongOperationComplete:" + originalActivity.Value;
        var responseActivity =  new Activity("event");
        responseActivity.Value = originalActivity;
        responseActivity.Name = "LongOperationResponse";
        responseActivity.From = new ChannelAccount("GenerateReport", "AzureFunction");
    
        var directLineSecret = Environment.GetEnvironmentVariable("DirectLineSecret");
        using(DirectLineClient client = new DirectLineClient(directLineSecret))
        {
            var conversation = await client.Conversations.StartConversationAsync();
            await client.Conversations.PostActivityAsync(conversation.ConversationId, responseActivity);
        }
    
        log.LogInformation($"Done...");
    }
    

봇 만들기

  1. C# 다중 턴 프롬프트 샘플의 복사본으로 시작합니다.

  2. 프로젝트에 Azure.Storage.Queues NuGet 패키지를 추가합니다.

  3. 이전에 만든 Azure Storage 계정의 연결 문자열과 스토리지 큐 이름을 봇의 구성 파일에 추가합니다.

    큐 이름이 이전에 큐 트리거 함수를 만드는 데 사용한 것과 동일한지 확인합니다. 또한 Azure Bot 리소스를 MicrosoftAppId 만들 때 이전에 생성한 및 MicrosoftAppPassword 속성에 대한 값을 추가합니다.

    appsettings.json

    {
      "MicrosoftAppId": "<your-bot-app-id>",
      "MicrosoftAppPassword": "<your-bot-app-password>",
      "StorageQueueName": "<your-azure-storage-queue-name>",
      "QueueStorageConnection": "<your-storage-connection-string>"
    }
    
  4. IConfiguration 검색하기 위해 DialogBot.cs 에 매개 변수를 추가합니다 MicrsofotAppId. 또한 Azure Function에서 에 LongOperationResponse 대한 처리기를 추가 OnEventActivityAsync 합니다.

    Bots\DialogBot.cs

    protected readonly IStatePropertyAccessor<DialogState> DialogState;
    protected readonly Dialog Dialog;
    protected readonly BotState ConversationState;
    protected readonly ILogger Logger;
    private readonly string _botId;
    
    /// <summary>
    /// Create an instance of <see cref="DialogBot{T}"/>.
    /// </summary>
    /// <param name="configuration"><see cref="IConfiguration"/> used to retrieve MicrosoftAppId
    /// which is used in ContinueConversationAsync.</param>
    /// <param name="conversationState"><see cref="ConversationState"/> used to store the DialogStack.</param>
    /// <param name="dialog">The RootDialog for this bot.</param>
    /// <param name="logger"><see cref="ILogger"/> to use.</param>
    public DialogBot(IConfiguration configuration, ConversationState conversationState, T dialog, ILogger<DialogBot<T>> logger)
    {
        _botId = configuration["MicrosoftAppId"] ?? Guid.NewGuid().ToString();
        ConversationState = conversationState;
        Dialog = dialog;
        Logger = logger;
        DialogState = ConversationState.CreateProperty<DialogState>(nameof(DialogState));
    }
    
    public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
    {
        await base.OnTurnAsync(turnContext, cancellationToken);
    
        // Save any state changes that might have occurred during the turn.
        await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    }
    
    protected override async Task OnEventActivityAsync(ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
    {
        // The event from the Azure Function will have a name of 'LongOperationResponse'
        if (turnContext.Activity.ChannelId == Channels.Directline && turnContext.Activity.Name == "LongOperationResponse")
        {
            // The response will have the original conversation reference activity in the .Value
            // This original activity was sent to the Azure Function via Azure.Storage.Queues in AzureQueuesService.cs.
            var continueConversationActivity = (turnContext.Activity.Value as JObject)?.ToObject<Activity>();
            await turnContext.Adapter.ContinueConversationAsync(_botId, continueConversationActivity.GetConversationReference(), async (context, cancellation) =>
            {
                Logger.LogInformation("Running dialog with Activity from LongOperationResponse.");
    
                // ContinueConversationAsync resets the .Value of the event being continued to Null, 
                //so change it back before running the dialog stack. (The .Value contains the response 
                //from the Azure Function)
                context.Activity.Value = continueConversationActivity.Value;
                await Dialog.RunAsync(context, DialogState, cancellationToken);
    
                // Save any state changes that might have occurred during the inner turn.
                await ConversationState.SaveChangesAsync(context, false, cancellationToken);
            }, cancellationToken);
        }
        else
        {
            await base.OnEventActivityAsync(turnContext, cancellationToken);
        }
    }
    
  5. 처리할 작업을 큐에 대기하는 Azure Queues 서비스를 만듭니다.

    AzureQueuesService.cs

    /// <summary>
    /// Service used to queue messages to an Azure.Storage.Queues.
    /// </summary>
    public class AzureQueuesService
    {
        private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings()
            {
                Formatting = Formatting.Indented,
                NullValueHandling = NullValueHandling.Ignore
            };
    
        private bool _createQueuIfNotExists = true;
        private readonly QueueClient _queueClient;
    
        /// <summary>
        /// Creates a new instance of <see cref="AzureQueuesService"/>.
        /// </summary>
        /// <param name="config"><see cref="IConfiguration"/> used to retrieve
        /// StorageQueueName and QueueStorageConnection from appsettings.json.</param>
        public AzureQueuesService(IConfiguration config)
        {
            var queueName = config["StorageQueueName"];
            var connectionString = config["QueueStorageConnection"];
    
            _queueClient = new QueueClient(connectionString, queueName);
        }
    
        /// <summary>
        /// Queue and Activity, with option in the Activity.Value to Azure.Storage.Queues
        ///
        /// <seealso cref="https://github.com/microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder.Azure/Queues/ContinueConversationLater.cs"/>
        /// </summary>
        /// <param name="referenceActivity">Activity to queue after a call to GetContinuationActivity.</param>
        /// <param name="option">The option the user chose, which will be passed within the .Value of the activity queued.</param>
        /// <param name="cancellationToken">Cancellation token for the async operation.</param>
        /// <returns>Queued <see cref="Azure.Storage.Queues.Models.SendReceipt.MessageId"/>.</returns>
        public async Task<string> QueueActivityToProcess(Activity referenceActivity, string option, CancellationToken cancellationToken)
        {
            if (_createQueuIfNotExists)
            {
                _createQueuIfNotExists = false;
                await _queueClient.CreateIfNotExistsAsync().ConfigureAwait(false);
            }
    
            // create ContinuationActivity from the conversation reference.
            var activity = referenceActivity.GetConversationReference().GetContinuationActivity();
            // Pass the user's choice in the .Value
            activity.Value = option;
    
            var message = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(activity, jsonSettings)));
    
            // Aend ResumeConversation event, it will get posted back to us with a specific value, giving us 
            // the ability to process it and do the right thing.
            var reciept = await _queueClient.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
            return reciept.Value.MessageId;
        }
    }
    

대화 상자

이전 대화 상자를 제거하고 작업을 지원하기 위해 새 대화 상자로 바꿉니다.

  1. UserProfileDialog.cs 파일을 제거합니다.

  2. 사용자에게 수행할 작업을 요청하는 사용자 지정 프롬프트 대화 상자를 추가합니다.

    Dialogs\LongOperationPrompt.cs

    /// <summary>
    /// <see cref="ActivityPrompt"/> implementation which will queue an activity,
    /// along with the <see cref="LongOperationPromptOptions.LongOperationOption"/>,
    /// and wait for an <see cref="ActivityTypes.Event"/> with name of "ContinueConversation"
    /// and Value containing the text: "LongOperationComplete".
    ///
    /// The result of this prompt will be the received Event Activity, which is sent by
    /// the Azure Function after it finishes the long operation.
    /// </summary>
    public class LongOperationPrompt : ActivityPrompt
    {
        private readonly AzureQueuesService _queueService;
    
        /// <summary>
        /// Create a new instance of <see cref="LongOperationPrompt"/>.
        /// </summary>
        /// <param name="dialogId">Id of this <see cref="LongOperationPrompt"/>.</param>
        /// <param name="validator">Validator to use for this prompt.</param>
        /// <param name="queueService"><see cref="AzureQueuesService"/> to use for Enqueuing the activity to process.</param>
        public LongOperationPrompt(string dialogId, PromptValidator<Activity> validator, AzureQueuesService queueService) 
            : base(dialogId, validator)
        {
            _queueService = queueService;
        }
    
        public async override Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default)
        {
            // When the dialog begins, queue the option chosen within the Activity queued.
            await _queueService.QueueActivityToProcess(dc.Context.Activity, (options as LongOperationPromptOptions).LongOperationOption, cancellationToken);
    
            return await base.BeginDialogAsync(dc, options, cancellationToken);
        }
    
        protected override Task<PromptRecognizerResult<Activity>> OnRecognizeAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, CancellationToken cancellationToken = default)
        {
            var result = new PromptRecognizerResult<Activity>() { Succeeded = false };
    
            if(turnContext.Activity.Type == ActivityTypes.Event
                && turnContext.Activity.Name == "ContinueConversation"
                && turnContext.Activity.Value != null
                // Custom validation within LongOperationPrompt.  
                // 'LongOperationComplete' is added to the Activity.Value in the Queue consumer (See: Azure Function)
                && turnContext.Activity.Value.ToString().Contains("LongOperationComplete", System.StringComparison.InvariantCultureIgnoreCase))
            {
                result.Succeeded = true;
                result.Value = turnContext.Activity;
            }
    
            return Task.FromResult(result);
        }
    }
    
  3. 사용자 지정 프롬프트에 대한 프롬프트 옵션 클래스를 추가합니다.

    Dialogs\LongOperationPromptOptions.cs

    /// <summary>
    /// Options sent to <see cref="LongOperationPrompt"/> demonstrating how a value
    /// can be passed along with the queued activity.
    /// </summary>
    public class LongOperationPromptOptions : PromptOptions
    {
        /// <summary>
        /// This is a property sent through the Queue, and is used
        /// in the queue consumer (the Azure Function) to differentiate 
        /// between long operations chosen by the user.
        /// </summary>
        public string LongOperationOption { get; set; }
    }
    
  4. 사용자 지정 프롬프트를 사용하여 사용자의 선택을 얻고 장기 실행 작업을 시작하는 대화 상자를 추가합니다.

    Dialogs\LongOperationDialog.cs

    /// <summary>
    /// This dialog demonstrates how to use the <see cref="LongOperationPrompt"/>.
    ///
    /// The user is provided an option to perform any of three long operations.
    /// Their choice is then sent to the <see cref="LongOperationPrompt"/>.
    /// When the prompt completes, the result is received as an Activity in the
    /// final Waterfall step.
    /// </summary>
    public class LongOperationDialog : ComponentDialog
    {
        public LongOperationDialog(AzureQueuesService queueService)
            : base(nameof(LongOperationDialog))
        {
            // This array defines how the Waterfall will execute.
            var waterfallSteps = new WaterfallStep[]
            {
                OperationTimeStepAsync,
                LongOperationStepAsync,
                OperationCompleteStepAsync,
            };
    
            // Add named dialogs to the DialogSet. These names are saved in the dialog state.
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
            AddDialog(new LongOperationPrompt(nameof(LongOperationPrompt), (vContext, token) =>
            {
                return Task.FromResult(vContext.Recognized.Succeeded);
            }, queueService));
            AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    
            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }
    
        private static async Task<DialogTurnResult> OperationTimeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it's a Prompt Dialog.
            // Running a prompt here means the next WaterfallStep will be run when the user's response is received.
            return await stepContext.PromptAsync(nameof(ChoicePrompt),
                new PromptOptions
                {
                    Prompt = MessageFactory.Text("Please select a long operation test option."),
                    Choices = ChoiceFactory.ToChoices(new List<string> { "option 1", "option 2", "option 3" }),
                }, cancellationToken);
        }
    
        private static async Task<DialogTurnResult> LongOperationStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            var value = ((FoundChoice)stepContext.Result).Value;
            stepContext.Values["longOperationOption"] = value;
    
            var prompt = MessageFactory.Text("...one moment please....");
            // The reprompt will be shown if the user messages the bot while the long operation is being performed.
            var retryPrompt = MessageFactory.Text($"Still performing the long operation: {value} ... (is the Azure Function executing from the queue?)");
            return await stepContext.PromptAsync(nameof(LongOperationPrompt),
                                                        new LongOperationPromptOptions
                                                        {
                                                            Prompt = prompt,
                                                            RetryPrompt = retryPrompt,
                                                            LongOperationOption = value,
                                                        }, cancellationToken);
        }
    
        private static async Task<DialogTurnResult> OperationCompleteStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["longOperationResult"] = stepContext.Result;
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Thanks for waiting. { (stepContext.Result as Activity).Value}"), cancellationToken);
    
            // Start over by replacing the dialog with itself.
            return await stepContext.ReplaceDialogAsync(nameof(WaterfallDialog), null, cancellationToken);
        }
    }
    

서비스 등록 및 대화 상자

Startup.cs에서 메서드를 ConfigureServices 업데이트하여 를 LongOperationDialog 등록하고 를 추가합니다AzureQueuesService.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddNewtonsoftJson();

    // Create the Bot Framework Adapter with error handling enabled.
    services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

    // In production, this should be a persistent storage provider.bot
    services.AddSingleton<IStorage>(new MemoryStorage());

    // Create the Conversation state. (Used by the Dialog system itself.)
    services.AddSingleton<ConversationState>();

    // The Dialog that will be run by the bot.
    services.AddSingleton<LongOperationDialog>();

    // Service used to queue into Azure.Storage.Queues
    services.AddSingleton<AzureQueuesService>();

    // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
    services.AddTransient<IBot, DialogBot<LongOperationDialog>>();
}

봇 테스트

  1. 아직 설치하지 않은 경우 Bot Framework Emulator 설치합니다.
  2. 샘플을 머신에서 로컬로 실행합니다.
  3. 에뮬레이터를 시작하고 봇에 연결합니다.
  4. 시작할 긴 작업을 선택합니다.
    • 봇은 잠시 메시지를 보내고 Azure 함수를 큐에 대기합니다.
    • 작업이 완료되기 전에 사용자가 봇과 상호 작용하려고 하면 봇이 여전히 작동하는 메시지로 회신합니다.
    • 작업이 완료되면 봇은 사용자에게 사전 대응 메시지를 보내 완료 사실을 알릴 수 있습니다.

사용자가 긴 작업을 시작하고 결국 작업이 완료되었다는 사전 대응 메시지를 수신하는 샘플 대본입니다.

추가 정보

도구 또는 기능 리소스
Azure Functions 함수 앱 만들기
C# 스크립트 Azure Functions
함수 앱 관리
Azure portal 봇 관리
직접 회선에 봇 연결
Azure Storage Azure Queue Storage
스토리지 계정을 만드는
Azure Portal에서 자격 증명 복사
큐를 사용하는 방법
봇 기본 사항 봇 작동 방식
폭포 대화 상자의 프롬프트
자동 관리 메시지
ngrok ngrok를 사용하여 봇 디버그