botbuilder-dialogs-adaptive
Version:
Rule system for the Microsoft BotBuilder dialog system.
369 lines (327 loc) • 15.2 kB
text/typescript
/**
* @module botbuilder-dialogs-adaptive
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity, ActivityTypes, StringUtils, TurnContext } from 'botbuilder';
import { ActivityTemplate } from '../templates';
import { ActivityTemplateConverter } from '../converters';
import { AdaptiveEvents } from '../adaptiveEvents';
import { BoolProperty, StringProperty, TemplateInterfaceProperty } from '../properties';
import { skillClientKey, skillConversationIdFactoryKey } from '../skillExtensions';
import { TelemetryLoggerConstants } from '../telemetryLoggerConstants';
import {
BoolExpression,
BoolExpressionConverter,
StringExpression,
StringExpressionConverter,
} from 'adaptive-expressions';
import {
BeginSkillDialogOptions,
Converter,
ConverterFactory,
DialogContext,
DialogTurnResult,
DialogInstance,
DialogReason,
DialogConfiguration,
DialogEvent,
DialogEvents,
DialogStateManager,
SkillDialog,
SkillDialogOptions,
TemplateInterface,
} from 'botbuilder-dialogs';
export interface BeginSkillConfiguration extends DialogConfiguration {
disabled?: BoolProperty;
activityProcessed?: BoolProperty;
resultProperty?: StringProperty;
botId?: StringProperty;
skillHostEndpoint?: StringProperty;
skillAppId?: StringProperty;
skillEndpoint?: StringProperty;
activity?: TemplateInterfaceProperty<Partial<Activity>, DialogStateManager>;
connectionName?: StringProperty;
allowInterruptions?: BoolProperty;
}
/**
* Begin a Skill.
*/
export class BeginSkill extends SkillDialog implements BeginSkillConfiguration {
static $kind = 'Microsoft.BeginSkill';
/**
* Optional expression which if is true will disable this action.
*/
disabled?: BoolExpression;
/**
* Value indicating whether the new dialog should process the activity.
*
* @remarks
* The default for this will be true, which means the new dialog should not look at the activity.
* You can set this to false to dispatch the activity to the new dialog.
*/
activityProcessed = new BoolExpression(true);
/**
* Optional property path to store the dialog result in.
*/
resultProperty?: StringExpression;
/**
* The Microsoft App ID that will be calling the skill.
*
* @remarks
* Defauls to a value of `=settings.MicrosoftAppId` which retrievs the bots ID from settings.
*/
botId = new StringExpression('=settings.MicrosoftAppId');
/**
* The callback Url for the skill host.
*
* @remarks
* Defauls to a value of `=settings.SkillHostEndpoint` which retrieves the endpoint from settings.
*/
skillHostEndpoint = new StringExpression('=settings.SkillHostEndpoint');
/**
* The Microsoft App ID for the skill.
*/
skillAppId: StringExpression;
/**
* The `/api/messages` endpoint for the skill.
*/
skillEndpoint: StringExpression;
/**
* Template for the activity.
*/
activity: TemplateInterface<Partial<Activity>, DialogStateManager>;
/**
* Optional. The OAuth Connection Name for the Parent Bot.
*/
connectionName: StringExpression;
/**
* The interruption policy.
*/
allowInterruptions: BoolExpression;
/**
* @param property The key of the conditional selector configuration.
* @returns The converter for the selector configuration.
*/
getConverter(property: keyof BeginSkillConfiguration): Converter | ConverterFactory {
switch (property) {
case 'disabled':
return new BoolExpressionConverter();
case 'activityProcessed':
return new BoolExpressionConverter();
case 'resultProperty':
return new StringExpressionConverter();
case 'botId':
return new StringExpressionConverter();
case 'skillHostEndpoint':
return new StringExpressionConverter();
case 'skillAppId':
return new StringExpressionConverter();
case 'skillEndpoint':
return new StringExpressionConverter();
case 'activity':
return new ActivityTemplateConverter();
case 'connectionName':
return new StringExpressionConverter();
case 'allowInterruptions':
return new BoolExpressionConverter();
default:
return undefined;
}
}
// Used to cache DialogOptions for multi-turn calls across servers.
private _dialogOptionsStateKey = `${this.constructor.name}.dialogOptionsData`;
/**
* Creates a new `BeginSkillDialog instance.
*
* @param options Optional options used to configure the skill dialog.
*/
constructor(options?: SkillDialogOptions) {
super(
Object.assign({ skill: {} } as SkillDialogOptions, options, {
// This is an alternative to the toJSON function because when the SkillDialogOptions are saved into the Storage,
// when the information is retrieved, it doesn't have the properties that were declared in the toJSON function.
_replacer(): Omit<SkillDialogOptions, 'conversationState' | 'skillClient' | 'conversationIdFactory'> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { conversationState, skillClient, conversationIdFactory, ...rest } = this;
return rest;
},
}),
);
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.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.
*/
async beginDialog(dc: DialogContext, options?: BeginSkillDialogOptions): Promise<DialogTurnResult> {
const dcState = dc.state;
if (this.disabled && this.disabled.getValue(dcState)) {
return await dc.endDialog();
}
// Setup the skill to call
const botId = this.botId.getValue(dcState);
const skillHostEndpoint = this.skillHostEndpoint.getValue(dcState);
if (botId) {
this.dialogOptions.botId = botId;
}
if (skillHostEndpoint) {
this.dialogOptions.skillHostEndpoint = skillHostEndpoint;
}
if (this.skillAppId) {
this.dialogOptions.skill.id = this.dialogOptions.skill.appId = this.skillAppId.getValue(dcState);
}
if (this.skillEndpoint) {
this.dialogOptions.skill.skillEndpoint = this.skillEndpoint.getValue(dcState);
}
if (this.connectionName) {
this.dialogOptions.connectionName = this.connectionName.getValue(dcState);
}
if (!this.dialogOptions.conversationState) {
this.dialogOptions.conversationState = dc.context.turnState.get('ConversationState');
}
if (!this.dialogOptions.skillClient) {
this.dialogOptions.skillClient = dc.context.turnState.get(skillClientKey);
}
if (!this.dialogOptions.conversationIdFactory) {
this.dialogOptions.conversationIdFactory = dc.context.turnState.get(skillConversationIdFactoryKey);
}
// Store the initialized dialogOptions in state so we can restore these values when the dialog is resumed.
dc.activeDialog.state[this._dialogOptionsStateKey] = this.dialogOptions;
// Get the activity to send to the skill.
options = {} as BeginSkillDialogOptions;
if (this.activityProcessed.getValue(dcState) && this.activity) {
// The parent consumed the activity in context, use the Activity property to start the skill.
const activity = await this.activity.bind(dc, dcState);
this.telemetryClient.trackEvent({
name: TelemetryLoggerConstants.GeneratorResultEvent,
properties: {
template: this.activity,
result: activity || '',
},
});
options.activity = activity;
} else {
// Send the turn context activity to the skill (pass through).
options.activity = dc.context.activity;
}
// Call the base to invoke the skill
return await super.beginDialog(dc, options);
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.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.
*/
async continueDialog(dc: DialogContext): Promise<DialogTurnResult> {
this.loadDialogOptions(dc.context, dc.activeDialog);
const activity = dc.context.activity;
if (activity.type == ActivityTypes.EndOfConversation) {
// Capture the result of the dialog if the property is set
if (this.resultProperty && activity.value) {
const dcState = dc.state;
dc.state.setValue(this.resultProperty.getValue(dcState), activity.value);
}
}
return await super.continueDialog(dc);
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.Dialog) should re-prompt the user for input.
*
* @param turnContext [TurnContext](xref:botbuilder-core.TurnContext), the context object for this turn.
* @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance), state information for this dialog.
* @returns A `Promise` representing the asynchronous operation.
*/
async repromptDialog(turnContext: TurnContext, instance: DialogInstance): Promise<void> {
this.loadDialogOptions(turnContext, instance);
return await super.repromptDialog(turnContext, instance);
}
/**
* Called when a child [Dialog](xref:botbuilder-dialogs.Dialog) completed its turn, returning control to this dialog.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param reason [DialogReason](xref:botbuilder-dialogs.DialogReason), 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.
*/
async resumeDialog(dc: DialogContext, reason: DialogReason, result?: any): Promise<DialogTurnResult<any>> {
this.loadDialogOptions(dc.context, dc.activeDialog);
return await super.resumeDialog(dc, reason, result);
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.Dialog) is ending.
*
* @param turnContext [TurnContext](xref:botbuilder-core.TurnContext), the context object for this turn.
* @param instance [DialogInstance](xref:botbuilder-dialogs.DialogInstance), state information associated with the instance of this dialog on the dialog stack.
* @param reason [DialogReason](xref:botbuilder-dialogs.DialogReason), reason why the dialog ended.
* @returns A `Promise` representing the asynchronous operation.
*/
async endDialog(turnContext: TurnContext, instance: DialogInstance, reason: DialogReason): Promise<void> {
this.loadDialogOptions(turnContext, instance);
return await super.endDialog(turnContext, instance, reason);
}
/**
* @protected
* Builds the compute Id for the [Dialog](xref:botbuilder-dialogs.Dialog).
* @returns A `string` representing the compute Id.
*/
protected onComputeId(): string {
const appId = this.skillAppId ? this.skillAppId.toString() : '';
if (this.activity instanceof ActivityTemplate) {
return `BeginSkill['${appId}','${StringUtils.ellipsis(this.activity.template.trim(), 30)}']`;
}
return `BeginSkill['${appId}','${StringUtils.ellipsis(this.activity && this.activity.toString().trim(), 30)}']`;
}
protected async onPreBubbleEvent(dc: DialogContext, e: DialogEvent): Promise<boolean> {
if (e.name === DialogEvents.activityReceived && dc.context.activity.type === ActivityTypes.Message) {
// Ask parent to perform recognition.
if (dc.parent) {
await dc.parent.emitEvent(AdaptiveEvents.recognizeUtterance, dc.context.activity, false);
}
// Should we allow interruptions.
let canInterrupt = true;
if (this.allowInterruptions) {
const { value: allowInterruptions, error } = this.allowInterruptions.tryGetValue(dc.state);
canInterrupt = !error && allowInterruptions;
}
// Stop bubbling if interruptions are NOT allowed
return !canInterrupt;
}
return false;
}
/**
* @private
* Regenerates the [SkillDialog.DialogOptions](xref:botbuilder-dialogs.SkillDialog.DialogOptions) based on the values used during the `BeginDialog` call.
* @remarks The [Dialog](xref:botbuilder-dialogs.Dialog) can be resumed in another server or after redeploying the bot, this code ensure that the options used are the ones
* used to call `BeginDialog`.
* Also, if `ContinueConversation` or other methods are called on a server different than the one where `BeginDialog` was called,
* `DialogOptions` will be empty and this code will make sure it has the right value.
*/
private loadDialogOptions(context: TurnContext, instance: DialogInstance): void {
const dialogOptions = <SkillDialogOptions>instance.state[this._dialogOptionsStateKey];
this.dialogOptions.botId = dialogOptions.botId;
this.dialogOptions.skillHostEndpoint = dialogOptions.skillHostEndpoint;
this.dialogOptions.conversationIdFactory = context.turnState.get(skillConversationIdFactoryKey);
if (this.dialogOptions.conversationIdFactory == null) {
throw new ReferenceError('Unable to locate skillConversationIdFactoryBase in HostContext.');
}
this.dialogOptions.skillClient = context.turnState.get(skillClientKey);
if (this.dialogOptions.skillClient == null) {
throw new ReferenceError('Unable to get an instance of skillHttpClient from turnState.');
}
this.dialogOptions.conversationState = context.turnState.get('ConversationState');
if (this.dialogOptions.conversationState == null) {
throw new ReferenceError('Unable to get an instance of conversationState from turnState.');
}
this.dialogOptions.connectionName = dialogOptions.connectionName;
// Set the skill to call.
this.dialogOptions.skill = dialogOptions.skill;
}
}