Implementar um consumidor de skills

APLICA-SE A: SDK v4

Você pode usar skills para estender outro bot. Um skill é um bot que pode executar um conjunto de tarefas para outro bot e que usa um manifesto para descrever sua interface. Um bot raiz é um bot voltado para o usuário que pode invocar um ou mais skills. Um bot raiz é um tipo de consumidor de skills.

  • Um consumidor de habilidade deve usar a validação de declarações para gerenciar quais habilidades podem acessá-lo.
  • Um consumidor de skills pode usar vários skills.
  • Os desenvolvedores que não têm acesso ao código-fonte do skill podem usar as informações do manifesto do skill para criar um consumidor de skills.

Este artigo demonstra como implementar um consumidor de skills que usa o skill de eco para ecoar a entrada do usuário. Para obter um exemplo de manifesto de skill e informações sobre como implementar o skill de eco, consulte como implementar um skill.

Para saber mais sobre como usar uma caixa de diálogo de skill para consumir um skill, confira como usar uma caixa de diálogo para consumir um skill.

Alguns tipos de consumidores de habilidades não podem usar alguns tipos de bots de habilidades. A tabela a seguir descreve quais combinações são compatíveis.

  Habilidade de multilocatário Habilidade de locatário único Habilidade de identidade gerenciada atribuída pelo usuário
Consumidor de multilocatário Com suporte Sem suporte Sem suporte
Consumidor de locatário único Sem suporte Compatível se ambos os aplicativos pertencerem ao mesmo locatário Compatível se ambos os aplicativos pertencerem ao mesmo locatário
Consumidor de identidade gerenciada atribuída pelo usuário Sem suporte Compatível se ambos os aplicativos pertencerem ao mesmo locatário Compatível se ambos os aplicativos pertencerem ao mesmo locatário

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

Observação

A partir da versão 4.11, você não precisa de um ID do aplicativo e de uma senha para testar um consumidor de habilidade localmente no Bot Framework Emulator. Uma assinatura do Azure ainda é necessária para implantar seu consumidor no Azure ou para consumir uma habilidade implantada.

Sobre este exemplo

O exemplo bot para bot de skills simples inclui projetos para dois bots:

  • O bot skill de eco, que implementa o skill.
  • O bot raiz simples, que implementa um bot raiz que consome o skill.

Este artigo se concentra no bot raiz, que inclui a lógica de suporte em seus objetos de bot e adaptador e inclui objetos usados para trocar atividades com um skill. Estão incluídos:

  • Um cliente de skills, usado para enviar atividades a um skill.
  • Um manipulador de skills, usado para receber atividades de um skill.
  • Um alocador de ID de conversa de skill, usado pelo cliente e manipulador de skill para converter entre a referência de conversa usuário-raiz e a referência de conversa raiz-skill.

Para obter informações sobre o bot de skill de eco, consulte como implementar um skill.

Recursos

Para bots implantados, a autenticação de bot para bot exige que cada bot participante tenha informações de identidade válidas. No entanto, você pode testar as habilidades de vários locatários e os consumidores de habilidades localmente com o Emulator sem um ID do aplicativo e senha.

Configuração do aplicativo

  1. Opcionalmente, adicione as informações de identidade do bot raiz ao seu arquivo de configuração. Se a habilidade ou o consumidor de habilidade fornecer informações de identidade, ambos deverão fazê-lo.
  2. Adicione o ponto de extremidade do host de habilidades (o serviço ou URL de retorno de chamada) ao qual as habilidades devem responder ao consumidor de habilidade.
  3. Adicione uma entrada para cada skill que o consumidor de skills usará. Cada entrada inclui:
    • Uma ID que o consumidor de skills usará para identificar cada skill.
    • Opcionalmente, o aplicativo da habilidade ou a ID do cliente.
    • O ponto de extremidade de mensagens do skill.

Observação

Se a habilidade ou o consumidor de habilidade fornecer informações de identidade, ambos deverão fazê-lo.

SimpleRootBot\appsettings.json

Opcionalmente, adicione as informações de identidade do bot raiz e adicione a ID do aplicativo ou a ID do cliente para o bot de habilidades de eco.

{
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",
  "SkillHostEndpoint": "http://localhost:3978/api/skills/",
  "BotFrameworkSkills": [
    {
      "Id": "EchoSkillBot",
      "AppId": "",
      "SkillEndpoint": "http://localhost:39783/api/messages"
    }
  ]
}

