botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
355 lines (326 loc) • 16.4 kB
text/typescript
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { telemetryTrackDialogView, TurnContext } from 'botbuilder-core';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus } from './dialog';
import { DialogContext } from './dialogContext';
import { DialogContainer } from './dialogContainer';
const PERSISTED_DIALOG_STATE = 'dialogs';
/**
* Base class for a dialog that contains other child dialogs.
*
* @remarks
* Component dialogs let you break your bot's logic up into components that can themselves be added
* as a dialog to another `ComponentDialog` or `DialogSet`. Components can also be exported as part
* of a node package and used within other bots.
*
* To define a new component derive a class from ComponentDialog and add your child dialogs within
* the classes constructor:
*
* ```JavaScript
* const { ComponentDialog, WaterfallDialog, TextPrompt, NumberPrompt } = require('botbuilder-dialogs');
*
* class FillProfileDialog extends ComponentDialog {
* constructor(dialogId) {
* super(dialogId);
*
* // Add control flow dialogs
* this.addDialog(new WaterfallDialog('start', [
* async (step) => {
* // Ask user their name
* return await step.prompt('namePrompt', `What's your name?`);
* },
* async (step) => {
* // Remember the users answer
* step.values['name'] = step.result;
*
* // Ask user their age.
* return await step.prompt('agePrompt', `Hi ${step.values['name']}. How old are you?`);
* },
* async (step) => {
* // Remember the users answer
* step.values['age'] = step.result;
*
* // End the component and return the completed profile.
* return await step.endDialog(step.values);
* }
* ]));
*
* // Add prompts
* this.addDialog(new TextPrompt('namePrompt'));
* this.addDialog(new NumberPrompt('agePrompt'))
* }
* }
* module.exports.FillProfileDialog = FillProfileDialog;
* ```
*
* You can then add new instances of your component to another `DialogSet` or `ComponentDialog`:
*
* ```JavaScript
* const dialogs = new DialogSet(dialogState);
* dialogs.add(new FillProfileDialog('fillProfile'));
* ```
* @param O (Optional) options that can be passed into the `DialogContext.beginDialog()` method.
*/
export class ComponentDialog<O extends object = {}> extends DialogContainer<O> {
/**
* ID of the child dialog that should be started anytime the component is started.
*
* @remarks
* This defaults to the ID of the first child dialog added using [addDialog()](#adddialog).
*/
protected initialDialogId: string;
/**
* Called when the dialog is started and pushed onto the parent's dialog stack.
* By default, this calls the
* Dialog.BeginDialogAsync(DialogContext, object, CancellationToken) method
* of the component dialog's initial dialog, as defined by InitialDialogId.
* Override this method in a derived class to implement interrupt logic.
*
* @param outerDC The parent [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param options Optional, initial information to pass to the 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 beginDialog(outerDC: DialogContext, options?: O): Promise<DialogTurnResult> {
await this.checkForVersionChange(outerDC);
telemetryTrackDialogView(this.telemetryClient, this.id);
// Start the inner dialog.
const innerDC: DialogContext = this.createChildContext(outerDC);
const turnResult: DialogTurnResult<any> = await this.onBeginDialog(innerDC, options);
// Check for end of inner dialog
if (turnResult.status !== DialogTurnStatus.waiting) {
if (turnResult.status === DialogTurnStatus.cancelled) {
await this.endComponent(outerDC, turnResult.result);
const cancelledTurnResult: DialogTurnResult = {
status: DialogTurnStatus.cancelled,
result: turnResult.result,
};
return cancelledTurnResult;
}
// Return result to calling dialog
return await this.endComponent(outerDC, turnResult.result);
}
// Just signal end of turn
return Dialog.EndOfTurn;
}
/**
* Called when the dialog is _continued_, where it is the active dialog and the
* user replies with a new [Activity](xref:botframework-schema.Activity).
* If this method is *not* overridden, the dialog automatically ends when the user replies.
*
* @param outerDC The parent [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 result may also contain a
* return value.
*/
async continueDialog(outerDC: DialogContext): Promise<DialogTurnResult> {
await this.checkForVersionChange(outerDC);
// Continue execution of inner dialog.
const innerDC: DialogContext = this.createChildContext(outerDC);
const turnResult: DialogTurnResult<any> = await this.onContinueDialog(innerDC);
// Check for end of inner dialog
if (turnResult.status !== DialogTurnStatus.waiting) {
// Return result to calling dialog
return await this.endComponent(outerDC, turnResult.result);
}
// Just signal end of turn
return Dialog.EndOfTurn;
}
/**
* Called when a child dialog on the parent's dialog stack completed this turn, returning
* control to this dialog component.
*
* @param outerDC The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param _reason Reason why the dialog resumed.
* @param _result Optional, value returned from the dialog that was called. The type
* of the value returned is dependent on the child dialog.
* @returns A Promise representing the asynchronous operation.
* @remarks
* If the task is successful, the result indicates whether this dialog is still
* active after this dialog turn has been processed.
* Generally, the child dialog was started with a call to
* beginDialog(DialogContext, object) in the parent's
* context. However, if the DialogContext.replaceDialog(string, object) method
* is called, the logical child dialog may be different than the original.
* If this method is *not* overridden, the dialog automatically calls its
* RepromptDialog(ITurnContext, DialogInstance) when the user replies.
*/
async resumeDialog(outerDC: DialogContext, _reason: DialogReason, _result?: any): Promise<DialogTurnResult> {
await this.checkForVersionChange(outerDC);
// Containers 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 container receiving an unexpected call to
// resumeDialog() when the pushed on dialog ends.
// To avoid the container prematurely ending we need to implement this method and simply
// ask our inner dialog stack to re-prompt.
await this.repromptDialog(outerDC.context, outerDC.activeDialog);
return Dialog.EndOfTurn;
}
/**
* Called when the dialog should re-prompt the user for input.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) object for this turn.
* @param instance State information for this dialog.
* @returns A Promise representing the asynchronous operation.
*/
async repromptDialog(context: TurnContext, instance: DialogInstance): Promise<void> {
// Forward to inner dialogs
const innerDC: DialogContext = this.createInnerDC(context, instance);
await innerDC.repromptDialog();
// Notify component.
await this.onRepromptDialog(context, instance);
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.Dialog) is ending.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) object for this turn.
* @param instance State information associated with the instance of this component
* [Dialog](xref:botbuilder-dialogs.Dialog) on its parent's dialog stack.
* @param reason Reason why the [Dialog](xref:botbuilder-dialogs.Dialog) ended.
* @returns A Promise representing the asynchronous operation.
* @remarks When this method is called from the parent dialog's context, the component [Dialog](xref:botbuilder-dialogs.Dialog)
* cancels all of the dialogs on its inner dialog stack before ending.
*/
async endDialog(context: TurnContext, instance: DialogInstance, reason: DialogReason): Promise<void> {
// Forward cancel to inner dialogs
if (reason === DialogReason.cancelCalled) {
const innerDC: DialogContext = this.createInnerDC(context, instance);
await innerDC.cancelAllDialogs();
}
// Notify component
await this.onEndDialog(context, instance, reason);
}
/**
* Adds a child [Dialog](xref:botbuilder-dialogs.Dialog) or prompt to the components internal [DialogSet](xref:botbuilder-dialogs.DialogSet).
*
* @param dialog The child [Dialog](xref:botbuilder-dialogs.Dialog) or prompt to add.
* @returns The [ComponentDialog](xref:botbuilder-dialogs.ComponentDialog) after the operation is complete.
* @remarks
* The [Dialog.id](xref:botbuilder-dialogs.Dialog.id) of the first child added to the component will be assigned to the initialDialogId property.
*/
addDialog(dialog: Dialog): this {
this.dialogs.add(dialog);
if (this.initialDialogId === undefined) {
this.initialDialogId = dialog.id;
}
return this;
}
/**
* Creates the inner dialog context
*
* @param outerDC the outer dialog context
* @returns The created Dialog Context.
*/
createChildContext(outerDC: DialogContext): DialogContext {
return this.createInnerDC(outerDC, outerDC.activeDialog);
}
/**
* Called anytime an instance of the component has been started.
*
* @remarks
* SHOULD be overridden by components that wish to perform custom interruption logic. The
* default implementation calls `innerDC.beginDialog()` with the dialog assigned to
* [initialDialogId](#initialdialogid).
* @param innerDC Dialog context for the components internal `DialogSet`.
* @param options (Optional) options that were passed to the component by its parent.
* @returns {Promise<DialogTurnResult>} A promise resolving to the dialog turn result.
*/
protected onBeginDialog(innerDC: DialogContext, options?: O): Promise<DialogTurnResult> {
return innerDC.beginDialog(this.initialDialogId, options);
}
/**
* Called anytime a multi-turn component receives additional activities.
*
* @remarks
* SHOULD be overridden by components that wish to perform custom interruption logic. The
* default implementation calls `innerDC.continueDialog()`.
* @param innerDC Dialog context for the components internal `DialogSet`.
* @returns {Promise<DialogTurnResult>} A promise resolving to the dialog turn result.
*/
protected onContinueDialog(innerDC: DialogContext): Promise<DialogTurnResult> {
return innerDC.continueDialog();
}
/**
* Called when the component is ending.
*
* @remarks
* If the `reason` code is equal to `DialogReason.cancelCalled`, then any active child dialogs
* will be cancelled before this method is called.
* @param _context Context for the current turn of conversation.
* @param _instance The components instance data within its parents dialog stack.
* @param _reason The reason the component is ending.
* @returns A promise representing the asynchronous operation.
*/
protected onEndDialog(_context: TurnContext, _instance: DialogInstance, _reason: DialogReason): Promise<void> {
return Promise.resolve();
}
/**
* Called when the component has been requested to re-prompt the user for input.
*
* @remarks
* The active child dialog will have already been asked to reprompt before this method is called.
* @param _context Context for the current turn of conversation.
* @param _instance The instance of the current dialog.
* @returns A promise representing the asynchronous operation.
*/
protected onRepromptDialog(_context: TurnContext, _instance: DialogInstance): Promise<void> {
return Promise.resolve();
}
/**
* Called when the components last active child dialog ends and the component is ending.
*
* @remarks
* SHOULD be overridden by components that wish to perform custom logic before the component
* ends. The default implementation calls `outerDC.endDialog()` with the `result` returned
* from the last active child dialog.
* @param outerDC Dialog context for the parents `DialogSet`.
* @param result Result returned by the last active child dialog. Can be a value of `undefined`.
* @returns {Promise<DialogTurnResult>} A promise resolving to the dialog turn result.
*/
protected endComponent(outerDC: DialogContext, result: any): Promise<DialogTurnResult> {
return outerDC.endDialog(result);
}
/**
* @private
* @param context [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation with the user.
* @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance) which contains the current state information for this dialog.
* @returns A new [DialogContext](xref:botbuilder-dialogs.DialogContext) instance.
* @remarks
* You should only call this if you don't have a dc to work with (such as OnResume())
*/
private createInnerDC(context: DialogContext, instance: DialogInstance): DialogContext;
/**
* @private
* @param context [TurnContext](xref:botbuilder-core.TurnContext) for the current turn of conversation with the user.
* @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance) which contains the current state information for this dialog.
* @returns A new [DialogContext](xref:botbuilder-dialogs.DialogContext) instance.
* @remarks
* You should only call this if you don't have a dc to work with (such as OnResume())
*/
private createInnerDC(context: TurnContext, instance: DialogInstance): DialogContext;
/**
* @private
* @param context [TurnContext](xref:botbuilder-core.TurnContext) or [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation with the user.
* @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance) which contains the current state information for this dialog.
* @returns A new [DialogContext](xref:botbuilder-dialogs.DialogContext) instance.
* @remarks
* You should only call this if you don't have a dc to work with (such as OnResume())
*/
private createInnerDC(context: TurnContext | DialogContext, instance: DialogInstance): DialogContext {
if (!instance) {
const dialogInstance = { state: {} };
instance = dialogInstance as DialogInstance;
}
const dialogState = instance.state[PERSISTED_DIALOG_STATE] || { dialogStack: [] };
instance.state[PERSISTED_DIALOG_STATE] = dialogState;
return new DialogContext(this.dialogs, context as TurnContext, dialogState);
}
}