-
Notifications
You must be signed in to change notification settings - Fork 275
/
prompt.ts
463 lines (413 loc) · 16.4 KB
/
prompt.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity, ActivityTypes, InputHints, MessageFactory, TurnContext } from 'botbuilder-core';
import { Choice, ChoiceFactory, ChoiceFactoryOptions } from '../choices';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogEvent } from '../dialog';
import { DialogContext } from '../dialogContext';
/**
* Controls the way that choices for a `ChoicePrompt` or yes/no options for a `ConfirmPrompt` are
* presented to a user.
*/
export enum ListStyle {
/**
* Don't include any choices for prompt.
*/
none,
/**
* Automatically select the appropriate style for the current channel.
*/
auto,
/**
* Add choices to prompt as an inline list.
*/
inline,
/**
* Add choices to prompt as a numbered list.
*/
list,
/**
* Add choices to prompt as suggested actions.
*/
suggestedAction,
/**
* Add choices to prompt as a HeroCard with buttons.
*/
heroCard,
}
/**
* Basic configuration options supported by all prompts.
*/
export interface PromptOptions {
/**
* (Optional) Initial prompt to send the user.
*/
prompt?: string | Partial<Activity>;
/**
* (Optional) Retry prompt to send the user.
*/
retryPrompt?: string | Partial<Activity>;
/**
* (Optional) List of choices associated with the prompt.
*/
choices?: (string | Choice)[];
/**
* (Optional) Property that can be used to override or set the value of ChoicePrompt.Style
* when the prompt is executed using DialogContext.prompt.
*/
style?: ListStyle;
/**
* (Optional) Additional validation rules to pass the prompts validator routine.
*/
validations?: object;
/**
* The locale to be use for recognizing the utterance.
*/
recognizeLanguage?: string;
}
/**
* Result returned by a prompts recognizer function.
*
* @param T Type of value being recognized.
*/
export interface PromptRecognizerResult<T> {
/**
* If `true` the users utterance was successfully recognized and [value](#value) contains the
* recognized result.
*/
succeeded: boolean;
/**
* Value that was recognized if [succeeded](#succeeded) is `true`.
*/
value?: T;
}
/**
* Function signature for providing a custom prompt validator.
*
* ```TypeScript
* type PromptValidator<T> = (prompt: PromptValidatorContext<T>) => Promise<boolean>;
* ```
*
* @remarks
* The validator should be an asynchronous function that returns `true` if
* `prompt.recognized.value` is valid and the prompt should end.
*
* > [!NOTE]
* > If the validator returns `false` the prompts default re-prompt logic will be run unless the
* > validator sends a custom re-prompt to the user using `prompt.context.sendActivity()`. In that
* > case the prompts default re-rpompt logic will not be run.
* @param T Type of recognizer result being validated.
* @param PromptValidator.prompt Contextual information containing the recognizer result and original options passed to the prompt.
*/
export type PromptValidator<T> = (prompt: PromptValidatorContext<T>) => Promise<boolean>;
/**
* Contextual information passed to a custom `PromptValidator`.
*
* @param T Type of recognizer result being validated.
*/
export interface PromptValidatorContext<T> {
/**
* The context for the current turn of conversation with the user.
*
* @remarks
* The validator can use this to re-prompt the user.
*/
readonly context: TurnContext;
/**
* Result returned from the prompts recognizer function.
*
* @remarks
* The `prompt.recognized.succeeded` field can be checked to determine of the recognizer found
* anything and then the value can be retrieved from `prompt.recognized.value`.
*/
readonly recognized: PromptRecognizerResult<T>;
/**
* A dictionary of values persisted for each conversational turn while the prompt is active.
*
* @remarks
* The validator can use this to persist things like turn counts or other state information.
*/
readonly state: object;
/**
* Original set of options passed to the prompt by the calling dialog.
*
* @remarks
* The validator can extend this interface to support additional prompt options.
*/
readonly options: PromptOptions;
/**
* A count of the number of times the prompt has been executed.
*
* A number indicating how many times the prompt was invoked (starting at 1 for the first time it was invoked).
*/
readonly attemptCount: number;
}
/**
* Base class for all prompts.
*
* @param T Type of value being returned by the prompts recognizer function.
*/
export abstract class Prompt<T> extends Dialog {
/**
* Creates a new Prompt instance.
*
* @param dialogId Unique ID of the prompt within its parent `DialogSet` or `ComponentDialog`.
* @param validator (Optional) custom validator used to provide additional validation and re-prompting logic for the prompt.
*/
protected constructor(dialogId: string, private validator?: PromptValidator<T>) {
super(dialogId);
}
/**
* Called when a prompt dialog is pushed onto the dialog stack and is being activated.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current
* turn of the conversation.
* @param options Optional. [PromptOptions](xref:botbuilder-dialogs.PromptOptions),
* additional information to pass to the prompt being started.
* @returns A `Promise` representing the asynchronous operation.
* @remarks
* If the task is successful, the result indicates whether the prompt is still
* active after the turn has been processed by the prompt.
*/
async beginDialog(dc: DialogContext, options: PromptOptions): Promise<DialogTurnResult> {
// Ensure prompts have input hint set
const opt: Partial<PromptOptions> = { ...options };
if (opt.prompt && typeof opt.prompt === 'object' && typeof opt.prompt.inputHint !== 'string') {
opt.prompt.inputHint = InputHints.ExpectingInput;
}
if (opt.retryPrompt && typeof opt.retryPrompt === 'object' && typeof opt.retryPrompt.inputHint !== 'string') {
opt.retryPrompt.inputHint = InputHints.ExpectingInput;
}
// Initialize prompt state
const state: PromptState = dc.activeDialog.state as PromptState;
state.options = opt;
state.state = {};
// Send initial prompt
await this.onPrompt(dc.context, state.state, state.options, false);
return Dialog.EndOfTurn;
}
/**
* Called when a prompt dialog is the active dialog and the user replied with a new activity.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @returns A `Promise` representing the asynchronous operation.
* @remarks
* If the task is successful, the result indicates whether the dialog is still
* active after the turn has been processed by the dialog.
* The prompt generally continues to receive the user's replies until it accepts the
* user's reply as valid input for the prompt.
*/
async continueDialog(dc: DialogContext): Promise<DialogTurnResult> {
// Don't do anything for non-message activities
if (dc.context.activity.type !== ActivityTypes.Message) {
return Dialog.EndOfTurn;
}
// Are we being continued after an interruption?
// - The stepCount will be 1 or more if we're running in the context of an AdaptiveDialog
// and we're coming back from an interruption.
const stepCount = dc.state.getValue('turn.stepCount');
if (typeof stepCount == 'number' && stepCount > 0) {
// re-prompt and then end
await this.repromptDialog(dc.context, dc.activeDialog);
return Dialog.EndOfTurn;
}
// Perform base recognition
const state: PromptState = dc.activeDialog.state as PromptState;
const recognized: PromptRecognizerResult<T> = await this.onRecognize(dc.context, state.state, state.options);
// Validate the return value
let isValid = false;
if (this.validator) {
if (state.state['attemptCount'] === undefined) {
state.state['attemptCount'] = 0;
}
isValid = await this.validator({
context: dc.context,
recognized: recognized,
state: state.state,
options: state.options,
attemptCount: ++state.state['attemptCount'],
});
} else if (recognized.succeeded) {
isValid = true;
}
// Return recognized value or re-prompt
if (isValid) {
return await dc.endDialog(recognized.value);
} else {
if (!dc.context.responded) {
await this.onPrompt(dc.context, state.state, state.options, true);
}
return Dialog.EndOfTurn;
}
}
/**
* Called before an event is bubbled to its parent.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param event [DialogEvent](xref:botbuilder-dialogs.DialogEvent), the event being raised.
* @returns Whether the event is handled by the current dialog and further processing should stop.
* @remarks
* This is a good place to perform interception of an event as returning `true` will prevent
* any further bubbling of the event to the dialogs parents and will also prevent any child
* dialogs from performing their default processing.
*/
protected async onPreBubbleEvent(dc: DialogContext, event: DialogEvent): Promise<boolean> {
if (event.name == 'activityReceived' && dc.context.activity.type == ActivityTypes.Message) {
// Perform base recognition
const state: PromptState = dc.activeDialog.state as PromptState;
const recognized: PromptRecognizerResult<T> = await this.onRecognize(
dc.context,
state.state,
state.options
);
return recognized.succeeded;
}
return false;
}
/**
* Called when a prompt dialog resumes being the active dialog on the dialog stack, such as
* when the previous active dialog on the stack completes.
*
* @param dc The DialogContext for the current turn of the conversation.
* @param _reason An enum indicating why the dialog resumed.
* @param _result Optional, value returned from the previous dialog on the stack.
* The type of the value returned is dependent on the previous dialog.
* @returns A Promise representing the asynchronous operation.
* @remarks
* If the task is successful, the result indicates whether the dialog is still
* active after the turn has been processed by the dialog.
*/
async resumeDialog(dc: DialogContext, _reason: DialogReason, _result?: any): Promise<DialogTurnResult> {
// Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
// on top of the stack which will result in the prompt receiving an unexpected call to
// resumeDialog() when the pushed on dialog ends.
// To avoid the prompt prematurely ending we need to implement this method and
// simply re-prompt the user.
await this.repromptDialog(dc.context, dc.activeDialog);
return Dialog.EndOfTurn;
}
/**
* Called when a prompt dialog has been requested to re-prompt the user for input.
*
* @param context [TurnContext](xref:botbuilder-core.TurnContext), context for the current
* turn of conversation with the user.
* @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance), the instance
* of the dialog on the stack.
* @returns A `Promise` representing the asynchronous operation.
*/
async repromptDialog(context: TurnContext, instance: DialogInstance): Promise<void> {
const state: PromptState = instance.state as PromptState;
await this.onPrompt(context, state.state, state.options, false);
}
/**
* Called anytime the derived class should send the user a prompt.
*
* @param context Context for the current turn of conversation with the user.
* @param state Additional state being persisted for the prompt.
* @param options Options that the prompt was started with in the call to `DialogContext.prompt()`.
* @param isRetry If `true` the users response wasn't recognized and the re-prompt should be sent.
*/
protected abstract onPrompt(
context: TurnContext,
state: object,
options: PromptOptions,
isRetry: boolean
): Promise<void>;
/**
* Called to recognize an utterance received from the user.
*
* @remarks
* The Prompt class filters out non-message activities so its safe to assume that the users
* utterance can be retrieved from `context.activity.text`.
* @param context Context for the current turn of conversation with the user.
* @param state Additional state being persisted for the prompt.
* @param options Options that the prompt was started with in the call to `DialogContext.prompt()`.
*/
protected abstract onRecognize(
context: TurnContext,
state: object,
options: PromptOptions
): Promise<PromptRecognizerResult<T>>;
/**
* Helper function to compose an output activity containing a set of choices.
*
* @param prompt The prompt to append the users choices to.
* @param channelId ID of the channel the prompt is being sent to.
* @param choices List of choices to append.
* @param style Configured style for the list of choices.
* @param options (Optional) options to configure the underlying ChoiceFactory call.
* @returns The composed activity ready to send to the user.
*/
protected appendChoices(
prompt: string | Partial<Activity>,
channelId: string,
choices: (string | Choice)[],
style: ListStyle,
options?: ChoiceFactoryOptions
): Partial<Activity> {
// Get base prompt text (if any)
let text = '';
if (typeof prompt === 'string') {
text = prompt;
} else if (prompt && prompt.text) {
text = prompt.text;
}
// Create temporary msg
let msg: Partial<Activity>;
switch (style) {
case ListStyle.inline:
msg = ChoiceFactory.inline(choices, text, undefined, options);
break;
case ListStyle.list:
msg = ChoiceFactory.list(choices, text, undefined, options);
break;
case ListStyle.suggestedAction:
msg = ChoiceFactory.suggestedAction(choices, text);
break;
case ListStyle.heroCard:
msg = ChoiceFactory.heroCard(choices as Choice[], text);
break;
case ListStyle.none:
msg = MessageFactory.text(text);
break;
default:
msg = ChoiceFactory.forChannel(channelId, choices, text, undefined, options);
break;
}
// Update prompt with text, actions and attachments
if (typeof prompt === 'object') {
// Clone the prompt Activity as to not modify the original prompt.
prompt = JSON.parse(JSON.stringify(prompt)) as Activity;
prompt.text = msg.text;
if (
msg.suggestedActions &&
Array.isArray(msg.suggestedActions.actions) &&
msg.suggestedActions.actions.length > 0
) {
prompt.suggestedActions = msg.suggestedActions;
}
if (msg.attachments) {
if (prompt.attachments) {
prompt.attachments = prompt.attachments.concat(msg.attachments);
} else {
prompt.attachments = msg.attachments;
}
}
return prompt;
} else {
msg.inputHint = InputHints.ExpectingInput;
return msg;
}
}
}
/**
* @private
*/
interface PromptState {
state: any;
options: PromptOptions;
}