Бөлісу құралы:


Реализация потребителя навыка

ОБЛАСТЬ ПРИМЕНЕНИЯ: ПАКЕТ SDK версии 4

Вы можете использовать навыки для расширения функциональности другого бота. Навыком здесь называется бот, который может выполнять ряд задач для другого бота и использует манифест для описания интерфейса. Корневым ботом называется бот, который взаимодействует с пользователем и может вызывать один или несколько навыков. Корневой бот — типичный вариант потребителя навыка.

  • Потребитель навыка должен использовать проверку утверждений для управления доступом к ней навыков.
  • Потребитель навыка может использовать несколько навыков.
  • Разработчики, у которых нет доступа к исходному коду навыка, могут использовать информацию из манифеста этого навыка для разработки потребителей.

В этой статье демонстрируется создание потребителя навыка, который использует эхо-навык для вывода на экран вводимых пользователем данных. Пример манифеста навыка и сведения о реализации эхо-навыка см. в статье о реализации навыка.

Сведения об использовании диалога навыка для получения навыка см. в статье Использование диалога для получения навыка.

Некоторые типы потребителей навыков не могут использовать некоторые типы ботов навыков. В следующей таблице описывается, какие сочетания поддерживаются.

  Навыки с несколькими клиентами Навык однотенантного клиента Навык управляемого удостоверения, назначаемого пользователем
Потребитель с несколькими клиентами Поддерживается Не поддерживается Не поддерживается
Потребитель с одним клиентом Не поддерживается Поддерживается, если оба приложения принадлежат к одному клиенту Поддерживается, если оба приложения принадлежат к одному клиенту
Потребитель управляемого удостоверения, назначаемого пользователем Не поддерживается Поддерживается, если оба приложения принадлежат к одному клиенту Поддерживается, если оба приложения принадлежат к одному клиенту

Примечание.

Пакеты SDK для JavaScript, C# и Python для Bot Framework по-прежнему будут поддерживаться, однако пакет SDK java отменяется с окончательной долгосрочной поддержкой, заканчивающейся в ноябре 2023 года.

Существующие боты, созданные с помощью пакета SDK для Java, будут продолжать функционировать.

Для создания нового бота рекомендуется использовать Power Virtual Agent и ознакомиться с выбором подходящего решения чат-бота.

Дополнительные сведения см. в статье "Будущее создания бота".

Необходимые компоненты

Примечание.

Начиная с версии 4.11, вам не нужен идентификатор приложения и пароль для локального тестирования потребителя навыка в эмуляторе Bot Framework. Подписка Azure по-прежнему необходима для развертывания потребителя в Azure или использования развернутого навыка.

Об этом примере

В пример простого навыка для ботов включены проекты двух ботов:

  • бот эхо-навыка, который реализует этот навык;
  • простой корневой бот, который реализует бот для использования этого навыка.

Эта статья посвящена корневому боту, в том числе логике поддержки в объектах бота и адаптера, а также объектам, которые используются для обмена действиями с помощью навыков. Например:

  • Клиент навыка, который используется для отправки действий в навык.
  • Обработчик навыков, который используется для получения действий от навыка.
  • Фабрика идентификаторов бесед с навыками, которую клиент и обработчик навыков используют для взаимного преобразования ссылок между беседами пользователя с корневым ботом и корневого бота с навыком.

Сведения о боте с эхо-навыком см. в статье о реализации навыка.

Ресурсы

Для развернутых ботов проверка подлинности "бот — бот" требует, чтобы каждый участвующий бот имеет допустимые сведения об удостоверениях. Однако вы можете протестировать многотенантные навыки и потребители навыков локально с помощью эмулятора без идентификатора приложения и пароля.

Конфигурация приложений

  1. При необходимости добавьте данные удостоверения корневого бота в файл конфигурации. Если пользователь навыка или навыка предоставляет сведения о удостоверении, оба должны.
  2. Добавьте конечную точку узла навыка (URL-адрес службы или обратного вызова), к которой навыки должны отвечать потребителю навыка.
  3. Добавьте запись для каждого навыка, который будет использоваться потребителем навыка. Каждая запись включает:
    • идентификатор, который потребитель навыка использует для идентификации навыка;
    • Кроме того, приложение или идентификатор клиента навыка.
    • конечная точка обмена сообщениями для навыка.