Configuração de skills

Este exemplo lê informações de cada skill no arquivo de configuração em uma coleção de objetos skills.

SimpleRootBot\SkillsConfiguration.cs

public class SkillsConfiguration
{
    public SkillsConfiguration(IConfiguration configuration)
    {
        var section = configuration?.GetSection("BotFrameworkSkills");
        var skills = section?.Get<BotFrameworkSkill[]>();
        if (skills != null)
        {
            foreach (var skill in skills)
            {
                Skills.Add(skill.Id, skill);
            }
        }

        var skillHostEndpoint = configuration?.GetValue<string>(nameof(SkillHostEndpoint));
        if (!string.IsNullOrWhiteSpace(skillHostEndpoint))
        {
            SkillHostEndpoint = new Uri(skillHostEndpoint);
        }
    }

    public Uri SkillHostEndpoint { get; }

    public Dictionary<string, BotFrameworkSkill> Skills { get; } = new Dictionary<string, BotFrameworkSkill>();
}

Alocador de ID de conversa

Ele cria a ID de conversa para uso com o skill e pode recuperar a ID de conversa do usuário original da ID da conversa de skills.

O alocador de ID de conversa deste exemplo é compatível com um cenário simples em que:

  • O bot raiz é projetado para consumir um skill específico.
  • O bot raiz tem apenas uma conversa ativa com um skill de cada vez.

O SDK fornece uma classe SkillConversationIdFactory que pode ser usada em qualquer habilidade sem exigir que o código-fonte seja replicado. O alocador de ID de conversação é configurado em Startup.cs.

Para compatibilidade com cenários mais complexos, crie seu alocador de ID de conversa de maneira que:

  • O método criar ID de conversa de skills obtém ou gera a ID de conversa de skill apropriada.
  • O método obter referência de conversa obtenha a conversa de usuário correta.

Cliente de skills e manipulador de skills

O consumidor de skills usa um cliente de skills para encaminhar atividades ao skill. O cliente usa as informações de configuração de skills e o alocador de ID de conversa para fazer isso.

O consumidor de skills usa um manipulador de skills para receber atividades de um skill. O manipulador usa o alocador de ID de conversa, a configuração de autenticação e um provedor de credenciais para fazer isso e também tem dependências no manipulador de atividade e no adaptador do bot raiz

SimpleRootBot\Startup.cs

services.AddSingleton<IBotFrameworkHttpAdapter>(sp => sp.GetService<CloudAdapter>());
services.AddSingleton<BotAdapter>(sp => sp.GetService<CloudAdapter>());

O tráfego HTTP da habilidade chegará ao ponto de extremidade do URL do serviço que o consumidor de habilidade anuncia para a habilidade. Use um manipulador de ponto de extremidade específico da linguagem para encaminhar o tráfego para o manipulador de skills.

O manipulador de skills padrão:

  • Se um ID do aplicativo e uma senha estiverem presentes, usará um objeto de configuração de autenticação para executar a autenticação bot para bot e a validação de declarações.
  • Usa o alocador de ID de conversa para converter da conversa consumidor-skill de volta para a conversa do usuário raiz.
  • Gera uma mensagem proativa para que o consumidor de skills possa restabelecer um contexto de usuário raiz e encaminhar atividades para o usuário.

Lógica do manipulador de atividades

É importante saber que a lógica do consumidor de skills deve:

  • Lembrar-se de que há skills ativos e encaminhar atividades de para eles, conforme apropriado.
  • Observar quando um usuário faz uma solicitação que deve ser encaminhada a um skill e iniciar o skill.
  • Procurar uma atividade endOfConversation em qualquer skill ativo, para observar quando ela é concluída.
  • Se apropriado, adicionar lógica para permitir que o usuário ou o consumidor de skills cancele um skill que ainda não foi concluído.
  • Salvar o estado antes de fazer a chamada para um skill, uma vez que qualquer resposta pode voltar a uma instância diferente do consumidor de skills.

SimpleRootBot\Bots\RootBot.cs

O bot raiz tem dependências do estado da conversa, das informações de skills, do cliente de skills e da configuração geral. O ASP.NET fornece esses objetos por meio de injeção de dependência. O bot raiz também define um acessador de propriedade de estado de conversa para controlar qual skill está ativo.

