Partilhar via


Implementar um consumidor de competências

APLICA-SE A: SDK v4

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

  • Um consumidor de habilidades deve usar a validação de reivindicações para gerenciar quais habilidades podem acessá-la.
  • Um consumidor de competências pode utilizar múltiplas competências.
  • Os desenvolvedores que não têm acesso ao código-fonte da habilidade podem usar as informações no manifesto da habilidade para projetar seu consumidor de habilidades.

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

Para obter informações sobre como usar uma caixa de diálogo de habilidade para consumir uma habilidade, consulte como usar uma caixa de diálogo para consumir uma habilidade.

Alguns tipos de consumidores de habilidades não são capazes de usar alguns tipos de bots de habilidades. A tabela a seguir descreve quais combinações são suportadas.

  Habilidade multilocatária Habilidade de inquilino único Habilidade de identidade gerenciada atribuída pelo usuário
Consumidor multilocatário Suportado Não suportado Não suportado
Consumidor de inquilino único Não suportado Suportado se ambas as aplicações pertencerem ao mesmo inquilino Suportado se ambas as aplicações pertencerem ao mesmo inquilino
Consumidor de identidade gerenciada atribuído pelo usuário Não suportado Suportado se ambas as aplicações pertencerem ao mesmo inquilino Suportado se ambas as aplicações pertencerem ao mesmo inquilino

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

Nota

A partir da versão 4.11, você não precisa de um ID de aplicativo e senha para testar um consumidor de habilidades 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 de habilidades simples de bot-to-bot inclui projetos para dois bots:

  • O bot de habilidade de eco, que implementa a habilidade.
  • O bot raiz simples, que implementa um bot raiz que consome a habilidade.

Este artigo se concentra no bot raiz, que inclui lógica de suporte em seus objetos de bot e adaptador e inclui objetos usados para trocar atividades com uma habilidade. Estes são, entre outros:

  • Um cliente de habilidade, usado para enviar atividades para uma habilidade.
  • Um manipulador de habilidades, usado para receber atividades de uma habilidade.
  • Uma fábrica de ID de conversação de habilidade, usada pelo cliente e manipulador de habilidades para traduzir entre a referência de conversação raiz do usuário e a referência de conversa de habilidade raiz.

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

Recursos

Para bots implantados, a autenticação de bot para bot requer que cada bot participante tenha informações de identidade válidas. No entanto, você pode testar habilidades multilocatárias e consumidores de habilidades localmente com o emulador sem um ID de aplicativo e senha.

Configuração da aplicação

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

Nota

Se o consumidor de habilidade ou habilidade fornece informações de identidade, ambos devem.

SimpleRootBot\appsettings.json

Opcionalmente, adicione as informações de identidade do bot raiz e adicione o aplicativo ou ID do cliente para o bot de habilidade 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 competências

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

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>();
}

Fábrica de ID de conversação

Isso cria o ID de conversa para uso com a habilidade e pode recuperar o ID de conversação do usuário original do ID de conversação de habilidade.

A fábrica de ID de conversação para este exemplo suporta um cenário simples em que:

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

O SDK fornece uma SkillConversationIdFactory classe que pode ser usada em qualquer habilidade sem exigir que o código-fonte seja replicado. A fábrica de ID de conversação está configurada em Startup.cs.

Para oferecer suporte a cenários mais complexos, projete sua fábrica de ID de conversação para que:

  • O método create skill conversation ID obtém ou gera o ID de conversação de habilidade apropriado.
  • O método de referência get conversation obtém a conversa de usuário correta.

Cliente de habilidades e manipulador de habilidades

O consumidor de habilidades usa um cliente de habilidade para encaminhar atividades para a habilidade. O cliente usa as informações de configuração de habilidades e a fábrica de ID de conversação para fazer isso.

O consumidor de habilidades usa um manipulador de habilidades para receber atividades de uma habilidade. O manipulador usa a fábrica de ID de conversação, a configuração de autenticação e um provedor de credenciais para fazer isso, e também tem dependências no adaptador e no manipulador de atividades 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 entrará no ponto de extremidade da URL do serviço que o consumidor da habilidade anuncia para a habilidade. Use um manipulador de ponto de extremidade específico do idioma para encaminhar o tráfego para o manipulador de habilidades.

O manipulador de habilidades padrão:

  • Se um ID de aplicativo e senha estiverem presentes, use um objeto de configuração de autenticação para executar a autenticação de bot para bot e a validação de declarações.
  • Usa a fábrica de ID de conversação para traduzir da conversa de habilidade do consumidor de volta para a conversa do usuário raiz.
  • Gera uma mensagem proativa para que o consumidor de habilidades possa restabelecer um contexto de turno do usuário raiz e encaminhar atividades para o usuário.

