UNPKG

botbuilder-dialogs

Version:

A dialog stack based conversation manager for Microsoft BotBuilder.

336 lines 14 kB
"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