Manipular interrupções do usuário

APLICA-SE A: SDK v4

O tratamento de interrupções é um aspecto importante de um bot robusto. Os usuários nem sempre seguirão o fluxo de conversa que você definiu passo a passo. Eles podem tentar fazer uma pergunta no meio do processo ou simplesmente querer cancelá-lo em vez de concluí-lo. Este artigo descreve algumas maneiras comuns de lidar com as interrupções no seu bot.

Observação

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

Os bots existentes criados com o SDK para Java continuarão a funcionar.

Para a criação de novos bots, considere usar o Power Virtual Agents e ler sobre como escolher a solução de chatbot correta.

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

Pré-requisitos

O exemplo do bot principal usa o reconhecimento de linguagem (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, confira Reconhecimento de linguagem natural e Adicionar reconhecimento de linguagem natural ao seu bot.

Observação

O reconhecimento de linguagem (LUIS) será desativado em 1º de outubro de 2025. A partir de 1º de abril de 2023, você não poderá criar recursos do LUIS. Uma versão mais recente do reconhecimento de linguagem já está disponível como parte da Linguagem de IA do Azure.

A compreensão da linguagem coloquial (CLU), um recurso da Linguagem de IA do Azure, é a versão atualizada do LUIS. Para obter mais informações sobre o suporte ao reconhecimento de linguagem no SDK do Bot Framework, confira Reconhecimento de linguagem natural.

Sobre este exemplo

O exemplo usado neste artigo traz um bot de reserva de voo que utiliza caixas de diálogo para obter informações do voo do usuário. A qualquer momento durante a conversa com o bot, o usuário pode solicitar ajudar ou cancelar comandos e gerar uma interrupção. Dois tipos de interrupções são abordados:

  • Nível de turno: ignorar o processamento no nível de turno, mas deixar o diálogo na pilha com as informações fornecidas. No próximo turno, continuar do ponto em que a conversa parou.
  • Nível de diálogo: cancelar o processamento completamente para que o bot comece tudo novamente.

Como definir e implementar a lógica de interrupção

Primeiramente, defina e implemente as interrupções de ajuda e cancelamento.

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

Dialogs\CancelAndHelpDialog.cs

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

public class CancelAndHelpDialog : ComponentDialog

Na classe CancelAndHelpDialog, o método OnContinueDialogAsync chama o método InterruptAsync para verificar se o usuário interrompeu o fluxo normal. Se o fluxo for interrompido, os métodos da classe base são chamados; caso contrário, o valor retornado de InterruptAsync será 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 "help", o método InterruptAsync enviará uma mensagem e, em seguida, chama DialogTurnResult (DialogTurnStatus.Waiting) para indicar que a caixa de diálogo principal está aguardando uma resposta do usuário. Dessa forma, o fluxo de conversa é interrompido durante somente um turno e, no próximo turno, continua do ponto em que a conversa parou.

Se o usuário digitar "cancelar", ele chamará CancelAllDialogsAsync em seu contexto interno de diálogo, e isso limpa sua pilha de diálogo, fazendo com que ele seja fechado com um status de cancelamento e nenhum valor de resultado. Para MainDialog (mostrado posteriormente), aparecerá que a caixa de diálogo de reserva foi finalizada e não retornou resultados, da mesma forma quando o usuário decide não confirmar a 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;
}

Como verificar se há interrupções em cada turno

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

Dialogs\MainDialog.cs

Assim que a nova atividade de mensagem chega, o bot executa o MainDialog. O MainDialog pergunta ao usuário no que pode ajudar. Então, ele inicia o BookingDialog no método MainDialog.ActStepAsync, com uma chamada para BeginDialogAsync, conforme 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 método FinalStepAsync da classe MainDialog, o diálogo de reserva é encerrado e a reserva é considerada finalizada 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 em BookingDialog não é mostrado aqui, porque não está diretamente relacionado com a manipulação da interrupção. Ele é usado para solicitar os detalhes de reserva aos usuários. Você pode encontrar esse código em Dialogs\BookingDialogs.cs.

Como lidar com erros inesperados

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

AdapterWithErrorHandler.cs

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

    {
        // 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 registro

Startup.cs

Por fim, em Startup.cs, o bot é criado como transitório e, em 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. Caso ainda não tenha feito isso, instale o Bot Framework Emulator.
  2. Execute o exemplo localmente em seu computador.
  3. Inicie o Emulador, conecte-se ao seu bot e envie mensagens conforme 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 logoff. Ele usa um padrão semelhante ao mostrado aqui para lidar com as interrupções.

  • Você deverá enviar uma resposta padrão em vez de não realizar nenhuma ação e deixar o usuário se perguntando o que está acontecendo. A resposta padrão deve informar o usuário quais comandos são reconhecidos pelo bot, de modo que o usuário possa voltar para um tópico adequado.

  • Em qualquer momento durante o turno, a propriedade respondido do contexto do turno indica se o bot enviou uma mensagem ao usuário dessa vez. Antes de o turno ser concluído, seu bot deve enviar alguma mensagem para o usuário, mesmo que seja um simples reconhecimento de sua entrada.