botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
496 lines (452 loc) • 17.8 kB
text/typescript
/**
* 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.');
}
}