Use a dialog to consume a skill

APPLIES TO: SDK v4

This article demonstrates how to use a skill dialog within a skill consumer. The skill dialog posts activities from the parent bot to the skill bot and returns the skill responses to the user. The skill bot accessed by this consumer can handle both message and event activities. For a sample skill manifest and information about implementing the skill, see how to use dialogs within a skill.

For information about using a skill bot outside of dialogs, see how to implement a skill consumer.

Prerequisites

Note

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

About this sample

The skills skillDialog sample includes projects for two bots:

  • The dialog root bot, which uses a skill dialog class to consume a skill.
  • The dialog skill bot, which uses a dialog to handle activities coming from skill consumers.

This article focuses on how to use a skill dialog class in a root bot to manage the skill, to send message and event activities and to cancel the skill.

For information about other aspects of creating a skill consumer, see how to implement a skill consumer.

For information about the dialog skill bot, see how to use dialogs within a skill.

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.

Application configuration

  1. Optionally, add the root bot's app ID and password to the config file.
  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 ID.
    • The skill's messaging endpoint.

Note

If either the skill or skill consumer uses an app ID and password, both must.

DialogRootBot\appsettings.json

Optionally, add the root bot's app ID and password and add the app ID for the echo skill bot to the BotFrameworkSkills array.

{
  "MicrosoftAppId": "TODO: Add here the App ID for the bot",
  "MicrosoftAppPassword": "TODO: Add here the password for the bot",

  "SkillHostEndpoint": "http://localhost:3978/api/skills/",
  "BotFrameworkSkills": [
    {
      "Id": "DialogSkillBot",
      "AppId": "TODO: Add here the App ID for the skill",
      "SkillEndpoint": "http://localhost:39783/api/messages"
    }
  ]
}

Dialog logic

The bot's main dialog includes a skill dialog for each skill this bot consumes. The skill dialog manages the skill through the various skill-related objects for you, such as the skill client and the skill conversation ID factory objects. The main dialog also demonstrates how to cancel the skill (through the skill dialog) based on user input.

The skill this bot uses supports a couple different features. It can book a flight or get the weather for a city. In addition, if it receives a message outside either of these contexts and a LUIS recognizer is configured, it attempts to interpret the user's intent.

The skill manifest (C#, JavaScript, Python) describes the actions the skill can perform, its input and output parameters, and the skill's endpoints. Of note, the skill can handle a "BookFlight" or "GetWeather" event. It can also handle messages.

The main dialog includes code to:

The main dialog inherits from the component dialog class. For more about component dialogs, see how to manage dialog complexity.

Initialize the main dialog

The main dialog includes dialogs (for managing conversation flow outside the skill) and a skill dialogs (for managing the skills). The waterfall includes the following steps, described in more detail in the next few sections.

  1. Prompt the user to select the skill to use. (The root bot consumes 1 skill.)
  2. Prompt the user to select the action to use for that skill. (The skill bot defines 3 actions.)
  3. Start the chosen skill with an initial activity based on the chosen action.
  4. Once the skill completes, display the results, if any. Then, restart the waterfall.

DialogRootBot\Dialogs\MainDialog.cs

The MainDialog class derives from ComponentDialog. In addition to conversation state, the dialog needs the root bot's app ID and references to the skill conversation ID factory, the skill HTTP client, and the skills configuration objects.

The dialog constructor checks its input parameters, adds skills dialogs, adds prompt and a waterfall dialogs for managing conversation flow outside the skill, and creates a property accessor for tracking the active skill, if any.

The constructor calls AddSkillDialogs, a helper method, to create a SkillDialog for each skill that is included in the configuration file, as read from the configuration file into a SkillsConfiguration object.

// Helper method that creates and adds SkillDialog instances for the configured skills.
private void AddSkillDialogs(ConversationState conversationState, SkillConversationIdFactoryBase conversationIdFactory, SkillHttpClient skillClient, SkillsConfiguration skillsConfig, string botId)
{
    foreach (var skillInfo in _skillsConfig.Skills.Values)
    {
        // Create the dialog options.
        var skillDialogOptions = new SkillDialogOptions
        {
            BotId = botId,
            ConversationIdFactory = conversationIdFactory,
            SkillClient = skillClient,
            SkillHostEndpoint = skillsConfig.SkillHostEndpoint,
            ConversationState = conversationState,
            Skill = skillInfo
        };

        // Add a SkillDialog for the selected skill.
        AddDialog(new SkillDialog(skillDialogOptions, skillInfo.Id));
    }
}

Select a skill

In its first step, the main dialog prompts the user for which skill they'd like to call, and uses the "SkllPrompt" choice prompt to get the answer. (This bot defines only one skill.)

DialogRootBot\Dialogs\MainDialog.cs

// Render a prompt to select the skill to call.
private async Task<DialogTurnResult> SelectSkillStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Create the PromptOptions from the skill configuration which contain the list of configured skills.
    var messageText = stepContext.Options?.ToString() ?? "What skill would you like to call?";
    var repromptMessageText = "That was not a valid choice, please select a valid skill.";
    var options = new PromptOptions
    {
        Prompt = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput),
        RetryPrompt = MessageFactory.Text(repromptMessageText, repromptMessageText, InputHints.ExpectingInput),
        Choices = _skillsConfig.Skills.Select(skill => new Choice(skill.Value.Id)).ToList()
    };

    // Prompt the user to select a skill.
    return await stepContext.PromptAsync("SkillPrompt", options, cancellationToken);
}

