Implement a skill

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.

  • A skill's interface is described by a manifest. Developers who don't have access to the skill's source code can use the information in the manifest to design their skill consumer.
  • A skill can use claims validation to manage which bots or users can access it.

This article demonstrates how to implement a skill that echoes the user's input.

Prerequisites

Note

Starting with version 4.11, you do not need an app ID and password to test a skill locally in the Emulator. An Azure subscription is still required to deploy your skill to Azure.

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 skill, which includes support logic in its bot and adapter.

For information about the simple root bot, see how to Implement a skill consumer.

Resources

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

To make the skill available to user-facing bots, register the skill with Azure. You can use a Bot Channels Registration. For more information, see how to register a bot with Azure Bot Service.

Application configuration

Optionally, add the skill's app ID and password to the skill's configuration file. (If either the skill or skill consumer uses an app ID and password, both must.)

The allowed callers array can restrict which skill consumers can access the skill. Add an "*" element, to accept calls from any skill consumer.

Note

If you are testing your skill locally without an app ID and password, neither the skill nor the skill consumer run the code to perform claims validation.

EchoSkillBot\appsettings.json

Add the skill's app ID and password to the appsettings.json file.

{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",

  // 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": [ "*" ]
}

Activity handler logic

To accept input parameters

The skill consumer can send information to the skill. One way to accept such information is to accept them via the value property on incoming messages. Another way is to handle event and invoke activities.

The skill in this example does not accept input parameters.

To continue or complete a conversation

When the skill sends an activity, the skill consumer should forward the activity on to the user.

However, you need to send an endOfConversation activity when the skill finishes; otherwise, the skill consumer will continue to forward user activities to the skill. Optionally, use the activity's value property to include a return value, and use the activity's code property to indicate why the skill is ending.

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

To cancel the skill

For multi-turn skills, you would also accept endOfConversation activities from a skill consumer, to allow the consumer to cancel the current conversation.

The logic for this skill does not change from turn to turn. If you implement a skill that allocates conversation resources, add resource cleanup code to the end-of-conversation handler.

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

Claims validator

This sample uses an allowed callers list for claims validation. The list is defined in the skill's configuration file and is read into the validator object when it's created.

You must add a claims validator to the authentication configuration. The claims are evaluated after the authentication header. Your validation code should throw an error or exception to reject the request. There are many reasons you may want to reject an otherwise authenticated request. For example:

  • The skill is part of a paid-for service. User's not in the database should not have access.
  • The skill is proprietary. Only certain skill consumers can call the skill.

Important

If you don't provide a claims validator, your bot will generate an error or exception upon receiving an activity from the skill consumer.

Derive a claims validator from the ClaimsValidator class. It will throw an UnauthorizedAccessException to reject an incoming request. Note that the IConfiguration.Get method returns null if the value in the configuration file is an empty array.

EchoSkillBot\Authentication\AllowedCallersClaimsValidator.cs

public class AllowedCallersClaimsValidator : ClaimsValidator
{
    private const string ConfigKey = "AllowedCallers";
    private readonly List<string> _allowedCallers;

    public AllowedCallersClaimsValidator(IConfiguration config)
    {
        if (config == null)
        {
            throw new ArgumentNullException(nameof(config));
        }

        // AllowedCallers is the setting in the appsettings.json file
        // that consists of the list of parent bot IDs that are allowed to access the skill.
        // To add a new parent bot, simply edit the AllowedCallers and add
        // the parent bot's Microsoft app ID to the list.
        // In this sample, we allow all callers if AllowedCallers contains an "*".
        var section = config.GetSection(ConfigKey);
        var appsList = section.Get<string[]>();
        if (appsList == null)
        {
            throw new ArgumentNullException($"\"{ConfigKey}\" not found in configuration.");
        }

        _allowedCallers = new List<string>(appsList);
    }

    public override Task ValidateClaimsAsync(IList<Claim> claims)
    {
        // If _allowedCallers contains an "*", we allow all callers.
        if (SkillValidation.IsSkillClaim(claims) && !_allowedCallers.Contains("*"))
        {
            // Check that the appId claim in the skill request is in the list of callers configured for this bot.
            var appId = JwtTokenValidation.GetAppIdFromClaims(claims);
            if (!_allowedCallers.Contains(appId))
            {
                throw new UnauthorizedAccessException($"Received a request from a bot with an app ID of \"{appId}\". To enable requests from this caller, add the app ID to your configuration file.");
            }
        }

        return Task.CompletedTask;
    }
}

Skill adapter

When an error occurs, the skill's adapter should clear conversation state for the skill, and it should also send an endOfConversation activity to the skill consumer. Use the code property of the activity to signal that the skill ended due to an error.

EchoSkillBot\SkillAdapterWithErrorHandler.cs

public SkillAdapterWithErrorHandler(IConfiguration configuration, ICredentialProvider credentialProvider, AuthenticationConfiguration authConfig, ILogger<BotFrameworkHttpAdapter> logger)
    : base(configuration, credentialProvider, authConfig, logger: logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    OnTurnError = HandleTurnError;
}

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

Service registration

The Bot Framework adapter uses an authentication configuration object (set when the adapter is created) to validate the authentication header on incoming requests.

This sample adds claims validation to the authentication configuration and uses the skill adapter with error handler described in the previous section.

EchoSkillBot\Startup.cs

// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedCallersClaimsValidator(sp.GetService<IConfiguration>()) });

// Create the Bot Framework Adapter with error handling enabled.
services.AddSingleton<IBotFrameworkHttpAdapter, SkillAdapterWithErrorHandler>();

Skill manifest

A skill manifest is a JSON file that describes the activities the skill can perform, its input and output parameters, and the skill's endpoints. The manifest contains the information you need to access the skill from another bot. The latest schema version is v2.1.

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

{
  "$schema": "https://schemas.botframework.com/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"
    }
  ]
}

The skill manifest schema is a JSON file that describes the schema of the skill manifest. The current schema version is 2.1.0.

Test the skill

At this point, you can test the skill in the Emulator as if it were a normal bot. However, to test it as a skill, you would need to implement a skill consumer.

Download and install the latest Bot Framework Emulator

  1. Run the echo skill bot locally on your machine. If you need instructions, refer to the README file for the C#, JavaScript, or Python sample.
  2. Use the Emulator to test the bot as shown below. Note that when you send an "end" or "stop" message to the skill, it sends an endOfConversation activity in addition to the reply message. The skill sends the endOfConversation activity to indicate the skill has finished.

test the echo skill

Next steps