Partilhar via


Lidar com interrupções do usuário

APLICA-SE A: SDK v4

Lidar com interrupções é um aspeto importante de um bot robusto. Os usuários nem sempre seguirão seu fluxo de conversa definido, passo a passo. Eles podem tentar fazer uma pergunta no meio do processo, ou simplesmente querer cancelá-la em vez de concluí-la. Este artigo descreve algumas maneiras comuns de lidar com interrupções de usuários em seu bot.

Nota

Os SDKs JavaScript, C# e Python do Bot Framework continuarão a ser suportados, no entanto, o Java SDK está sendo desativado com suporte final de longo prazo terminando em novembro de 2023.

Os bots existentes construídos com o Java SDK continuarão a funcionar.

Para a criação de novos bots, considere usar o Microsoft Copilot Studio e leia sobre como escolher a solução de copilot certa.

Para obter mais informações, consulte O futuro da criação de bots.

Pré-requisitos

O exemplo de bot principal usa Language Understanding (LUIS) para identificar as intenções do usuário; no entanto, identificar a intenção do usuário não é o foco deste artigo. Para obter informações sobre como identificar as intenções do usuário, consulte Compreensão de linguagem natural e Adicionar compreensão de linguagem natural ao seu bot.

Nota

O Language Understanding (LUIS) será aposentado em 1 de outubro de 2025. A partir de 1 de abril de 2023, não será possível criar novos recursos LUIS. Uma versão mais recente do entendimento de idiomas agora está disponível como parte do Azure AI Language.

O entendimento de linguagem conversacional (CLU), um recurso do Azure AI Language, é a versão atualizada do LUIS. Para obter mais informações sobre o suporte à compreensão de linguagem no Bot Framework SDK, consulte Compreensão de linguagem natural.

Sobre este exemplo

O exemplo usado neste artigo modela um bot de reserva de voo que usa caixas de diálogo para obter informações de voo do usuário. A qualquer momento durante a conversa com o bot, o usuário pode emitir comandos de ajuda ou cancelar para causar uma interrupção. Existem dois tipos de interrupções tratadas:

  • Nível de turno: ignore o processamento no nível de turno, mas deixe a caixa de diálogo na pilha com as informações fornecidas. No turno seguinte, continue de onde a conversa parou.
  • Nível de diálogo: cancele o processamento completamente, para que o bot possa começar tudo de novo.

Definir e implementar a lógica de interrupção

Primeiro, defina e implemente a ajuda e cancele interrupções.

Para usar caixas de diálogo, instale o pacote NuGet Microsoft.Bot.Builder.Dialogs .

Caixas de diálogo\CancelAndHelpDialog.cs

Implemente a classe para lidar com interrupções do CancelAndHelpDialog usuário. As caixas de diálogo BookingDialog canceláveis e DateResolverDialog derivam dessa classe.

public class CancelAndHelpDialog : ComponentDialog

CancelAndHelpDialog Na classe, o OnContinueDialogAsync método chama o InterruptAsync método para verificar se o usuário interrompeu o fluxo normal. Se o fluxo for interrompido, os métodos de classe base serão chamados; caso contrário, o valor de retorno do InterruptAsync é retornado.

protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
    var result = await InterruptAsync(innerDc, cancellationToken);
    if (result != null)
    {
        return result;
    }

    return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}

Se o usuário digitar "ajuda", o InterruptAsync método envia uma mensagem e, em seguida, chama DialogTurnResult (DialogTurnStatus.Waiting) para indicar que a caixa de diálogo na parte superior está aguardando uma resposta do usuário. Desta forma, o fluxo de conversa é interrompido apenas por um turno, e o próximo turno continua de onde a conversa parou.

Se o usuário digitar "cancelar", ele chamará CancelAllDialogsAsync seu contexto de diálogo interno, o que limpa sua pilha de diálogo e faz com que ele saia com um status cancelado e nenhum valor de resultado. Para o MainDialog (mostrado mais tarde), aparecerá que a caixa de diálogo de reserva terminou e retornou nulo, semelhante a quando o usuário opta por não confirmar sua reserva.

private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
    if (innerDc.Context.Activity.Type == ActivityTypes.Message)
    {
        var text = innerDc.Context.Activity.Text.ToLowerInvariant();

        switch (text)
        {
            case "help":
            case "?":
                var helpMessage = MessageFactory.Text(HelpMsgText, HelpMsgText, InputHints.ExpectingInput);
                await innerDc.Context.SendActivityAsync(helpMessage, cancellationToken);
                return new DialogTurnResult(DialogTurnStatus.Waiting);

            case "cancel":
            case "quit":
                var cancelMessage = MessageFactory.Text(CancelMsgText, CancelMsgText, InputHints.IgnoringInput);
                await innerDc.Context.SendActivityAsync(cancelMessage, cancellationToken);
                return await innerDc.CancelAllDialogsAsync(cancellationToken);
        }
    }

    return null;
}

Verifique se há interrupções a cada turno

Depois que a classe de manipulação de interrupção for implementada, revise o que acontece quando esse bot recebe uma nova mensagem do usuário.

Caixas de diálogo\MainDialog.cs