Select a skill action

In the next step, the main dialog:

  1. Saves information about the skill the user selected.
  2. Prompts the user for which skill action they'd like to use, and uses the "SkillActionPrompt" choice prompt to get the answer.
    • It uses a helper method to get a list of actions to choose from.
    • The prompt validator associated with this prompt will default to sending the skill a message if the user's input doesn't match one of the choices.

The choices included in this bot help test the actions defined for this skill. More typically, you would read the options from the skill's manifest, and present options to the user based on that list.

DialogRootBot\Dialogs\MainDialog.cs

// Render a prompt to select the action for the skill.
private async Task<DialogTurnResult> SelectSkillActionStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Get the skill info based on the selected skill.
    var selectedSkillId = ((FoundChoice)stepContext.Result).Value;
    var selectedSkill = _skillsConfig.Skills.FirstOrDefault(s => s.Value.Id == selectedSkillId).Value;

    // Remember the skill selected by the user.
    stepContext.Values[_selectedSkillKey] = selectedSkill;

    // Create the PromptOptions with the actions supported by the selected skill.
    var messageText = $"Select an action # to send to **{selectedSkill.Id}** or just type in a message and it will be forwarded to the skill";
    var options = new PromptOptions
    {
        Prompt = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput),
        Choices = GetSkillActions(selectedSkill)
    };

    // Prompt the user to select a skill action.
    return await stepContext.PromptAsync("SkillActionPrompt", options, cancellationToken);
}
// Helper method to create Choice elements for the actions supported by the skill.
private IList<Choice> GetSkillActions(BotFrameworkSkill skill)
{
    // Note: the bot would probably render this by reading the skill manifest.
    // We are just using hardcoded skill actions here for simplicity.

    var choices = new List<Choice>();
    switch (skill.Id)
    {
        case "DialogSkillBot":
            choices.Add(new Choice(SkillActionBookFlight));
            choices.Add(new Choice(SkillActionBookFlightWithInputParameters));
            choices.Add(new Choice(SkillActionGetWeather));
            break;
    }

    return choices;
}
// This validator defaults to Message if the user doesn't select an existing option.
private Task<bool> SkillActionPromptValidator(PromptValidatorContext<FoundChoice> promptContext, CancellationToken cancellationToken)
{
    if (!promptContext.Recognized.Succeeded)
    {
        // Assume the user wants to send a message if an item in the list is not selected.
        promptContext.Recognized.Value = new FoundChoice { Value = SkillActionMessage };
    }

    return Task.FromResult(true);
}

Start a skill

In the next step, the main dialog:

  1. Retrieves information about the skill and skill activity the user selected.
  2. Uses a helper method to create the activity to initially send to the skill.
  3. Creates the dialog options with which to start the skill dialog. This includes the initial activity to send.
  4. Saves state before calling the skill. (This is necessary, as the skill response might come to a different instance of the skill consumer.)
  5. Begins the skill dialog, passing in the skill ID to call and the options with which to call it.

DialogRootBot\Dialogs\MainDialog.cs

// Starts the SkillDialog based on the user's selections.
private async Task<DialogTurnResult> CallSkillActionStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var selectedSkill = (BotFrameworkSkill)stepContext.Values[_selectedSkillKey];

    Activity skillActivity;
    switch (selectedSkill.Id)
    {
        case "DialogSkillBot":
            skillActivity = CreateDialogSkillBotActivity(((FoundChoice)stepContext.Result).Value, stepContext.Context);
            break;

        // We can add other case statements here if we support more than one skill.
        default:
            throw new Exception($"Unknown target skill id: {selectedSkill.Id}.");
    }

    // Create the BeginSkillDialogOptions and assign the activity to send.
    var skillDialogArgs = new BeginSkillDialogOptions { Activity = skillActivity };

    // Save active skill in state.
    await _activeSkillProperty.SetAsync(stepContext.Context, selectedSkill, cancellationToken);

    // Start the skillDialog instance with the arguments. 
    return await stepContext.BeginDialogAsync(selectedSkill.Id, skillDialogArgs, cancellationToken);
}

