实现技能

适用于:SDK v4

可以使用技能扩展其他机器人。 技能是可以为另一机器人执行一组任务的机器人。

  • 清单描述技能的界面。 无权访问技能源代码的开发人员可以使用清单中的信息来设计其技能使用者。
  • 技能可以使用声明验证来管理哪些机器人或用户可以访问它。

本文演示如何实现一项技能来回显用户的输入。

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

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

注意

Bot Framework JavaScript、C# 和 Python SDK 将继续受支持,但 Java SDK 即将停用,最终长期支持将于 2023 年 11 月结束。

使用 Java SDK 构建的现有机器人将继续正常运行。

要生成新的机器人,请考虑使用 Microsoft Copilot Studio 并阅读选择正确的助理解决方案

有关详细信息,请参阅机器人构建的未来

先决条件

注意

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

关于此示例

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

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

本文重点介绍技能,其中包括其机器人和适配器中的支持逻辑。

有关简单根机器人的信息,请参阅如何实现技能使用者

资源

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

若要使技能可供面向用户的机器人使用,请在 Azure 中注册该技能。 有关详细信息,请参阅如何使用 Azure AI 机器人服务注册机器人

应用程序配置

(可选)将技能的标识信息添加到其配置文件。 如果技能或技能使用者提供标识信息,则两者都必须提供。

允许的调用方数组可以对哪些技能使用者可以访问技能进行限制。 要接受来自任何技能使用者的调用,请添加“*”元素。

注意

如果在没有机器人标识信息的情况下本地测试技能,则技能和技能使用者都不会运行用于执行声明验证的代码。

EchoSkillBot\appsettings.json

(可选)将技能的标识信息添加到 appsettings.json 文件中。

{
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",

  // This is a comma separate list with the App IDs that will have access to the skill.
  // This setting is used in AllowedCallersClaimsValidator.
  // Examples: 
  //    [ "*" ] allows all callers.
  //    [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2".
  "AllowedCallers": [ "*" ]
}

活动处理程序逻辑

接受输入参数

技能使用者可以向技能发送信息。 接受此类信息的一种方法是通过传入消息上的 value 属性接受它们。 另一种方法是处理事件和调用活动。

此示例中的技能不接受输入参数。

继续或完成聊天

当技能发送活动时,技能使用者应将活动转发给用户。

但是,你需要在技能完成时发送 endOfConversation 活动;否则,技能使用者将继续将用户活动转发到技能。 可以选择使用活动的 value 属性来包括返回值,使用活动的 code 属性来指示技能的结束原因。

EchoSkillBot\Bots\EchoBot.cs

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    if (turnContext.Activity.Text.Contains("end") || turnContext.Activity.Text.Contains("stop"))
    {
        // Send End of conversation at the end.
        var messageText = $"ending conversation from the skill...";
        await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken);
        var endOfConversation = Activity.CreateEndOfConversationActivity();
        endOfConversation.Code = EndOfConversationCodes.CompletedSuccessfully;
        await turnContext.SendActivityAsync(endOfConversation, cancellationToken);
    }
    else
    {
        var messageText = $"Echo: {turnContext.Activity.Text}";
        await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput), cancellationToken);
        messageText = "Say \"end\" or \"stop\" and I'll end the conversation and back to the parent.";
        await turnContext.SendActivityAsync(MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput), cancellationToken);
    }
}

取消技能

对于多轮次技能,还可以接受来自技能使用者的 endOfConversation 活动,允许使用者取消当前聊天。

此技能的逻辑在轮次转换过程中保持不变。 如果实现一项可分配聊天资源的技能,请将资源清理代码添加到“结束聊天”处理程序。

EchoSkillBot\Bots\EchoBot.cs

protected override Task OnEndOfConversationActivityAsync(ITurnContext<IEndOfConversationActivity> turnContext, CancellationToken cancellationToken)
{
    // This will be called if the root bot is ending the conversation.  Sending additional messages should be
    // avoided as the conversation may have been deleted.
    // Perform cleanup of resources if needed.
    return Task.CompletedTask;
}

声明验证程序

此示例使用允许的调用方列表进行声明验证。 技能的配置文件定义列表。 然后,验证程序对象读取列表。

