UNPKG

botbuilder-dialogs-adaptive

Version:

Rule system for the Microsoft BotBuilder dialog system.

369 lines (327 loc) • 15.2 kB
/** * @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; } }