botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
336 lines • 14 kB
JavaScript
"use strict";
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.WaterfallDialog = void 0;
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const uuid_1 = require("uuid");
const botbuilder_core_1 = require("botbuilder-core");
const botbuilder_core_2 = require("botbuilder-core");
const dialog_1 = require("./dialog");
const waterfallStepContext_1 = require("./waterfallStepContext");
/**
* A waterfall is a dialog that's optimized for prompting a user with a series of questions.
*
* @remarks
* Waterfalls accept a stack of functions which will be executed in sequence. Each waterfall step
* can ask a question of the user and the user's response will be passed to the next step in the
* waterfall via `step.result`. A special `step.value` object can be used to persist values between
* steps:
*
* ```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;
* ```
*/
class WaterfallDialog extends dialog_1.Dialog {
/**
* Creates a new waterfall dialog containing the given array of steps.
*
* @remarks
* See the [addStep()](#addstep) function for details on creating a valid step function.
* @param dialogId Unique ID of the dialog within the component or set its being added to.
* @param steps (Optional) array of asynchronous waterfall step functions.
*/
constructor(dialogId, steps) {
super(dialogId);
this.steps = [];
if (steps) {
this.steps = steps.slice(0);
}
}
/**
* Gets the dialog version, composed of the ID and number of steps.
*
* @returns Dialog version, composed of the ID and number of steps.
*/
getVersion() {
// Simply return the id + number of steps to help detect when new steps have
// been added to a given waterfall.
return `${this.id}:${this.steps.length}`;
}
/**
* Adds a new step to the waterfall.
*
* @remarks
* All step functions should be asynchronous and return a `DialogTurnResult`. The
* `WaterfallStepContext` passed into your function derives from `DialogContext` and contains
* numerous stack manipulation methods which return a `DialogTurnResult` so you can typically
* just return the result from the DialogContext method you call.
*
* The step function itself can be either an asynchronous closure:
*
* ```JavaScript
* const helloDialog = new WaterfallDialog('hello');
*
* helloDialog.addStep(async (step) => {
* await step.context.sendActivity(`Hello World!`);
* return await step.endDialog();
* });
* ```
*
* A named async function:
*
* ```JavaScript
* async function helloWorldStep(step) {
* await step.context.sendActivity(`Hello World!`);
* return await step.endDialog();
* }
*
* helloDialog.addStep(helloWorldStep);
* ```
*
* Or a class method that's been bound to its `this` pointer:
*
* ```JavaScript
* helloDialog.addStep(this.helloWorldStep.bind(this));
* ```
* @param step Asynchronous step function to call.
* @returns Waterfall dialog for fluent calls to `addStep()`.
*/
addStep(step) {
this.steps.push(step);
return this;
}
/**
* Called when the [WaterfallDialog](xref:botbuilder-dialogs.WaterfallDialog) is started and pushed onto the dialog stack.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param options Optional, initial information to pass to the [Dialog](xref:botbuilder-dialogs.Dialog).
* @returns A Promise representing the asynchronous operation.
* @remarks
* If the task is successful, the result indicates whether the [Dialog](xref:botbuilder-dialogs.Dialog) is still
* active after the turn has been processed by the dialog.
*/
beginDialog(dc, options) {
return __awaiter(this, void 0, void 0, function* () {
// Initialize waterfall state
const state = dc.activeDialog.state;
state.options = options || {};
state.values = {
instanceId: uuid_1.v4(),
};
this.telemetryClient.trackEvent({
name: 'WaterfallStart',
properties: {
DialogId: this.id,
InstanceId: state.values['instanceId'],
},
});
botbuilder_core_2.telemetryTrackDialogView(this.telemetryClient, this.id);
// Run the first step
return yield this.runStep(dc, 0, dialog_1.DialogReason.beginCalled);
});
}
/**
* Called when the [WaterfallDialog](xref:botbuilder-dialogs.WaterfallDialog) is _continued_, where it is the active dialog and the
* user replies with a new [Activity](xref:botframework-schema.Activity).
*
* @param dc The [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.
*/
continueDialog(dc) {
return __awaiter(this, void 0, void 0, function* () {
// Don't do anything for non-message activities
if (dc.context.activity.type !== botbuilder_core_1.ActivityTypes.Message) {
return dialog_1.Dialog.EndOfTurn;
}
// Run next step with the message text as the result.
return yield this.resumeDialog(dc, dialog_1.DialogReason.continueCalled, dc.context.activity.text);
});
}
/**
* Called when a child [WaterfallDialog](xref:botbuilder-dialogs.WaterfallDialog) completed its turn, returning control to this dialog.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of the conversation.
* @param reason [Reason](xref:botbuilder-dialogs.DialogReason) 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.
*/
resumeDialog(dc, reason, result) {
return __awaiter(this, void 0, void 0, function* () {
// Increment step index and run step
const state = dc.activeDialog.state;
return yield this.runStep(dc, state.stepIndex + 1, reason, result);
});
}
/**
* Called when an individual waterfall step is being executed.
*
* @remarks
* SHOULD be overridden by derived class that want to add custom logging semantics.
*
* ```JavaScript
* class LoggedWaterfallDialog extends WaterfallDialog {
* async onStep(step) {
* console.log(`Executing step ${step.index} of the "${this.id}" waterfall.`);
* return await super.onStep(step);
* }
* }
* ```
* @param step Context object for the waterfall step to execute.
* @returns A promise with the DialogTurnResult.
*/
onStep(step) {
return __awaiter(this, void 0, void 0, function* () {
// Log Waterfall Step event.
const stepName = this.waterfallStepName(step.index);
const state = step.activeDialog.state;
const properties = {
DialogId: this.id,
InstanceId: state.values['instanceId'],
StepName: stepName,
};
this.telemetryClient.trackEvent({ name: 'WaterfallStep', properties: properties });
return yield this.steps[step.index](step);
});
}
/**
* Executes a step of the [WaterfallDialog](xref:botbuilder-dialogs.WaterfallDialog).
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param index The index of the current waterfall step to execute.
* @param reason The [Reason](xref:botbuilder-dialogs.DialogReason) the waterfall step is being executed.
* @param result Optional, result returned by a dialog called in the previous waterfall step.
* @returns A Promise that represents the work queued to execute.
*/
runStep(dc, index, reason, result) {
return __awaiter(this, void 0, void 0, function* () {
if (index < this.steps.length) {
// Update persisted step index
const state = dc.activeDialog.state;
state.stepIndex = index;
// Create step context
let nextCalled = false;
const step = new waterfallStepContext_1.WaterfallStepContext(dc, {
index: index,
options: state.options,
reason: reason,
result: result,
values: state.values,
onNext: (stepResult) => __awaiter(this, void 0, void 0, function* () {
if (nextCalled) {
throw new Error(`WaterfallStepContext.next(): method already called for dialog and step '${this.id}[${index}]'.`);
}
nextCalled = true;
return yield this.resumeDialog(dc, dialog_1.DialogReason.nextCalled, stepResult);
}),
});
// Execute step
return yield this.onStep(step);
}
else {
// End of waterfall so just return to parent
return yield dc.endDialog(result);
}
});
}
/**
* Called when the dialog is ending.
*
* @param context Context for the current turn of conversation.
* @param instance The instance of the current dialog.
* @param reason The reason the dialog is ending.
*/
endDialog(context, instance, reason) {
return __awaiter(this, void 0, void 0, function* () {
const state = instance.state;
const instanceId = state.values['instanceId'];
if (reason === dialog_1.DialogReason.endCalled) {
this.telemetryClient.trackEvent({
name: 'WaterfallComplete',
properties: {
DialogId: this.id,
InstanceId: instanceId,
},
});
}
else if (reason === dialog_1.DialogReason.cancelCalled) {
const index = state.stepIndex;
const stepName = this.waterfallStepName(index);
this.telemetryClient.trackEvent({
name: 'WaterfallCancel',
properties: {
DialogId: this.id,
StepName: stepName,
InstanceId: instanceId,
},
});
}
});
}
/**
* Identifies the step name by its position index.
*
* @param index Step position
* @returns A string that identifies the step name.
*/
waterfallStepName(index) {
// Log Waterfall Step event. Each event has a distinct name to hook up
// to the Application Insights funnel.
let stepName = '';
if (this.steps[index]) {
try {
stepName = this.steps[index].name;
}
finally {
if (stepName === undefined || stepName === '') {
stepName = 'Step' + (index + 1) + 'of' + this.steps.length;
}
}
}
return stepName;
}
}
exports.WaterfallDialog = WaterfallDialog;
//# sourceMappingURL=waterfallDialog.js.map