Lógica do manipulador de atividades

Note-se que a lógica do consumidor de competências deve:

  • Lembre-se se existem habilidades ativas e encaminhe atividades para elas, conforme apropriado.
  • Observe quando um usuário faz uma solicitação que deve ser encaminhada para uma habilidade e inicie a habilidade.
  • Procure uma endOfConversation atividade de qualquer habilidade ativa, para perceber quando ela é concluída.
  • Se apropriado, adicione lógica para permitir que o usuário ou consumidor de habilidade cancele uma habilidade que ainda não foi concluída.
  • Salve o estado antes de fazer a chamada para uma habilidade, pois qualquer resposta pode voltar para uma instância diferente do consumidor de habilidade.

SimpleRootBot\Bots\RootBot.cs

O bot raiz tem dependências no estado da conversação, nas informações de habilidades, no cliente de habilidades e na configuração geral. ASP.NET fornece esses objetos por meio da injeção de dependência. O bot raiz também define um acessador de propriedade de estado de conversação para rastrear qual habilidade está ativa.

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 para uma habilidade. Ele salva o estado da conversa antes de invocar a habilidade 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}");
    }
}

De notar, o bot raiz inclui lógica para encaminhar atividades para a habilidade, iniciar a habilidade a pedido do usuário e parar a habilidade quando a habilidade for 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 erros por turno

Quando ocorre um erro, o adaptador limpa o estado da conversação 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 de conversação para qualquer habilidade ativa antes de limpar o estado da conversa no consumidor de habilidades. Isso permite que a habilidade libere todos os recursos associados à conversa de habilidade do consumidor antes que o consumidor de habilidade libere a conversa.

SimpleRootBot\AdapterWithErrorHandler.cs

Neste exemplo, a lógica de 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 final de competências

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

SimpleRootBot\Controllers\SkillController.cs

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

Registo do serviço

Inclua 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 a habilidade do consumidor no emulador como se fosse um bot normal; no entanto, você precisa executar os bots de consumidor de habilidade e habilidade ao mesmo tempo. Veja como implementar uma habilidade para obter informações sobre como configurá-la.

Baixe e instale o mais recente Bot Framework Emulator

  1. Execute o bot de habilidade de eco e o bot raiz simples localmente em sua máquina. Se precisar de instruções, consulte o README arquivo para o exemplo de C#, JavaScript, Java ou Python.
  2. Use o emulador para testar o bot como mostrado abaixo. Quando você envia uma end mensagem ou stop para a habilidade, a habilidade envia para o bot raiz uma endOfConversation atividade, além da mensagem de resposta. A endOfConversation propriedade code da atividade indica que a habilidade foi concluída com êxito.

Exemplo de transcrição de uma interação com o consumidor de competências.

Mais sobre depuração

Como o tráfego entre habilidades e consumidores de habilidades é autenticado, há etapas extras ao depurar esses bots.

  • O consumidor de competências e todas as competências que consome, direta ou indiretamente, devem estar a correr.
  • Se os bots estiverem sendo executados localmente e se algum deles tiver um ID de aplicativo e senha, todos os bots deverão ter IDs e senhas válidas.
  • Se todos os bots estiverem implantados, veja como Depurar um bot de qualquer canal usando devtunnel.
  • Se alguns dos bots estiverem sendo executados localmente e alguns forem implantados, veja como Depurar uma habilidade ou consumidor de habilidades.

Caso contrário, você pode depurar um consumidor de habilidade ou habilidade da mesma forma que depurar outros bots. Para obter mais informações, consulte Depurando um bot e Depurando com o emulador do Bot Framework.

Informações adicionais

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

Para permitir que o usuário cancele uma habilidade de várias etapas

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

Para trocar dados entre os bots raiz e de habilidade

Para enviar parâmetros para a habilidade, o consumidor de habilidade pode definir a propriedade value nas mensagens que envia para a habilidade. Para receber valores de retorno da habilidade, o consumidor de habilidade deve verificar a propriedade value quando a habilidade enviar uma endOfConversation atividade.

Para usar múltiplas habilidades

  • Se uma habilidade estiver ativa, o bot raiz precisa determinar qual habilidade está ativa e encaminhar a mensagem do usuário para a habilidade correta.
  • Se nenhuma habilidade estiver ativa, o bot raiz precisará determinar qual habilidade iniciar, se houver, com base no estado do bot e na entrada do usuário.
  • Se você quiser permitir que o usuário alterne entre várias habilidades simultâneas, o bot raiz precisa determinar com qual das habilidades ativas o usuário pretende interagir antes de encaminhar a mensagem do usuário.

Para usar um modo de entrega de respostas esperadas

Para usar o modo de entrega de respostas esperadas:

  • Clone a atividade a partir 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 de resposta de chamada retornadas 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.