實作技能取用者

適用於: SDK v4

您可以使用技能來擴充另一個 Bot。 技能是 Bot,可以針對另一個 Bot 執行一組工作,並使用指令清單來描述其介面。 根 Bot 是使用者面向的 Bot,可叫用一或多個技能。 根 Bot 是一種技能取用者

  • 技能取用者必須使用宣告驗證來管理哪些技能可以存取。
  • 技能取用者可以使用多個技能。
  • 無法存取技能原始程式碼的開發人員可以使用技能指令清單中的資訊來設計其技能取用者。

本文示範如何實作使用回應技能來回應使用者輸入的技能取用者。 如需實作回應技能的範例技能指令清單和相關信息,請參閱如何 實作技能

如需使用技能對話框來取用技能的相關信息,請參閱如何使用 對話來取用技能

某些類型的技能取用者無法使用某些類型的技能 Bot。 下表描述支援哪些組合。

  多租使用者技能 單一租使用者技能 使用者指派的受控識別技能
多租用戶取用者 支援 不支援 不支援
單一租用戶取用者 不支援 如果兩個應用程式都屬於相同的租使用者,則支援 如果兩個應用程式都屬於相同的租使用者,則支援
使用者指派的受控識別取用者 不支援 如果兩個應用程式都屬於相同的租使用者,則支援 如果兩個應用程式都屬於相同的租使用者,則支援

注意

Bot Framework JavaScript、C# 和 Python SDK 將會繼續受到支援,不過,Java SDK 即將淘汰,最終長期支援將於 2023 年 11 月結束。

使用 Java SDK 建置的現有 Bot 將繼續運作。

針對新的 Bot 建置,請考慮使用 Power Virtual Agents ,並閱讀 選擇正確的聊天機器人解決方案

如需詳細資訊,請參閱 Bot 建置的未來。

必要條件

注意

從 4.11 版開始,您不需要應用程式識別碼和密碼,即可在 Bot Framework 模擬器本機測試技能取用者。 仍然需要 Azure 訂用帳戶,才能將取用者部署至 Azure 或取用已部署的技能。

關於此範例

技能 簡單的 Bot 對 Bot 範例包含兩個 Bot 的專案:

  • 顯技能 Bot,可實作技能。
  • 簡單的 根 Bot,其會實作取用技能的根 Bot。

本文著重於根 Bot,其中包含其 Bot 和配接器物件中的支持邏輯,並包含用來與技能交換活動的物件。 包括:

  • 用來將活動傳送至技能的技能用戶端。
  • 技能處理程式,用來接收技能的活動。
  • 技能客戶端和處理程式用來在使用者根對話參考與根技能交談參考之間轉譯的技能交談標識碼處理站。

如需回應技能 Bot 的相關信息,請參閱如何 實作技能

資源

對於已部署的 Bot,Bot 對 Bot 驗證會要求每個參與的 Bot 都有有效的身分識別資訊。 不過,您可以使用模擬器在本機測試多租使用者技能和技能取用者,而不需要應用程式標識碼和密碼。

應用程式設定

  1. 或者,將根 Bot 的身分識別資訊新增至其組態檔。 如果技能或技能取用者提供身分識別資訊,則兩者都必須。
  2. 將技能主機端點(服務或回呼 URL)新增至技能取用者應回復的技能。
  3. 為技能取用者將使用的每個技能新增專案。 每個項目都包含:
    • 技能取用者將用來識別每個技能的標識碼。
    • 或者,技能的應用程式或用戶端標識符。
    • 技能的傳訊端點。

注意

如果技能或技能取用者提供身分識別資訊,則兩者都必須。

SimpleRootBot\appsettings.json

選擇性地新增根 Bot 的身分識別資訊,並新增回應技能 Bot 的應用程式或用戶端識別碼。

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

技能設定

此範例會將組態檔中每個技能的資訊讀入技能物件的集合

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

交談標識碼處理站

這會建立與技能搭配使用的交談標識碼,而且可以從技能交談標識碼復原原始使用者交談標識碼。

此範例的交談標識碼處理站支援下列簡單案例:

  • 根 Bot 的設計目的是要取用一個特定技能。
  • 根 Bot 一次只有一個作用中的交談與技能。

SDK 提供可 SkillConversationIdFactory 跨任何技能使用的類別,而不需要復寫原始程式碼。 交談標識碼處理站是在 Startup.cs 中 設定

若要支援更複雜的案例,請設計對話標識碼處理站,以便:

  • 建立 技能交談標識碼 方法會取得或產生適當的技能交談標識碼。
  • 取得交談參考方法會取得正確的使用者交談。

技能用戶端和技能處理程式

技能取用者會使用技能用戶端將活動轉送至技能。 用戶端會使用技能設定資訊和交談標識碼處理站來執行此動作。

技能取用者會使用技能處理程式從技能接收活動。 處理程式會使用交談標識符處理站、驗證組態和認證提供者來執行此動作,而且也具有根 Bot 配接器和活動處理程式的相依性

