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.
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.
Conversational language understanding (CLU), a feature of Azure AI Language, is the updated version of LUIS.
For more information about language understanding support in the Bot Framework SDK, see Natural language understanding.
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 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 Bot Framework Emulator without identity information.
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
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.)
If you're using the LUIS model, Add the LUIS app ID, API key, and API host name.
{
"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": [ "*" ]
}
MicrosoftAppId=
MicrosoftAppPassword=
server.port=39783
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=*
dialog-skill-bot/config.py
PORT = 39783
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
APP_TYPE = os.environ.get("MicrosoftAppType", "MultiTenant")
APP_TENANTID = os.environ.get("MicrosoftAppTenantId", "")
# Callers to only those specified, '*' allows any caller.
# Example: os.environ.get("AllowedCallers", ["aaaaaaaa-1111-aaaa-aaaa-aaaaaaaa"])
ALLOWED_CALLERS = os.environ.get("AllowedCallers", ["*"])
LUIS_APP_ID = os.environ.get("LuisAppId", "")
LUIS_API_KEY = os.environ.get("LuisAPIKey", "")
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 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.
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);
}
dialogSkillBot/dialogs/activityRouterDialog.js
constructor(conversationState, luisRecognizer = undefined) {
super(ACTIVITY_ROUTER_DIALOG);
if (!conversationState) throw new Error('[MainDialog]: Missing parameter \'conversationState\' is required');
this.luisRecognizer = luisRecognizer;
// Define the main dialog and its related components.
// This is a sample "book a flight" dialog.
this.addDialog(new BookingDialog())
.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.processActivity.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
}
DialogSkillBot\Dialogs\ActivityRouterDialog.java
private final DialogSkillBotRecognizer luisRecognizer;
public ActivityRouterDialog(DialogSkillBotRecognizer luisRecognizer) {
super("ActivityRouterDialog");
this.luisRecognizer = luisRecognizer;
addDialog(new BookingDialog());
List<WaterfallStep> stepList = new ArrayList<WaterfallStep>();
stepList.add(this::processActivity);
addDialog(new WaterfallDialog("WaterfallDialog", stepList));
// The initial child Dialog to run.
setInitialDialogId("WaterfallDialog");
}
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);
}
}
dialogSkillBot/dialogs/activityRouterDialog.js
async processActivity(stepContext) {
// A skill can send trace activities, if needed.
const traceActivity = {
type: ActivityTypes.Trace,
timestamp: new Date(),
text: 'ActivityRouterDialog.processActivity()',
label: `Got activityType: ${ stepContext.context.activity.type }`
};
await stepContext.context.sendActivity(traceActivity);
switch (stepContext.context.activity.type) {
case ActivityTypes.Event:
return await this.onEventActivity(stepContext);
case ActivityTypes.Message:
return await this.onMessageActivity(stepContext);
default:
// Catch all for unhandled intents.
await stepContext.context.sendActivity(
`Unrecognized ActivityType: "${ stepContext.context.activity.type }".`,
undefined,
InputHints.IgnoringInput
);
return { status: DialogTurnStatus.complete };
}
}
/**
* This method performs different tasks based on event name.
*/
async onEventActivity(stepContext) {
const activity = stepContext.context.activity;
const traceActivity = {
type: ActivityTypes.Trace,
timestamp: new Date(),
text: 'ActivityRouterDialog.onEventActivity()',
label: `Name: ${ activity.name }, Value: ${ JSON.stringify(activity.value) }`
};
await stepContext.context.sendActivity(traceActivity);
// Resolve what to execute based on the event name.
switch (activity.name) {
case 'BookFlight':
return await this.beginBookFlight(stepContext);
case 'GetWeather':
return await this.beginGetWeather(stepContext);
default:
// We didn't get an event name we can handle.
await stepContext.context.sendActivity(
`Unrecognized EventName: "${ stepContext.context.activity.name }".`,
undefined,
InputHints.IgnoringInput
);
return { status: DialogTurnStatus.complete };
}
}
DialogSkillBot\Dialogs\ActivityRouterDialog.java
private CompletableFuture<DialogTurnResult> processActivity(WaterfallStepContext stepContext) {
// A skill can send trace activities, if needed.
TurnContext.traceActivity(
stepContext.getContext(),
String.format(
"{%s}.processActivity() Got ActivityType: %s",
this.getClass().getName(),
stepContext.getContext().getActivity().getType()
)
);
switch (stepContext.getContext().getActivity().getType()) {
case ActivityTypes.EVENT:
return onEventActivity(stepContext);
case ActivityTypes.MESSAGE:
return onMessageActivity(stepContext);
default:
String defaultMessage = String
.format("Unrecognized ActivityType: \"%s\".", stepContext.getContext().getActivity().getType());
// We didn't get an activity type we can handle.
return stepContext.getContext()
.sendActivity(MessageFactory.text(defaultMessage, defaultMessage, InputHints.IGNORING_INPUT))
.thenCompose(result -> {
return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.COMPLETE));
});
}
}
// This method performs different tasks super. on the event name.
private CompletableFuture<DialogTurnResult> onEventActivity(WaterfallStepContext stepContext) {
Activity activity = stepContext.getContext().getActivity();
TurnContext.traceActivity(
stepContext.getContext(),
String.format(
"%s.onEventActivity(), label: %s, Value: %s",
this.getClass().getName(),
activity.getName(),
GetObjectAsJsonString(activity.getValue())
)
);
// Resolve what to execute super. on the event name.
switch (activity.getName()) {
case "BookFlight":
return beginBookFlight(stepContext);
case "GetWeather":
return beginGetWeather(stepContext);
default:
String message = String.format("Unrecognized EventName: \"%s\".", activity.getName());
// We didn't get an event name we can handle.
stepContext.getContext().sendActivity(MessageFactory.text(message, message, InputHints.IGNORING_INPUT));
return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.COMPLETE));
}
}
async def process_activity(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
current_activity_type = step_context.context.activity.type
# A skill can send trace activities, if needed.
await step_context.context.send_trace_activity(
f"{ActivityRouterDialog.__name__}.process_activity()",
label=f"Got ActivityType: {current_activity_type}",
)
if current_activity_type == ActivityTypes.event:
return await self._on_event_activity(step_context)
if current_activity_type == ActivityTypes.message:
return await self._on_message_activity(step_context)
else:
# We didn't get an activity type we can handle.
await step_context.context.send_activity(
MessageFactory.text(
f'Unrecognized ActivityType: "{current_activity_type}".',
input_hint=InputHints.ignoring_input,
)
)
return DialogTurnResult(DialogTurnStatus.Complete)
async def _on_event_activity(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
"""
This method performs different tasks based on the event name.
"""
activity = step_context.context.activity
# Resolve what to execute based on the event name.
if activity.name == "BookFlight":
return await self._begin_book_flight(step_context)
if activity.name == "GetWeather":
return await self._begin_get_weather(step_context)
# We didn't get an activity name we can handle.
await step_context.context.send_activity(
MessageFactory.text(
f'Unrecognized ActivityName: "{activity.name}".',
input_hint=InputHints.ignoring_input,
)
)
return 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.
// 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);
}
dialogSkillBot/dialogs/activityRouterDialog.js
/**
* This method just gets a message activity and runs it through LUIS.
*/
async onMessageActivity(stepContext) {
const activity = stepContext.context.activity;
const traceActivity = {
type: ActivityTypes.Trace,
timestamp: new Date(),
text: 'ActivityRouterDialog.onMessageActivity()',
label: `Text: ${ activity.text }, Value: ${ JSON.stringify(activity.value) }`
};
await stepContext.context.sendActivity(traceActivity);
if (!this.luisRecognizer || !this.luisRecognizer.isConfigured) {
await stepContext.context.sendActivity(
'NOTE: LUIS is not configured. To enable all capabilities, please add \'LuisAppId\', \'LuisAPIKey\' and \'LuisAPIHostName\' to the appsettings.json file.',
undefined,
InputHints.IgnoringInput
);
} else {
// Call LUIS with the utterance.
const luisResult = await this.luisRecognizer.executeLuisQuery(stepContext.context);
const topIntent = LuisRecognizer.topIntent(luisResult);
// Create a message showing the LUIS result.
let resultString = '';
resultString += `LUIS results for "${ activity.text }":\n`;
resultString += `Intent: "${ topIntent }", Score: ${ luisResult.intents[topIntent].score }\n`;
await stepContext.context.sendActivity(resultString, undefined, InputHints.IgnoringInput);
switch (topIntent.intent) {
case 'BookFlight':
return await this.beginBookFlight(stepContext);
case 'GetWeather':
return await this.beginGetWeather(stepContext);
default: {
// Catch all for unhandled intents.
const didntUnderstandMessageText = `Sorry, I didn't get that. Please try asking in a different way (intent was ${ topIntent.intent })`;
await stepContext.context.sendActivity(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
break;
}
}
}
return { status: DialogTurnStatus.complete };
}
DialogSkillBot\Dialogs\ActivityRouterDialog.java
// This method just gets a message activity and runs it through LUS.
private CompletableFuture<DialogTurnResult> onMessageActivity(WaterfallStepContext stepContext) {
Activity activity = stepContext.getContext().getActivity();
TurnContext.traceActivity(
stepContext.getContext(),
String.format(
"%s.onMessageActivity(), label: %s, Value: %s",
this.getClass().getName(),
activity.getName(),
GetObjectAsJsonString(activity.getValue())
)
);
if (!luisRecognizer.getIsConfigured()) {
String message = "NOTE: LUIS instanceof not configured. To enable all capabilities, add 'LuisAppId',"
+ " 'LuisAPKey' and 'LuisAPHostName' to the appsettings.json file.";
return stepContext.getContext()
.sendActivity(MessageFactory.text(message, message, InputHints.IGNORING_INPUT))
.thenCompose(
result -> CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.COMPLETE))
);
} else {
// Call LUIS with the utterance.
return luisRecognizer.recognize(stepContext.getContext(), RecognizerResult.class)
.thenCompose(luisResult -> {
// Create a message showing the LUS results.
StringBuilder sb = new StringBuilder();
sb.append(String.format("LUIS results for \"%s\":", activity.getText()));
sb.append(
String.format(
"Intent: \"%s\" Score: %s",
luisResult.getTopScoringIntent().intent,
luisResult.getTopScoringIntent().score
)
);
return stepContext.getContext()
.sendActivity(MessageFactory.text(sb.toString(), sb.toString(), InputHints.IGNORING_INPUT))
.thenCompose(result -> {
switch (luisResult.getTopScoringIntent().intent.toLowerCase()) {
case "bookflight":
return beginBookFlight(stepContext);
case "getweather":
return beginGetWeather(stepContext);
default:
// Catch all for unhandled intents.
String didntUnderstandMessageText = String.format(
"Sorry, I didn't get that. Please try asking in a different "
+ "way (intent was %s)",
luisResult.getTopScoringIntent().intent
);
Activity didntUnderstandMessage = MessageFactory.text(
didntUnderstandMessageText,
didntUnderstandMessageText,
InputHints.IGNORING_INPUT
);
return stepContext.getContext()
.sendActivity(didntUnderstandMessage)
.thenCompose(
stepResult -> CompletableFuture
.completedFuture(new DialogTurnResult(DialogTurnStatus.COMPLETE))
);
}
});
// Start a dialog if we recognize the intent.
});
}
}
async def _on_message_activity(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
"""
This method just gets a message activity and runs it through LUIS.
"""
activity = step_context.context.activity
if not self._luis_recognizer.is_configured:
await step_context.context.send_activity(
MessageFactory.text(
"NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and"
" 'LuisAPIHostName' to the config.py file.",
input_hint=InputHints.ignoring_input,
)
)
else:
# Call LUIS with the utterance.
luis_result = await self._luis_recognizer.recognize(step_context.context)
message = f'LUIS results for "{activity.Text}":\n'
intent, intent_score = None, None
if luis_result.intents:
max_value_key = max(
luis_result.intents, key=lambda key: luis_result.intents[key]
)
intent, intent_score = max_value_key, luis_result.intents[max_value_key]
message += f'Intent: "{intent}" Score: {intent_score}\n'
await step_context.context.send_activity(
MessageFactory.text(message, input_hint=InputHints.ignoring_input,)
)
# Start a dialog if we recognize the intent.
top_intent = luis_result.get_top_scoring_intent().intent
if top_intent == "BookFlight":
return await self._begin_book_flight(step_context)
if top_intent == "GetWeather":
return await self._begin_get_weather(step_context)
# Catch all for unhandled intents.
didnt_understand_message_text = f"Sorry, I didn't get that. Please try asking in a different way (intent was {top_intent})"
await step_context.context.send_activity(
MessageFactory.text(
didnt_understand_message_text,
didnt_understand_message_text,
input_hint=InputHints.ignoring_input,
)
)
return 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.
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);
}
async beginGetWeather(stepContext) {
const activity = stepContext.context.activity;
const location = activity.value || {};
// We haven't implemented the GetWeatherDialog so we just display a TODO message.
const getWeatherMessageText = `TODO: get weather for here (lat: ${ location.latitude }, long: ${ location.longitude })`;
await stepContext.context.sendActivity(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
return { status: DialogTurnStatus.complete };
}
DialogSkillBot\Dialogs\ActivityRouterDialog.java
private CompletableFuture<DialogTurnResult> beginBookFlight(WaterfallStepContext stepContext) {
Activity activity = stepContext.getContext().getActivity();
BookingDetails bookingDetails = new BookingDetails();
if (activity.getValue() != null) {
try {
bookingDetails = Serialization.safeGetAs(activity.getValue(), BookingDetails.class);
} catch (JsonProcessingException e) {
// we already initialized bookingDetails above, so the flow will run as if
// no details were sent.
}
}
// Start the booking dialog.
Dialog bookingDialog = findDialog("BookingDialog");
return stepContext.beginDialog(bookingDialog.getId(), bookingDetails);
}
private static CompletableFuture<DialogTurnResult> beginGetWeather(WaterfallStepContext stepContext) {
Activity activity = stepContext.getContext().getActivity();
Location location = new Location();
if (activity.getValue() != null) {
try {
location = Serialization.safeGetAs(activity.getValue(), Location.class);
} catch (JsonProcessingException e) {
// something went wrong, so we create an empty Location so we won't get a null
// reference below when we acess location.
location = new Location();
}
}
// We haven't implemented the GetWeatherDialog so we just display a TODO
// message.
String getWeatherMessageText = String
.format("TODO: get weather for here (lat: %s, long: %s)", location.getLatitude(), location.getLongitude());
Activity getWeatherMessage =
MessageFactory.text(getWeatherMessageText, getWeatherMessageText, InputHints.IGNORING_INPUT);
return stepContext.getContext().sendActivity(getWeatherMessage).thenCompose(result -> {
return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.COMPLETE));
});
}
async def _begin_get_weather(
self, step_context: WaterfallStepContext
) -> DialogTurnResult:
activity = step_context.context.activity
location = Location()
if activity.value:
location.from_json(activity.value)
# We haven't implemented the GetWeatherDialog so we just display a TODO message.
get_weather_message = f"TODO: get weather for here (lat: {location.latitude}, long: {location.longitude}"
await step_context.context.send_activity(
MessageFactory.text(
get_weather_message, get_weather_message, InputHints.ignoring_input,
)
)
return 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.
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.
{
"$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.
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.
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.
Next, the bot asks you to choose an action for the skill. Choose "BookFlight".
The skill begins its book-flight action; answer the prompts.
When the skill completes, the root bot displays the booking details before prompting again for the skill you'd like to call.
Select DialogSkillBot again and "BookFlight".
Answer the first prompt, then enter "cancel" to cancel the action.
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.