Skip to content

Commit 705f7fb

Browse files
authored
Added interruptions support to SkillDialog (#3507)
* Added RepromptDialogAsync and ResumeDialogAsync to SkillDialog. Moved skill request handling logic into Dialog.RunAsync Updated sample DialogToDialog sample to handle interruptions
1 parent 0833051 commit 705f7fb

File tree

5 files changed

+140
-56
lines changed

5 files changed

+140
-56
lines changed

FunctionalTests/Skills/DialogToDialog/DialogRootBot/Dialogs/MainDialog.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public MainDialog(ConversationState conversationState, SkillConversationIdFactor
5353
// ChoicePrompt to render available skills and skill actions
5454
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
5555

56+
// Register the tangent
57+
AddDialog(new TangentDialog());
58+
5659
// Create SkillDialog instances for the configured skills
5760
foreach (var skillInfo in _skillsConfig.Skills.Values)
5861
{
@@ -98,6 +101,12 @@ protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogCont
98101
return await innerDc.ReplaceDialogAsync(InitialDialogId, "Canceled! \n\n What skill would you like to call?", cancellationToken);
99102
}
100103

104+
if (activeSkill != null && activity.Type == ActivityTypes.Message && activity.Text.Equals("tangent", StringComparison.CurrentCultureIgnoreCase))
105+
{
106+
// Start tangent.
107+
return await innerDc.BeginDialogAsync(nameof(TangentDialog), cancellationToken: cancellationToken);
108+
}
109+
101110
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
102111
}
103112

@@ -189,7 +198,7 @@ private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepCon
189198
message += $" Result: {JsonConvert.SerializeObject(stepContext.Result)}";
190199
await stepContext.Context.SendActivityAsync(MessageFactory.Text(message, inputHint: InputHints.IgnoringInput), cancellationToken: cancellationToken);
191200
}
192-
201+
193202
// Clear the skill selected by the user.
194203
stepContext.Values[_selectedSkillKey] = null;
195204

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Bot.Builder;
7+
using Microsoft.Bot.Builder.Dialogs;
8+
using Microsoft.Bot.Schema;
9+
10+
namespace Microsoft.BotBuilderSamples.DialogRootBot.Dialogs
11+
{
12+
public class TangentDialog : ComponentDialog
13+
{
14+
public TangentDialog(string dialogId = nameof(TangentDialog))
15+
: base(dialogId)
16+
{
17+
AddDialog(new TextPrompt(nameof(TextPrompt)));
18+
var waterfallSteps = new WaterfallStep[]
19+
{
20+
Step1Async,
21+
Step2Async
22+
};
23+
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
24+
25+
InitialDialogId = nameof(WaterfallDialog);
26+
}
27+
28+
private async Task<DialogTurnResult> Step1Async(WaterfallStepContext stepContext, CancellationToken cancellationToken)
29+
{
30+
var promptMessage = MessageFactory.Text("Tangent step 1 of 2", InputHints.ExpectingInput);
31+
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
32+
}
33+
34+
private async Task<DialogTurnResult> Step2Async(WaterfallStepContext stepContext, CancellationToken cancellationToken)
35+
{
36+
var promptMessage = MessageFactory.Text("Tangent step 2 of 2", InputHints.ExpectingInput);
37+
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
38+
}
39+
}
40+
}
Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using System.Linq;
54
using System.Threading;
65
using System.Threading.Tasks;
76
using Microsoft.Bot.Builder;
87
using Microsoft.Bot.Builder.Dialogs;
9-
using Microsoft.Bot.Schema;
108

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

2523
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
2624
{
27-
var dialogSet = new DialogSet(_conversationState.CreateProperty<DialogState>("DialogState")) { TelemetryClient = _mainDialog.TelemetryClient };
28-
dialogSet.Add(_mainDialog);
29-
30-
var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
31-
if (turnContext.Activity.Type == ActivityTypes.EndOfConversation && dialogContext.Stack.Any())
32-
{
33-
// Handle remote cancellation request if we have something in the stack.
34-
var activeDialogContext = GetActiveDialogContext(dialogContext);
35-
36-
// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
37-
await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken);
38-
var remoteCancelText = "**SkillBot.** The current mainDialog in the skill was **canceled** by a request **from the host**, do some cleanup here if needed.";
39-
await turnContext.SendActivityAsync(MessageFactory.Text(remoteCancelText, inputHint: InputHints.IgnoringInput), cancellationToken);
40-
}
41-
else
42-
{
43-
// Run the Dialog with the new message Activity and capture the results so we can send end of conversation if needed.
44-
var result = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
45-
if (result.Status == DialogTurnStatus.Empty)
46-
{
47-
var startMessageText = $"**SkillBot.** Starting {_mainDialog.Id} (.Net Core 3.1).";
48-
await turnContext.SendActivityAsync(MessageFactory.Text(startMessageText, inputHint: InputHints.IgnoringInput), cancellationToken);
49-
result = await dialogContext.BeginDialogAsync(_mainDialog.Id, null, cancellationToken).ConfigureAwait(false);
50-
}
51-
52-
// Send end of conversation if it is complete
53-
if (result.Status == DialogTurnStatus.Complete || result.Status == DialogTurnStatus.Cancelled)
54-
{
55-
var endMessageText = "**SkillBot.** The mainDialog in the skill has **completed**. Sending EndOfConversation.";
56-
await turnContext.SendActivityAsync(MessageFactory.Text(endMessageText, inputHint: InputHints.IgnoringInput), cancellationToken);
57-
58-
// Send End of conversation at the end.
59-
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = result.Result };
60-
await turnContext.SendActivityAsync(activity, cancellationToken);
61-
}
62-
}
25+
await _mainDialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
6326