Примечание.

Если пользователь навыка или навыка предоставляет сведения о удостоверении, оба должны.

SimpleRootBot\appsettings.json

При необходимости добавьте сведения об удостоверениях корневого бота и добавьте идентификатор приложения или клиента для бота эхо-навыка.

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

Настройка навыков

Наш пример бота считывает сведения о каждом навыке из файла конфигурации в коллекцию объектов skill.

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

Фабрика идентификаторов беседы

Этот элемент создает идентификатор беседы, который используется для работы с навыком, и может восстановить исходный идентификатор беседы с пользователем по идентификатору беседы с навыком.

Фабрика идентификаторов бесед для этого примера поддерживает простой сценарий со следующими характеристиками:

  • корневой бот предназначен для использования одного конкретного навыка;
  • корневой бот поддерживает только одну активную беседу с навыком в конкретный момент времени.

Пакет SDK предоставляет SkillConversationIdFactory класс, который можно использовать для любого навыка, не требуя реплика исходного кода. Фабрика идентификаторов беседы настроена в Startup.cs.

В более сложных сценариях фабрика идентификаторов бесед должна поддерживать следующие действия:

  • метод создания идентификатора беседы с навыком получает или создает идентификатор беседы с навыком;
  • метод получения ссылки на беседу получает доступ к правильной беседе с пользователем.

Клиент навыка и обработчик навыка

Потребитель навыка использует клиент навыка, чтобы пересылать действия в навык. Для этого клиент навыка использует сведения о конфигурации навыков и фабрику идентификаторов бесед.

Потребитель навыка использует обработчик навыка для получения действий от навыка. Обработчик использует для этого фабрику идентификаторов бесед, конфигурацию проверки подлинности и поставщик учетных данных, а также имеет зависимости от адаптера корневого бота и обработчика действий.

SimpleRootBot\Startup.cs

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

HTTP-трафик из навыка будет поступать в конечную точку URL-адреса службы, которую потребитель навыка объявляет навыку. Для передачи трафика обработчику навыка примените обработчик конечной точки для выбранного языка.

Обработчик навыка по умолчанию выполняет следующее:

  • Если идентификатор приложения и пароль присутствуют, использует объект конфигурации проверки подлинности для проверки подлинности ботов и утверждений.
  • использует фабрику идентификаторов бесед для преобразования ссылки на беседу потребителя с навыком в ссылку на беседу пользователя с корневым ботом;
  • создает упреждающее сообщение, которое позволяет потребителю навыка восстановить контекст реплики в беседе пользователя с корневым ботом и пересылать действия пользователю.

Логика обработчика действий

Обратите внимание, что логика потребителя навыка должна обеспечить следующее:

  • запоминать наличие активных навыков и правильно передавать в них действия;
  • обнаруживать запросы от пользователя, которые нужно передать в навык, и запускать соответствующий навык;
  • обнаруживать действие endOfConversation, поступающее от любого активного навыка, чтобы зафиксировать его завершение;
  • если это уместно, по запросу пользователя или потребителя навыка останавливать навык, который еще не завершил работу;
  • сохранять состояние перед вызовом навыка, так как любой ответ может поступить к другому экземпляру потребителя навыка

SimpleRootBot\Bots\RootBot.cs

Корневой бот имеет зависимости от состояния беседы, сведений о навыках, клиента навыка и общей конфигурации. В ASP.NET эти объекты реализуются путем внедрения зависимостей. Также корневой бот определяет метод доступа к свойству состояния беседы, чтобы отслеживать активные навыки.

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

В нашем примере есть вспомогательный метод для перенаправления действий в навык. Он сохраняет состояние беседы перед вызовом навыка и проверяет, успешно ли выполнен HTTP-запрос.

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

Обратите внимание, что в корневом элементе бота предусмотрена логика для перенаправления действий в навык, запуска навыка по запросу пользователя и остановки навыка по завершении выполнения навыка.

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

Глобальный обработчик ошибок с репликами

При возникновении ошибки адаптер очищает состояние беседы, чтобы сбросить параметры беседы с пользователем и избавиться от состояния ошибки.

