botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
367 lines • 20.2 kB
JavaScript
"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