6427
// Save any state changes that might have occured during the turn.
6528
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
6629
}
67-
68-
// Recursively walk up the DC stack to find the active DC.
69-
private DialogContext GetActiveDialogContext(DialogContext dialogContext)
70-
{
71-
var child = dialogContext.Child;
72-
if (child == null)
73-
{
74-
return dialogContext;
75-
}
76-
77-
return GetActiveDialogContext(child);
78-
}
7930
}
8031
}

libraries/Microsoft.Bot.Builder.Dialogs/DialogExtensions.cs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System.Linq;
5+
using System.Security.Claims;
6+
using System.Security.Principal;
47
using System.Threading;
58
using System.Threading.Tasks;
9+
using Microsoft.Bot.Builder.TraceExtensions;
10+
using Microsoft.Bot.Connector.Authentication;
11+
using Microsoft.Bot.Schema;
612

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

3136
var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
32-
var results = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
33-
if (results.Status == DialogTurnStatus.Empty)
37+
38+
if (turnContext.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims))
39+
{
40+
// The bot is running as a skill.
41+
if (turnContext.Activity.Type == ActivityTypes.EndOfConversation && dialogContext.Stack.Any())
42+
{
43+
// Handle remote cancellation request if we have something in the stack.
44+
var activeDialogContext = GetActiveDialogContext(dialogContext);
45+
46+
var remoteCancelText = "Skill was canceled by a request from the host.";
47+
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{remoteCancelText}", cancellationToken: cancellationToken).ConfigureAwait(false);
48+
49+
// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
50+
await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false);
51+
}
52+
else
53+
{
54+
// Process a reprompt event sent from the parent.
55+
if (turnContext.Activity.Type == ActivityTypes.Event && turnContext.Activity.Name == DialogEvents.RepromptDialog && dialogContext.Stack.Any())
56+
{
57+
await dialogContext.RepromptDialogAsync(cancellationToken).ConfigureAwait(false);
58+
return;
59+
}
60+
61+
// Run the Dialog with the new message Activity and capture the results so we can send end of conversation if needed.
62+
var result = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
63+
if (result.Status == DialogTurnStatus.Empty)
64+
{
65+
var startMessageText = $"Starting {dialog.Id}.";
66+
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{startMessageText}", cancellationToken: cancellationToken).ConfigureAwait(false);
67+
result = await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
68+
}
69+
70+
// Send end of conversation if it is completed or cancelled.
71+
if (result.Status == DialogTurnStatus.Complete || result.Status == DialogTurnStatus.Cancelled)
72+
{
73+
var endMessageText = $"Dialog {dialog.Id} has **completed**. Sending EndOfConversation.";
74+
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{endMessageText}", value: result.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
75+
76+
// Send End of conversation at the end.
77+
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = result.Result };
78+
await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
79+
}
80+
}
81+
}
82+
else
3483
{
35-
await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
84+
// The bot is running as a standard bot.
85+
var results = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
86+
if (results.Status == DialogTurnStatus.Empty)
87+
{
88+
await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken).ConfigureAwait(false);
89+
}
3690
}
3791
}
92+
93+
// Recursively walk up the DC stack to find the active DC.
94+
private static DialogContext GetActiveDialogContext(DialogContext dialogContext)
95+
{
96+
var child = dialogContext.Child;
97+
if (child == null)
98+
{
99+
return dialogContext;
100+
}
101+
102+
return GetActiveDialogContext(child);
103+
}
38104
}
39105
}

libraries/Microsoft.Bot.Builder.Dialogs/SkillDialog.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
8484
return EndOfTurn;
8585
}
8686

87+
public override async Task RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken = default)
88+
{
89+
// Create and send an envent to the skill so it can resume the dialog.
90+
var repromptEvent = Activity.CreateEventActivity();
91+
repromptEvent.Name = DialogEvents.RepromptDialog;
92+
93+
// Apply conversation reference and common properties from incoming activity before sending.
94+
repromptEvent.ApplyConversationReference(turnContext.Activity.GetConversationReference(), true);
95+
96+
await SendToSkillAsync(turnContext, (Activity)repromptEvent, cancellationToken).ConfigureAwait(false);
97+
}
98+
99+
public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
100+
{
101+
await RepromptDialogAsync(dc.Context, dc.ActiveDialog, cancellationToken).ConfigureAwait(false);
102+
return EndOfTurn;
103+
}
104+
87105
public override async Task EndDialogAsync(ITurnContext turnContext, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken = default)
88106
{
89107
// Send of of conversation to the skill if the dialog has been cancelled.

0 commit comments

Comments
 (0)