实现技能消费者

适用于:SDK v4

可以使用技能扩展其他机器人。 技能是可为另一个机器人执行一组任务的机器人,并使用清单来描述其接口。 根机器人是面向用户的机器人,可调用一项或多项技能。 根机器人是一种技能消费者

  • 技能使用者可以使用声明验证来管理哪些技能可以访问它。
  • 技能使用者可以使用多项技能。
  • 无权访问技能源代码的开发人员可以使用技能清单中的信息来设计其技能使用者。

本文演示如何实现一个使用回显技能来回显用户输入的技能使用者。 如需示例技能清单并了解如何实现回显技能,请参阅如何实现技能

有关如何通过技能对话使用技能的信息,请参阅如何通过对话使用技能

某些类型的技能使用者无法使用某些类型的技能机器人。 下表介绍支持的组合方式。

  多租户技能 单租户技能 用户分配的托管标识技能
多租户消费者 支持 不支持 不支持
单租户使用者 不支持 如果两个应用都属于同一租户,则受支持 如果两个应用都属于同一租户,则受支持
用户分配的托管标识使用方 不支持 如果两个应用都属于同一租户,则受支持 如果两个应用都属于同一租户,则受支持

重要

Bot Framework SDK 和 Bot Framework Emulator 已在 GitHub 上存档。 项目不再更新或维护。 自 2025 年 12 月 31 日起,Bot Framework SDK 的支持票证将不再提供服务。

若要使用所选的 AI 服务、业务流程和知识生成代理,请考虑使用 Microsoft 365 代理 SDK。 代理 SDK 对 C#、JavaScript 或 Python 具有语言支持。 可以在 aka.ms/agents 了解有关代理 SDK 的详细信息。 如果现有的机器人是使用 Bot Framework SDK 生成的,则可以将机器人更新到代理 SDK。 查看 Bot Framework SDK 到代理 SDK 迁移指南的核心更改和更新。

如果要构建设计为在 Microsoft Teams 中工作的协作代理,请考虑使用 Teams SDK。 它为在 Teams 环境中运行的代理提供 Teams 特定的 API、自适应卡支持和内置 AI 协同调度功能。 可以在 Teams SDK(Teams AI 库)中了解详细信息。

如果要查找基于 SaaS 的代理平台,请考虑 Microsoft Copilot Studio

先决条件

注意

从版本 4.11 开始,在 Bot Framework Emulator 中以本地方式测试技能使用者不需要应用 ID 和密码。 将使用者部署到 Azure 或使用部署的技能仍需要 Azure 订阅。

关于此示例

skills-simple-bot-to-bot 示例包含下面的两个机器人的项目:

  • 实现该技能的 回显技能机器人
  • 简单根机器人,其实现的根机器人使用此技能。

本文重点介绍根机器人,其中包括其机器人和适配器对象中的支持逻辑,并包括用于与技能交换活动的对象。 其中包括:

  • 用于向技能发送活动的技能客户端。
  • 用于接收来自技能的活动的技能处理程序。
  • 一种技能会话 ID 工厂,用于技能客户端和处理程序在用户-根会话引用与根-技能会话引用之间进行转换。

有关回显技能机器人的信息,请参阅如何实现技能

资源

对于部署的机器人,机器人到机器人身份验证要求每个参与的机器人都有有效的标识信息。 但是,无需应用 ID 和密码,即可在本地使用 Emulator 测试多租户技能和技能使用者。

应用程序配置

  1. (可选)将根机器人的标识信息添加到其配置文件中。 如果技能或技能使用者任一方提供身份信息,则双方都必须提供。

  2. 添加技能主机端点(服务或回调 URL),技能应通过该端点向技能使用方回复。

  3. 针对技能使用者将要使用的每项技能添加一个条目。 每个条目包括:

    • 一个 ID,供技能使用者用来标识每项技能。
    • (可选)技能的应用或客户端 ID。
    • 技能的消息传送终结点。

注意

如果技能或技能使用者中的任一方提供身份信息,则双方都必须提供。

SimpleRootBot\appsettings.json

(可选)添加根 bot 的身份信息,并为 Echo 技能 bot 添加应用 ID 或客户端 ID。

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

聊天 ID 工厂

这会创建与技能配合使用的聊天 ID,并可从技能聊天 ID 恢复原始用户聊天 ID。

此示例的会话 ID 工厂支持一个简单的场景,其中:

  • Root 机器人旨在仅调用一种特定技能。
  • 根机器人一次只能与一个技能保持一个活跃会话。

