UNPKG

botbuilder-dialogs

Version:

A dialog stack based conversation manager for Microsoft BotBuilder.

367 lines 20.2 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.SkillDialog = void 0; const botbuilder_core_1 = require("botbuilder-core"); const dialog_1 = require("./dialog"); const dialogEvents_1 = require("./dialogEvents"); const turnPath_1 = require("./memory/turnPath"); /** * A specialized Dialog that can wrap remote calls to a skill. * * @remarks * The options parameter in beginDialog must be a BeginSkillDialogOptions instance * with the initial parameters for the dialog. */ class SkillDialog extends dialog_1.Dialog { /** * A sample dialog that can wrap remote calls to a skill. * * @remarks * The options parameter in `beginDialog()` must be a `SkillDialogArgs` object with the initial parameters * for the dialog. * * @param dialogOptions The options to execute the skill dialog. * @param dialogId The id of the dialog. */ constructor(dialogOptions, dialogId) { super(dialogId); // This key uses a simple namespace as Symbols are not serializable. this.DeliveryModeStateKey = 'SkillDialog.deliveryMode'; this.SkillConversationIdStateKey = 'SkillDialog.skillConversationId'; if (!dialogOptions) { throw new TypeError('Missing dialogOptions parameter'); } this.dialogOptions = dialogOptions; } /** * Called when the skill 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 Initial information to pass to the dialog. * @returns A Promise representing the asynchronous operation. * @remarks * If the task is successful, the result indicates whether the dialog is still active after the turn has been processed by the dialog. */ beginDialog(dc, options) { return __awaiter(this, void 0, void 0, function* () { const dialogArgs = this.validateBeginDialogArgs(options); // Create deep clone of the original activity to avoid altering it before forwarding it. const clonedActivity = this.cloneActivity(dialogArgs.activity); // Apply conversation reference and common properties from incoming activity before sending. const skillActivity = botbuilder_core_1.TurnContext.applyConversationReference(clonedActivity, botbuilder_core_1.TurnContext.getConversationReference(dc.context.activity), true); // Store delivery mode and connection name in dialog state for later use. dc.activeDialog.state[this.DeliveryModeStateKey] = dialogArgs.activity.deliveryMode; // Create the conversationId and store it in the dialog context state so we can use it later. const skillConversationId = yield this.createSkillConversationId(dc.context, dc.context.activity); dc.activeDialog.state[this.SkillConversationIdStateKey] = skillConversationId; // Send the activity to the skill. const eocActivity = yield this.sendToSkill(dc.context, skillActivity, skillConversationId); if (eocActivity) { return yield dc.endDialog(eocActivity.value); } return dialog_1.Dialog.EndOfTurn; }); } /** * Called when the skill dialog is _continued_, where it is the active dialog and the * user replies with a new [Activity](xref:botframework-schema.Activity). * * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @returns A Promise representing the asynchronous operation. * @remarks * If the task is successful, the result indicates whether the dialog is still * active after the turn has been processed by the dialog. The result may also contain a * return value. */ continueDialog(dc) { return __awaiter(this, void 0, void 0, function* () { // with adaptive dialogs, ResumeDialog is not called directly. Instead the Interrupted flag is set, which // acts as the signal to the SkillDialog to resume the skill. if (dc.state.getValue(turnPath_1.TurnPath.interrupted)) { // resume dialog execution dc.state.setValue(turnPath_1.TurnPath.interrupted, false); return this.resumeDialog(dc, dialog_1.DialogReason.endCalled); } if (!this.onValidateActivity(dc.context.activity)) { return dialog_1.Dialog.EndOfTurn; } // Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if received from the Skill) if (dc.context.activity.type === botbuilder_core_1.ActivityTypes.EndOfConversation) { return dc.endDialog(dc.context.activity.value); } // Create deep clone of the original activity to avoid altering it before forwarding it. const skillActivity = this.cloneActivity(dc.context.activity); skillActivity.deliveryMode = dc.activeDialog.state[this.DeliveryModeStateKey]; const skillConversationId = dc.activeDialog.state[this.SkillConversationIdStateKey]; // Just forward to the remote skill const eocActivity = yield this.sendToSkill(dc.context, skillActivity, skillConversationId); if (eocActivity) { return dc.endDialog(eocActivity.value); } return dialog_1.Dialog.EndOfTurn; }); } /** * Called when the skill dialog is ending. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) object for this turn. * @param instance State information associated with the instance of this dialog on the dialog stack. * @param reason [Reason](xref:botbuilder-dialogs.DialogReason) why the dialog ended. * @returns A Promise representing the asynchronous operation. */ endDialog(context, instance, reason) { const _super = Object.create(null, { endDialog: { get: () => super.endDialog } }); return __awaiter(this, void 0, void 0, function* () { // Send of of conversation to the skill if the dialog has been cancelled. if (reason == dialog_1.DialogReason.cancelCalled || reason == dialog_1.DialogReason.replaceCalled) { const reference = botbuilder_core_1.TurnContext.getConversationReference(context.activity); // Apply conversation reference and common properties from incoming activity before sending. const activity = botbuilder_core_1.TurnContext.applyConversationReference({ type: botbuilder_core_1.ActivityTypes.EndOfConversation }, reference, true); activity.channelData = context.activity.channelData; const skillConversationId = this.getSkillConversationIdFromInstance(instance); // connectionName is not applicable during endDialog as we don't expect an OAuthCard in response. yield this.sendToSkill(context, activity, skillConversationId); } yield _super.endDialog.call(this, context, instance, reason); }); } /** * Called when the skill dialog should re-prompt the user for input. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) object for this turn. * @param instance State information for this dialog. * @returns A Promise representing the asynchronous operation. */ repromptDialog(context, instance) { return __awaiter(this, void 0, void 0, function* () { // Create and send an envent to the skill so it can resume the dialog. const repromptEvent = { type: botbuilder_core_1.ActivityTypes.Event, name: dialogEvents_1.DialogEvents.repromptDialog }; const reference = botbuilder_core_1.TurnContext.getConversationReference(context.activity); // Apply conversation reference and common properties from incoming activity before sending. const activity = botbuilder_core_1.TurnContext.applyConversationReference(repromptEvent, reference, true); const skillConversationId = this.getSkillConversationIdFromInstance(instance); // connectionName is not applicable for a reprompt as we don't expect an OAuthCard in response. yield this.sendToSkill(context, activity, skillConversationId); }); } /** * Called when a child skill dialog completed its turn, returning control to this dialog. * * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of the conversation. * @param _reason [Reason](xref:botbuilder-dialogs.DialogReason) 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.repromptDialog(dc.context, dc.activeDialog); return dialog_1.Dialog.EndOfTurn; }); } /** * @protected * Validates the activity sent during continueDialog. * @remarks * Override this method to implement a custom validator for the activity being sent during the continueDialog. * This method can be used to ignore activities of a certain type if needed. * If this method returns false, the dialog will end the turn without processing the activity. * @param _activity The Activity for the current turn of conversation. * @returns True if the activity is valid, false if not. */ onValidateActivity(_activity) { return true; } /** * @private * Clones the Activity entity. * @param activity Activity to clone. */ cloneActivity(activity) { return JSON.parse(JSON.stringify(activity)); } /** * @private */ validateBeginDialogArgs(options) { if (!options) { throw new TypeError('Missing options parameter'); } if (!options.activity) { throw new TypeError('"activity" is undefined or null in options.'); } return options; } /** * @private */ sendToSkill(context, activity, skillConversationId) { return __awaiter(this, void 0, void 0, function* () { if (activity.type === botbuilder_core_1.ActivityTypes.Invoke) { // Force ExpectReplies for invoke activities so we can get the replies right away and send them back to the channel if needed. // This makes sure that the dialog will receive the Invoke response from the skill and any other activities sent, including EoC. activity.deliveryMode = botbuilder_core_1.DeliveryModes.ExpectReplies; } // Always save state before forwarding // (the dialog stack won't get updated with the skillDialog and things won't work if you don't) const skillInfo = this.dialogOptions.skill; yield this.dialogOptions.conversationState.saveChanges(context, true); const response = yield this.dialogOptions.skillClient.postActivity(this.dialogOptions.botId, skillInfo.appId, skillInfo.skillEndpoint, this.dialogOptions.skillHostEndpoint, skillConversationId, activity); // Inspect the skill response status if (!isSuccessStatusCode(response.status)) { throw new Error(`Error invoking the skill id: "${skillInfo.id}" at "${skillInfo.skillEndpoint}" (status is ${response.status}). \r\n ${response.body}`); } let eocActivity; let sentInvokeResponses = false; const activitiesFromSkill = response.body && response.body.activities; if (activity.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies && Array.isArray(activitiesFromSkill)) { for (const activityFromSkill of activitiesFromSkill) { if (activityFromSkill.type === botbuilder_core_1.ActivityTypes.EndOfConversation) { // Capture the EndOfConversation activity if it was sent from skill eocActivity = activityFromSkill; // The conversation has ended, so cleanup the conversation id. yield this.dialogOptions.conversationIdFactory.deleteConversationReference(skillConversationId); } else if (!sentInvokeResponses && (yield this.interceptOAuthCards(context, activityFromSkill, this.dialogOptions.connectionName))) { // Do nothing. The token exchange succeeded, so no OAuthCard needs to be shown to the user. sentInvokeResponses = true; } else { // If an invoke response has already been sent we should ignore future invoke responses as this // represents a bug in the skill. if (activityFromSkill.type === botbuilder_core_1.ActivityTypes.InvokeResponse) { if (sentInvokeResponses) { continue; } sentInvokeResponses = true; } yield context.sendActivity(activityFromSkill); } } } return eocActivity; }); } /** * Tells us if we should intercept the OAuthCard message. * @remarks * The SkillDialog only attempts to intercept OAuthCards when the following criteria are met: * 1. An OAuthCard was sent from the skill * 2. The SkillDialog was called with a connectionName * 3. The current adapter supports token exchange * If any of these criteria are false, return false. * @private */ interceptOAuthCards(context, activity, connectionName) { return __awaiter(this, void 0, void 0, function* () { if (!connectionName || !('exchangeToken' in context.adapter)) { // The adapter may choose not to support token exchange, in which case we fallback to showing skill's OAuthCard to the user. return false; } const oAuthCardAttachment = (activity.attachments || []).find((c) => c.contentType === botbuilder_core_1.CardFactory.contentTypes.oauthCard); if (oAuthCardAttachment) { const tokenExchangeProvider = context.adapter; const oAuthCard = oAuthCardAttachment.content; const uri = oAuthCard && oAuthCard.tokenExchangeResource && oAuthCard.tokenExchangeResource.uri; if (uri) { try { const result = yield tokenExchangeProvider.exchangeToken(context, connectionName, context.activity.from.id, { uri }); if (result && result.token) { // If token above is null or undefined, then SSO has failed and we return false. // If not, send an invoke to the skill with the token. return yield this.sendTokenExchangeInvokeToSkill(activity, oAuthCard.tokenExchangeResource.id, oAuthCard.connectionName, result.token); } } catch (_a) { // Failures in token exchange are not fatal. They simply mean that the user needs to be shown the skill's OAuthCard. return false; } } } return false; }); } /** * @private */ sendTokenExchangeInvokeToSkill(incomingActivity, id, connectionName, token) { return __awaiter(this, void 0, void 0, function* () { const ref = botbuilder_core_1.TurnContext.getConversationReference(incomingActivity); const activity = botbuilder_core_1.TurnContext.applyConversationReference(Object.assign({}, incomingActivity), ref); activity.type = botbuilder_core_1.ActivityTypes.Invoke; activity.name = botbuilder_core_1.tokenExchangeOperationName; activity.value = { connectionName, id, token }; // Send the activity to the Skill const skillInfo = this.dialogOptions.skill; const response = yield this.dialogOptions.skillClient.postActivity(this.dialogOptions.botId, skillInfo.appId, skillInfo.skillEndpoint, this.dialogOptions.skillHostEndpoint, incomingActivity.conversation.id, activity); // Check response status: true if success, false if failure return isSuccessStatusCode(response.status); }); } /** * @private * Create a conversationId to interact with the skill and send the [Activity](xref:botframework-schema.Activity). * @param context [TurnContext](xref:botbuilder-core.TurnContext) for the current turn of conversation with the user. * @param activity [Activity](xref:botframework-schema.Activity) to send. * @returns The Skill Conversation ID. */ createSkillConversationId(context, activity) { return __awaiter(this, void 0, void 0, function* () { const conversationIdFactoryOptions = { fromBotOAuthScope: context.turnState.get(context.adapter.OAuthScopeKey), fromBotId: this.dialogOptions.botId, activity: activity, botFrameworkSkill: this.dialogOptions.skill, }; // Create a conversationId to interact with the skill and send the activity let skillConversationId; try { skillConversationId = yield this.dialogOptions.conversationIdFactory.createSkillConversationIdWithOptions(conversationIdFactoryOptions); } catch (err) { if (err.message !== 'Not Implemented') throw err; // If the SkillConversationIdFactoryBase implementation doesn't support createSkillConversationIdWithOptions(), // use createSkillConversationId() instead. skillConversationId = yield this.dialogOptions.conversationIdFactory.createSkillConversationId(botbuilder_core_1.TurnContext.getConversationReference(activity)); } return skillConversationId; }); } /** * @private * Gets the Skill Conversation ID from a given instance. * @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance) from which to look for its ID. * @returns Instance conversation ID. */ getSkillConversationIdFromInstance(instance) { if (instance && instance.state) { return instance.state[this.SkillConversationIdStateKey]; } return null; } } exports.SkillDialog = SkillDialog; function isSuccessStatusCode(status) { return status >= botbuilder_core_1.StatusCodes.OK && status < botbuilder_core_1.StatusCodes.MULTIPLE_CHOICES; } //# sourceMappingURL=skillDialog.js.map