public static readonly string ActiveSkillPropertyName = $"{typeof(RootBot).FullName}.ActiveSkillProperty";
private readonly IStatePropertyAccessor<BotFrameworkSkill> _activeSkillProperty;
private readonly string _botId;
private readonly ConversationState _conversationState;
private readonly BotFrameworkAuthentication _auth;
private readonly SkillConversationIdFactoryBase _conversationIdFactory;
private readonly SkillsConfiguration _skillsConfig;
private readonly BotFrameworkSkill _targetSkill;

public RootBot(BotFrameworkAuthentication auth, ConversationState conversationState, SkillsConfiguration skillsConfig, SkillConversationIdFactoryBase conversationIdFactory, IConfiguration configuration)
{
    _auth = auth ?? throw new ArgumentNullException(nameof(auth));
    _conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
    _skillsConfig = skillsConfig ?? throw new ArgumentNullException(nameof(skillsConfig));
    _conversationIdFactory = conversationIdFactory ?? throw new ArgumentNullException(nameof(conversationIdFactory));

    if (configuration == null)
    {
        throw new ArgumentNullException(nameof(configuration));
    }

    _botId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;

    // We use a single skill in this example.
    var targetSkillId = "EchoSkillBot";
    _skillsConfig.Skills.TryGetValue(targetSkillId, out _targetSkill);

    // Create state property to track the active skill
    _activeSkillProperty = conversationState.CreateProperty<BotFrameworkSkill>(ActiveSkillPropertyName);
}

Este exemplo tem um método auxiliar para encaminhar atividades a um skill. Ele salva o estado da conversa antes de invocar o skill e verifica se a solicitação HTTP foi bem-sucedida.

private async Task SendToSkill(ITurnContext turnContext, BotFrameworkSkill targetSkill, CancellationToken cancellationToken)
{
    // NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
    // will have access to current accurate state.
    await _conversationState.SaveChangesAsync(turnContext, force: true, cancellationToken: cancellationToken);

    // Create a conversationId to interact with the skill and send the activity
    var options = new SkillConversationIdFactoryOptions
    {
        FromBotOAuthScope = turnContext.TurnState.Get<string>(BotAdapter.OAuthScopeKey),
        FromBotId = _botId,
        Activity = turnContext.Activity,
        BotFrameworkSkill = targetSkill
    };
    var skillConversationId = await _conversationIdFactory.CreateSkillConversationIdAsync(options, cancellationToken);

    using var client = _auth.CreateBotFrameworkClient();

    // route the activity to the skill
    var response = await client.PostActivityAsync(_botId, targetSkill.AppId, targetSkill.SkillEndpoint, _skillsConfig.SkillHostEndpoint, skillConversationId, turnContext.Activity, cancellationToken);

    // Check response status
    if (!(response.Status >= 200 && response.Status <= 299))
    {
        throw new HttpRequestException($"Error invoking the skill id: \"{targetSkill.Id}\" at \"{targetSkill.SkillEndpoint}\" (status is {response.Status}). \r\n {response.Body}");
    }
}

É importante observar que o bot raiz inclui a lógica para encaminhar atividades para a habilidade, iniciar a habilidade mediante solicitação do usuário e interromper a habilidade quando ela é concluída.

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    if (turnContext.Activity.Text.Contains("skill"))
    {
        await turnContext.SendActivityAsync(MessageFactory.Text("Got it, connecting you to the skill..."), cancellationToken);

        // Save active skill in state
        await _activeSkillProperty.SetAsync(turnContext, _targetSkill, cancellationToken);

        // Send the activity to the skill
        await SendToSkill(turnContext, _targetSkill, cancellationToken);
        return;
    }

    // just respond
    await turnContext.SendActivityAsync(MessageFactory.Text("Me no nothin'. Say \"skill\" and I'll patch you through"), cancellationToken);

    // Save conversation state
    await _conversationState.SaveChangesAsync(turnContext, force: true, cancellationToken: cancellationToken);
}