必须将声明验证程序添加到身份验证配置。 声明在身份验证标头之后进行评估。 验证代码应引发错误或异常来拒绝请求。 你可能希望拒绝通过身份验证的请求的原因有很多。 例如:

  • 技能是付费服务的一部分。 不在数据库中的用户不应拥有访问权限。
  • 技能是专有的。 只有特定的技能使用者可以调用技能。

重要

如果你不提供声明验证程序,则机器人将在收到来自技能使用者的活动时生成错误或异常。

SDK 提供了一个 AllowedCallersClaimsValidator 类,用于根据有权调用技能的应用程序的简单 ID 列表添加应用程序级授权。 如果该列表包含星号 (*),则允许所有调用方。 在 Startup.cs 中配置声明验证程序。

技能适配器

出现错误时,技能的适配器应清除技能的聊天状态,并且还应将 endOfConversation 活动发送给技能使用者。 要表示技能因错误而结束,请使用活动的 code 属性。

EchoSkillBot\SkillAdapterWithErrorHandler.cs

private async Task HandleTurnError(ITurnContext turnContext, Exception exception)
{
    // Log any leaked exception from the application.
    _logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

    await SendErrorMessageAsync(turnContext, exception);
    await SendEoCToParentAsync(turnContext, exception);
}

private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
    try
    {
        // Send a message to the user.
        var errorMessageText = "The skill 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.
        // Note: we return the entire exception in the value property to help the developer;
        // this should not be done in production.
        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 SendEoCToParentAsync(ITurnContext turnContext, Exception exception)
{
    try
    {
        // Send an EndOfConversation activity to the skill caller with the error to end the conversation,
        // and let the caller decide what to do.
        var endOfConversation = Activity.CreateEndOfConversationActivity();
        endOfConversation.Code = "SkillError";
        endOfConversation.Text = exception.Message;
        await turnContext.SendActivityAsync(endOfConversation);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Exception caught in SendEoCToParentAsync : {ex}");
    }
}

服务注册

Bot Framework 适配器使用身份验证配置对象(在创建适配器时设置)来验证传入请求的身份验证标头。

此示例将声明验证添加到身份验证配置,并使用上一节所述的包含错误处理程序的技能适配器。

EchoSkillBot\Startup.cs

    options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
});

// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp =>
{
    var allowedCallers = new List<string>(sp.GetService<IConfiguration>().GetSection("AllowedCallers").Get<string[]>());

    var claimsValidator = new AllowedCallersClaimsValidator(allowedCallers);

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

// Create the Bot Framework Authentication to be used with the Bot Adapter.
services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

技能清单

技能清单是一个 JSON 文件,用于描述技能可以执行的活动、其输入和输出参数以及技能的终结点。 清单包含从其他机器人访问技能所需的信息。 最新架构版本为 v2.1

EchoSkillBot\wwwroot\manifest\echoskillbot-manifest-1.0.json

{
  "$schema": "https://schemas.botframework.azure.cn/schemas/skills/skill-manifest-2.0.0.json",
  "$id": "EchoSkillBot",
  "name": "Echo Skill bot",
  "version": "1.0",
  "description": "This is a sample echo skill",
  "publisherName": "Microsoft",
  "privacyUrl": "https://echoskillbot.contoso.com/privacy.html",
  "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
  "license": "",
  "iconUrl": "https://echoskillbot.contoso.com/icon.png",
  "tags": [
    "sample",
    "echo"
  ],
  "endpoints": [
    {
      "name": "default",
      "protocol": "BotFrameworkV3",
      "description": "Default endpoint for the skill",
      "endpointUrl": "http://echoskillbot.contoso.com/api/messages",
      "msAppId": "00000000-0000-0000-0000-000000000000"
    }
  ]
}

技能清单架构是一个 JSON 文件,用于描述技能清单的架构。 当前版本为 2.1.0

测试技能

此时,可以在 Emulator 中测试技能,就像它是普通机器人一样。 但是,若要将其作为技能进行测试,需实现技能使用者

下载并安装最新的 Bot Framework Emulator

  1. 在计算机上以本地方式运行回显技能机器人。 如需说明,请参阅 C#JavaScriptJavaPython 示例的 README 文件。
  2. 使用模拟器测试机器人。 向技能发送“终止”或“停止”消息时,会发送除回复消息以外的 endOfConversation 活动。 技能发送 endOfConversation 活动,表示技能已完成。

显示对话活动结束的示例记录。

有关调试的更多信息

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

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

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

后续步骤