À medida que a nova atividade de mensagem chega, o bot executa o MainDialog. O MainDialog solicita ao usuário o que ele pode ajudar. E então ele começa o BookingDialog MainDialog.ActStepAsync no método, com uma chamada para BeginDialogAsync como mostrado abaixo.

private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if (!_luisRecognizer.IsConfigured)
    {
        // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
        return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken);
    }

    // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
    var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);
    switch (luisResult.TopIntent().intent)
    {
        case FlightBooking.Intent.BookFlight:
            await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);

            // Initialize BookingDetails with any entities we may have found in the response.
            var bookingDetails = new BookingDetails()
            {
                // Get destination and origin from the composite entities arrays.
                Destination = luisResult.ToEntities.Airport,
                Origin = luisResult.FromEntities.Airport,
                TravelDate = luisResult.TravelDate,
            };

            // Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder.
            return await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken);

        case FlightBooking.Intent.GetWeather:
            // We haven't implemented the GetWeatherDialog so we just display a TODO message.
            var getWeatherMessageText = "TODO: get weather flow here";
            var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
            break;

        default:
            // Catch all for unhandled intents
            var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
            var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
            break;
    }

    return await stepContext.NextAsync(null, cancellationToken);
}

Em seguida, no FinalStepAsync método da aula, o diálogo de MainDialog reserva terminou e a reserva é considerada completa ou cancelada.

private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // If the child dialog ("BookingDialog") was cancelled, the user failed to confirm or if the intent wasn't BookFlight
    // the Result here will be null.
    if (stepContext.Result is BookingDetails result)
    {
        // Now we have all the booking details call the booking service.

        // If the call to the booking service was successful tell the user.

        var timeProperty = new TimexProperty(result.TravelDate);
        var travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
        var messageText = $"I have you booked to {result.Destination} from {result.Origin} on {travelDateMsg}";
        var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
        await stepContext.Context.SendActivityAsync(message, cancellationToken);
    }

    // Restart the main dialog with a different message the second time around
    var promptMessage = "What else can I do for you?";
    return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage, cancellationToken);
}

O código não BookingDialog é mostrado aqui, pois não está diretamente relacionado ao tratamento de interrupções. Ele é usado para solicitar aos usuários detalhes da reserva. Você pode encontrar esse código em Dialogs\BookingDialogs.cs.

Lidar com erros inesperados

O manipulador de erros do adaptador lida com todas as exceções que não foram capturadas no bot.

AdapterWithErrorHandler.cs

No exemplo, o manipulador do OnTurnError adaptador recebe todas as exceções lançadas pela lógica de turno do bot. Se houver uma exceção lançada, o manipulador excluirá o estado da conversa atual para evitar que o bot fique preso em um loop de erro causado por estar em um estado incorreto.

    {
        // Log any leaked exception from the application.
        // NOTE: In production environment, you should consider logging this to
        // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
        // to add telemetry capture to your bot.
        logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

        // Send a message to the user
        var errorMessageText = "The bot encountered an error or bug.";
        var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
        await turnContext.SendActivityAsync(errorMessage);

        errorMessageText = "To continue to run this bot, please fix the bot source code.";
        errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
        await turnContext.SendActivityAsync(errorMessage);

        if (conversationState != null)
        {
            try
            {
                // Delete the conversationState for the current conversation to prevent the
                // bot from getting stuck in a error-loop caused by being in a bad state.
                // ConversationState should be thought of as similar to "cookie-state" in a Web pages.
                await conversationState.DeleteAsync(turnContext);
            }
            catch (Exception e)
            {
                logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
            }
        }

        // Send a trace activity, which will be displayed in the Bot Framework Emulator
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
    };
}

Serviços de registo

Startup.cs

Finalmente, no Startup.cs, o bot é criado como um transitório e, a cada turno, uma nova instância do bot é criada.


// Register the BookingDialog.

Para referência, aqui estão as definições de classe que são usadas na chamada para criar o bot acima.

public class DialogAndWelcomeBot<T> : DialogBot<T>
public class DialogBot<T> : ActivityHandler
    where T : Dialog
public class MainDialog : ComponentDialog

Testar o bot

  1. Se você ainda não fez isso, instale o Bot Framework Emulator.
  2. Execute a amostra localmente na sua máquina.
  3. Inicie o emulador, conecte-se ao seu bot e envie mensagens como mostrado abaixo.

Informações adicionais

  • O exemplo 24.bot-authentication-msgraph em C#, JavaScript, Python ou Java mostra como lidar com uma solicitação de logout. Ele usa um padrão semelhante ao mostrado aqui para lidar com interrupções.

  • Você deve enviar uma resposta padrão em vez de não fazer nada e deixar o usuário se perguntando o que está acontecendo. A resposta padrão deve dizer ao usuário quais comandos o bot entende para que o usuário possa voltar aos trilhos.

  • Em qualquer ponto do turno, a propriedade response do contexto turn indica se o bot enviou uma mensagem ao usuário nesse turno. Antes que o turno termine, seu bot deve enviar alguma mensagem para o usuário, mesmo que seja um simples reconhecimento de sua entrada.