protected override async Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
    // forget skill invocation
    await _activeSkillProperty.DeleteAsync(turnContext, cancellationToken);

    // Show status message, text and value returned by the skill
    var eocActivityMessage = $"Received {ActivityTypes.EndOfConversation}.\n\nCode: {turnContext.Activity.Code}";
    if (!string.IsNullOrWhiteSpace(turnContext.Activity.Text))
    {
        eocActivityMessage += $"\n\nText: {turnContext.Activity.Text}";
    }

    if ((turnContext.Activity as Activity)?.Value != null)
    {
        eocActivityMessage += $"\n\nValue: {JsonConvert.SerializeObject((turnContext.Activity as Activity)?.Value)}";
    }

    await turnContext.SendActivityAsync(MessageFactory.Text(eocActivityMessage), cancellationToken);

    // We are back at the root
    await turnContext.SendActivityAsync(MessageFactory.Text("Back in the root bot. Say \"skill\" and I'll patch you through"), cancellationToken);

    // Save conversation state
    await _conversationState.SaveChangesAsync(turnContext, cancellationToken: cancellationToken);
}

Manipulador de erro em ciclo

Quando ocorre um erro, o adaptador limpa o estado da conversa para redefinir a conversa com o usuário e evitar a persistência de um estado de erro.

É uma boa prática enviar uma atividade de fim da conversa para qualquer habilidade ativa antes de limpar o estado da conversa no consumidor de habilidade. Isso permite que os skills liberem os recursos associados à conversa consumidora de skills antes que o consumidor de skills libere a conversa.

SimpleRootBot\AdapterWithErrorHandler.cs

Nesse exemplo, a lógica do erro de turno é dividida entre alguns métodos auxiliares.

private async Task HandleTurnError(ITurnContext turnContext, Exception exception)
{
    // 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}");

    await SendErrorMessageAsync(turnContext, exception);
    await EndSkillConversationAsync(turnContext);
    await ClearConversationStateAsync(turnContext);
}

private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
    try
    {
        // 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);

        // Send a trace activity, which will be displayed in the Bot Framework Emulator
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught in SendErrorMessageAsync : {ex}");
    }
}

private async Task EndSkillConversationAsync(ITurnContext turnContext)
{
    if (_skillsConfig == null)
    {
        return;
    }

    try
    {
        // Inform the active skill that the conversation is ended so that it has
        // a chance to clean up.
        // Note: ActiveSkillPropertyName is set by the RooBot while messages are being
        // forwarded to a Skill.
        var activeSkill = await _conversationState.CreateProperty<BotFrameworkSkill>(RootBot.ActiveSkillPropertyName).GetAsync(turnContext, () => null);
        if (activeSkill != null)
        {
            var botId = _configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;

            var endOfConversation = Activity.CreateEndOfConversationActivity();
            endOfConversation.Code = "RootSkillError";
            endOfConversation.ApplyConversationReference(turnContext.Activity.GetConversationReference(), true);

            await _conversationState.SaveChangesAsync(turnContext, true);

            using var client = _auth.CreateBotFrameworkClient();

            await client.PostActivityAsync(botId, activeSkill.AppId, activeSkill.SkillEndpoint, _skillsConfig.SkillHostEndpoint, endOfConversation.Conversation.Id, (Activity)endOfConversation, CancellationToken.None);
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught on attempting to send EndOfConversation : {ex}");
    }
}

private async Task ClearConversationStateAsync(ITurnContext turnContext)
{
    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 ex)
    {
        _logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}");
    }
}

Ponto de extremidade de skills

O bot define um ponto de extremidade que encaminha as atividades de skills de entrada para o manipulador de skills do bot raiz.

SimpleRootBot\Controllers\SkillController.cs

[ApiController]
[Route("api/skills")]
public class SkillController : ChannelServiceController
{
    public SkillController(ChannelServiceHandlerBase handler)
        : base(handler)
    {
    }
}

Registro do serviço

Inclui um objeto de configuração de autenticação com qualquer validação de declarações, além de todos os objetos adicionais. Este exemplo usa a mesma lógica de configuração de autenticação para validar atividades de usuários e habilidades.

SimpleRootBot\Startup.cs

// Register the skills configuration class
services.AddSingleton<SkillsConfiguration>();

// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp =>
{
    var allowedSkills = sp.GetService<SkillsConfiguration>().Skills.Values.Select(s => s.AppId).ToList();

    var claimsValidator = new AllowedSkillsClaimsValidator(allowedSkills);

    // If TenantId is specified in config, add the tenant as a valid JWT token issuer for Bot to Skill conversation.
    // The token issuer for MSI and single tenant scenarios will be the tenant where the bot is registered.
    var validTokenIssuers = new List<string>();
    var tenantId = sp.GetService<IConfiguration>().GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value;

    if (!string.IsNullOrWhiteSpace(tenantId))
    {
        // For SingleTenant/MSI auth, the JWT tokens will be issued from the bot's home tenant.
        // Therefore, these issuers need to be added to the list of valid token issuers for authenticating activity requests.
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1, tenantId));
        validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, tenantId));
    }

    return new AuthenticationConfiguration
    {
        ClaimsValidator = claimsValidator,
        ValidTokenIssuers = validTokenIssuers
    };
});

Testar o bot raiz

Você pode testar o consumidor de skills no Emulador como se fosse um bot normal. No entanto, você precisa executar os bots skills e consumidores de skill ao mesmo tempo. Consulte como implementar um skill para obter informações sobre como configurar o skill.

Baixe e instale o Bot Framework Emulator mais recente

  1. Execute o bot skill de eco e o bot raiz simples localmente em seu computador. Se precisar de instruções, confira o arquivo README do exemplo em C#, JavaScript, Java ou Python.
  2. Use o Emulador para testar o bot, conforme mostrado abaixo. Quando você envia uma mensagem end ou stop para a habilidade, a habilidade envia uma atividade endOfConversation para o bot raiz, além da mensagem de resposta. A propriedade código da atividade endOfConversation indica que o skill foi concluído com êxito.

Exemplo de transcrição de uma interação com o consumidor de habilidades.

Mais sobre depuração

Como o tráfego entre as habilidades e os consumidores de habilidades é autenticado, há etapas adicionais na depuração desses bots.

  • O consumidor de habilidades e todas as habilidades que ele consome, direta ou indiretamente, devem estar em execução.
  • Se os bots estiverem sendo executados localmente e se algum dos bots tiver um ID do aplicativo e uma senha, então todos os bots deverão ter IDs e senhas válidos.
  • Se os bots estiverem todos implantados, confira como Depurar um bot em qualquer canal usando o ngrok.
  • Se alguns dos bots estiverem sendo executados localmente e alguns estiverem implantados, confira como Depurar uma habilidade ou um consumidor de habilidades.

Caso contrário, você poderá depurar um consumidor de habilidades ou uma habilidade da mesma forma como você depura outros bots. Para obter mais informações, confira Depurar um bot e Depurar com o Bot Framework Emulator.

Informações adicionais

Aqui estão algumas coisas a serem consideradas ao implementar um bot raiz mais complexo.

Para permitir que o usuário cancele um skill de várias etapas

O bot raiz deve verificar a mensagem do usuário antes de encaminhá-la para o skill ativo. Se o usuário quiser cancelar o processo atual, o bot raiz poderá enviar uma atividade endOfConversation para o skill, em vez de encaminhar a mensagem.

Para trocar dados entre os bots skills e raiz

Para enviar parâmetros para o skill, o consumidor de skills pode definir a propriedade valor nas mensagens que ele envia para o skill. Para receber valores retornados do skill, o consumidor de skills deve verificar a propriedade valor quando o skill envia uma atividade endOfConversation.

Para usar vários skills

  • Se um skill estiver ativo, o bot raiz precisará determinar qual skill está ativo e encaminhar a mensagem do usuário para o skill correto.
  • Se nenhum skill estiver ativo, o bot raiz precisará determinar qual skill deve ser iniciado, se houver algum, com base no estado do bot e na entrada do usuário.
  • Se você quiser permitir que o usuário alterne entre vários skills simultâneos, o bot raiz precisará determinar a quais dos skills ativos o usuário está pretendendo interagir antes de encaminhar a mensagem do usuário.

Usar um modo de entrega de respostas esperadas

Para usar o modo de entrega de respostas esperadas:

  • Clone a atividade do contexto de turno.
  • Defina a propriedade do modo de entrega da nova atividade como "ExpectReplies" antes de enviar a atividade do bot raiz para a habilidade.
  • Leia as respostas esperadas do corpo da resposta de invocação retornado da resposta da solicitação.
  • Processe cada atividade, seja dentro do bot raiz ou enviando-a para o canal que iniciou a solicitação original.

As respostas esperadas podem ser úteis em situações em que o bot que responde a uma atividade precisa ser a mesma instância do bot que recebeu a atividade.