UNPKG

botbuilder-dialogs-adaptive

Version:

Rule system for the Microsoft BotBuilder dialog system.

296 lines (266 loc) • 12.2 kB
/** * @module botbuilder-dialogs-adaptive */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { StringUtils } from 'botbuilder'; import { Converter, ConverterFactory, Dialog, DialogConfiguration, DialogContext, DialogDependencies, DialogReason, DialogTurnResult, } from 'botbuilder-dialogs'; import { ActionContext } from '../actionContext'; import { DialogListConverter } from '../converters'; import { TelemetryLoggerConstants } from '../telemetryLoggerConstants'; const OFFSET_KEY = 'this.offset'; export enum ActionScopeCommands { GotoAction = 'goto', BreakLoop = 'break', ContinueLoop = 'continue', } export interface ActionScopeResult { actionScopeCommand: string; actionId?: string; } export interface ActionScopeConfiguration extends DialogConfiguration { actions?: string[] | Dialog[]; } /** * `ActionScope` manages execution of a block of actions, and supports Goto, Continue and Break semantics. */ export class ActionScope<O extends object = {}> extends Dialog<O> implements DialogDependencies, ActionScopeConfiguration { /** * Creates a new `ActionScope` instance. * * @param actions The actions for the scope. */ constructor(actions: Dialog[] = []) { super(); this.actions = actions; } /** * The actions to execute. */ actions: Dialog[] = []; /** * @param property The key of the conditional selector configuration. * @returns The converter for the selector configuration. */ getConverter(property: keyof ActionScopeConfiguration): Converter | ConverterFactory { switch (property) { case 'actions': return DialogListConverter; default: return super.getConverter(property); } } /** * Gets a unique `string` which represents the version of this dialog. If the version * changes between turns the dialog system will emit a DialogChanged event. * * @returns Unique `string` which should only change when dialog has changed in a * way that should restart the dialog. */ getVersion(): string { const versions = this.actions.map((action): string => action.getVersion() || '').join(''); return StringUtils.hash(versions); } /** * Gets the child [Dialog](xref:botbuilder-dialogs.Dialog) dependencies so they can be added to the containers [Dialog](xref:botbuilder-dialogs.Dialog) set. * * @returns The child [Dialog](xref:botbuilder-dialogs.Dialog) dependencies. */ getDependencies(): Dialog[] { return this.actions; } /** * 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?: O): Promise<DialogTurnResult> { if (this.actions && this.actions.length > 0) { return await this.beginAction(dc, 0); } else { return await dc.endDialog(); } } /** * 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> { return await this.onNextAction(dc); } /** * 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> { if (result && typeof result === 'object' && Object.hasOwnProperty.call(result, 'actionScopeCommand')) { return await this.onActionScopeResult(dc, result as ActionScopeResult); } return await this.onNextAction(dc, result); } /** * @protected * Called when returning control to this [Dialog](xref:botbuilder-dialogs.Dialog) with an [ActionScopeResult](xref:botbuilder-dialogs-adaptive.ActionScopeResult) * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param actionScopeResult The [ActionScopeResult](xref:botbuilder-dialogs-adaptive.ActionScopeResult). * @returns A `Promise` representing the asynchronous operation. */ protected async onActionScopeResult( dc: DialogContext, actionScopeResult: ActionScopeResult, ): Promise<DialogTurnResult> { switch (actionScopeResult.actionScopeCommand) { case ActionScopeCommands.GotoAction: return await this.onGotoAction(dc, actionScopeResult); case ActionScopeCommands.BreakLoop: return await this.onBreakLoop(dc, actionScopeResult); case ActionScopeCommands.ContinueLoop: return await this.onContinueLoop(dc, actionScopeResult); default: throw new Error(`Unknown action scope command returned: ${actionScopeResult.actionScopeCommand}.`); } } /** * @protected * Called when returning control to this [Dialog](xref:botbuilder-dialogs.Dialog) with an [ActionScopeResult](xref:botbuilder-dialogs-adaptive.ActionScopeResult) * with the property `ActionCommand` set to `GoToAction`. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param actionScopeResult The [ActionScopeResult](xref:botbuilder-dialogs-adaptive.ActionScopeResult). * @returns A `Promise` representing the asynchronous operation. */ protected async onGotoAction(dc: DialogContext, actionScopeResult: ActionScopeResult): Promise<DialogTurnResult> { const offset = this.actions.findIndex((action: Dialog): boolean => { return action.id == actionScopeResult.actionId; }); if (offset >= 0) { return await this.beginAction(dc, offset); } else if (dc.stack.length > 1) { return await dc.endDialog(actionScopeResult); } else { throw new Error(`GotoAction: could not find an action of '${actionScopeResult.actionId}'`); } } /** * @protected * Called when returning control to this [Dialog](xref:botbuilder-dialogs.Dialog) with an [ActionScopeResult](xref:botbuilder-dialogs-adaptive.ActionScopeResult) * with the property `ActionCommand` set to `BreakLoop`. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param actionScopeResult Contains the actions scope result. * @returns A `Promise` representing the asynchronous operation. */ protected async onBreakLoop(dc: DialogContext, actionScopeResult: ActionScopeResult): Promise<DialogTurnResult> { return await dc.endDialog(actionScopeResult); } /** * @protected * Called when returning control to this [Dialog](xref:botbuilder-dialogs.Dialog) with an [ActionScopeResult](xref:botbuilder-dialogs-adaptive.ActionScopeResult) * with the property `ActionCommand` set to `ContinueLoop`. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param actionScopeResult Contains the actions scope result. * @returns A `Promise` representing the asynchronous operation. */ protected async onContinueLoop(dc: DialogContext, actionScopeResult: ActionScopeResult): Promise<DialogTurnResult> { return await dc.endDialog(actionScopeResult); } /** * @protected * Called when the [Dialog](xref:botbuilder-dialogs.Dialog) continues to the next action. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @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. */ protected async onNextAction(dc: DialogContext, result?: any): Promise<DialogTurnResult> { // Check for any plan changes let hasChanges = false; let root = dc; let parent = dc; while (parent) { const ac = parent as ActionContext; if (ac && ac.changes && ac.changes.length > 0) { hasChanges = true; } root = parent; parent = root.parent; } // Apply any changes if (hasChanges) { // Recursively call continueDialog() to apply changes and continue execution. return await root.continueDialog(); } // Increment our offset into the actions and being the next action const nextOffset = dc.state.getValue(OFFSET_KEY, 0) + 1; if (nextOffset < this.actions.length) { return await this.beginAction(dc, nextOffset); } // else we fire the end of actions return await this.onEndOfActions(dc, result); } /** * @protected * Called when the [Dialog](xref:botbuilder-dialogs.Dialog)'s action ends. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @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. */ protected async onEndOfActions(dc: DialogContext, result?: any): Promise<DialogTurnResult> { return await dc.endDialog(result); } /** * @protected * Starts a new [Dialog](xref:botbuilder-dialogs.Dialog) and pushes it onto the dialog stack. * @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation. * @param offset 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. */ protected async beginAction(dc: DialogContext, offset: number): Promise<DialogTurnResult> { dc.state.setValue(OFFSET_KEY, offset); if (!this.actions || this.actions.length <= offset) { return await dc.endDialog(); } const action = this.actions[offset]; const actionName = action.constructor.name; const properties: { [key: string]: string } = { DialogId: action.id, Kind: `Microsoft.${actionName}`, ActionId: `Microsoft.${action.id}`, }; this.telemetryClient.trackEvent({ name: TelemetryLoggerConstants.DialogActionEvent, properties: properties }); return await dc.beginDialog(action.id); } /** * @protected * Builds the compute Id for the dialog. * @returns A `string` representing the compute Id. */ protected onComputeId(): string { const ids = this.actions.map((action: Dialog): string => action.id); return `ActionScope[${StringUtils.ellipsisHash(ids.join(','), 50)}]`; } }