Рекомендуется отправить конец действия беседы любому активному навыку перед очисткой состояния беседы в потребителе навыка. Это позволит навыку освободить все ресурсы, связанные с беседой между потребителем и навыком прежде, чем потребитель очистит эту беседу.

SimpleRootBot\AdapterWithErrorHandler.cs

В этом примере логика ошибки поворота разделена между несколькими вспомогательными методами.

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

Конечная точка навыка

Бот определяет конечную точку, которая перенаправляет входящие действия навыка в обработчик навыка корневого бота.

SimpleRootBot\Controllers\SkillController.cs

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

Регистрация службы

Добавьте объект конфигурации проверки подлинности со всеми необходимыми проверками утверждений, а также любые дополнительные объекты. В этом примере используется та же логика конфигурации проверки подлинности для проверки действий как пользователей, так и навыков.

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

Тестирование корневого бота

Вы можете протестировать потребитель навыка в эмуляторе, как любой обычный бот, но при этом навык и потребитель навыка должны одновременно работать в режиме ботов. Сведения о настройке навыка см. в статье о реализации навыка.

Скачайте и установите последнюю версию Bot Framework Emulator.

  1. Запустите бот эхо-навыка и простой корневой бот на локальном компьютере. Если вам нужны инструкции, ознакомьтесь с файлом README для примера C#, JavaScript, JavaScript или Python.
  2. Примените эмулятор для тестирования бота, как показано ниже. При отправке или stop сообщению end навыку навык отправляется корневому боту endOfConversation действие в дополнение к сообщению ответа. Свойство code действия endOfConversation указывает, что навык успешно завершен.

Пример расшифровки взаимодействия с потребителем навыка.

Дополнительные сведения об отладке

Так как трафик между навыками и потребителями навыков проходит проверку подлинности, при отладке таких ботов выполняются дополнительные действия.

  • Потребитель навыка и все навыки, которые он потребляет, прямо или косвенно, должны работать.
  • Если боты работают локально и если у любого из ботов есть идентификатор приложения и пароль, все боты должны иметь действительные идентификаторы и пароли.
  • Если боты развернуты, узнайте, как отладить бота из любого канала с помощью ngrok.
  • Если некоторые боты выполняются локально, а некоторые развертываются, см. инструкции по отладке навыка или потребителя навыков.

В противном случае можно отладить потребителя навыка или навыка, как отладить другие боты. Дополнительные сведения см. в статье отладка бота и отладка с помощью эмулятора Bot Framework.

Дополнительная информация:

Ниже приведены некоторые аспекты, которые нужно учитывать при реализации более сложного корневого бота.

Возможность отменить выполнение многоэтапного навыка

Корневой бот должен проверять сообщение пользователя, прежде чем перенаправить его активному навыку. Если пользователь хочет отменить текущий процесс, корневой бот отправляет в навык действие endOfConversation, а не само сообщение пользователя.

Обмен данными между корневым ботом и навыком

Чтобы отправить параметры навыку, потребитель навыка может задать свойство value для сообщений, отправляемых в навык. Чтобы получить возвращаемые значения от навыка, потребитель навыка должен проверять свойство value, когда получает действие endOfConversation от навыка.

Использование нескольких навыков

  • Если навык уже активен, корневой бот должен определить активный навык и перенаправить сообщение пользователя в нужный навык.
  • Если активных навыков нет, корневой бот должен определить навык для запуска (при его наличии), используя сведения о состоянии бота и сообщении пользователя.
  • Если вы хотите, чтобы пользователь мог переключаться между несколькими параллельно выполняющимися навыками, корневой бот должен определять, с каким из активных навыков намерен взаимодействовать пользователь, прежде чем перенаправлять сообщение от пользователя.

Использование режима доставки ожидаемых ответов

Чтобы использовать режим доставки ожидаемых ответов :

  • Клонируйте действие из контекста поворота.
  • Задайте для свойства режима доставки нового действия значение "ExpectReplies" перед отправкой действия от корневого бота к навыку.
  • Чтение ожидаемых ответов из текста ответа вызова , возвращенного из ответа запроса.
  • Обработайте каждое действие в корневом боте или отправив его в канал, инициирующий исходный запрос.

Ожидается, что ответы могут быть полезны в ситуациях, когда бот, который отвечает на действие, должен быть тем же экземпляром бота, который получил действие.