botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
217 lines • 11.7 kB
JavaScript
/**
* @module botbuilder-dialogs
*/
/**
* 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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isFromParentToSkill = exports.getActiveDialogContext = exports.shouldSendEndOfConversationToParent = exports.internalRun = exports.runDialog = void 0;
const botframework_connector_1 = require("botframework-connector");
const dialog_1 = require("./dialog");
const dialogEvents_1 = require("./dialogEvents");
const dialogSet_1 = require("./dialogSet");
const memory_1 = require("./memory");
const botbuilder_core_1 = require("botbuilder-core");
/**
* Runs a dialog from a given context and accessor.
*
* @param dialog The [Dialog](xref:botbuilder-dialogs.Dialog) to run.
* @param context [TurnContext](xref:botbuilder-core.TurnContext) object for the current turn of conversation with the user.
* @param accessor Defined methods for accessing the state property created in a BotState object.
*/
function runDialog(dialog, context, accessor) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!dialog) {
throw new Error('runDialog(): missing dialog');
}
if (!context) {
throw new Error('runDialog(): missing context');
}
if (!context.activity) {
throw new Error('runDialog(): missing context.activity');
}
if (!accessor) {
throw new Error('runDialog(): missing accessor');
}
const dialogSet = new dialogSet_1.DialogSet(accessor);
dialogSet.telemetryClient = (_a = context.turnState.get(botbuilder_core_1.BotTelemetryClientKey)) !== null && _a !== void 0 ? _a : dialog.telemetryClient;
dialogSet.add(dialog);
const dialogContext = yield dialogSet.createContext(context);
yield internalRun(context, dialog.id, dialogContext);
});
}
exports.runDialog = runDialog;
/**
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param dialogId The dialog ID.
* @param dialogContext The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param dialogStateManagerConfiguration Configuration for the dialog state manager.
* @returns {Promise<DialogTurnResult>} a promise resolving to the dialog turn result.
*/
function internalRun(context, dialogId, dialogContext, dialogStateManagerConfiguration) {
return __awaiter(this, void 0, void 0, function* () {
// map TurnState into root dialog context.services
context.turnState.forEach((service, key) => {
dialogContext.services.push(key, service);
});
const dialogStateManager = new memory_1.DialogStateManager(dialogContext, dialogStateManagerConfiguration);
yield dialogStateManager.loadAllScopes();
dialogContext.context.turnState.push('DialogStateManager', dialogStateManager);
let dialogTurnResult = null;
// Loop as long as we are getting valid OnError handled we should continue executing the actions for the turn.
// NOTE: We loop around this block because each pass through we either complete the turn and break out of the loop
// or we have had an exception AND there was an OnError action which captured the error. We need to continue the
// turn based on the actions the OnError handler introduced.
let endOfTurn = false;
while (!endOfTurn) {
try {
dialogTurnResult = yield innerRun(context, dialogId, dialogContext);
// turn successfully completed, break the loop
endOfTurn = true;
}
catch (err) {
// fire error event, bubbling from the leaf.
const handled = yield dialogContext.emitEvent(dialogEvents_1.DialogEvents.error, err, true, true);
if (!handled) {
// error was NOT handled, throw the exception and end the turn.
// (This will trigger the Adapter.OnError handler and end the entire dialog stack)
throw err;
}
}
}
// save all state scopes to their respective botState locations.
yield dialogStateManager.saveAllChanges();
// return the redundant result because the DialogManager contract expects it
return dialogTurnResult;
});
}
exports.internalRun = internalRun;
function innerRun(context, dialogId, dialogContext) {
return __awaiter(this, void 0, void 0, function* () {
// Handle EoC and Reprompt event from a parent bot (can be root bot to skill or skill to skill)
if (isFromParentToSkill(context)) {
// Handle remote cancellation request from parent.
if (context.activity.type === botbuilder_core_1.ActivityTypes.EndOfConversation) {
if (!dialogContext.stack.length) {
// No dialogs to cancel, just return.
return { status: dialog_1.DialogTurnStatus.empty };
}
const activeDialogContext = getActiveDialogContext(dialogContext);
// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
return activeDialogContext.cancelAllDialogs(true);
}
// Process a reprompt event sent from the parent.
if (context.activity.type === botbuilder_core_1.ActivityTypes.Event && context.activity.name === dialogEvents_1.DialogEvents.repromptDialog) {
if (!dialogContext.stack.length) {
// No dialogs to reprompt, just return.
return { status: dialog_1.DialogTurnStatus.empty };
}
yield dialogContext.repromptDialog();
return dialog_1.Dialog.EndOfTurn;
}
}
// Continue or start the dialog.
let result = yield dialogContext.continueDialog();
if (result.status === dialog_1.DialogTurnStatus.empty) {
result = yield dialogContext.beginDialog(dialogId);
}
yield sendStateSnapshotTrace(dialogContext);
if (result.status === dialog_1.DialogTurnStatus.complete || result.status === dialog_1.DialogTurnStatus.cancelled) {
if (shouldSendEndOfConversationToParent(context, result)) {
// Send End of conversation at the end.
const code = result.status == dialog_1.DialogTurnStatus.complete
? botbuilder_core_1.EndOfConversationCodes.CompletedSuccessfully
: botbuilder_core_1.EndOfConversationCodes.UserCancelled;
const activity = {
type: botbuilder_core_1.ActivityTypes.EndOfConversation,
value: result.result,
locale: context.activity.locale,
code: code,
};
yield context.sendActivity(activity);
}
}
return result;
});
}
/**
* Helper to determine if we should send an EoC to the parent or not.
*
* @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
* @param turnResult The dialog turn result.
* @returns True if should send EoC, otherwise false.
*/
function shouldSendEndOfConversationToParent(context, turnResult) {
if (!(turnResult.status == dialog_1.DialogTurnStatus.complete || turnResult.status == dialog_1.DialogTurnStatus.cancelled)) {
// The dialog is still going, don't return EoC.
return false;
}
const claimIdentity = context.turnState.get(context.adapter.BotIdentityKey);
// Inspect the cached ClaimsIdentity to determine if the bot was called from another bot.
if (claimIdentity && botframework_connector_1.SkillValidation.isSkillClaim(claimIdentity.claims)) {
// EoC Activities returned by skills are bounced back to the bot by SkillHandler.
// In those cases we will have a SkillConversationReference instance in state.
const skillConversationReference = context.turnState.get(botbuilder_core_1.SkillConversationReferenceKey);
if (skillConversationReference) {
// If the skillConversationReference.OAuthScope is for one of the supported channels, we are at the root and we should not send an EoC.
return (skillConversationReference.oAuthScope !== botframework_connector_1.AuthenticationConstants.ToBotFromChannelTokenIssuer &&
skillConversationReference.oAuthScope !== botframework_connector_1.GovernmentConstants.ToBotFromChannelTokenIssuer);
}
return true;
}
return false;
}
exports.shouldSendEndOfConversationToParent = shouldSendEndOfConversationToParent;
/**
* Recursively walk up the DC stack to find the active DC.
*
* @param dialogContext [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation with the user.
* @returns Active [DialogContext](xref:botbuilder-dialogs.DialogContext).
*/
function getActiveDialogContext(dialogContext) {
const child = dialogContext.child;
if (!child) {
return dialogContext;
}
return getActiveDialogContext(child);
}
exports.getActiveDialogContext = getActiveDialogContext;
/**
* Determines if the skill is acting as a skill parent.
*
* @param context [TurnContext](xref:botbuilder-core.TurnContext) object for the current turn of conversation with the user.
* @returns A boolean representing if the skill is acting as a skill parent.
*/
function isFromParentToSkill(context) {
// If a SkillConversationReference exists, it was likely set by the SkillHandler and the bot is acting as a parent.
if (context.turnState.get(botbuilder_core_1.SkillConversationReferenceKey)) {
return false;
}
// Inspect the cached ClaimsIdentity to determine if the bot is acting as a skill.
const identity = context.turnState.get(context.adapter.BotIdentityKey);
return identity && botframework_connector_1.SkillValidation.isSkillClaim(identity.claims);
}
exports.isFromParentToSkill = isFromParentToSkill;
// Helper to send a trace activity with a memory snapshot of the active dialog DC.
const sendStateSnapshotTrace = (dialogContext) => __awaiter(void 0, void 0, void 0, function* () {
const adapter = dialogContext.context.adapter;
const claimIdentity = dialogContext.context.turnState.get(adapter.BotIdentityKey);
const traceLabel = claimIdentity && botframework_connector_1.SkillValidation.isSkillClaim(claimIdentity.claims) ? 'Skill State' : 'Bot State';
// Send trace of memory
const snapshot = getActiveDialogContext(dialogContext).state.getMemorySnapshot();
const traceActivity = botbuilder_core_1.ActivityEx.createTraceActivity('BotState', 'https://www.botframework.com/schemas/botState', snapshot, traceLabel);
yield dialogContext.context.sendActivity(traceActivity);
});
//# sourceMappingURL=dialogHelper.js.map
;