Skip to content

Commit 0a0fe72

Browse files
JonathanFingoldKaiqb
authored andcommitted
Update code samples to include prompt option validations (#64)
1 parent 54859f5 commit 0a0fe72

7 files changed

Lines changed: 277 additions & 232 deletions

File tree

SDKV4-Samples/dotnet_core/DialogPromptBot/DialogPromptBot.cs

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License.
33

44
using System;
5-
using System.Collections;
65
using System.Collections.Generic;
76
using System.Linq;
87
using System.Threading;
@@ -17,31 +16,36 @@ namespace Microsoft.BotBuilderSamples
1716
{
1817
/// <summary>
1918
/// Represents a bot that processes incoming activities.
20-
/// For each user interaction, an instance of this class is created and the OnTurnAsync method is called.
21-
/// This is a Transient lifetime service. Transient lifetime services are created
22-
/// each time they're requested. For each Activity received, a new instance of this
23-
/// class is created. Objects that are expensive to construct, or have a lifetime
24-
/// beyond the single turn, should be carefully managed.
25-
/// For example, the <see cref="MemoryStorage"/> object and associated
26-
/// <see cref="IStatePropertyAccessor{T}"/> object are created with a singleton lifetime.
19+
/// For each user interaction, an instance of this class is created and the OnTurnAsync method
20+
/// is called. This is a Transient lifetime service. Transient lifetime services are created
21+
/// each time they're requested. For each Activity received, a new instance of this class is
22+
/// created. Objects that are expensive to construct, or have a lifetime beyond the single
23+
/// turn, should be carefully managed. For example, the <see cref="MemoryStorage"/> object and
24+
/// associated <see cref="IStatePropertyAccessor{T}"/> object are created with a singleton lifetime.
2725
/// </summary>
2826
public class DialogPromptBot : IBot
2927
{
3028
// Define identifiers for our dialogs and prompts.
3129
private const string ReservationDialog = "reservationDialog";
32-
private const string PartySizePrompt = "partyPrompt";
30+
private const string SizeRangePrompt = "sizeRangePrompt";
3331
private const string LocationPrompt = "locationPrompt";
3432
private const string ReservationDatePrompt = "reservationDatePrompt";
3533

34+
// Define keys for tracked values within the dialog.
35+
private const string LocationKey = "location";
36+
private const string PartySizeKey = "partySize";
37+
3638
private readonly DialogSet _dialogSet;
3739
private readonly DialogPromptBotAccessors _accessors;
3840
private readonly ILogger _logger;
3941

4042
/// <summary>
4143
/// Initializes a new instance of the <see cref="DialogPromptBot"/> class.
4244
/// </summary>
43-
/// <param name="accessors">A class containing <see cref="IStatePropertyAccessor{T}"/> used to manage state.</param>
44-
/// <param name="loggerFactory">A <see cref="ILoggerFactory"/> that is hooked to the Azure App Service provider.</param>
45+
/// <param name="accessors">A class containing <see cref="IStatePropertyAccessor{T}"/> used
46+
/// to manage state.</param>
47+
/// <param name="loggerFactory">A <see cref="ILoggerFactory"/> that is hooked to the Azure
48+
/// App Service provider.</param>
4549
public DialogPromptBot(DialogPromptBotAccessors accessors, ILoggerFactory loggerFactory)
4650
{
4751
if (loggerFactory == null)
@@ -55,18 +59,20 @@ public DialogPromptBot(DialogPromptBotAccessors accessors, ILoggerFactory logger
5559

5660
// Create the dialog set and add the prompts, including custom validation.
5761
_dialogSet = new DialogSet(_accessors.DialogStateAccessor);
58-
_dialogSet.Add(new NumberPrompt<int>(PartySizePrompt, PartySizeValidatorAsync));
62+
63+
_dialogSet.Add(new NumberPrompt<int>(SizeRangePrompt, RangeValidatorAsync));
5964
_dialogSet.Add(new ChoicePrompt(LocationPrompt));
6065
_dialogSet.Add(new DateTimePrompt(ReservationDatePrompt, DateValidatorAsync));
6166

6267
// Define the steps of the waterfall dialog and add it to the set.
63-
WaterfallStep[] steps = new WaterfallStep[]
68+
var steps = new WaterfallStep[]
6469
{
6570
PromptForPartySizeAsync,
6671
PromptForLocationAsync,
6772
PromptForReservationDateAsync,
6873
AcknowledgeReservationAsync,
6974
};
75+
7076
_dialogSet.Add(new WaterfallDialog(ReservationDialog, steps));
7177
}
7278

@@ -77,25 +83,29 @@ public DialogPromptBot(DialogPromptBotAccessors accessors, ILoggerFactory logger
7783
/// </summary>
7884
/// <param name="turnContext">A <see cref="ITurnContext"/> containing all the data needed
7985
/// for processing this conversation turn. </param>
80-
/// <param name="cancellationToken">(Optional) A <see cref="CancellationToken"/> that can be used by other objects
81-
/// or threads to receive notice of cancellation.</param>
86+
/// <param name="cancellationToken">(Optional) A <see cref="CancellationToken"/> that can
87+
/// be used by other objects or threads to receive notice of cancellation.</param>
8288
/// <returns>A <see cref="Task"/> that represents the work queued to execute.</returns>
8389
/// <seealso cref="BotStateSet"/>
8490
/// <seealso cref="ConversationState"/>
8591
/// <seealso cref="IMiddleware"/>
86-
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
92+
public async Task OnTurnAsync(
93+
ITurnContext turnContext,
94+
CancellationToken cancellationToken = default(CancellationToken))
8795
{
8896
switch (turnContext.Activity.Type)
8997
{
9098
// On a message from the user:
9199
case ActivityTypes.Message:
92100

93101
// Get the current reservation info from state.
94-
Reservation reservation = await _accessors.ReservationAccessor.GetAsync(
95-
turnContext, () => null, cancellationToken);
102+
var reservation = await _accessors.ReservationAccessor.GetAsync(
103+
turnContext,
104+
() => null,
105+
cancellationToken);
96106

97107
// Generate a dialog context for our dialog set.
98-
DialogContext dc = await _dialogSet.CreateContextAsync(turnContext, cancellationToken);
108+
var dc = await _dialogSet.CreateContextAsync(turnContext, cancellationToken);
99109

100110
if (dc.ActiveDialog is null)
101111
{
@@ -116,7 +126,7 @@ await turnContext.SendActivityAsync(
116126
else
117127
{
118128
// Continue the dialog.
119-
DialogTurnResult dialogTurnResult = await dc.ContinueDialogAsync(cancellationToken);
129+
var dialogTurnResult = await dc.ContinueDialogAsync(cancellationToken);
120130

121131
// If the dialog completed this turn, record the reservation info.
122132
if (dialogTurnResult.Status is DialogTurnStatus.Complete)
@@ -129,13 +139,15 @@ await _accessors.ReservationAccessor.SetAsync(
129139

130140
// Send a confirmation message to the user.
131141
await turnContext.SendActivityAsync(
132-
$"Your party of {reservation.Size} is confirmed for {reservation.Date}.",
142+
$"Your party of {reservation.Size} is confirmed for " +
143+
$"{reservation.Date} in {reservation.Location}.",
133144
cancellationToken: cancellationToken);
134145
}
135146
}
136147

137148
// Save the updated dialog state into the conversation state.
138-
await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
149+
await _accessors.ConversationState.SaveChangesAsync(
150+
turnContext, false, cancellationToken);
139151
break;
140152
}
141153
}
@@ -152,23 +164,33 @@ private async Task<DialogTurnResult> PromptForPartySizeAsync(
152164
{
153165
// Prompt for the party size. The result of the prompt is returned to the next step of the waterfall.
154166
return await stepContext.PromptAsync(
155-
PartySizePrompt,
167+
SizeRangePrompt,
156168
new PromptOptions
157169
{
158170
Prompt = MessageFactory.Text("How many people is the reservation for?"),
159171
RetryPrompt = MessageFactory.Text("How large is your party?"),
172+
Validations = new Range { Min = 3, Max = 8 },
160173
},
161174
cancellationToken);
162175
}
163176

164-
private async Task<DialogTurnResult> PromptForLocationAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
177+
/// <summary>Second step of the main dialog: prompt for location.</summary>
178+
/// <param name="stepContext">The context for the waterfall step.</param>
179+
/// <param name="cancellationToken">A cancellation token that can be used by other objects
180+
/// or threads to receive notice of cancellation.</param>
181+
/// <returns>A task that represents the work queued to execute.</returns>
182+
/// <remarks>If the task is successful, the result contains information from this step.</remarks>
183+
private async Task<DialogTurnResult> PromptForLocationAsync(
184+
WaterfallStepContext stepContext,
185+
CancellationToken cancellationToken)
165186
{
166187
// Record the party size information in the current dialog state.
167-
int size = (int)stepContext.Result;
168-
stepContext.Values["size"] = size;
188+
var size = (int)stepContext.Result;
189+
stepContext.Values[PartySizeKey] = size;
169190

191+
// Prompt for the location.
170192
return await stepContext.PromptAsync(
171-
"locationPrompt",
193+
LocationPrompt,
172194
new PromptOptions
173195
{
174196
Prompt = MessageFactory.Text("Please choose a location."),
@@ -178,8 +200,7 @@ private async Task<DialogTurnResult> PromptForLocationAsync(WaterfallStepContext
178200
cancellationToken);
179201
}
180202

181-
/// <summary>Second step of the main dialog: record the party size and prompt for the
182-
/// reservation date.</summary>
203+
/// <summary>Third step of the main dialog: prompt for the reservation date.</summary>
183204
/// <param name="stepContext">The context for the waterfall step.</param>
184205
/// <param name="cancellationToken">A cancellation token that can be used by other objects
185206
/// or threads to receive notice of cancellation.</param>
@@ -190,10 +211,10 @@ private async Task<DialogTurnResult> PromptForReservationDateAsync(
190211
CancellationToken cancellationToken = default(CancellationToken))
191212
{
192213
// Record the party size information in the current dialog state.
193-
var location = stepContext.Result;
194-
stepContext.Values["location"] = location;
214+
var location = (stepContext.Result as FoundChoice).Value;
215+
stepContext.Values[LocationKey] = location;
195216

196-
// Prompt for the party size. The result of the prompt is returned to the next step of the waterfall.
217+
// Prompt for the reservation date.
197218
return await stepContext.PromptAsync(
198219
ReservationDatePrompt,
199220
new PromptOptions
@@ -204,7 +225,7 @@ private async Task<DialogTurnResult> PromptForReservationDateAsync(
204225
cancellationToken);
205226
}
206227

207-
/// <summary>Third step of the main dialog: return the collected party size and reservation date.</summary>
228+
/// <summary>Last step of the main dialog: return the collected reservation information.</summary>
208229
/// <param name="stepContext">The context for the waterfall step.</param>
209230
/// <param name="cancellationToken">A cancellation token that can be used by other objects
210231
/// or threads to receive notice of cancellation.</param>
@@ -215,19 +236,20 @@ private async Task<DialogTurnResult> AcknowledgeReservationAsync(
215236
CancellationToken cancellationToken = default(CancellationToken))
216237
{
217238
// Retrieve the reservation date.
218-
DateTimeResolution resolution = (stepContext.Result as IList<DateTimeResolution>).First();
219-
string time = resolution.Value ?? resolution.Start;
239+
var resolution = (stepContext.Result as IList<DateTimeResolution>).First();
240+
var time = resolution.Value ?? resolution.Start;
220241

221242
// Send an acknowledgement to the user.
222243
await stepContext.Context.SendActivityAsync(
223244
"Thank you. We will confirm your reservation shortly.",
224245
cancellationToken: cancellationToken);
225246

226247
// Return the collected information to the parent context.
227-
Reservation reservation = new Reservation
248+
var reservation = new Reservation
228249
{
229250
Date = time,
230-
Size = (int)stepContext.Values["size"],
251+
Size = (int)stepContext.Values[PartySizeKey],
252+
Location = (string)stepContext.Values[LocationKey],
231253
};
232254
return await stepContext.EndDialogAsync(reservation, cancellationToken);
233255
}
@@ -239,7 +261,7 @@ await stepContext.Context.SendActivityAsync(
239261
/// <returns>A task that represents the work queued to execute.</returns>
240262
/// <remarks>Reservations can be made for groups of 6 to 20 people.
241263
/// If the task is successful, the result indicates whether the input was valid.</remarks>
242-
private async Task<bool> PartySizeValidatorAsync(
264+
private async Task<bool> RangeValidatorAsync(
243265
PromptValidatorContext<int> promptContext,
244266
CancellationToken cancellationToken)
245267
{
@@ -253,11 +275,17 @@ await promptContext.Context.SendActivityAsync(
253275
}
254276

255277
// Check whether the party size is appropriate.
256-
int size = promptContext.Recognized.Value;
257-
if (size < 6 || size > 20)
278+
var size = promptContext.Recognized.Value;
279+
var validRange = promptContext.Options.Validations as Range;
280+
if (size < validRange.Min || size > validRange.Max)
258281
{
259-
await promptContext.Context.SendActivityAsync(
260-
"Sorry, we can only take reservations for parties of 6 to 20.",
282+
await promptContext.Context.SendActivitiesAsync(
283+
new Activity[]
284+
{
285+
MessageFactory.Text($"Sorry, we can only take reservations for parties " +
286+
$"of {validRange.Min} to {validRange.Max}."),
287+
promptContext.Options.RetryPrompt,
288+
},
261289
cancellationToken: cancellationToken);
262290
return false;
263291
}
@@ -287,9 +315,9 @@ await promptContext.Context.SendActivityAsync(
287315

288316
// Check whether any of the recognized date-times are appropriate,
289317
// and if so, return the first appropriate date-time.
290-
DateTime earliest = DateTime.Now.AddHours(1.0);
291-
DateTimeResolution value = promptContext.Recognized.Value.FirstOrDefault(v =>
292-
DateTime.TryParse(v.Value ?? v.Start, out DateTime time) && DateTime.Compare(earliest, time) <= 0);
318+
var earliest = DateTime.Now.AddHours(1.0);
319+
var value = promptContext.Recognized.Value.FirstOrDefault(v =>
320+
DateTime.TryParse(v.Value ?? v.Start, out var time) && DateTime.Compare(earliest, time) <= 0);
293321
if (value != null)
294322
{
295323
promptContext.Recognized.Value.Clear();
@@ -303,10 +331,21 @@ await promptContext.Context.SendActivityAsync(
303331
return false;
304332
}
305333

334+
/// <summary>Describes an acceptable range of values.</summary>
335+
public class Range
336+
{
337+
public int Min { get; set; }
338+
339+
public int Max { get; set; }
340+
}
341+
342+
/// <summary>Holds a user's reservation information.</summary>
306343
public class Reservation
307344
{
308345
public int Size { get; set; }
309346

347+
public string Location { get; set; }
348+
310349
public string Date { get; set; }
311350
}
312351
}

SDKV4-Samples/dotnet_core/DialogPromptBot/DialogPromptBot.csproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" />
2020
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.7" />
2121
<PackageReference Include="AsyncUsageAnalyzers" Version="1.0.0-alpha003" PrivateAssets="all" />
22-
<PackageReference Include="Microsoft.Bot.Builder" Version="4.1.1" />
23-
<PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.1.1" />
24-
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.1.1" />
25-
<PackageReference Include="Microsoft.Bot.Configuration" Version="4.1.1" />
26-
<PackageReference Include="Microsoft.Bot.Connector" Version="4.1.1" />
27-
<PackageReference Include="Microsoft.Bot.Schema" Version="4.1.1" />
22+
<PackageReference Include="Microsoft.Bot.Builder" Version="4.2.2" />
23+
<PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.2.2" />
24+
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.2.2" />
25+
<PackageReference Include="Microsoft.Bot.Configuration" Version="4.2.2" />
26+
<PackageReference Include="Microsoft.Bot.Connector" Version="4.2.2" />
27+
<PackageReference Include="Microsoft.Bot.Schema" Version="4.2.2" />
2828
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="2.1.1" />
2929
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta008" PrivateAssets="all" />
3030
</ItemGroup>

0 commit comments

Comments
 (0)