UNPKG

botbuilder-dialogs

Version:

A dialog stack based conversation manager for Microsoft BotBuilder.

217 lines 11.7 kB
"use strict"; /** * @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