Summarize the skill result

In the last step, the main dialog:

  1. If the skill returned a value, display the result to the user.
  2. Clears the active skill from dialog state.
  3. Removes the active skill property from conversation state.
  4. Restarts itself (the main dialog).

DialogRootBot\Dialogs\MainDialog.cs

// The SkillDialog has ended, render the results (if any) and restart MainDialog.
private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activeSkill = await _activeSkillProperty.GetAsync(stepContext.Context, () => null, cancellationToken);

    // Check if the skill returned any results and display them.
    if (stepContext.Result != null)
    {
        var message = $"Skill \"{activeSkill.Id}\" invocation complete.";
        message += $" Result: {JsonConvert.SerializeObject(stepContext.Result)}";
        await stepContext.Context.SendActivityAsync(MessageFactory.Text(message, message, inputHint: InputHints.IgnoringInput), cancellationToken: cancellationToken);
    }

    // Clear the skill selected by the user.
    stepContext.Values[_selectedSkillKey] = null;

    // Clear active skill in state.
    await _activeSkillProperty.DeleteAsync(stepContext.Context, cancellationToken);

    // Restart the main dialog with a different message the second time around.
    return await stepContext.ReplaceDialogAsync(InitialDialogId, $"Done with \"{activeSkill.Id}\". \n\n What skill would you like to call?", cancellationToken);
}

Allow the user to cancel the skill

The main dialog overrides the default behavior of the on continue dialog method to allow the user to cancel the current skill, if any. The method:

  • If there is an active skill and the user sends an "abort" message, cancel all dialogs and queue the main dialog to restart from the beginning.
  • Then, call the base implementation of the on continue dialog method to continue processing the current turn.

DialogRootBot\Dialogs\MainDialog.cs

protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
    // This is an example on how to cancel a SkillDialog that is currently in progress from the parent bot.
    var activeSkill = await _activeSkillProperty.GetAsync(innerDc.Context, () => null, cancellationToken);
    var activity = innerDc.Context.Activity;
    if (activeSkill != null && activity.Type == ActivityTypes.Message && activity.Text.Equals("abort", StringComparison.CurrentCultureIgnoreCase))
    {
        // Cancel all dialogs when the user says abort.
        // The SkillDialog automatically sends an EndOfConversation message to the skill to let the
        // skill know that it needs to end its current dialogs, too.
        await innerDc.CancelAllDialogsAsync(cancellationToken);
        return await innerDc.ReplaceDialogAsync(InitialDialogId, "Canceled! \n\n What skill would you like to call?", cancellationToken);
    }

    return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}

Activity handler logic

Since skill logic for each turn is handled by a main dialog, the activity handler looks much like it would for other dialog samples.

DialogRootBot\Bots\RootBot.cs

public class RootBot<T> : ActivityHandler
    where T : Dialog
private readonly ConversationState _conversationState;
private readonly Dialog _mainDialog;

public RootBot(ConversationState conversationState, T mainDialog)
{
    _conversationState = conversationState;
    _mainDialog = mainDialog;
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
    if (turnContext.Activity.Type != ActivityTypes.ConversationUpdate)
    {
        // Run the Dialog with the Activity.
        await _mainDialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
    }
    else
    {
        // Let the base class handle the activity.
        await base.OnTurnAsync(turnContext, cancellationToken);
    }

    // Save any state changes that might have occurred during the turn.
    await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}

Service registration

The services needed to use a skill dialog are the same as those needed for a skill consumer in general. See how to implement a skill consumer for a discussion of the required services.

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 use dialogs within a skill for information on how to configure the skill.

Download and install the latest Bot Framework Emulator.

  1. Run the dialog skill bot and dialog root 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.
    • When you first join the conversation, the bot displays a welcome message and asks you what skill you would like to call. The skill bot for this sample has just one skill.
    • Select DialogSkillBot.
  3. The bot next asks you to choose an action for the skill. Choose "BookFlight".
    1. Answer the prompts.
    2. The skill completes, and the root bot displays the booking details before prompting again for the skill you'd like to call.
  4. Select DialogSkillBot again and "BookFlight".
    1. Answer the first prompt, then enter "abort" to interrupt the skill.
    2. The root bot cancels the skill and prompts for the skill you'd like to call.

Additional information

See how to implement a skill consumer for how to implement a skill consumer in general.