Use dialogs within a skill

APPLIES TO: SDK v4

This article demonstrates how to create a skill that supports multiple actions. It supports these actions using dialogs. The main dialog receives the initial input from the skill consumer, and then starts the appropriate action. For information about implementing the skill consumer for the associated sample code, see how to consume a skill using dialogs.

This article assumes you're already familiar with creating skills. For how to create a skill bot in general, see how to implement a skill.

Prerequisites

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 skill is an adaptation of the core bot sample. (For more about the core bot, see how to add natural language understanding to your bot.)

This article focuses on how to use a dialog within a skill bot to manage multiple actions.

For information about the skill consumer bot, see how to consume a skill using dialogs.

Resources

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

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

Optionally, the skill bot can use a flight-booking LUIS model. To use this model, use the CognitiveModels/FlightBooking.json file to create, train, and publish the LUIS model.

Application configuration

  1. Optionally, add the skill's identity information to the skill's configuration file. (If either the skill or skill consumer specifies an identity, both must.)

  2. If you're using the LUIS model, Add the LUIS app ID, API key, and API host name.

DialogSkillBot\appsettings.json

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

  "LuisAppId": "",
  "LuisAPIKey": "",
  "LuisAPIHostName": "",

  // 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-routing logic

The skill 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, it can use LUIS to try to interpret the message. The skill's manifest describes these actions, their input and output parameters, and the skill's endpoints. Of note, the skill can handle a "BookFlight" or "GetWeather" event. It can also handle message activities.

The skill defines an activity-routing dialog it uses to select which action to initiate, based on the initial incoming activity from the skill consumer. If provided, the LUIS model can recognize book-flight and get-weather intents in an initial message.

The book-flight action is a multi-step process, implemented as a separate dialog. Once the action begins, incoming activities are handled by that dialog. The get-weather action has placeholder logic that would be replaced in a fully implemented bot.

The activity-routing dialog includes code to:

The dialogs used in the skill inherit from the component dialog class. For more about component dialogs, see how to manage dialog complexity.

Initialize the dialog

The activity-routing dialog includes a child dialog for booking a flight. The main waterfall dialog has one step that will start an action based on the initial activity received.

It also accepts a LUIS recognizer. If this recognizer is initialized, the dialog will use it to interpret the intent of an initial message activity.

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private readonly DialogSkillBotRecognizer _luisRecognizer;

public ActivityRouterDialog(DialogSkillBotRecognizer luisRecognizer)
    : base(nameof(ActivityRouterDialog))
{
    _luisRecognizer = luisRecognizer;

    AddDialog(new BookingDialog());
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { ProcessActivityAsync }));

    // The initial child Dialog to run.
    InitialDialogId = nameof(WaterfallDialog);
}

Process an initial activity

In the first (and only) step of the main waterfall dialog, the skill checks the incoming activity type.

  • Event activities are forwarded to an on event activity handler that starts the appropriate action based on the name of the event.
  • Message activities are forwarded to an on message activity handler that performs additional processing before deciding what to do.

If the skill doesn't recognize the type of the incoming activity or the name of the event, it sends an error message and ends.

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private async Task<DialogTurnResult> ProcessActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // A skill can send trace activities, if needed.
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.ProcessActivityAsync()", label: $"Got ActivityType: {stepContext.Context.Activity.Type}", cancellationToken: cancellationToken);

    switch (stepContext.Context.Activity.Type)
    {
        case ActivityTypes.Event:
            return await OnEventActivityAsync(stepContext, cancellationToken);

        case ActivityTypes.Message:
            return await OnMessageActivityAsync(stepContext, cancellationToken);

        default:
            // We didn't get an activity type we can handle.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized ActivityType: \"{stepContext.Context.Activity.Type}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
            return new DialogTurnResult(DialogTurnStatus.Complete);
    }
}
// This method performs different tasks based on the event name.
private async Task<DialogTurnResult> OnEventActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnEventActivityAsync()", label: $"Name: {activity.Name}. Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);

    // Resolve what to execute based on the event name.
    switch (activity.Name)
    {
        case "BookFlight":
            return await BeginBookFlight(stepContext, cancellationToken);

        case "GetWeather":
            return await BeginGetWeather(stepContext, cancellationToken);

        default:
            // We didn't get an event name we can handle.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized EventName: \"{activity.Name}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
            return new DialogTurnResult(DialogTurnStatus.Complete);
    }
}

Handle message activities

If the LUIS recognizer is configured, the skill calls LUIS and then starts an action based on the intent. If the LUIS recognizer isn't configured or the intent isn't supported, the skill sends an error message and ends.

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

// This method just gets a message activity and runs it through LUIS. 
private async Task<DialogTurnResult> OnMessageActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnMessageActivityAsync()", label: $"Text: \"{activity.Text}\". Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);

    if (!_luisRecognizer.IsConfigured)
    {
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' to the appsettings.json file.", inputHint: InputHints.IgnoringInput), cancellationToken);
    }
    else
    {
        // Call LUIS with the utterance.
        var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);

        // Create a message showing the LUIS results.
        var sb = new StringBuilder();
        sb.AppendLine($"LUIS results for \"{activity.Text}\":");
        var (intent, intentScore) = luisResult.Intents.FirstOrDefault(x => x.Value.Equals(luisResult.Intents.Values.Max()));
        sb.AppendLine($"Intent: \"{intent}\" Score: {intentScore.Score}");

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(sb.ToString(), inputHint: InputHints.IgnoringInput), cancellationToken);

        // Start a dialog if we recognize the intent.
        switch (luisResult.TopIntent().intent)
        {
            case FlightBooking.Intent.BookFlight:
                return await BeginBookFlight(stepContext, cancellationToken);

            case FlightBooking.Intent.GetWeather:
                return await BeginGetWeather(stepContext, cancellationToken);

            default:
                // Catch all for unhandled intents.
                var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
                var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
                await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
                break;
        }
    }

    return new DialogTurnResult(DialogTurnStatus.Complete);
}

