Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated Dialog Manager to work with skills #2343

Merged
merged 14 commits into from
Jul 9, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions libraries/botbuilder-dialogs/src/dialogHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Activity,
TurnContext,
} from 'botbuilder-core';
import { DialogContext, DialogState } from './dialogContext';
import { Dialog, DialogTurnStatus } from './dialog';
import { Dialog, DialogTurnStatus, DialogTurnResult } from './dialog';
import { DialogEvents } from './dialogEvents';
import { DialogSet } from './dialogSet';
import { AuthConstants, GovConstants, isSkillClaim } from './prompts/skillsHelpers';
Expand Down Expand Up @@ -80,7 +80,7 @@ export async function runDialog(dialog: Dialog, context: TurnContext, accessor:
}

if (result.status === DialogTurnStatus.complete || result.status === DialogTurnStatus.cancelled) {
if (sendEoCToParent(context)) {
if (shouldSendEndOfConversationToParent(context, result)) {
const endMessageText = `Dialog ${ dialog.id } has **completed**. Sending EndOfConversation.`;
await context.sendTraceActivity(telemetryEventName, result.result, undefined, `${ endMessageText }`);

Expand All @@ -95,7 +95,12 @@ export async function runDialog(dialog: Dialog, context: TurnContext, accessor:
* Helper to determine if we should send an EoC to the parent or not.
* @param context
*/
function sendEoCToParent(context: TurnContext): boolean {
export function shouldSendEndOfConversationToParent(context: TurnContext, turnResult: DialogTurnResult): boolean {
if (!(turnResult.status == DialogTurnStatus.complete || turnResult.status == DialogTurnStatus.cancelled)) {
// The dialog is still going, don't return EoC.
return false;
}

Comment on lines +98 to +103
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chon219, this is a combination that combines DialogManager.ShouldSendEndOfConversationToParent() and DialogExtensions.SendEoCToParent(), correct?

I think we should also take this change in C#. What do you think @gabog?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevengum I think so. We have two implementations of this function in c# but it seems that we only need one.

const claimIdentity = context.turnState.get(context.adapter.BotIdentityKey);
// Inspect the cached ClaimsIdentity to determine if the bot was called from another bot.
if (claimIdentity && isSkillClaim(claimIdentity.claims)) {
Expand All @@ -114,7 +119,7 @@ function sendEoCToParent(context: TurnContext): boolean {
}

// Recursively walk up the DC stack to find the active DC.
function getActiveDialogContext(dialogContext: DialogContext): DialogContext {
export function getActiveDialogContext(dialogContext: DialogContext): DialogContext {
const child = dialogContext.child;
if (!child) {
return dialogContext;
Expand All @@ -123,7 +128,7 @@ function getActiveDialogContext(dialogContext: DialogContext): DialogContext {
return getActiveDialogContext(child);
}

function isFromParentToSkill(context: TurnContext): boolean {
export function isFromParentToSkill(context: TurnContext): boolean {
// If a SkillConversationReference exists, it was likely set by the SkillHandler and the bot is acting as a parent.
if (context.turnState.get(SkillConversationReferenceKey)) {
return false;
Expand Down
175 changes: 133 additions & 42 deletions libraries/botbuilder-dialogs/src/dialogManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
* Licensed under the MIT License.
*/

import { TurnContext, BotState, ConversationState, UserState, ActivityTypes, BotStateSet, TurnContextStateCollection } from 'botbuilder-core';
import { TurnContext, BotState, ConversationState, UserState, ActivityTypes, BotStateSet, TurnContextStateCollection, Activity } from 'botbuilder-core';
import { DialogContext, DialogState } from './dialogContext';
import { DialogTurnResult, Dialog, DialogTurnStatus } from './dialog';
import { Configurable } from './configurable';
import { DialogSet } from './dialogSet';
import { DialogStateManagerConfiguration, DialogStateManager } from './memory';
import { DialogEvents } from './dialogEvents';
import { DialogTurnStateConstants } from './dialogTurnStateConstants';
import { isSkillClaim } from './prompts/skillsHelpers';
import { isFromParentToSkill, getActiveDialogContext, shouldSendEndOfConversationToParent } from './dialogHelper';

const LAST_ACCESS = '_lastAccess';
const CONVERSATION_STATE = 'ConversationState';
Expand Down Expand Up @@ -51,64 +53,74 @@ export interface DialogManagerConfiguration {
}

export class DialogManager extends Configurable {
private dialogSet: DialogSet = new DialogSet();
private rootDialogId: string;
private dialogStateProperty: string;
private _rootDialogId: string;
private readonly _dialogStateProperty: string;
chon219 marked this conversation as resolved.
Show resolved Hide resolved
private readonly _initialTurnState: TurnContextStateCollection = new TurnContextStateCollection();

public constructor(rootDialog?: Dialog, dialogStateProperty?: string) {
super();
if (rootDialog) { this.rootDialog = rootDialog; }
this.dialogStateProperty = dialogStateProperty || 'DialogStateProperty';
this._dialogStateProperty = dialogStateProperty || 'DialogStateProperty';
this._initialTurnState.set(DialogTurnStateConstants.dialogManager, this);
}

/**
* Bots persisted conversation state.
*/
public conversationState: ConversationState;

/**
* Optional. Bots persisted user state.
*/
public userState?: UserState;

chon219 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Values that will be copied to the `TurnContext.turnState` at the beginning of each turn.
*/
public get initialTurnState(): TurnContextStateCollection {
return this._initialTurnState;
}

/**
* Bots persisted conversation state.
*/
public conversationState: ConversationState;

/**
* Root dialog to start from [onTurn()](#onturn) method.
*/
public set rootDialog(dialog: Dialog) {
this.dialogSet.add(dialog);
this.rootDialogId = dialog.id;
public set rootDialog(value: Dialog) {
this.dialogs = new DialogSet();
if (value) {
this._rootDialogId = value.id;
this.dialogs.telemetryClient = value.telemetryClient;
this.dialogs.add(value);
} else {
this._rootDialogId = undefined;
}
chon219 marked this conversation as resolved.
Show resolved Hide resolved
}

public get rootDialog(): Dialog {
return this.rootDialogId ? this.dialogSet.find(this.rootDialogId) : undefined;
return this._rootDialogId ? this.dialogs.find(this._rootDialogId) : undefined;
}

/**
* Optional. Bots persisted user state.
* Global dialogs that you want to have be callable.
*/
public userState?: UserState;
public dialogs: DialogSet = new DialogSet();

/**
* Optional. Number of milliseconds to expire the bots conversation state after.
* Optional. Path resolvers and memory scopes used for conversations with the bot.
*/
public expireAfter?: number;
public stateConfiguration?: DialogStateManagerConfiguration;

/**
* Optional. Path resolvers and memory scopes used for conversations with the bot.
* Optional. Number of milliseconds to expire the bots conversation state after.
*/
public stateConfiguration?: DialogStateManagerConfiguration;
public expireAfter?: number;

public configure(config: Partial<DialogManagerConfiguration>): this {
return super.configure(config);
}

public async onTurn(context: TurnContext): Promise<DialogManagerResult> {
// Ensure properly configured
if (!this.rootDialogId) { throw new Error(`DialogManager.onTurn: the bot's 'rootDialog' has not been configured.`); }
if (!this._rootDialogId) { throw new Error(`DialogManager.onTurn: the bot's 'rootDialog' has not been configured.`); }

// Copy initial turn state to context
this.initialTurnState.forEach((value, key) => {
Expand Down Expand Up @@ -153,31 +165,37 @@ export class DialogManager extends Configurable {
await lastAccessProperty.set(context, lastAccess.toISOString());

// get dialog stack
const dialogsProperty = this.conversationState.createProperty(this.dialogStateProperty);
const dialogsProperty = this.conversationState.createProperty(this._dialogStateProperty);
const dialogState: DialogState = await dialogsProperty.get(context, {});

// Create DialogContext
const dc = new DialogContext(this.dialogSet, context, dialogState);
const dc = new DialogContext(this.dialogs, context, dialogState);

// Configure dialog state manager and load scopes
const dialogStateManager = new DialogStateManager(dc, this.stateConfiguration);
await dialogStateManager.loadAllScopes();

let turnResult: DialogTurnResult;
while (true) {
/**
* Loop as long as we are getting valid onError handled we should continue executing the actions for the turn.
* NOTE: We loop around this block because each pass through we either complete the turn and break out of the loop
* or we have had an exception AND there was an onError action which captured the error. We need to continue the
* turn based on the actions the onError handler introduced.
*/
let endOfTurn = false;
while (!endOfTurn) {
try {
if (dc.activeDialog) {
// Continue dialog execution
// - This will apply any queued up interruptions and execute the current/next step(s).
turnResult = await dc.continueDialog();
if (turnResult.status == DialogTurnStatus.empty) {
// Begin root dialog
turnResult = await dc.beginDialog(this.rootDialogId);
}
const claimIdentity = context.turnState.get(context.adapter.BotIdentityKey);
if (claimIdentity && isSkillClaim(claimIdentity.claims)) {
// The bot is running as a skill.
turnResult = await this.handleSkillOnTurn(dc);
} else {
turnResult = await dc.beginDialog(this.rootDialogId);
// The bot is running as a root bot.
turnResult = await this.handleBotOnTurn(dc);
}
break;

// turn successfully completed, break the loop
endOfTurn = true;
} catch (err) {
const handled = await dc.emitEvent(DialogEvents.error, err, true, true);
if (!handled) {
Expand All @@ -192,20 +210,93 @@ export class DialogManager extends Configurable {
// Save BotState changes
await botStateSet.saveAllChanges(dc.context, false);

// Send trace of memory to emulator
let snapshotDc = dc;
while (snapshotDc.child) {
snapshotDc = snapshotDc.child;
}
const snapshot: object = snapshotDc.state.getMemorySnapshot();
return { turnResult: turnResult };
}

// Helper to send a trace activity with a memory snapshot of the active dialog DC.
private async sendStateSnapshotTrace(dc: DialogContext, traceLabel: string): Promise<void> {
// send trace of memory
const snapshot: object = getActiveDialogContext(dc).state.getMemorySnapshot();
await dc.context.sendActivity({
type: ActivityTypes.Trace,
name: 'BotState',
valueType: 'https://www.botframework.com/schemas/botState',
value: snapshot,
label: 'Bot State'
label: traceLabel
});
}

return { turnResult: turnResult };
private async handleSkillOnTurn(dc: DialogContext): Promise<DialogTurnResult> {
// The bot is running as a skill.
const turnContext = dc.context;

// Process remote cancellation.
if (turnContext.activity.type === ActivityTypes.EndOfConversation && dc.activeDialog && isFromParentToSkill(turnContext)) {
// Handle remote cancellation request from parent.
const activeDialogContext = getActiveDialogContext(dc);

const remoteCancelText = 'Skill was canceled through an EndOfConversation activity from the parent.';
await turnContext.sendTraceActivity(`DialogManager.onTurn()`, undefined, undefined, remoteCancelText);

// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
return await activeDialogContext.cancelAllDialogs(true);
}

// Handle reprompt
// Process a reprompt event sent from the parent.
if (turnContext.activity.type === ActivityTypes.Event && turnContext.activity.name == DialogEvents.repromptDialog) {
if (!dc.activeDialog) {
return { status: DialogTurnStatus.empty };
}

await dc.repromptDialog();
return { status: DialogTurnStatus.waiting };
}

// Continue execution
// - This will apply any queued up interruptions and execute the current/next step(s).
var turnResult = await dc.continueDialog();
if (turnResult.status == DialogTurnStatus.empty) {
// restart root dialog
var startMessageText = `Starting ${ this._rootDialogId }.`;
await turnContext.sendTraceActivity('DialogManager.onTurn()', undefined, undefined, startMessageText);
turnResult = await dc.beginDialog(this._rootDialogId);
}

await this.sendStateSnapshotTrace(dc, 'Skill State');

if (shouldSendEndOfConversationToParent(turnContext, turnResult)) {
var endMessageText = `Dialog ${ this._rootDialogId } has **completed**. Sending EndOfConversation.`;
await turnContext.sendTraceActivity('DialogManager.onTurn()', turnResult.result, undefined, endMessageText);

// Send End of conversation at the end.
const activity: Partial<Activity> = { type: ActivityTypes.EndOfConversation, value: turnResult.result, locale: turnContext.activity.locale };
await turnContext.sendActivity(activity);
}

return turnResult;
}

private async handleBotOnTurn(dc: DialogContext): Promise<DialogTurnResult> {
let turnResult: DialogTurnResult;

// the bot is running as a root bot.
if (!dc.activeDialog) {
// start root dialog
turnResult = await dc.beginDialog(this._rootDialogId);
} else {
// Continue execution
// - This will apply any queued up interruptions and execute the current/next step(s).
turnResult = await dc.continueDialog();

if (turnResult.status == DialogTurnStatus.empty) {
// restart root dialog
turnResult = await dc.beginDialog(this._rootDialogId);
}
}

await this.sendStateSnapshotTrace(dc, 'Bot State');
chon219 marked this conversation as resolved.
Show resolved Hide resolved

return turnResult;
}
}
Loading