botbuilder-dialogs-adaptive
Version:
Rule system for the Microsoft BotBuilder dialog system.
982 lines • 70 kB
JavaScript
"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