Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public MainDialog(ConversationState conversationState, SkillConversationIdFactor
// ChoicePrompt to render available skills and skill actions
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));

// Register the tangent
AddDialog(new TangentDialog());

// Create SkillDialog instances for the configured skills
foreach (var skillInfo in _skillsConfig.Skills.Values)
{
Expand Down Expand Up @@ -98,6 +101,12 @@ protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogCont
return await innerDc.ReplaceDialogAsync(InitialDialogId, "Canceled! \n\n What skill would you like to call?", cancellationToken);
}

if (activeSkill != null && activity.Type == ActivityTypes.Message && activity.Text.Equals("tangent", StringComparison.CurrentCultureIgnoreCase))
{
// Start tangent.
return await innerDc.BeginDialogAsync(nameof(TangentDialog), cancellationToken: cancellationToken);
}

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

Expand Down Expand Up @@ -189,7 +198,7 @@ private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepCon
message += $" Result: {JsonConvert.SerializeObject(stepContext.Result)}";
await stepContext.Context.SendActivityAsync(MessageFactory.Text(message, inputHint: InputHints.IgnoringInput), cancellationToken: cancellationToken);
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs
{
public class TangentDialog : ComponentDialog
{
public TangentDialog(string dialogId = nameof(TangentDialog))
: base(dialogId)
{
AddDialog(new TextPrompt(nameof(TextPrompt)));
var waterfallSteps = new WaterfallStep[]
{
Step1Async,
Step2Async
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));

InitialDialogId = nameof(WaterfallDialog);
}

private async Task<DialogTurnResult> Step1Async(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var promptMessage = MessageFactory.Text("Tangent step 1 of 2", InputHints.ExpectingInput);
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
}

private async Task<DialogTurnResult> Step2Async(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var promptMessage = MessageFactory.Text("Tangent step 2 of 2", InputHints.ExpectingInput);
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

namespace Microsoft.BotBuilderSamples.DialogSkillBot.Bots
{
Expand All @@ -24,57 +22,10 @@ public SkillBot(ConversationState conversationState, T mainDialog)

public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
var dialogSet = new DialogSet(_conversationState.CreateProperty<DialogState>("DialogState")) { TelemetryClient = _mainDialog.TelemetryClient };
dialogSet.Add(_mainDialog);

var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
if (turnContext.Activity.Type == ActivityTypes.EndOfConversation && dialogContext.Stack.Any())
{
// Handle remote cancellation request if we have something in the stack.
var activeDialogContext = GetActiveDialogContext(dialogContext);

// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken);
var remoteCancelText = "**SkillBot.** The current mainDialog in the skill was **canceled** by a request **from the host**, do some cleanup here if needed.";
await turnContext.SendActivityAsync(MessageFactory.Text(remoteCancelText, inputHint: InputHints.IgnoringInput), cancellationToken);
}
else
{
// Run the Dialog with the new message Activity and capture the results so we can send end of conversation if needed.
var result = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (result.Status == DialogTurnStatus.Empty)
{
var startMessageText = $"**SkillBot.** Starting {_mainDialog.Id} (.Net Core 3.1).";
await turnContext.SendActivityAsync(MessageFactory.Text(startMessageText, inputHint: InputHints.IgnoringInput), cancellationToken);
result = await dialogContext.BeginDialogAsync(_mainDialog.Id, null, cancellationToken).ConfigureAwait(false);
}

// Send end of conversation if it is complete
if (result.Status == DialogTurnStatus.Complete || result.Status == DialogTurnStatus.Cancelled)
{
var endMessageText = "**SkillBot.** The mainDialog in the skill has **completed**. Sending EndOfConversation.";
await turnContext.SendActivityAsync(MessageFactory.Text(endMessageText, inputHint: InputHints.IgnoringInput), cancellationToken);

// Send End of conversation at the end.
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = result.Result };
await turnContext.SendActivityAsync(activity, cancellationToken);
}
}
await _mainDialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);

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

// Recursively walk up the DC stack to find the active DC.
private DialogContext GetActiveDialogContext(DialogContext dialogContext)
{
var child = dialogContext.Child;
if (child == null)
{
return dialogContext;
}

return GetActiveDialogContext(child);
}
}
}
76 changes: 71 additions & 5 deletions libraries/Microsoft.Bot.Builder.Dialogs/DialogExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;