Begin a multi-step action

The book-flight action starts a multi-step dialog to get the booking details from the user.

The get-weather action isn't implemented. Currently, it sends a placeholder message and then ends.

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private async Task<DialogTurnResult> BeginBookFlight(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    var bookingDetails = new BookingDetails();
    if (activity.Value != null)
    {
        bookingDetails = JsonConvert.DeserializeObject<BookingDetails>(JsonConvert.SerializeObject(activity.Value));
    }

    // Start the booking dialog.
    var bookingDialog = FindDialog(nameof(BookingDialog));
    return await stepContext.BeginDialogAsync(bookingDialog.Id, bookingDetails, cancellationToken);
}
private static async Task<DialogTurnResult> BeginGetWeather(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    var location = new Location();
    if (activity.Value != null)
    {
        location = JsonConvert.DeserializeObject<Location>(JsonConvert.SerializeObject(activity.Value));
    }

    // We haven't implemented the GetWeatherDialog so we just display a TODO message.
    var getWeatherMessageText = $"TODO: get weather for here (lat: {location.Latitude}, long: {location.Longitude}";
    var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
    await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
    return new DialogTurnResult(DialogTurnStatus.Complete);
}

Return a result

The skill starts a booking dialog for the book-flight action. Since the activity-routing dialog has just one step, when the booking dialog ends, the activity-routing dialog also ends, and the dialog result from the booking dialog becomes the dialog result for the activity-routing dialog.

The get-weather action simply ends without setting a return value.

Canceling a multi-step action

The booking dialog and its child date-resolver dialog both derive from the base cancel-and-help dialog, which checks messages from the user.

  • On "help" or "?", it displays a help message, and then continues the conversation flow on the following turn.
  • On "cancel" or "quit", it cancels all dialogs, which ends the skill.

For more information, see how to handle user interruptions.

Service registration

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

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.

DialogSkillBot\wwwroot\manifest\dialogchildbot-manifest-1.0.json

{
  "$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.0.0.json",
  "$id": "DialogSkillBot",
  "name": "Skill bot with dialogs",
  "version": "1.0",
  "description": "This is a sample skill definition for multiple activity types.",
  "publisherName": "Microsoft",
  "privacyUrl": "https://dialogskillbot.contoso.com/privacy.html",
  "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
  "license": "",
  "iconUrl": "https://dialogskillbot.contoso.com/icon.png",
  "tags": [
    "sample",
    "travel",
    "weather",
    "luis"
  ],
  "endpoints": [
    {
      "name": "default",
      "protocol": "BotFrameworkV3",
      "description": "Default endpoint for the skill.",
      "endpointUrl": "https://dialogskillbot.contoso.com/api/messages",
      "msAppId": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "activities": {
    "bookFlight": {
      "description": "Books a flight (multi turn).",
      "type": "event",
      "name": "BookFlight",
      "value": {
        "$ref": "#/definitions/bookingInfo"
      },
      "resultValue": {
        "$ref": "#/definitions/bookingInfo"
      }
    },
    "getWeather": {
      "description": "Retrieves and returns the weather for the user's location.",
      "type": "event",
      "name": "GetWeather",
      "value": {
        "$ref": "#/definitions/location"
      },
      "resultValue": {
        "$ref": "#/definitions/weatherReport"
      }
    },
    "passthroughMessage": {
      "type": "message",
      "description": "Receives the user's utterance and attempts to resolve it using the skill's LUIS models.",
      "value": {
        "type": "object"
      }
    }
  },
  "definitions": {
    "bookingInfo": {
      "type": "object",
      "required": [
        "origin"
      ],
      "properties": {
        "origin": {
          "type": "string",
          "description": "This is the origin city for the flight."
        },
        "destination": {
          "type": "string",
          "description": "This is the destination city for the flight."
        },
        "travelDate": {
          "type": "string",
          "description": "The date for the flight in YYYY-MM-DD format."
        }
      }
    },
    "weatherReport": {
      "type": "array",
      "description": "Array of forecasts for the next week.",
      "items": [
        {
          "type": "string"
        }
      ]
    },
    "location": {
      "type": "object",
      "description": "Location metadata.",
      "properties": {
        "latitude": {
          "type": "number",
          "title": "Latitude"
        },
        "longitude": {
          "type": "number",
          "title": "Longitude"
        },
        "postalCode": {
          "type": "string",
          "title": "Postal code"
        }
      }
    }
  }
}

The skill manifest schema is a JSON file that describes the schema of the skill manifest. The latest schema version is v2.1.

Test the skill bot

You can test the skill in the Emulator with the skill consumer. To do so, you need to run both the skill and skill consumer bots at the same time. See how to use a dialog to consume 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 sample's README file for C#, JavaScript, Java, or Python.
  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. Next, the bot asks you to choose an action for the skill. Choose "BookFlight".
    1. The skill begins its book-flight action; answer the prompts.
    2. When the skill completes, 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 "cancel" to cancel the action.
    2. The skill bot ends without completing the action, and the consumer prompts for the skill you'd like to call.

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 ngrok.
  • 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 Emulator.

Additional information