UNPKG

botbuilder-dialogs-adaptive

Version:

Rule system for the Microsoft BotBuilder dialog system.

422 lines • 20 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.InputDialog = exports.InputState = void 0; /** * @module botbuilder-dialogs-adaptive */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const templates_1 = require("../templates"); const converters_1 = require("../converters"); const botbuilder_1 = require("botbuilder"); const adaptiveEvents_1 = require("../adaptiveEvents"); const attachmentInput_1 = require("./attachmentInput"); const telemetryLoggerConstants_1 = require("../telemetryLoggerConstants"); const adaptive_expressions_1 = require("adaptive-expressions"); const botbuilder_dialogs_1 = require("botbuilder-dialogs"); var InputState; (function (InputState) { InputState["missing"] = "missing"; InputState["unrecognized"] = "unrecognized"; InputState["invalid"] = "invalid"; InputState["valid"] = "valid"; })(InputState = exports.InputState || (exports.InputState = {})); /** * Defines input dialogs. */ class InputDialog extends botbuilder_dialogs_1.Dialog { /** * Initializes a new instance of the [InputDialog](xref:botbuilder-dialogs-adaptive.InputDialog) class * * @param property Optional. The value expression which the input will be bound to. * @param prompt Optional. The [Activity](xref:botframework-schema.Activity) to send to the user, * if a string is specified it will instantiates an [ActivityTemplate](xref:botbuilder-dialogs-adaptive.ActivityTemplate). */ constructor(property, prompt) { super(); /** * The expressions to run to validate the input. */ this.validations = []; if (property) { this.property = new adaptive_expressions_1.StringExpression(property); } if (prompt) { if (typeof prompt === 'string') { this.prompt = new templates_1.ActivityTemplate(prompt); } else { this.prompt = new templates_1.StaticActivityTemplate(prompt); } } } /** * @param property The key of the conditional selector configuration. * @returns The converter for the selector configuration. */ getConverter(property) { switch (property) { case 'alwaysPrompt': return new adaptive_expressions_1.BoolExpressionConverter(); case 'allowInterruptions': return new adaptive_expressions_1.BoolExpressionConverter(); case 'property': return new adaptive_expressions_1.StringExpressionConverter(); case 'value': return new adaptive_expressions_1.ValueExpressionConverter(); case 'prompt': return new converters_1.ActivityTemplateConverter(); case 'unrecognizedPrompt': return new converters_1.ActivityTemplateConverter(); case 'invalidPrompt': return new converters_1.ActivityTemplateConverter(); case 'defaultValueResponse': return new converters_1.ActivityTemplateConverter(); case 'maxTurnCount': return new adaptive_expressions_1.IntExpressionConverter(); case 'defaultValue': return new adaptive_expressions_1.ValueExpressionConverter(); case 'disabled': return new adaptive_expressions_1.BoolExpressionConverter(); default: return super.getConverter(property); } } /** * Called when the [Dialog](xref:botbuilder-dialogs.Dialog) 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 [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation. */ beginDialog(dc, options) { return __awaiter(this, void 0, void 0, function* () { if (this.disabled && this.disabled.getValue(dc.state)) { return yield dc.endDialog(); } // Initialize and persist options const opts = yield this.onInitializeOptions(dc, options || {}); dc.state.setValue(InputDialog.OPTIONS_PROPERTY, opts); // Initialize turn count & input dc.state.setValue(InputDialog.TURN_COUNT_PROPERTY, 0); if (this.property && this.alwaysPrompt && this.alwaysPrompt.getValue(dc.state)) { dc.state.deleteValue(this.property.getValue(dc.state)); } // Recognize input const state = this.alwaysPrompt && this.alwaysPrompt.getValue(dc.state) ? InputState.missing : yield this.recognizeInput(dc, 0); if (state == InputState.valid) { // Return input const property = this.property.getValue(dc.state); const value = dc.state.getValue(InputDialog.VALUE_PROPERTY); dc.state.setValue(property, value); return yield dc.endDialog(value); } else { // Prompt user dc.state.setValue(InputDialog.TURN_COUNT_PROPERTY, 1); return yield this.promptUser(dc, state); } }); } /** * Called when the [Dialog](xref:botbuilder-dialogs.Dialog) is _continued_, where it is the active dialog and the user replies with a new activity. * * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @returns A [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation. */ continueDialog(dc) { return __awaiter(this, void 0, void 0, function* () { const activity = dc.context.activity; // Interrupted dialogs reprompt so we can ignore the incoming activity. const interrupted = dc.state.getValue(botbuilder_dialogs_1.TurnPath.interrupted, false); if (!interrupted && activity.type !== botbuilder_1.ActivityTypes.Message) { return botbuilder_dialogs_1.Dialog.EndOfTurn; } // Are we continuing after an interruption? const turnCount = dc.state.getValue(InputDialog.TURN_COUNT_PROPERTY, 0); const state = yield this.recognizeInput(dc, interrupted ? 0 : turnCount); if (state === InputState.valid) { const input = dc.state.getValue(InputDialog.VALUE_PROPERTY); if (this.property) { dc.state.setValue(this.property.getValue(dc.state), input); } return yield dc.endDialog(input); } else if (!this.maxTurnCount || turnCount < this.maxTurnCount.getValue(dc.state)) { if (!interrupted) { dc.state.setValue(InputDialog.TURN_COUNT_PROPERTY, turnCount + 1); } return yield this.promptUser(dc, state); } else { if (this.defaultValue) { if (this.defaultValueResponse) { const response = yield this.defaultValueResponse.bind(dc, dc.state); this.telemetryClient.trackEvent({ name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent, properties: { template: this.defaultValueResponse, result: response || '', context: telemetryLoggerConstants_1.TelemetryLoggerConstants.InputDialogResultEvent, }, }); if (response != null) { yield dc.context.sendActivity(response); } } const property = this.property.getValue(dc.state); const value = this.defaultValue.getValue(dc.state); dc.state.setValue(property, value); return yield dc.endDialog(value); } } return yield dc.endDialog(); }); } /** * Called when a child [Dialog](xref:botbuilder-dialogs.Dialog) completes its turn, returning control to this dialog. * * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param _reason [DialogReason](xref:botbuilder-dialogs.DialogReason), reason why the dialog resumed. * @param _result Optional. Value returned from the [Dialog](xref:botbuilder-dialogs.Dialog) that was called. * The type of the value returned is dependent on the child dialog. * @returns A [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation. */ resumeDialog(dc, _reason, _result) { return __awaiter(this, void 0, void 0, function* () { // Re-send initial prompt return yield this.promptUser(dc, InputState.missing); }); } /** * @protected * Called before an event is bubbled to its parent. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param event [DialogEvent](xref:botbuilder-dialogs.DialogEvent), the event being raised. * @returns Whether the event is handled by the current [Dialog](xref:botbuilder-dialogs.Dialog) and further processing should stop. */ onPreBubbleEvent(dc, event) { return __awaiter(this, void 0, void 0, function* () { if (event.name === botbuilder_dialogs_1.DialogEvents.activityReceived && dc.context.activity.type === botbuilder_1.ActivityTypes.Message) { if (dc.parent) { yield dc.parent.emitEvent(adaptiveEvents_1.AdaptiveEvents.recognizeUtterance, dc.context.activity, false); } // should we allow interruptions let canInterrupt = true; if (this.allowInterruptions) { const allowInterruptions = this.allowInterruptions.getValue(dc.state); canInterrupt = !!allowInterruptions; } // stop bubbling if interruptions are NOT allowed return !canInterrupt; } return false; }); } /** * @protected * Method which processes options. * @param _dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param options Initial information to pass to the dialog. * @returns A promise representing the asynchronous operation. */ onInitializeOptions(_dc, options) { return Promise.resolve(Object.assign({}, options)); } /** * @protected * Method which renders the prompt to the user given the current input state. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param state Dialog [InputState](xref:botbuilder-dialogs-adaptive.InputState). * @returns An [Activity](xref:botframework-schema.Activity) `Promise` representing the asynchronous operation. */ onRenderPrompt(dc, state) { return __awaiter(this, void 0, void 0, function* () { let msg; let template; switch (state) { case InputState.unrecognized: if (this.unrecognizedPrompt) { template = this.unrecognizedPrompt; msg = yield this.unrecognizedPrompt.bind(dc, dc.state); } else if (this.invalidPrompt) { template = this.invalidPrompt; msg = yield this.invalidPrompt.bind(dc, dc.state); } break; case InputState.invalid: if (this.invalidPrompt) { template = this.invalidPrompt; msg = yield this.invalidPrompt.bind(dc, dc.state); } else if (this.unrecognizedPrompt) { template = this.unrecognizedPrompt; msg = yield this.unrecognizedPrompt.bind(dc, dc.state); } break; } if (!msg) { template = this.prompt; if (!template) throw new Error('InputDialog is missing Prompt.'); msg = yield this.prompt.bind(dc, dc.state); } if (msg != null && (typeof (msg === null || msg === void 0 ? void 0 : msg.inputHint) !== 'string' || !msg.inputHint)) { msg.inputHint = botbuilder_1.InputHints.ExpectingInput; } this.trackGeneratorResultEvent(dc, template, msg); return msg; }); } /** * @protected * Track GeneratorResultEvent telemetry event with InputDialogResultEvent context. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param activityTemplate used to create the Activity. * @param msg The Partial [Activity](xref:botframework-schema.Activity) which will be sent. */ trackGeneratorResultEvent(dc, activityTemplate, msg) { this.telemetryClient.trackEvent({ name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent, properties: { template: activityTemplate, result: msg, context: telemetryLoggerConstants_1.TelemetryLoggerConstants.InputDialogResultEvent, }, }); } /** * Helper function to compose an output activity containing a set of choices. * * @param prompt The prompt to append the users choices to. * @param channelId ID of the channel the prompt is being sent to. * @param choices List of choices to append. * @param style Configured style for the list of choices. * @param options (Optional) options to configure the underlying ChoiceFactory call. * @returns A bound activity ready to send to the user. */ appendChoices(prompt, channelId, choices, style, options) { // Create temporary msg let msg; const text = prompt.text || ''; switch (style) { case botbuilder_dialogs_1.ListStyle.inline: msg = botbuilder_dialogs_1.ChoiceFactory.inline(choices, text, null, options); break; case botbuilder_dialogs_1.ListStyle.list: msg = botbuilder_dialogs_1.ChoiceFactory.list(choices, text, null, options); break; case botbuilder_dialogs_1.ListStyle.suggestedAction: msg = botbuilder_dialogs_1.ChoiceFactory.suggestedAction(choices, text); break; case botbuilder_dialogs_1.ListStyle.heroCard: msg = botbuilder_dialogs_1.ChoiceFactory.heroCard(choices, text); break; case botbuilder_dialogs_1.ListStyle.none: msg = botbuilder_1.MessageFactory.text(text); break; default: msg = botbuilder_dialogs_1.ChoiceFactory.forChannel(channelId, choices, text, null, options); break; } // Update clone of prompt with text, actions and attachments const clone = JSON.parse(JSON.stringify(prompt)); clone.text = msg.text; if (msg.suggestedActions && Array.isArray(msg.suggestedActions.actions) && msg.suggestedActions.actions.length > 0) { clone.suggestedActions = msg.suggestedActions; } if (msg.attachments) { clone.attachments = msg.attachments; } if (!clone.inputHint) { clone.inputHint = botbuilder_1.InputHints.ExpectingInput; } return clone; } /** * @private */ recognizeInput(dc, turnCount) { return __awaiter(this, void 0, void 0, function* () { let input; if (this.property) { const property = this.property.getValue(dc.state); input = dc.state.getValue(property); dc.state.deleteValue(property); } if (!input && this.value) { const value = this.value.getValue(dc.state); input = value; } const activityProcessed = dc.state.getValue(botbuilder_dialogs_1.TurnPath.activityProcessed); if (!activityProcessed && !input && turnCount > 0) { if (this instanceof attachmentInput_1.AttachmentInput) { input = dc.context.activity.attachments || []; } else { input = dc.context.activity.text; // if there is no visible text AND we have a value object, then fallback to that. if (!input && dc.context.activity.value != undefined) { input = dc.context.activity.value; } } } dc.state.setValue(InputDialog.VALUE_PROPERTY, input); if (input) { const state = yield this.onRecognizeInput(dc); if (state == InputState.valid) { for (let i = 0; i < this.validations.length; i++) { const validation = this.validations[i]; const exp = new adaptive_expressions_1.ExpressionParser().parse(validation); const { value } = exp.tryEvaluate(dc.state); if (!value) { return InputState.invalid; } } dc.state.setValue(botbuilder_dialogs_1.TurnPath.activityProcessed, true); return InputState.valid; } else { return state; } } else { return InputState.missing; } }); } /** * @private */ promptUser(dc, state) { return __awaiter(this, void 0, void 0, function* () { const prompt = yield this.onRenderPrompt(dc, state); if (prompt == null) { throw new Error(`Call to onRenderPrompt() returned a null activity for state ${state}.`); } yield dc.context.sendActivity(prompt); return botbuilder_dialogs_1.Dialog.EndOfTurn; }); } } exports.InputDialog = InputDialog; InputDialog.OPTIONS_PROPERTY = 'this.options'; InputDialog.VALUE_PROPERTY = 'this.value'; InputDialog.TURN_COUNT_PROPERTY = 'this.turnCount'; //# sourceMappingURL=inputDialog.js.map