UNPKG

botbuilder-dialogs-adaptive

Version:

Rule system for the Microsoft BotBuilder dialog system.

982 lines • 70 kB
"use strict"; /** * @module botbuilder-dialogs-adaptive */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ 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()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AdaptiveDialog = void 0; const adaptive_expressions_1 = require("adaptive-expressions"); const botbuilder_1 = require("botbuilder"); const botbuilder_dialogs_1 = require("botbuilder-dialogs"); const cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const isEqual_1 = __importDefault(require("lodash/isEqual")); const actionContext_1 = require("./actionContext"); const adaptiveEvents_1 = require("./adaptiveEvents"); const conditions_1 = require("./conditions"); const converters_1 = require("./converters"); const entityAssignment_1 = require("./entityAssignment"); const entityAssignmentComparer_1 = require("./entityAssignmentComparer"); const entityAssignments_1 = require("./entityAssignments"); const entityInfo_1 = require("./entityInfo"); const languageGeneratorExtensions_1 = require("./languageGeneratorExtensions"); const recognizers_1 = require("./recognizers"); const valueRecognizer_1 = require("./recognizers/valueRecognizer"); const schemaHelper_1 = require("./schemaHelper"); const selectors_1 = require("./selectors"); const telemetryLoggerConstants_1 = require("./telemetryLoggerConstants"); // eslint-disable-next-line @typescript-eslint/no-explicit-any function isDialogDependencies(val) { return typeof val.getDependencies === 'function'; } /** * The Adaptive Dialog models conversation using events and events to adapt dynamically to changing conversation flow. */ class AdaptiveDialog extends botbuilder_dialogs_1.DialogContainer { /** * Creates a new `AdaptiveDialog` instance. * * @param dialogId (Optional) unique ID of the component within its parents dialog set. */ constructor(dialogId) { super(dialogId); this.adaptiveKey = '_adaptive'; this.defaultOperationKey = '$defaultOperation'; this.expectedOnlyKey = '$expectedOnly'; this.entitiesKey = '$entities'; this.instanceKey = '$instance'; this.operationsKey = '$operations'; this.requiresValueKey = '$requiresValue'; this.propertyNameKey = 'PROPERTYName'; this.propertyEnding = 'Property'; this.utteranceKey = 'utterance'; this.generatorTurnKey = Symbol('generatorTurn'); this.changeTurnKey = Symbol('changeTurn'); this._recognizerSet = new recognizers_1.RecognizerSet(); this.installedDependencies = false; this.needsTracker = false; /** * Trigger handlers to respond to conditions which modify the executing plan. */ this.triggers = []; /** * Whether to end the dialog when there are no actions to execute. * * @remarks * If true, when there are no actions to execute, the current dialog will end. * If false, when there are no actions to execute, the current dialog will simply end the turn and still be active. * Defaults to a value of true. */ this.autoEndDialog = new adaptive_expressions_1.BoolExpression(true); /** * The property to return as the result when the dialog ends when there are no more Actions and `AutoEndDialog = true`. * * @remarks * Defaults to a value of `dialog.result`. */ this.defaultResultProperty = 'dialog.result'; } /** * Sets the JSON Schema for the dialog. */ set schema(value) { this.dialogSchema = new schemaHelper_1.SchemaHelper(value); } /** * Gets the JSON Schema for the dialog. * * @returns The dialog schema. */ get schema() { return this.dialogSchema ? this.dialogSchema.schema : undefined; } /** * @param property The key of the conditional selector configuration. * @returns The converter for the selector configuration. */ getConverter(property) { switch (property) { case 'recognizer': return converters_1.RecognizerConverter; case 'generator': return new converters_1.LanguageGeneratorConverter(); case 'autoEndDialog': return new adaptive_expressions_1.BoolExpressionConverter(); case 'dialogs': return converters_1.DialogSetConverter; default: return super.getConverter(property); } } /** * @protected * Ensures all dependencies for the class are installed. */ ensureDependenciesInstalled() { if (this.installedDependencies) { return; } this.installedDependencies = true; // Install each trigger actions let id = 0; for (let i = 0; i < this.triggers.length; i++) { const trigger = this.triggers[i]; // Install any dependencies if (isDialogDependencies(trigger)) { trigger.getDependencies().forEach((child) => this.dialogs.add(child)); } if (trigger.runOnce) { this.needsTracker = true; } if (!trigger.priority) { trigger.priority = new adaptive_expressions_1.IntExpression(id); } if (!trigger.id) { trigger.id = id.toString(); id++; } } if (!this.selector) { // Default to MostSpecificSelector const selector = new selectors_1.MostSpecificSelector(); selector.selector = new selectors_1.FirstSelector(); this.selector = selector; } this.selector.initialize(this.triggers, true); } //--------------------------------------------------------------------------------------------- // Base Dialog Overrides //--------------------------------------------------------------------------------------------- /** * @protected * Gets the internal version string. * @returns Internal version string. */ getInternalVersion() { if (!this._internalVersion) { // change the container version if any dialogs are added or removed. let version = this.dialogs.getVersion(); // change version if the schema has changed. if (this.schema) { version += JSON.stringify(this.schema); } // change if triggers type/constraint change this.triggers.forEach((trigger) => { version += trigger.getExpression().toString(); }); this._internalVersion = botbuilder_1.StringUtils.hash(version); } return this._internalVersion; } onComputeId() { return 'AdaptiveDialog[]'; } /** * Called when the 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. * @returns A Promise representing the asynchronous operation. */ beginDialog(dc, options) { return __awaiter(this, void 0, void 0, function* () { yield this.checkForVersionChange(dc); // Install dependencies on first access this.ensureDependenciesInstalled(); // Initialize dialog state if (options) { // Replace initial activeDialog.State with clone of options dc.activeDialog.state = JSON.parse(JSON.stringify(options)); } // Initialize event counter const dcState = dc.state; if (dcState.getValue(botbuilder_dialogs_1.DialogPath.eventCounter) == undefined) { dcState.setValue(botbuilder_dialogs_1.DialogPath.eventCounter, 0); } // Initialize list of required properties if (this.dialogSchema && dcState.getValue(botbuilder_dialogs_1.DialogPath.requiredProperties) == undefined) { // RequiredProperties control what properties must be filled in. dcState.setValue(botbuilder_dialogs_1.DialogPath.requiredProperties, this.dialogSchema.required); } // Initialize change tracker if (this.needsTracker && dcState.getValue(AdaptiveDialog.conditionTracker) == undefined) { this.triggers.forEach((trigger) => { if (trigger.runOnce && trigger.condition) { const references = trigger.condition.toExpression().references(); const paths = dcState.trackPaths(references); const triggerPath = `${AdaptiveDialog.conditionTracker}.${trigger.id}.`; dcState.setValue(triggerPath + 'paths', paths); dcState.setValue(triggerPath + 'lastRun', 0); } }); } dc.activeDialog.state[this.adaptiveKey] = {}; const properties = { DialogId: this.id, Kind: 'Microsoft.AdaptiveDialog', context: telemetryLoggerConstants_1.TelemetryLoggerConstants.DialogStartEvent, }; this.telemetryClient.trackEvent({ name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent, properties: properties, }); (0, botbuilder_1.telemetryTrackDialogView)(this.telemetryClient, this.id); // Evaluate events and queue up action changes const event = { name: adaptiveEvents_1.AdaptiveEvents.beginDialog, value: options, bubble: false }; yield this.onDialogEvent(dc, event); // Continue action execution return yield this.continueActions(dc); }); } /** * Called when the 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 Promise representing the asynchronous operation. */ continueDialog(dc) { return __awaiter(this, void 0, void 0, function* () { yield this.checkForVersionChange(dc); this.ensureDependenciesInstalled(); // Continue action execution return yield this.continueActions(dc); }); } /** * Called when the dialog is ending. * * @param turnContext The context object for this turn. * @param instance State information associated with the instance of this dialog on the dialog stack. * @param reason Reason why the dialog ended. * @returns A Promise representing the asynchronous operation. */ endDialog(turnContext, instance, reason) { const _super = Object.create(null, { endDialog: { get: () => super.endDialog } }); return __awaiter(this, void 0, void 0, function* () { const properties = { DialogId: this.id, Kind: 'Microsoft.AdaptiveDialog', }; if (reason === botbuilder_dialogs_1.DialogReason.cancelCalled) { this.telemetryClient.trackEvent({ name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent, properties: Object.assign(Object.assign({}, properties), { context: telemetryLoggerConstants_1.TelemetryLoggerConstants.DialogCancelEvent }), }); } else if (reason === botbuilder_dialogs_1.DialogReason.endCalled) { this.telemetryClient.trackEvent({ name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent, properties: Object.assign(Object.assign({}, properties), { context: telemetryLoggerConstants_1.TelemetryLoggerConstants.CompleteEvent }), }); } yield _super.endDialog.call(this, turnContext, instance, reason); }); } /** * @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 The [DialogEvent](xref:botbuilder-dialogs.DialogEvent) being raised. * @returns Whether the event is handled by the current dialog and further processing should stop. */ onPreBubbleEvent(dc, event) { return __awaiter(this, void 0, void 0, function* () { const actionContext = this.toActionContext(dc); // Process event and queue up any potential interruptions return yield this.processEvent(actionContext, event, true); }); } /** * @protected * Called after an event was bubbled to all parents and wasn't handled. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param event The [DialogEvent](xref:botbuilder-dialogs.DialogEvent) being raised. * @returns Whether the event is handled by the current dialog and further processing should stop. */ onPostBubbleEvent(dc, event) { return __awaiter(this, void 0, void 0, function* () { const actionContext = this.toActionContext(dc); // Process event and queue up any potential interruptions return yield this.processEvent(actionContext, event, false); }); } /** * Called when a child dialog completed its turn, returning control to this dialog. * * @param dc The dialog context for the current turn of the conversation. * @param _reason Reason 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* () { yield this.checkForVersionChange(dc); // Containers are typically leaf nodes on the stack but the dev is free to push other dialogs // on top of the stack which will result in the container receiving an unexpected call to // resumeDialog() when the pushed on dialog ends. // To avoid the container prematurely ending we need to implement this method and simply // ask our inner dialog stack to re-prompt. yield this.repromptDialog(dc.context, dc.activeDialog); return botbuilder_dialogs_1.Dialog.EndOfTurn; }); } /** * Reprompts the user. * * @param context The context object for the turn. * @param instance Current state information for this dialog. * @returns A Promise representing the asynchronous operation. */ repromptDialog(context, instance) { const _super = Object.create(null, { repromptDialog: { get: () => super.repromptDialog } }); return __awaiter(this, void 0, void 0, function* () { if (context instanceof botbuilder_dialogs_1.DialogContext) { // Forward to current sequence action const state = instance.state[this.adaptiveKey]; if (state && state.actions && state.actions.length > 0) { // we need to mockup a DialogContext so that we can call RepromptDialog // for the active step const childContext = this.createChildContext(context); yield childContext.repromptDialog(); } } else { yield _super.repromptDialog.call(this, context, instance); } }); } /** * Creates a child [DialogContext](xref:botbuilder-dialogs.DialogContext) for the given context. * * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @returns The child [DialogContext](xref:botbuilder-dialogs.DialogContext) or null if no [AdaptiveDialogState.actions](xref:botbuilder-dialogs-adaptive.AdaptiveDialogState.actions) are found for the given context. */ createChildContext(dc) { var _a; const activeDialogState = dc.activeDialog.state; const state = activeDialogState[this.adaptiveKey]; if (!state) { activeDialogState[this.adaptiveKey] = { actions: [] }; } else if (((_a = state.actions) === null || _a === void 0 ? void 0 : _a.length) > 0) { const childContext = new botbuilder_dialogs_1.DialogContext(this.dialogs, dc, state.actions[0]); this.onSetScopedServices(childContext); return childContext; } return undefined; } /** * Gets [Dialog](xref:botbuilder-dialogs.Dialog) enumerated dependencies. * * @returns [Dialog](xref:botbuilder-dialogs.Dialog)'s enumerated dependencies. */ getDependencies() { this.ensureDependenciesInstalled(); return []; } //--------------------------------------------------------------------------------------------- // Event Processing //--------------------------------------------------------------------------------------------- /** * @protected * Event processing implementation. * @param actionContext The [ActionContext](xref:botbuilder-dialogs-adaptive.ActionContext) for the current turn of conversation. * @param dialogEvent The [DialogEvent](xref:botbuilder-dialogs.DialogEvent) being raised. * @param preBubble A flag indicator for preBubble processing. * @returns A Promise representation of a boolean indicator or the result. */ processEvent(actionContext, dialogEvent, preBubble) { return __awaiter(this, void 0, void 0, function* () { // Save into turn actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.dialogEvent, dialogEvent); let activity = actionContext.state.getValue(botbuilder_dialogs_1.TurnPath.activity); // some dialogevents get promoted into turn state for general access outside of the dialogevent. // This allows events to be fired (in the case of ChooseIntent), or in interruption (Activity) // Triggers all expressed against turn.recognized or turn.activity, and this mapping maintains that // any event that is emitted updates those for the rest of rule evaluation. switch (dialogEvent.name) { case adaptiveEvents_1.AdaptiveEvents.recognizedIntent: { // we have received a RecognizedIntent event // get the value and promote to turn.recognized, topintent, topscore and lastintent const recognizedResult = actionContext.state.getValue(`${botbuilder_dialogs_1.TurnPath.dialogEvent}.value`); const { intent, score } = (0, botbuilder_1.getTopScoringIntent)(recognizedResult); actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.recognized, recognizedResult); actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.topIntent, intent); actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.topScore, score); actionContext.state.setValue(botbuilder_dialogs_1.DialogPath.lastIntent, intent); // process entities for ambiguity processing (We do this regardless of who handles the event) this.processEntities(actionContext, activity); break; } case adaptiveEvents_1.AdaptiveEvents.activityReceived: // we received an ActivityReceived event, promote the activity into turn.activity actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.activity, dialogEvent.value); activity = dialogEvent.value; break; } this.ensureDependenciesInstalled(); // Count of events processed let count = actionContext.state.getValue(botbuilder_dialogs_1.DialogPath.eventCounter); actionContext.state.setValue(botbuilder_dialogs_1.DialogPath.eventCounter, ++count); // Look for triggered rule let handled = yield this.queueFirstMatch(actionContext); if (handled) { return true; } // Perform default processing if (preBubble) { switch (dialogEvent.name) { case adaptiveEvents_1.AdaptiveEvents.beginDialog: if (!actionContext.state.getValue(botbuilder_dialogs_1.TurnPath.activityProcessed)) { const activityReceivedEvent = { name: adaptiveEvents_1.AdaptiveEvents.activityReceived, value: actionContext.context.activity, bubble: false, }; handled = yield this.processEvent(actionContext, activityReceivedEvent, true); } break; case adaptiveEvents_1.AdaptiveEvents.activityReceived: if (activity.type === botbuilder_1.ActivityTypes.Message) { // Recognize utterance (ignore handled) const recognizeUtteranceEvent = { name: adaptiveEvents_1.AdaptiveEvents.recognizeUtterance, value: activity, bubble: false, }; yield this.processEvent(actionContext, recognizeUtteranceEvent, true); // Emit leading RecognizedIntent event const recognized = actionContext.state.getValue(botbuilder_dialogs_1.TurnPath.recognized); const recognizedIntentEvent = { name: adaptiveEvents_1.AdaptiveEvents.recognizedIntent, value: recognized, bubble: false, }; handled = yield this.processEvent(actionContext, recognizedIntentEvent, true); } // Has an interruption occurred? // - Setting this value to true causes any running inputs to re-prompt when they're // continued. The developer can clear this flag if they want the input to instead // process the users utterance when its continued. if (handled) { actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.interrupted, true); } break; case adaptiveEvents_1.AdaptiveEvents.recognizeUtterance: if (activity.type === botbuilder_1.ActivityTypes.Message) { // Recognize utterance const recognizedResult = yield this.onRecognize(actionContext, activity); // TODO figure out way to not use turn state to pass this value back to caller. actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.recognized, recognizedResult); const { intent, score } = (0, botbuilder_1.getTopScoringIntent)(recognizedResult); actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.topIntent, intent); actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.topScore, score); actionContext.state.setValue(botbuilder_dialogs_1.DialogPath.lastIntent, intent); handled = true; } break; case adaptiveEvents_1.AdaptiveEvents.repromptDialog: // AdaptiveDialogs handle new RepromptDialog as it gives access to the dialogContext. yield this.repromptDialog(actionContext, actionContext.activeDialog); handled = true; break; } } else { switch (dialogEvent.name) { case adaptiveEvents_1.AdaptiveEvents.beginDialog: if (!actionContext.state.getValue(botbuilder_dialogs_1.TurnPath.activityProcessed)) { const activityReceivedEvent = { name: adaptiveEvents_1.AdaptiveEvents.activityReceived, value: activity, bubble: false, }; // Emit trailing ActivityReceived event handled = yield this.processEvent(actionContext, activityReceivedEvent, false); } break; case adaptiveEvents_1.AdaptiveEvents.activityReceived: if (activity.type === botbuilder_1.ActivityTypes.Message) { // Do we have an empty sequence? if (actionContext.actions.length === 0) { const unknownIntentEvent = { name: adaptiveEvents_1.AdaptiveEvents.unknownIntent, bubble: false, }; // Emit trailing UnknownIntent event handled = yield this.processEvent(actionContext, unknownIntentEvent, false); } else { handled = false; } } // Has an interruption occurred? // - Setting this value to true causes any running inputs to re-prompt when they're // continued. The developer can clear this flag if they want the input to instead // process the users utterance when its continued. if (handled) { actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.interrupted, true); } break; } } return handled; }); } /** * @protected * Recognizes intent for current activity given the class recognizer set, if set is null no intent will be recognized. * @param actionContext The [ActionContext](xref:botbuilder-dialogs-adaptive.ActionContext) for the current turn of conversation. * @param activity [Activity](xref:botbuilder-schema.Activity) to recognize. * @returns A Promise representing a [RecognizerResult](xref:botbuilder.RecognizerResult). */ onRecognize(actionContext, activity) { return __awaiter(this, void 0, void 0, function* () { const { text } = activity; const noneIntent = { text: text !== null && text !== void 0 ? text : '', intents: { None: { score: 0.0 } }, entities: {}, }; if (this.recognizer) { if (this._recognizerSet.recognizers.length === 0) { this._recognizerSet.recognizers.push(this.recognizer); this._recognizerSet.recognizers.push(new valueRecognizer_1.ValueRecognizer()); } const recognized = yield this._recognizerSet.recognize(actionContext, activity); const intents = Object.entries(recognized.intents); if (intents.length > 0) { // Score // Gathers all the intents with the highest Score value. const scoreSorted = intents.sort(([, a], [, b]) => b.score - a.score); const [[, firstItemScore]] = scoreSorted; const topIntents = scoreSorted.filter(([, e]) => e.score == firstItemScore.score); // Priority // Gathers the Intent with the highest Priority (0 being the highest). // Note: this functionality is based on the FirstSelector.SelectAsync method. let [topIntent] = topIntents; if (topIntents.length > 1) { let highestPriority = Number.MAX_SAFE_INTEGER; for (const [key, intent] of topIntents) { const [triggerIntent] = this.triggers.filter((x) => x instanceof conditions_1.OnIntent && x.intent == key); const priority = triggerIntent.currentPriority(actionContext); if (priority >= 0 && priority < highestPriority) { topIntent = [key, intent]; highestPriority = priority; } } } const [key, value] = topIntent; recognized.intents = { [key]: value }; } return recognized; } else { return noneIntent; } }); } queueFirstMatch(actionContext) { return __awaiter(this, void 0, void 0, function* () { const selection = yield this.selector.select(actionContext); if (selection.length > 0) { const evt = selection[0]; const properties = { DialogId: this.id, Expression: evt.getExpression().toString(), Kind: `Microsoft.${evt.constructor.name}`, ConditionId: evt.id, context: telemetryLoggerConstants_1.TelemetryLoggerConstants.TriggerEvent, }; this.telemetryClient.trackEvent({ name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent, properties: properties, }); const changes = yield evt.execute(actionContext); if (changes && changes.length > 0) { actionContext.queueChanges(changes[0]); return true; } } return false; }); } //--------------------------------------------------------------------------------------------- // Action Execution //--------------------------------------------------------------------------------------------- /** * @protected * Waits for pending actions to complete and moves on to [OnEndOfActions](xref:botbuilder-dialogs-adaptive.OnEndOfActions). * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @returns A Promise representation of [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult). */ continueActions(dc) { return __awaiter(this, void 0, void 0, function* () { // Apply any queued up changes const actionContext = this.toActionContext(dc); yield actionContext.applyChanges(); // Get a unique instance ID for the current stack entry. // - We need to do this because things like cancellation can cause us to be removed // from the stack and we want to detect this so we can stop processing actions. const instanceId = this.getUniqueInstanceId(actionContext); // Initialize local interruption detection // Any steps containing a dialog stack after the first step indicates the action was interrupted. // We want to force a re-prompt and then end the turn when we encounter an interrupted step. let interrupted = false; // Create context for active action let actionDC = this.createChildContext(actionContext); while (actionDC) { let result; if (actionDC.stack.length === 0) { // Start step const nextAction = actionContext.actions[0]; result = yield actionDC.beginDialog(nextAction.dialogId, nextAction.options); } else { // Set interrupted flag only if it is undefined if (interrupted && actionDC.state.getValue(botbuilder_dialogs_1.TurnPath.interrupted) === undefined) { actionDC.state.setValue(botbuilder_dialogs_1.TurnPath.interrupted, true); } // Continue step execution result = yield actionDC.continueDialog(); } // Is the step waiting for input or were we cancelled? if (result.status === botbuilder_dialogs_1.DialogTurnStatus.waiting || this.getUniqueInstanceId(actionContext) !== instanceId) { return result; } // End current step yield this.endCurrentAction(actionContext); if (result.status === botbuilder_dialogs_1.DialogTurnStatus.completeAndWait) { // Child dialog completed, but wants us to wait for a new activity result.status = botbuilder_dialogs_1.DialogTurnStatus.waiting; return result; } let parentChanges = false; let root = actionContext; let parent = actionContext.parent; while (parent) { const ac = parent; if (ac && ac.changes && ac.changes.length > 0) { parentChanges = true; } root = parent; parent = root.parent; } // Execute next step if (parentChanges) { // Recursively call continueDialog() to apply parent changes and continue execution return yield root.continueDialog(); } // Apply any locale changes and fetch next action yield actionContext.applyChanges(); actionDC = this.createChildContext(actionContext); interrupted = true; } return yield this.onEndOfActions(actionContext); }); } /** * @protected * Provides the ability to set scoped services for the current [DialogContext](xref:botbuilder-dialogs.DialogContext). * @param dialogContext The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. */ onSetScopedServices(dialogContext) { if (this.generator) { dialogContext.services.set(languageGeneratorExtensions_1.languageGeneratorKey, this.generator); } } /** * @protected * Removes the most current action from the given [ActionContext](xref:botbuilder-dialogs-adaptive.ActionContext) if there are any. * @param actionContext The [ActionContext](xref:botbuilder-dialogs-adaptive.ActionContext) for the current turn of conversation. * @returns A Promise representing a boolean indicator for the result. */ endCurrentAction(actionContext) { return __awaiter(this, void 0, void 0, function* () { if (actionContext.actions.length > 0) { actionContext.actions.shift(); } return false; }); } /** * @protected * Awaits for completed actions to finish processing entity assignments and finishes the turn. * @param actionContext The [ActionContext](xref:botbuilder-dialogs-adaptive.ActionContext) for the current turn of conversation. * @returns A Promise representation of [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult). */ onEndOfActions(actionContext) { return __awaiter(this, void 0, void 0, function* () { // Is the current dialog still on the stack? if (actionContext.activeDialog) { // Completed actions so continue processing entity assignments const handled = yield this.processQueues(actionContext); if (handled) { // Still processing assignments return yield this.continueActions(actionContext); } else if (this.autoEndDialog.getValue(actionContext.state)) { const result = actionContext.state.getValue(this.defaultResultProperty); return yield actionContext.endDialog(result); } return botbuilder_dialogs_1.Dialog.EndOfTurn; } return { status: botbuilder_dialogs_1.DialogTurnStatus.cancelled }; }); } getUniqueInstanceId(dc) { return dc.stack.length > 0 ? `${dc.stack.length}:${dc.activeDialog.id}` : ''; } toActionContext(dc) { const activeDialogState = dc.activeDialog.state; let state = activeDialogState[this.adaptiveKey]; if (!state) { state = { actions: [] }; activeDialogState[this.adaptiveKey] = state; } if (!state.actions) { state.actions = []; } const dialogState = { dialogStack: dc.stack }; const actionContext = new actionContext_1.ActionContext(dc.dialogs, dc, dialogState, state.actions, this.changeTurnKey); actionContext.parent = dc.parent; // use configuration of dc's state if (!actionContext.parent) { actionContext.state.configuration = dc.state.configuration; } return actionContext; } /** * This function goes through the entity assignments and emits events if present. * * @param actionContext The ActionContext. * @returns true if the event was handled. */ processQueues(actionContext) { var _a; return __awaiter(this, void 0, void 0, function* () { let evt; let handled = false; const assignments = entityAssignments_1.EntityAssignments.read(actionContext); const nextAssignment = assignments.nextAssignment; if (nextAssignment) { (_a = nextAssignment.raisedCount) !== null && _a !== void 0 ? _a : (nextAssignment.raisedCount = 0); if (nextAssignment.raisedCount++ === 0) { // Reset retries when new form event is first issued actionContext.state.deleteValue(botbuilder_dialogs_1.DialogPath.retries); } evt = { name: nextAssignment.event, value: nextAssignment.alternative ? nextAssignment.alternatives : nextAssignment, bubble: false, }; if (nextAssignment.event === adaptiveEvents_1.AdaptiveEvents.assignEntity) { // TODO: (from C#) For now, I'm going to dereference to a one-level array value. There is a bug in the current code in the distinction between // @ which is supposed to unwrap down to non-array and @@ which returns the whole thing. @ in the curent code works by doing [0] which // is not enough. let entity = nextAssignment.value.value; if (!Array.isArray(entity)) { entity = [entity]; } actionContext.state.setValue(`${botbuilder_dialogs_1.TurnPath.recognized}.entities.${nextAssignment.value.name}`, entity); assignments.dequeue(actionContext); } actionContext.state.setValue(botbuilder_dialogs_1.DialogPath.lastEvent, evt.name); handled = yield this.processEvent(actionContext, evt, true); if (!handled) { // If event wasn't handled, remove it. if (nextAssignment && nextAssignment.event !== adaptiveEvents_1.AdaptiveEvents.assignEntity) { assignments.dequeue(actionContext); } // See if more assignments or end of actions. handled = yield this.processQueues(actionContext); } } else { // Emit end of actions evt = { name: adaptiveEvents_1.AdaptiveEvents.endOfActions, bubble: false, }; actionContext.state.setValue(botbuilder_dialogs_1.DialogPath.lastEvent, evt.name); handled = yield this.processEvent(actionContext, evt, true); } return handled; }); } /** * Process entities to identify ambiguity and possible assignment to properties. Broadly the steps are: * Normalize entities to include meta-data * Check to see if an entity is in response to a previous ambiguity event * Assign entities to possible properties * Merge new queues into existing queues of ambiguity events * * @param actionContext The ActionContext. * @param activity The Activity. */ processEntities(actionContext, activity) { if (this.dialogSchema) { const lastEvent = actionContext.state.getValue(botbuilder_dialogs_1.DialogPath.lastEvent); if (lastEvent) { actionContext.state.deleteValue(botbuilder_dialogs_1.DialogPath.lastEvent); } const assignments = entityAssignments_1.EntityAssignments.read(actionContext); const entities = this.normalizeEntities(actionContext); const utterance = activity.type === botbuilder_1.ActivityTypes.Message ? activity.text : ''; // Utterance is a special entity that corresponds to the full utterance entities[this.utteranceKey] = [ Object.assign(new entityInfo_1.EntityInfo(), { priority: Number.MAX_SAFE_INTEGER, coverage: 1, start: 0, end: utterance.length, name: this.utteranceKey, score: 0, type: 'string', value: utterance, text: utterance, }), ]; const recognized = this.assignEntities(actionContext, entities, assignments, lastEvent); const unrecognized = this.splitUtterance(utterance, recognized); // Utterance is a special entity that corresponds to the full utterance actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.unrecognizedText, unrecognized); actionContext.state.setValue(botbuilder_dialogs_1.TurnPath.recognizedEntities, recognized); assignments.write(actionContext); } } splitUtterance(utterance, recognized) { const unrecognized = []; let current = 0; for (let i = 0; i < recognized.length; i++) { const entity = recognized[i]; if (entity.start > current) { unrecognized.push(utterance.substr(current, entity.start - current).trim()); } current = entity.end; } if (current < utterance.length) { unrecognized.push(utterance.substr(current)); } return unrecognized; } normalizeEntities(actionContext) { var _a, _b, _c; const entityToInfo = {}; const text = actionContext.state.getValue(`${botbuilder_dialogs_1.TurnPath.recognized}.text`); const entities = actionContext.state.getValue(`${botbuilder_dialogs_1.TurnPath.recognized}.entities`); if (entities) { const turn = actionContext.state.getValue(botbuilder_dialogs_1.DialogPath.eventCounter); const operations = (_a = (this.dialogSchema.schema && this.dialogSchema.schema[this.operationsKey])) !== null && _a !== void 0 ? _a : []; const properties = Object.keys((_c = (_b = this.dialogSchema) === null || _b === void 0 ? void 0 : _b.schema['properties']) !== null && _c !== void 0 ? _c : {}); this.expandEntityObject(entities, null, null, null, operations, properties, turn, text, entityToInfo); } // When there are multiple possible resolutions for the same entity that overlap, pick the // one that covers the most of the utterance. for (const name in entityToInfo) { const infos = entityToInfo[name]; infos.sort((entity1, entity2) => { let val = 0; if (entity1.start === entity2.start) { if (entity1.end > entity2.end) { val = -1; } else if (entity1.end < entity2.end) { val = +1; } } else if (entity1.start < entity2.start) { val = -1; } else { val = +1; } return val; }); for (let i = 0; i < infos.length; ++i) { const current = infos[i]; for (let j = i + 1; j < infos.length;) { const alt = infos[j]; if (entityInfo_1.EntityInfo.covers(current, alt)) { infos.splice(j, 1); } else { ++j; } } } } return entityToInfo; } expandEntityObject(entities, op, property, rootInstance, operations, properties, turn, text, entityToInfo) { Object.keys(entities).forEach((entityName) => { const instances = entities[this.instanceKey][entityName]; this.expandEntities(entityName, entities[entityName], instances, rootInstance, op, property, operations, properties, turn, text, entityToInfo); }); } // There's a decent amount of type casting in this method and expandEntity() due to the complex // nested structure of `entities`. It can be solved by defining types, but the changes necessary to remove the type casting // are numerous and it can be argued that it adds complexity to do so. expandEntities(name, entities, instances, rootInstance, op, property, operations, properties, turn, text, entityToInfo) { if (!name.startsWith('$')) { // Entities representing schema properties end in "Property" to prevent name collisions with the property itself. const propName = this.stripProperty(name); let entityName; let isOp = false; let isProperty = false; if (operations.includes(name)) { op = name; isOp = true; } else if (properties.includes(propName)) { property = propName; isProperty = true; } else { entityName = name; } entities.forEach((entity, index) => { const instance = instances[index]; let root = rootInstance; if (!root) { // Keep the root entity name and position to help with overlap. root = (0, cloneDeep_1.default)(instance); root.type = `${name}${index}`; } if (entityName) { this.expandEntity(entityName, entity, instance, root, op, property, turn, text, entityToInfo); } else if (typeof entity === 'object' && entity !== null) { if ((0, isEmpty_1.default)(entity)) { if (isOp) { // Handle operator with no children. this.expandEntity(op, null, instance, root, op, property, turn, text, entityToInfo); } else if (isProperty) { // Handle property with no children. this.expandEntity(property, null, instance, root, op, property, turn, text, entityToInfo); } } else { this.expandEntityObje