/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { TurnContext } from '@microsoft/agents-hosting' import { ExceptionHelper } from '@microsoft/agents-activity' import { Errors } from './errorHelper' import { DialogContext } from './dialogContext' import { Configurable } from './configurable' import { DialogTurnResult } from './dialogTurnResult' import { DialogEvent } from './dialogEvent' import { DialogReason } from './dialogReason' import { DialogInstance } from './dialogInstance' import { DialogTurnStatus } from './dialogTurnStatus' /** * Defines the core behavior for all dialogs. */ export abstract class Dialog 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 } /** * Creates a new instance of the Dialog class. * * @param dialogId Optional. unique ID of the dialog. */ constructor (dialogId?: string) { super() this.id = dialogId } /** * Unique ID of the dialog. * * @returns The Id for the dialog. * * @remarks * This will be automatically generated if not specified. */ 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 } /** * An encoded string used to aid in the detection of agent changes on re-deployment. * * @returns Unique string which should only change when dialog has changed in a way that should restart the dialog. * * @remarks * This defaults to returning the dialog's `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 agent, * an error will be thrown resulting in the agent error handler logic being run. * * Returning an empty string will disable version tracking for the component all together. * */ 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 {@link DialogContext} calls this method when it creates * a new {@link DialogInstance} for this dialog, pushes it * onto the dialog stack, and starts the dialog. * * A dialog that represents a single-turn conversation should await * {@link DialogContext.endDialog} before exiting this method. * */ abstract beginDialog (dc: DialogContext, options?: O): Promise /** * When overridden in a derived class, continues the dialog. * * @param dc The context for the current dialog turn. * @returns {Promise} A promise resolving to the dialog turn result. * * @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 {@link DialogContext} calls this method when it continues * the dialog. * * To signal to the dialog context that this dialog has completed, await * {@link DialogContext.endDialog} before exiting this method. * */ async continueDialog (dc: DialogContext): Promise { // 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 * @param result Optional. The return value, if any, from the dialog that ended. * @returns {Promise} A promise resolving to the dialog turn result. * * @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 {@link 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 {@link DialogContext.beginDialog} or {@link 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 {@link DialogContext.endDialog} before exiting this method. * */ async resumeDialog (dc: DialogContext, reason: DialogReason, result?: any): Promise { // By default just end the current dialog and return result to parent. return dc.endDialog(result) } /** * When overridden in a derived class, prompts the user again 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 {@link DialogContext} calls this method when the current * dialog should re-request input from the user. This method is implemented for prompt dialogs. * */ async repromptDialog (_context: TurnContext, _instance: DialogInstance): Promise { // 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 {@link DialogContext} calls this method when the current * dialog is ending. * */ async endDialog (_context: TurnContext, _instance: DialogInstance, _reason: DialogReason): Promise { // No-op by default } /** * Called when an event has been raised, using {@link DialogContext.emitEvent | DialogContext.emitEvent method }, * by either the current dialog or a dialog that the current dialog started. * * @param dialogContext - The dialog context for the current turn of conversation. * @param event - The event being raised. * @returns True if the event is handled by the current dialog and bubbling should stop. */ async onDialogEvent (dialogContext: DialogContext, event: DialogEvent): Promise { // Before bubble let handled = await this.onPreBubbleEvent(dialogContext, event) // Bubble as needed if (!handled && event.bubble && dialogContext.parent !== undefined) { handled = await dialogContext.parent.emitEvent(event.name, event.value, true, false) } // Post bubble if (!handled) { handled = await this.onPostBubbleEvent(dialogContext, event) } return handled } /** * Called before an event is bubbled to its parent. * * @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. * * @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, _e: DialogEvent): Promise { return false } /** * Called after an event was bubbled to all parents and wasn't handled. * * @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. * * @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. */ protected async onPostBubbleEvent (_dc: DialogContext, _e: DialogEvent): Promise { 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 `(this.hashedLabel(''))`. */ protected onComputeId (): string { throw ExceptionHelper.generateException( Error, Errors.OnComputeIdNotImplemented ) } }