SDK 提供的 SkillConversationIdFactory 类可跨任何技能使用,无需复制源代码。 对话 ID 中心在 Startup.cs 中配置。

若要支持更复杂的场景,请将会话 ID 工厂设计为满足以下要求:

  • “创建技能聊天 ID”方法获取或生成相应的技能聊天 ID。
  • get conversation reference 方法获取正确的用户会话。

技能客户端和技能处理器

技能使用方使用技能客户端将活动转发给该技能。 客户端使用技能配置信息和会话 ID 生成器来实现这一点。

技能使用方使用技能处理程序来接收来自技能的活动。 该处理程序使用会话 ID 工厂、身份验证配置和凭据提供程序来实现这一点,并且还依赖于根机器人的适配器和活动处理程序

SimpleRootBot\Startup.cs

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

来自技能的 HTTP 流量会进入技能使用者播发到技能的服务 URL 终结点。 使用特定于语言的终结点处理程序将流量转发到技能处理程序。

默认技能处理程序:

  • 如果提供了应用 ID 和密码,则使用身份验证配置对象执行机器人到机器人身份验证和声明验证。
  • 使用会话 ID 工厂将使用方-技能会话转换回根用户会话。
  • 生成一条主动消息,以便技能使用方重新建立根用户轮次上下文,并将活动消息转发给用户。

活动处理程序逻辑

请注意,技能使用者逻辑应执行以下操作:

  • 记住是否存在任何处于活动状态的技能,并视情况将活动转发给这些技能。
  • 注意用户何时提出某个应转发给技能的请求,并启动技能。
  • 从任何活动技能中查找 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
    };
});

测试根机器人

可以在 Emulator 中测试技能使用者,就像它是普通机器人一样;但是,你需要同时运行技能和技能使用者机器人。 请参阅如何实现技能,了解如何配置技能。

下载并安装最新的 Bot Framework Emulator

  1. 在计算机上以本地方式运行回显技能机器人和简单的根机器人。 如需说明,请参阅 READMEJavaScriptJavaPython 示例的 文件。

  2. 使用 Emulator 测试机器人,如下所示。 向技能发送 endstop 消息时,该技能还会向根机器人发送除回复消息以外的 endOfConversation 活动。 endOfConversation 活动的 code 属性指示技能已成功完成。

与技能使用者交互的示例脚本。

有关调试的更多信息

由于技能与技能使用方之间的流量已通过身份验证,因此在调试此类机器人时需要执行额外的步骤。

  • 技能调用方及其直接或间接调用的所有技能都必须处于运行状态。
  • 如果机器人在本地运行,并且任何机器人有应用 ID 和密码,则所有机器人都必须具有有效的 ID 和密码。
  • 如果所有机器人均已部署,请参阅如何使用 devtunnel 从任何通道调试机器人
  • 如果某些机器人在本地运行,而另一些机器人已部署,请参阅如何调试技能或技能使用方

否则,你可以像调试其他机器人一样调试技能使用方或技能。 有关详细信息,请参阅调试机器人使用 Bot Framework Emulator 执行调试

其他信息

下面是实现更复杂的根机器人时要考虑的一些事项。

允许用户取消多步技能

根机器人在将用户的消息转发给活动技能之前,应对该消息进行检查。 如果用户想要取消当前流程,根机器人可以向技能发送一个 endOfConversation 活动,而不是转发该消息。

在根机器人和技能机器人之间交换数据

若要将参数发送给技能,技能使用者可以在其发送给技能的消息上设置 value 属性。 若要从该技能接收返回值,技能使用方应在该技能发送 endOfConversation 活动时检查 value 属性。

使用多项技能

  • 如果某项技能处于活动状态,则根机器人需要确定哪项技能处于活动状态,然后将用户的消息转发给正确的技能。
  • 如果没有技能处于活动状态,则根机器人需要根据机器人状态和用户输入来确定要启动的技能(如果有)。
  • 如果希望允许用户在多个并发技能之间切换,则根机器人在转发用户的消息之前需确定用户要与哪项活动技能交互。

使用预期回复的传递模式

若要使用预期答复传递模式:

  • 从轮次上下文中克隆活动对象。
  • 在将活动从根机器人发送到技能之前,将新活动的"传递模式"属性设置为 "ExpectReplies"。
  • 从请求响应返回的调用响应正文中读取预期的答复
  • 处理每个活动:要么在根机器人中处理,要么将该活动发送到发起原始请求的通道。

如果对某个活动进行答复的机器人需要与接收该活动的机器人是同一个实例,则预期答复会很有用。