/
dialog.ts
495 lines (452 loc) · 17.4 KB
/
dialog.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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BotTelemetryClient, NullTelemetryClient, TurnContext } from 'botbuilder-core';
import { DialogContext } from './dialogContext';
import { Configurable } from './configurable';
/**
* Contains state information for an instance of a dialog on the stack.
*
* @template T Optional. The type that represents state information for the dialog.
*
* @remarks
* This contains information for a specific instance of a dialog on a dialog stack.
* The dialog stack is associated with a specific dialog context and dialog set.
* Information about the dialog stack as a whole is persisted to storage using a dialog state object.
*
* **See also**
* - [DialogState](xref:botbuilder-dialogs.DialogState)
* - [DialogContext](xref:botbuilder-dialogs.DialogContext)
* - [DialogSet](xref:botbuilder-dialogs.DialogSet)
* - [Dialog](xref:botbuilder-dialogs.Dialog)
*/
export interface DialogInstance<T = any> {
/**
* ID of this dialog
*
* @remarks
* Dialog state is associated with a specific dialog set.
* This ID is the the dialog's [id](xref:botbuilder-dialogs.Dialog.id) within that dialog set.
*
* **See also**
* - [DialogState](xref:botbuilder-dialogs.DialogState)
* - [DialogSet](xref:botbuilder-dialogs.DialogSet)
*/
id: string;
/**
* The state information for this instance of this dialog.
*/
state: T;
/**
* Hash code used to detect that a dialog has changed since the curent instance was started.
*/
version?: string;
}
/**
* Indicates why a dialog method is being called.
*
* @remarks
* Use a dialog context to control the dialogs in a dialog set. The dialog context will pass a reference to itself
* to the dialog method it calls. It also passes in the _reason_ why the specific method is being called.
*
* **See also**
* - [DialogContext](xref:botbuilder-dialogs.DialogContext)
* - [DialogSet](xref:botbuilder-dialogs.DialogSet)
* - [Dialog](xref:botbuilder-dialogs.Dialog)
*/
export enum DialogReason {
/**
* The dialog is being started from
* [DialogContext.beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog) or
* [DialogContext.replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog).
*/
beginCalled = 'beginCalled',
/**
* The dialog is being continued from
* [DialogContext.continueDialog](xref:botbuilder-dialogs.DialogContext.continueDialog).
*/
continueCalled = 'continueCalled',
/**
* The dialog is being ended from
* [DialogContext.endDialog](xref:botbuilder-dialogs.DialogContext.endDialog).
*/
endCalled = 'endCalled',
/**
* The dialog is being ended from
* [DialogContext.replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog).
*/
replaceCalled = 'replaceCalled',
/**
* The dialog is being cancelled from
* [DialogContext.cancelAllDialogs](xref:botbuilder-dialogs.DialogContext.cancelAllDialogs).
*/
cancelCalled = 'cancelCalled',
/**
* A step in a [WaterfallDialog](xref:botbuilder-dialogs.WaterfallDialog) is being called
* because the previous step in the waterfall dialog called
* [WaterfallStepContext.next](xref:botbuilder-dialogs.WaterfallStepContext.next).
*/
nextCalled = 'nextCalled',
}
/**
* Represents the state of the dialog stack after a dialog context attempts to begin, continue,
* or otherwise manipulate one or more dialogs.
*
* **See also**
* - [DialogContext](xref:botbuilder-dialogs.DialogContext)
* - [Dialog](xref:botbuilder-dialogs.Dialog)
*/
export enum DialogTurnStatus {
/**
* The dialog stack is empty.
*
* @remarks
* Indicates that the dialog stack was initially empty when the operation was attempted.
*/
empty = 'empty',
/**
* The active dialog on top of the stack is waiting for a response from the user.
*/
waiting = 'waiting',
/**
* The last dialog on the stack completed successfully.
*
* @remarks
* Indicates that a result might be available and the stack is now empty.
*
* **See also**
* - [DialogTurnResult.result](xref:botbuilder-dialogs.DialogTurnResult.result)
*/
complete = 'complete',
/**
* All dialogs on the stack were cancelled and the stack is empty.
*/
cancelled = 'cancelled',
/**
* Current dialog completed successfully, but turn should end.
*/
completeAndWait = 'completeAndWait',
}
export interface DialogEvent {
/**
* Flag indicating whether the event will be bubbled to the parent `DialogContext`.
*/
bubble: boolean;
/**
* Name of the event being raised.
*/
name: string;
/**
* Optional. Value associated with the event.
*/
value?: any;
}
export interface DialogConfiguration {
/**
* Static id of the dialog.
*/
id?: string;
/**
* Telemetry client the dialog should use.
*/
telemetryClient?: BotTelemetryClient;
}
/**
* Represents the result of a dialog context's attempt to begin, continue,
* or otherwise manipulate one or more dialogs.
*
* @template T Optional. The type that represents a result returned by the active dialog when it
* successfully completes.
*
* @remarks
* This can be used to determine if a dialog completed and a result is available, or if the stack
* was initially empty and a dialog should be started.
*
* ```JavaScript
* const dc = await dialogs.createContext(turnContext);
* const result = await dc.continueDialog();
*
* if (result.status == DialogTurnStatus.completed) {
* const survey = result.result;
* await submitSurvey(survey);
* } else if (result.status == DialogTurnStatus.empty) {
* await dc.beginDialog('surveyDialog');
* }
* ```
*
* **See also**
* - [DialogContext](xref:botbuilder-dialogs.DialogContext)
* - [DialogSet](xref:botbuilder-dialogs.DialogSet)
* - [Dialog](xref:botbuilder-dialogs.Dialog)
*/
export interface DialogTurnResult<T = any> {
/**
* The state of the dialog stack after a dialog context's attempt.
*/
status: DialogTurnStatus;
/**
* The result, if any, returned by the last dialog on the stack.
*
* @remarks
* A result value is available only if
* the stack is now empty,
* the last dialog on the stack completed normally,
* and the last dialog returned a result to the dialog context.
*/
result?: T;
/**
* If true, a `DialogCommand` has ended its parent container and the parent should not perform
* any further processing.
*/
parentEnded?: boolean;
}
/**
* Defines the core behavior for all dialogs.
*/
export abstract class Dialog<O extends object = {}> extends Configurable {
private _id: string;
/**
* Gets a default end-of-turn result.
*
* @remarks
* This result indicates that a dialog (or a logical step within a dialog) has completed
* processing for the current turn, is still active, and is waiting for more input.
*/
static EndOfTurn: DialogTurnResult = { status: DialogTurnStatus.waiting };
/**
* The telemetry client for logging events.
* Default this to the NullTelemetryClient, which does nothing.
*/
protected _telemetryClient: BotTelemetryClient = new NullTelemetryClient();
/**
* Creates a new instance of the [Dialog](xref:botbuilder-dialogs.Dialog) class.
*
* @param dialogId Optional. unique ID of the dialog.
*/
constructor(dialogId?: string) {
super();
this.id = dialogId;
}
/**
* Unique ID of the dialog.
*
* @remarks
* This will be automatically generated if not specified.
* @returns The Id for the dialog.
*/
get id(): string {
if (this._id === undefined) {
this._id = this.onComputeId();
}
return this._id;
}
/**
* Sets the unique ID of the dialog.
*/
set id(value: string) {
this._id = value;
}
/**
* Gets the telemetry client for this dialog.
*
* @returns The [BotTelemetryClient](xref:botbuilder.BotTelemetryClient) to use for logging.
*/
get telemetryClient(): BotTelemetryClient {
return this._telemetryClient;
}
/**
* Sets the telemetry client for this dialog.
*/
set telemetryClient(client: BotTelemetryClient) {
this._telemetryClient = client ? client : new NullTelemetryClient();
}
/**
* An encoded string used to aid in the detection of bot changes on re-deployment.
*
* @remarks
* This defaults to returning the dialogs [id](#id) but can be overridden to provide more
* precise change detection logic. Any dialog on the stack that has its version change will
* result in a `versionChanged` event will be raised. If this event is not handled by the bot,
* an error will be thrown resulting in the bots error handler logic being run.
*
* Returning an empty string will disable version tracking for the component all together.
* @returns Unique string which should only change when dialog has changed in a way that should restart the dialog.
*/
getVersion(): string {
return this.id;
}
/**
* When overridden in a derived class, starts the dialog.
*
* @param dc The context for the current dialog turn.
* @param options Optional. Arguments to use when the dialog starts.
*
* @remarks
* Derived dialogs must override this method.
*
* The [DialogContext](xref:botbuilder-dialogs.DialogContext) calls this method when it creates
* a new [DialogInstance](xref:botbuilder-dialogs.DialogInstance) for this dialog, pushes it
* onto the dialog stack, and starts the dialog.
*
* A dialog that represents a single-turn conversation should await
* [DialogContext.endDialog](xref:botbuilder-dialogs.DialogContext.endDialog) before exiting this method.
*
* **See also**
* - [DialogContext.beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog)
* - [DialogContext.replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog)
*/
abstract beginDialog(dc: DialogContext, options?: O): Promise<DialogTurnResult>;
/**
* When overridden in a derived class, continues the dialog.
*
* @param dc The context for the current dialog turn.
*
* @remarks
* Derived dialogs that support multiple-turn conversations should override this method.
* By default, this method signals that the dialog is complete and returns.
*
* The [DialogContext](xref:botbuilder-dialogs.DialogContext) calls this method when it continues
* the dialog.
*
* To signal to the dialog context that this dialog has completed, await
* [DialogContext.endDialog](xref:botbuilder-dialogs.DialogContext.endDialog) before exiting this method.
*
* **See also**
* - [DialogContext.continueDialog](xref:botbuilder-dialogs.DialogContext.continueDialog)
* @returns {Promise<DialogTurnResult>} A promise resolving to the dialog turn result.
*/
async continueDialog(dc: DialogContext): Promise<DialogTurnResult> {
// By default just end the current dialog.
return dc.endDialog();
}
/**
* When overridden in a derived class, resumes the dialog after the dialog above it on the stack completes.
*
* @param dc The context for the current dialog turn.
* @param reason The reason the dialog is resuming. This will typically be
* [DialogReason.endCalled](xref:botbuilder-dialogs.DialogReason.endCalled)
* @param result Optional. The return value, if any, from the dialog that ended.
*
* @remarks
* Derived dialogs that support multiple-turn conversations should override this method.
* By default, this method signals that the dialog is complete and returns.
*
* The [DialogContext](xref:botbuilder-dialogs.DialogContext) calls this method when it resumes
* the dialog. If the previous dialog on the stack returned a value, that value is in the `result`
* parameter.
*
* To start a _child_ dialog, use [DialogContext.beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog)
* or [DialogContext.prompt](xref:botbuilder-dialogs.DialogContext.prompt); however, this dialog will not
* necessarily be the one that started the child dialog.
* To signal to the dialog context that this dialog has completed, await
* [DialogContext.endDialog](xref:botbuilder-dialogs.DialogContext.endDialog) before exiting this method.
*
* **See also**
* - [DialogContext.endDialog](xref:botbuilder-dialogs.DialogContext.endDialog)
* @returns {Promise<DialogTurnResult>} A promise resolving to the dialog turn result.
*/
async resumeDialog(dc: DialogContext, reason: DialogReason, result?: any): Promise<DialogTurnResult> {
// By default just end the current dialog and return result to parent.
return dc.endDialog(result);
}
/**
* When overridden in a derived class, reprompts the user for input.
*
* @param _context The context object for the turn.
* @param _instance Current state information for this dialog.
*
* @remarks
* Derived dialogs that support validation and re-prompt logic should override this method.
* By default, this method has no effect.
*
* The [DialogContext](xref:botbuilder-dialogs.DialogContext) calls this method when the current
* dialog should re-request input from the user. This method is implemented for prompt dialogs.
*
* **See also**
* - [DialogContext.repromptDialog](xref:botbuilder-dialogs.DialogContext.repromptDialog)
* - [Prompt](xref:botbuilder-dialogs.Prompt)
*/
async repromptDialog(_context: TurnContext, _instance: DialogInstance): Promise<void> {
// No-op by default
}
/**
* When overridden in a derived class, performs clean up for the dialog before it ends.
*
* @param _context The context object for the turn.
* @param _instance Current state information for this dialog.
* @param _reason The reason the dialog is ending.
*
* @remarks
* Derived dialogs that need to perform logging or cleanup before ending should override this method.
* By default, this method has no effect.
*
* The [DialogContext](xref:botbuilder-dialogs.DialogContext) calls this method when the current
* dialog is ending.
*
* **See also**
* - [DialogContext.cancelAllDialogs](xref:botbuilder-dialogs.DialogContext.cancelAllDialogs)
* - [DialogContext.endDialog](xref:botbuilder-dialogs.DialogContext.endDialog)
* - [DialogContext.replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog)
*/
async endDialog(_context: TurnContext, _instance: DialogInstance, _reason: DialogReason): Promise<void> {
// No-op by default
}
/**
* Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a dialog that the current dialog started.
*
* @param dc - The dialog context for the current turn of conversation.
* @param e - The event being raised.
* @returns True if the event is handled by the current dialog and bubbling should stop.
*/
async onDialogEvent(dc: DialogContext, e: DialogEvent): Promise<boolean> {
// Before bubble
let handled = await this.onPreBubbleEvent(dc, e);
// Bubble as needed
if (!handled && e.bubble && dc.parent != undefined) {
handled = await dc.parent.emitEvent(e.name, e.value, true, false);
}
// Post bubble
if (!handled) {
handled = await this.onPostBubbleEvent(dc, e);
}
return handled;
}
/**
* Called before an event is bubbled to its parent.
*
* @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.
* @param _dc The dialog context for the current turn of conversation.
* @param _e The event being raised.
* @returns Whether the event is handled by the current dialog and further processing should stop.
*/
protected async onPreBubbleEvent(_dc: DialogContext, _e: DialogEvent): Promise<boolean> {
return false;
}
/**
* Called after an event was bubbled to all parents and wasn't handled.
*
* @remarks
* This is a good place to perform default processing logic for an event. Returning `true` will
* prevent any processing of the event by child dialogs.
* @param _dc The dialog context for the current turn of conversation.
* @param _e The event being raised.
* @returns Whether the event is handled by the current dialog and further processing should stop.
*/
protected async onPostBubbleEvent(_dc: DialogContext, _e: DialogEvent): Promise<boolean> {
return false;
}
/**
* Called when a unique ID needs to be computed for a dialog.
*
* @remarks
* SHOULD be overridden to provide a more contextually relevant ID. The preferred pattern for
* ID's is `<dialog type>(this.hashedLabel('<dialog args>'))`.
*/
protected onComputeId(): string {
throw new Error('Dialog.onComputeId(): not implemented.');
}
}