Implement a skill consumer

APPLIES TO: SDK v4

You can use skills to extend another bot. A skill is a bot that can perform a set of tasks for another bot and uses a manifest to describe its interface. A root bot is a user-facing bot that can invoke one or more skills. A root bot is a type of skill consumer.

  • A skill consumer must use claims validation to manage which skills can access it.
  • A skill consumer can use multiple skills.
  • Developers who don't have access to the skill's source code can use the information in the skill's manifest to design their skill consumer.

This article demonstrates how to implement a skill consumer that uses the echo skill to echo the user's input. For a sample skill manifest and information about implementing the echo skill, see how to implement a skill.

For information about using a skill dialog to consume a skill, see how to use a dialog to consume a skill.

Some types of skill consumers are not able to use some types of skill bots. The following table describes which combinations are supported.

  Multi-tenant skill Single-tenant skill User-assigned managed identity skill
Multi-tenant consumer Supported Not supported Not supported
Single-tenant consumer Not supported Supported if both apps belong to same tenant Supported if both apps belong to same tenant
User-assigned managed identity consumer Not supported Supported if both apps belong to same tenant Supported if both apps belong to same tenant

Note

The Bot Framework JavaScript, C#, and Python SDKs will continue to be supported, however, the Java SDK is being retired with final long-term support ending in November 2023.

Existing bots built with the Java SDK will continue to function.

For new bot building, consider using Microsoft Copilot Studio and read about choosing the right copilot solution.

For more information, see The future of bot building.

Prerequisites

Note

Starting with version 4.11, you don't need an app ID and password to test a skill consumer locally in the Bot Framework Emulator. An Azure subscription is still required to deploy your consumer to Azure or to consume a deployed skill.

About this sample

The skills simple bot-to-bot sample includes projects for two bots:

  • The echo skill bot, which implements the skill.
  • The simple root bot, which implements a root bot that consumes the skill.

This article focuses on the root bot, which includes support logic in its bot and adapter objects and includes objects used to exchange activities with a skill. These include:

  • A skill client, used to send activities to a skill.
  • A skill handler, used to receive activities from a skill.
  • A skill conversation ID factory, used by the skill client and handler to translate between the user-root conversation reference and the root-skill conversation reference.

For information about the echo skill bot, see how to Implement a skill.

Resources

For deployed bots, bot-to-bot authentication requires that each participating bot has valid identity information. However, you can test multi-tenant skills and skill consumers locally with the Emulator without an app ID and password.

Application configuration

  1. Optionally, add the root bot's identity information to its config file. If either the skill or skill consumer provides identity information, both must.
  2. Add the skill host endpoint (the service or callback URL) to which the skills should reply to the skill consumer.
  3. Add an entry for each skill the skill consumer will use. Each entry includes:
    • An ID the skill consumer will use to identify each skill.
    • Optionally, the skill's app or client ID.
    • The skill's messaging endpoint.

Note

If either the skill or skill consumer provides identity information, both must.

SimpleRootBot\appsettings.json

Optionally, add the root bot's identity information and add the app or client ID for the echo skill bot.

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

Skills configuration

This sample reads information for each skill in the configuration file into a collection of skill objects.

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

Conversation ID factory

This creates the conversation ID for use with the skill and can recover the original user conversation ID from the skill conversation ID.

The conversation ID factory for this sample supports a simple scenario where:

  • The root bot is designed to consume one specific skill.
  • The root bot has only one active conversation with a skill at a time.

The SDK provides a SkillConversationIdFactory class that can be used across any skill without requiring the source code to be replicated. The conversation ID factory is configured in Startup.cs.

To support more complex scenarios, design your conversation ID factory so that:

  • The create skill conversation ID method gets or generates the appropriate skill conversation ID.
  • The get conversation reference method gets the correct user conversation.

Skill client and skill handler

The skill consumer uses a skill client to forward activities to the skill. The client uses the skills configuration information and conversation ID factory to do so.

The skill consumer uses a skill handler to receive activities from a skill. The handler uses the conversation ID factory, the authentication configuration, and a credential provider to do so, and also has dependencies on the root bot's adapter and activity handler

SimpleRootBot\Startup.cs

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

HTTP traffic from the skill will come into the service URL endpoint that the skill consumer advertises to the skill. Use a language-specific endpoint handler to forward traffic to the skill handler.

