botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
526 lines • 23.7 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DialogContext = void 0;
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const botbuilder_core_1 = require("botbuilder-core");
const dialog_1 = require("./dialog");
const memory_1 = require("./memory");
const dialogContainer_1 = require("./dialogContainer");
const dialogEvents_1 = require("./dialogEvents");
const dialogTurnStateConstants_1 = require("./dialogTurnStateConstants");
const dialogContextError_1 = require("./dialogContextError");
/**
* Wraps a promise in a try-catch that automatically enriches errors with extra dialog context.
*
* @param dialogContext source dialog context from which enriched error properties are sourced
* @param promise a promise to await inside a try-catch for error enrichment
* @returns A promise representing the asynchronous operation.
*/
const wrapErrors = (dialogContext, promise) => __awaiter(void 0, void 0, void 0, function* () {
try {
return yield promise;
}
catch (err) {
if (err instanceof dialogContextError_1.DialogContextError) {
throw err;
}
else {
throw new dialogContextError_1.DialogContextError(err, dialogContext);
}
}
});
/**
* @private
*/
const ACTIVITY_RECEIVED_EMITTED = Symbol('ActivityReceivedEmitted');
/**
* The context for the current dialog turn with respect to a specific [DialogSet](xref:botbuilder-dialogs.DialogSet).
*
* @remarks
* This includes the turn context, information about the dialog set, and the state of the dialog stack.
*
* From code outside of a dialog in the set, use [DialogSet.createContext](xref:botbuilder-dialogs.DialogSet.createContext)
* to create the dialog context. Then use the methods of the dialog context to manage the progression of dialogs in the set.
*
* When you implement a dialog, the dialog context is a parameter available to the various methods you override or implement.
*
* For example:
* ```JavaScript
* const dc = await dialogs.createContext(turnContext);
* const result = await dc.continueDialog();
* ```
*/
class DialogContext {
/**
* Creates an new instance of the [DialogContext](xref:botbuilder-dialogs.DialogContext) class.
*
* @param dialogs The [DialogSet](xref:botbuilder-dialogs.DialogSet) for which to create the dialog context.
* @param contextOrDC The [TurnContext](xref:botbuilder-core.TurnContext) or [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of the bot.
* @param state The state object to use to read and write [DialogState](xref:botbuilder-dialogs.DialogState) to storage.
* @remarks Passing in a [DialogContext](xref:botbuilder-dialogs.DialogContext) instance will clone the dialog context.
*/
constructor(dialogs, contextOrDC, state) {
/**
* Gets the services collection which is contextual to this dialog context.
*/
this.services = new botbuilder_core_1.TurnContextStateCollection();
this.dialogs = dialogs;
if (contextOrDC instanceof DialogContext) {
this.context = contextOrDC.context;
this.parent = contextOrDC;
if (this.parent.services) {
this.parent.services.forEach((value, key) => {
this.services.set(key, value);
});
}
}
else {
this.context = contextOrDC;
}
if (!Array.isArray(state.dialogStack)) {
state.dialogStack = [];
}
this.stack = state.dialogStack;
this.state = new memory_1.DialogStateManager(this);
this.state.setValue(memory_1.TurnPath.activity, this.context.activity);
}
/**
* @returns Dialog context for child if the active dialog is a container.
*/
get child() {
const instance = this.activeDialog;
if (instance != undefined) {
// Is active dialog a container?
const dialog = this.findDialog(instance.id);
if (dialog instanceof dialogContainer_1.DialogContainer) {
return dialog.createChildContext(this);
}
}
return undefined;
}
/**
* @returns The state information for the dialog on the top of the dialog stack, or `undefined` if
* the stack is empty.
*/
get activeDialog() {
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined;
}
/**
* @deprecated This property serves no function.
* @returns The current dialog manager instance. This property is deprecated.
*/
get dialogManager() {
return this.context.turnState.get(dialogTurnStateConstants_1.DialogTurnStateConstants.dialogManager);
}
/**
* Obtain the CultureInfo in DialogContext.
*
* @returns a locale string.
*/
getLocale() {
var _a;
const _turnLocaleProperty = 'turn.locale';
const turnLocaleValue = this.state.getValue(_turnLocaleProperty);
if (turnLocaleValue) {
return turnLocaleValue;
}
const locale = (_a = this.context.activity) === null || _a === void 0 ? void 0 : _a.locale;
if (locale !== undefined) {
return locale;
}
return Intl.DateTimeFormat().resolvedOptions().locale;
}
/**
* Starts a dialog instance and pushes it onto the dialog stack.
* Creates a new instance of the dialog and pushes it onto the stack.
*
* @param dialogId ID of the dialog to start.
* @param options Optional. Arguments to pass into the dialog when it starts.
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
* @remarks
* If there's already an active dialog on the stack, that dialog will be paused until
* it is again the top dialog on the stack.
*
* The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes
* the status of the dialog stack after this method completes.
*
* This method throws an exception if the requested dialog can't be found in this dialog context
* or any of its ancestors.
*
* For example:
* ```JavaScript
* const result = await dc.beginDialog('greeting', { name: user.name });
* ```
*
* **See also**
* - [endDialog](xref:botbuilder-dialogs.DialogContext.endDialog)
* - [prompt](xref:botbuilder-dialogs.DialogContext.prompt)
* - [replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog)
* - [Dialog.beginDialog](xref:botbuilder-dialogs.Dialog.beginDialog)
*/
beginDialog(dialogId, options) {
return __awaiter(this, void 0, void 0, function* () {
// Lookup dialog
const dialog = this.findDialog(dialogId);
if (!dialog) {
throw new dialogContextError_1.DialogContextError(`DialogContext.beginDialog(): A dialog with an id of '${dialogId}' wasn't found.`, this);
}
// Push new instance onto stack.
const instance = {
id: dialogId,
state: {},
};
this.stack.push(instance);
// Call dialogs begin() method.
return wrapErrors(this, dialog.beginDialog(this, options));
});
}
/**
* Cancels all dialogs on the dialog stack, and clears stack.
*
* @param cancelParents Optional. If `true` all parent dialogs will be cancelled as well.
* @param eventName Optional. Name of a custom event to raise as dialogs are cancelled. This defaults to [cancelDialog](xref:botbuilder-dialogs.DialogEvents.cancelDialog).
* @param eventValue Optional. Value to pass along with custom cancellation event.
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
* @remarks
* This calls each dialog's [Dialog.endDialog](xref:botbuilder-dialogs.Dialog.endDialog) method before
* removing the dialog from the stack.
*
* If there were any dialogs on the stack initially, the [status](xref:botbuilder-dialogs.DialogTurnResult.status)
* of the return value is [cancelled](xref:botbuilder-dialogs.DialogTurnStatus.cancelled); otherwise, it's
* [empty](xref:botbuilder-dialogs.DialogTurnStatus.empty).
*
* This example clears a dialog stack, `dc`, before starting a 'bookFlight' dialog.
* ```JavaScript
* await dc.cancelAllDialogs();
* return await dc.beginDialog('bookFlight');
* ```
*
* **See also**
* - [endDialog](xref:botbuilder-dialogs.DialogContext.endDialog)
*/
cancelAllDialogs(cancelParents = false, eventName, eventValue) {
return __awaiter(this, void 0, void 0, function* () {
eventName = eventName || dialogEvents_1.DialogEvents.cancelDialog;
if (this.stack.length > 0 || this.parent != undefined) {
// Cancel all local and parent dialogs while checking for interception
let notify = false;
// eslint-disable-next-line @typescript-eslint/no-this-alias
let dc = this;
while (dc != undefined) {
if (dc.stack.length > 0) {
// Check to see if the dialog wants to handle the event
// - We skip notifying the first dialog which actually called cancelAllDialogs()
if (notify) {
const handled = yield dc.emitEvent(eventName, eventValue, false, false);
if (handled) {
break;
}
}
// End the active dialog
yield dc.endActiveDialog(dialog_1.DialogReason.cancelCalled);
}
else {
dc = cancelParents ? dc.parent : undefined;
}
notify = true;
}
return { status: dialog_1.DialogTurnStatus.cancelled };
}
else {
return { status: dialog_1.DialogTurnStatus.empty };
}
});
}
/**
* Searches for a dialog with a given ID.
*
* @param dialogId ID of the dialog to search for.
* @returns The dialog for the provided ID.
* @remarks
* If the dialog to start is not found in the [DialogSet](xref:botbuilder-dialogs.DialogSet) associated
* with this dialog context, it attempts to find the dialog in its parent dialog context.
*
* **See also**
* - [dialogs](xref:botbuilder-dialogs.DialogContext.dialogs)
* - [parent](xref:botbuilder-dialogs.DialogContext.parent)
*/
findDialog(dialogId) {
let dialog = this.dialogs.find(dialogId);
if (!dialog && this.parent) {
dialog = this.parent.findDialog(dialogId);
}
return dialog;
}
/**
* Helper function to simplify formatting the options for calling a prompt dialog.
*
* @param dialogId ID of the prompt dialog to start.
* @param promptOrOptions The text of the initial prompt to send the user,
* or the [Activity](xref:botframework-schema.Activity) to send as the initial prompt.
* @param choices Optional. Array of choices for the user to choose from,
* for use with a [ChoicePrompt](xref:botbuilder-dialogs.ChoicePrompt).
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
* @remarks This helper method formats the object to use as the `options` parameter, and then calls
* beginDialog to start the specified prompt dialog.
*
* ```JavaScript
* return await dc.prompt('confirmPrompt', `Are you sure you'd like to quit?`);
* ```
*/
prompt(dialogId, promptOrOptions, choices) {
return __awaiter(this, void 0, void 0, function* () {
let options;
if ((typeof promptOrOptions === 'object' && promptOrOptions.type !== undefined) ||
typeof promptOrOptions === 'string') {
options = { prompt: promptOrOptions };
}
else {
options = Object.assign({}, promptOrOptions);
}
if (choices) {
options.choices = choices;
}
return wrapErrors(this, this.beginDialog(dialogId, options));
});
}
/**
* Continues execution of the active dialog, if there is one, by passing this dialog context to its
* [Dialog.continueDialog](xref:botbuilder-dialogs.Dialog.continueDialog) method.
*
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
* @remarks
* After the call completes, you can check the turn context's [responded](xref:botbuilder-core.TurnContext.responded)
* property to determine if the dialog sent a reply to the user.
*
* The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes
* the status of the dialog stack after this method completes.
*
* Typically, you would call this from within your bot's turn handler.
*
* For example:
* ```JavaScript
* const result = await dc.continueDialog();
* if (result.status == DialogTurnStatus.empty && dc.context.activity.type == ActivityTypes.message) {
* // Send fallback message
* await dc.context.sendActivity(`I'm sorry. I didn't understand.`);
* }
* ```
*/
continueDialog() {
return __awaiter(this, void 0, void 0, function* () {
// if we are continuing and haven't emitted the activityReceived event, emit it
// NOTE: This is backward compatible way for activity received to be fired even if you have legacy dialog loop
if (!this.context.turnState.has(ACTIVITY_RECEIVED_EMITTED)) {
this.context.turnState.set(ACTIVITY_RECEIVED_EMITTED, true);
// Dispatch "activityReceived" event
// - This fired from teh leaf and will queue up any interruptions.
yield this.emitEvent(dialogEvents_1.DialogEvents.activityReceived, this.context.activity, true, true);
}
// Check for a dialog on the stack
const instance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog = this.findDialog(instance.id);
if (!dialog) {
throw new dialogContextError_1.DialogContextError(`DialogContext.continueDialog(): Can't continue dialog. A dialog with an id of '${instance.id}' wasn't found.`, this);
}
// Continue execution of dialog
return wrapErrors(this, dialog.continueDialog(this));
}
else {
return { status: dialog_1.DialogTurnStatus.empty };
}
});
}
/**
* Ends a dialog and pops it off the stack. Returns an optional result to the dialog's parent.
*
* @param result Optional. A result to pass to the parent logic. This might be the next dialog
* on the stack, or if this was the last dialog on the stack, a parent dialog context or
* the bot's turn handler.
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
* @remarks
* The _parent_ dialog is the next dialog on the dialog stack, if there is one. This method
* calls the parent's [Dialog.resumeDialog](xref:botbuilder-dialogs.Dialog.resumeDialog) method,
* passing the result returned by the ending dialog. If there is no parent dialog, the turn ends
* and the result is available to the bot through the returned object's
* [result](xref:botbuilder-dialogs.DialogTurnResult.result) property.
*
* The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes
* the status of the dialog stack after this method completes.
*
* Typically, you would call this from within the logic for a specific dialog to signal back to
* the dialog context that the dialog has completed, the dialog should be removed from the stack,
* and the parent dialog should resume.
*
* For example:
* ```JavaScript
* return await dc.endDialog(returnValue);
* ```
*
* **See also**
* - [beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog)
* - [replaceDialog](xref:botbuilder-dialogs.DialogContext.replaceDialog)
* - [Dialog.endDialog](xref:botbuilder-dialogs.Dialog.endDialog)
*/
endDialog(result) {
return __awaiter(this, void 0, void 0, function* () {
// End the active dialog
yield this.endActiveDialog(dialog_1.DialogReason.endCalled, result);
// Resume parent dialog
const instance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog = this.findDialog(instance.id);
if (!dialog) {
throw new dialogContextError_1.DialogContextError(`DialogContext.endDialog(): Can't resume previous dialog. A dialog with an id of '${instance.id}' wasn't found.`, this);
}
// Return result to previous dialog
return wrapErrors(this, dialog.resumeDialog(this, dialog_1.DialogReason.endCalled, result));
}
else {
// Signal completion
return { status: dialog_1.DialogTurnStatus.complete, result: result };
}
});
}
/**
* Ends the active dialog and starts a new dialog in its place.
*
* @param dialogId ID of the dialog to start.
* @param options Optional. Arguments to pass into the new dialog when it starts.
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
* @remarks
* This is particularly useful for creating a loop or redirecting to another dialog.
*
* The [status](xref:botbuilder-dialogs.DialogTurnResult.status) of returned object describes
* the status of the dialog stack after this method completes.
*
* This method is similar to ending the current dialog and immediately beginning the new one.
* However, the parent dialog is neither resumed nor otherwise notified.
*
* **See also**
* - [beginDialog](xref:botbuilder-dialogs.DialogContext.beginDialog)
* - [endDialog](xref:botbuilder-dialogs.DialogContext.endDialog)
*/
replaceDialog(dialogId, options) {
return __awaiter(this, void 0, void 0, function* () {
// End the active dialog
yield this.endActiveDialog(dialog_1.DialogReason.replaceCalled);
// Start replacement dialog
return this.beginDialog(dialogId, options);
});
}
/**
* Requests the active dialog to re-prompt the user for input.
*
* @remarks
* This calls the active dialog's [repromptDialog](xref:botbuilder-dialogs.Dialog.repromptDialog) method.
*
* For example:
* ```JavaScript
* await dc.repromptDialog();
* ```
*/
repromptDialog() {
return __awaiter(this, void 0, void 0, function* () {
// Try raising event first
const handled = yield this.emitEvent(dialogEvents_1.DialogEvents.repromptDialog, undefined, false, false);
if (!handled) {
// Check for a dialog on the stack
const instance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog = this.findDialog(instance.id);
if (!dialog) {
throw new dialogContextError_1.DialogContextError(`DialogContext.repromptDialog(): Can't find a dialog with an id of '${instance.id}'.`, this);
}
// Ask dialog to re-prompt if supported
yield wrapErrors(this, dialog.repromptDialog(this.context, instance));
}
}
});
}
/**
* Searches for a dialog with a given ID.
*
* @remarks
* Emits a named event for the current dialog, or someone who started it, to handle.
* @param name Name of the event to raise.
* @param value Optional. Value to send along with the event.
* @param bubble Optional. Flag to control whether the event should be bubbled to its parent if not handled locally. Defaults to a value of `true`.
* @param fromLeaf Optional. Whether the event is emitted from a leaf node.
* @returns `true` if the event was handled.
*/
emitEvent(name, value, bubble = true, fromLeaf = false) {
return __awaiter(this, void 0, void 0, function* () {
// Initialize event
const dialogEvent = {
bubble: bubble,
name: name,
value: value,
};
// Find starting dialog
// eslint-disable-next-line @typescript-eslint/no-this-alias
let dc = this;
if (fromLeaf) {
// eslint-disable-next-line no-constant-condition
while (true) {
const childDc = dc.child;
if (childDc != undefined) {
dc = childDc;
}
else {
break;
}
}
}
// Dispatch to active dialog first
// - The active dialog will decide if it should bubble the event to its parent.
const instance = dc.activeDialog;
if (instance != undefined) {
const dialog = dc.findDialog(instance.id);
if (dialog != undefined) {
return wrapErrors(this, dialog.onDialogEvent(dc, dialogEvent));
}
}
return false;
});
}
/**
* @private
* @param reason
* @param result
*/
endActiveDialog(reason, result) {
return __awaiter(this, void 0, void 0, function* () {
const instance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog = this.findDialog(instance.id);
if (dialog) {
// Notify dialog of end
yield wrapErrors(this, dialog.endDialog(this.context, instance, reason));
}
// Pop dialog off stack
this.stack.pop();
this.state.setValue(memory_1.TurnPath.lastResult, result);
}
});
}
}
exports.DialogContext = DialogContext;
//# sourceMappingURL=dialogContext.js.map
;