SimpleRootBot\Startup.cs

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

來自技能的 HTTP 流量會進入技能取用者向技能公告的服務 URL 端點。 使用語言特定的端點處理程式,將流量轉送至技能處理程式。

預設技能處理程式:

  • 如果應用程式識別碼和密碼存在,請使用驗證組態對象來執行 Bot 對 Bot 驗證和宣告驗證。
  • 使用交談標識碼處理站,將取用者技能交談轉譯回根使用者交談。
  • 產生主動式訊息,讓技能取用者可以重新建立根使用者回合內容,並將活動轉寄給使用者。

活動處理程序邏輯

請注意,技能取用者邏輯應該:

  • 請記住,是否有任何作用中的技能,並視需要轉送活動給它們。
  • 請注意,當使用者提出應轉送至技能的要求,並啟動技能時,請注意。
  • 尋找 endOfConversation 任何作用中技能的活動,以在完成時注意。
  • 如果適用,請新增邏輯,讓使用者或技能取用者取消尚未完成的技能。
  • 在呼叫技能之前儲存狀態,因為任何回應都可能會回到技能取用者的不同實例。

SimpleRootBot\Bots\RootBot.cs

根 Bot 具有交談狀態、技能資訊、技能用戶端和一般設定的相依性。 ASP.NET 透過相依性插入提供這些物件。 根 Bot 也會定義交談狀態屬性存取子,以追蹤哪些技能為作用中。

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

請注意,根 Bot 包含將活動轉送至技能、在使用者要求下啟動技能,以及在技能完成時停止技能的邏輯。

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

技能端點

Bot 會定義端點,以將連入技能活動轉送至根 Bot 的技能處理程式。

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

您可以在模擬器中測試技能取用者,就像是一般 Bot 一樣;不過,您必須同時執行技能與技能取用者 Bot。 如需如何設定技能的資訊,請參閱如何 實作技能

下載並安裝最新的 Bot Framework 模擬器

  1. 在本機計算機上執行回應技能 Bot 和簡單的根 Bot。 如果您需要指示,請參閱 README C#JavaScriptJavaPython 範例的 檔案。
  2. 使用模擬器來測試 Bot,如下所示。 當您將 或 stop 訊息傳送end至技能時,除了回復訊息之外,技能也會傳送至根 Bot endOfConversation 活動。 活動的 endOfConversation 程式 代碼 屬性表示技能已順利完成。

與技能取用者互動的範例文字記錄。

深入瞭解偵錯

由於技能與技能取用者之間的流量已經過驗證,因此偵錯這類 Bot 時會有額外的步驟。

  • 技能取用者及其直接或間接取用的所有技能都必須執行。
  • 如果 Bot 在本機執行,且任何 Bot 都有應用程式識別碼和密碼,則所有 Bot 都必須具有有效的標識碼和密碼。
  • 如果 Bot 全部部署,請參閱如何使用 ngrok 從任何通道對 Bot 進行偵錯。
  • 如果某些 Bot 在本機執行,且有些 Bot 已部署,請參閱如何偵錯技能或技能取用

否則,您可以偵錯技能取用者或技能,就像偵錯其他 Bot 一樣。 如需詳細資訊,請參閱使用 Bot Framework 模擬器進行 Bot 偵錯和偵錯。

其他資訊

以下是實作更複雜的根 Bot 時要考慮的一些事項。

允許使用者取消多步驟技能

根 Bot 應該先檢查使用者的訊息,再將它轉送至作用中的技能。 如果使用者想要取消目前的程式,根 Bot 可以將活動傳送 endOfConversation 至技能,而不是轉送訊息。

在根和技能 Bot 之間交換數據

若要將參數傳送至技能,技能取用者可以在傳送至技能的訊息上設定 value 屬性。 若要從技能接收傳回值,技能取用者應該在技能傳送endOfConversation活動時檢查 value 屬性。

使用多個技能

  • 如果技能為作用中,根 Bot 必須判斷哪個技能為作用中,並將使用者的訊息轉送至正確的技能。
  • 如果沒有作用中的技能,根 Bot 必須根據 Bot 狀態和使用者的輸入來判斷要啟動的技能。
  • 如果您想要允許使用者在多個並行技能之間切換,根 Bot 必須判斷使用者想要與其互動的作用中技能,再轉送使用者的訊息。

使用預期回復的傳遞模式

若要使用 預期的回復 傳遞模式:

  • 從回合內容複製活動。
  • 將新活動的傳遞模式屬性設定為 「ExpectReplies」,再將活動從根 Bot 傳送至技能。
  • 從要求回應傳回的叫用回應本文讀取預期的回復
  • 處理根 Bot 內的每個活動,或將它傳送至起始原始要求的通道。

在回復活動的 Bot 必須是接收活動的 Bot 實例時,預期回復可能會很有用。