namespace Microsoft.Bot.Builder.Dialogs
{
Expand All @@ -24,16 +30,76 @@ public static class DialogExtensions
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task RunAsync(this Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken)
{
var dialogSet = new DialogSet(accessor);
dialogSet.TelemetryClient = dialog.TelemetryClient;
var dialogSet = new DialogSet(accessor) { TelemetryClient = dialog.TelemetryClient };
dialogSet.Add(dialog);

var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
var results = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (results.Status == DialogTurnStatus.Empty)

if (turnContext.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims))
{
// The bot is running as a skill.
if (turnContext.Activity.Type == ActivityTypes.EndOfConversation && dialogContext.Stack.Any())
{
// Handle remote cancellation request if we have something in the stack.
var activeDialogContext = GetActiveDialogContext(dialogContext);

var remoteCancelText = "Skill was canceled by a request from the host.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{remoteCancelText}", cancellationToken: cancellationToken).ConfigureAwait(false);

// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
// Process a reprompt event sent from the parent.
if (turnContext.Activity.Type == ActivityTypes.Event && turnContext.Activity.Name == DialogEvents.RepromptDialog && dialogContext.Stack.Any())
{
await dialogContext.RepromptDialogAsync(cancellationToken).ConfigureAwait(false);
return;
}

// Run the Dialog with the new message Activity and capture the results so we can send end of conversation if needed.
var result = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (result.Status == DialogTurnStatus.Empty)
{
var startMessageText = $"Starting {dialog.Id}.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{startMessageText}", cancellationToken: cancellationToken).ConfigureAwait(false);
result = await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
}

// Send end of conversation if it is completed or cancelled.
if (result.Status == DialogTurnStatus.Complete || result.Status == DialogTurnStatus.Cancelled)
{
var endMessageText = $"Dialog {dialog.Id} has **completed**. Sending EndOfConversation.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{endMessageText}", value: result.Result, cancellationToken: cancellationToken).ConfigureAwait(false);

// Send End of conversation at the end.
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = result.Result };
await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
}
}
}
else
{
await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
// The bot is running as a standard bot.
var results = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (results.Status == DialogTurnStatus.Empty)
{
await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
}
}
}

// Recursively walk up the DC stack to find the active DC.
private static DialogContext GetActiveDialogContext(DialogContext dialogContext)
{
var child = dialogContext.Child;
if (child == null)
{
return dialogContext;
}

return GetActiveDialogContext(child);
}
}
}
18 changes: 18 additions & 0 deletions libraries/Microsoft.Bot.Builder.Dialogs/SkillDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
return EndOfTurn;
}

public override async Task RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken = default)
{
// Create and send an envent to the skill so it can resume the dialog.
var repromptEvent = Activity.CreateEventActivity();
repromptEvent.Name = DialogEvents.RepromptDialog;

// Apply conversation reference and common properties from incoming activity before sending.
repromptEvent.ApplyConversationReference(turnContext.Activity.GetConversationReference(), true);

await SendToSkillAsync(turnContext, (Activity)repromptEvent, cancellationToken).ConfigureAwait(false);
}

public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
{
await RepromptDialogAsync(dc.Context, dc.ActiveDialog, cancellationToken).ConfigureAwait(false);
return EndOfTurn;
}

public override async Task EndDialogAsync(ITurnContext turnContext, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken = default)
{
// Send of of conversation to the skill if the dialog has been cancelled.
Expand Down