The default skill handler:

  • If an app ID and password are present, uses an authentication configuration object to perform both bot-to-bot authentication and claims validation.
  • Uses the conversation ID factory to translate from the consumer-skill conversation back to the root-user conversation.
  • Generates a proactive message so that the skill consumer can reestablish a root-user turn context and forward activities to the user.

Activity handler logic

Of note, the skill consumer logic should:

  • Remember whether there are any active skills and forward activities to them as appropriate.
  • Notice when a user makes a request that should be forwarded to a skill, and start the skill.
  • Look for an endOfConversation activity from any active skill, to notice when it completes.
  • If appropriate, add logic to let the user or skill consumer cancel a skill that has not completed yet.
  • Save state before making the call to a skill, as any response may come back to a different instance of the skill consumer.

SimpleRootBot\Bots\RootBot.cs

The root bot has dependencies on conversation state, the skills information, the skill client, and the general configuration. ASP.NET provides these objects through dependency injection. The root bot also defines a conversation state property accessor to track which skill is active.

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

This sample has a helper method for forwarding activities to a skill. It saves conversation state before invoking the skill, and it checks whether the HTTP request was successful.

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

Of note, the root bot includes logic for forwarding activities to the skill, starting the skill at the user's request, and stopping the skill when the skill completes.

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

On turn error handler

When an error occurs, the adapter clears conversation state to reset the conversation with the user and avoid persisting an error state.

It's a good practice to send an end of conversation activity to any active skill before clearing conversation state in the skill consumer. This lets the skill release any resources associated with the consumer-skill conversation before the skill consumer releases the conversation.

SimpleRootBot\AdapterWithErrorHandler.cs

In this sample, the turn error logic is split up among a few helper methods.

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

Skills endpoint

The bot defines an endpoint that forwards incoming skill activities to the root bot's skill handler.

SimpleRootBot\Controllers\SkillController.cs

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

Service registration

Include an authentication configuration object with any claims validation, plus all the additional objects. This sample uses the same authentication configuration logic for validating activities from both users and skills.

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

Test the root bot

You can test the skill consumer in the Emulator as if it were a normal bot; however, you need to run both the skill and skill consumer bots at the same time. See how to implement a skill for information on how to configure the skill.

Download and install the latest Bot Framework Emulator

  1. Run the echo skill bot and simple root bot locally on your machine. If you need instructions, refer to the README file for the C#, JavaScript, Java, or Python sample.
  2. Use the Emulator to test the bot as shown below. When you send an end or stop message to the skill, the skill sends to the root bot an endOfConversation activity, in addition to the reply message. The endOfConversation activity's code property indicates that the skill completed successfully.

Example transcript of an interaction with the skill consumer.

More about debugging

Since traffic between skills and skill consumers is authenticated, there are extra steps when debugging such bots.

  • The skill consumer and all the skills it consumes, directly or indirectly, must be running.
  • If the bots are running locally and if any of the bots has an app ID and password, then all bots must have valid IDs and passwords.
  • If the bots are all deployed, see how to Debug a bot from any channel using devtunnel.
  • If some of the bots are running locally and some are deployed, then see how to Debug a skill or skill consumer.

Otherwise, you can debug a skill consumer or skill much like you debug other bots. For more information, see Debugging a bot and Debug with the Bot Framework Emulator.

Additional information

Here are some things to consider when implementing a more complex root bot.

To allow the user to cancel a multi-step skill

The root bot should check the user's message before forwarding it to the active skill. If the user wants to cancel the current process, the root bot can send an endOfConversation activity to the skill, instead of forwarding the message.

To exchange data between the root and skill bots

To send parameters to the skill, the skill consumer can set the value property on messages it sends to the skill. To receive return values from the skill, the skill consumer should check the value property when the skill sends an endOfConversation activity.

To use multiple skills

  • If a skill is active, the root bot needs to determine which skill is active and forward the user's message to the correct skill.
  • If no skill is active, the root bot needs to determine which skill to start, if any, based on bot state and the user's input.
  • If you want to allow the user to switch between multiple concurrent skills, the root bot needs to determine which of the active skills the user is intending to interact with before forwarding the user's message.

To use a delivery mode of expect replies

To use the expect replies delivery mode:

  • Clone the activity from the turn context.
  • Set the delivery mode property of the new activity to "ExpectReplies" before sending the activity from root bot to skill.
  • Read expected replies from the invoke response body returned from the request response.
  • Process each activity, either within the root bot or by sending it on to the channel that initiated the original request.

Expect replies can be useful in situations in which the bot that replies to an activity needs to be the same instance of the bot that received the activity.