diff --git a/libraries/botbuilder-core/src/skills/skillConversationIdFactory.ts b/libraries/botbuilder-core/src/skills/skillConversationIdFactory.ts index 463b1c6790..6b6fd29e24 100644 --- a/libraries/botbuilder-core/src/skills/skillConversationIdFactory.ts +++ b/libraries/botbuilder-core/src/skills/skillConversationIdFactory.ts @@ -7,6 +7,7 @@ import { SkillConversationIdFactoryOptions } from './skillConversationIdFactoryO import { SkillConversationReference } from './skillConversationReference'; import { Storage } from '../storage'; import { TurnContext } from '../turnContext'; +import { v4 as uuid } from 'uuid'; export class SkillConversationIdFactory extends SkillConversationIdFactoryBase { constructor(private readonly storage: Storage) { @@ -16,12 +17,7 @@ export class SkillConversationIdFactory extends SkillConversationIdFactoryBase { public async createSkillConversationIdWithOptions(options: SkillConversationIdFactoryOptions): Promise { const conversationReference = TurnContext.getConversationReference(options.activity); - const skillConversationId = [ - options.fromBotId, - options.botFrameworkSkill.appId, - conversationReference.conversation?.id, - conversationReference.channelId, - ].join('-'); + const skillConversationId = uuid(); const skillConversationReference: SkillConversationReference = { conversationReference: conversationReference as ConversationReference, diff --git a/libraries/botbuilder-dialogs-adaptive-testing/package.json b/libraries/botbuilder-dialogs-adaptive-testing/package.json index ea77ffcc82..fee6d3332d 100644 --- a/libraries/botbuilder-dialogs-adaptive-testing/package.json +++ b/libraries/botbuilder-dialogs-adaptive-testing/package.json @@ -8,7 +8,8 @@ "clean": "rimraf _ts3.4 lib tsconfig.tsbuildinfo", "lint": "eslint . --ext .js,.ts", "postbuild": "downlevel-dts lib _ts3.4/lib --checksum", - "test": "yarn build && mocha tests/*.test.js", + "test": "npm-run-all build test:mocha", + "test:mocha": "mocha tests/*.test.js", "test:compat": "api-extractor run --verbose" }, "main": "./lib/index.js", diff --git a/libraries/botbuilder-dialogs-adaptive-testing/tests/adaptiveDialog.test.js b/libraries/botbuilder-dialogs-adaptive-testing/tests/adaptiveDialog.test.js index 576afc2f37..fe84fe808b 100644 --- a/libraries/botbuilder-dialogs-adaptive-testing/tests/adaptiveDialog.test.js +++ b/libraries/botbuilder-dialogs-adaptive-testing/tests/adaptiveDialog.test.js @@ -7,147 +7,151 @@ describe('AdaptiveDialogTests', function () { resourceExplorer = makeResourceExplorer('AdaptiveDialogTests'); }); - it('ActivityAndIntentEvents', async () => { + it('ActivityAndIntentEvents', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ActivityAndIntentEvents'); }); - it('ActivityEvents', async () => { + it('ActivityEvents', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ActivityEvents'); }); - it('AdaptiveCardSubmit', async () => { + it('AdaptiveCardSubmit', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AdaptiveCardSubmit'); }); - it('AllowInterruption', async () => { + it('AllowInterruption', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AllowInterruption'); }); - it('AllowInterruptionAlwaysWithFailedValidation', async () => { + it('AllowInterruptionAlwaysWithFailedValidation', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AllowInterruptionAlwaysWithFailedValidation'); }); - it('AllowInterruptionNever', async () => { + it('AllowInterruptionNever', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AllowInterruptionNever'); }); - it('AllowInterruptionNeverWithInvalidInput', async () => { + it('AllowInterruptionNeverWithInvalidInput', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AllowInterruptionNeverWithInvalidInput'); }); - it('AllowInterruptionNeverWithMaxCount', async () => { + it('AllowInterruptionNeverWithMaxCount', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AllowInterruptionNeverWithMaxCount'); }); - it('AllowInterruptionNeverWithUnrecognizedInput', async () => { + it('AllowInterruptionNeverWithUnrecognizedInput', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_AllowInterruptionNeverWithUnrecognizedInput'); }); - it('BeginDialog', async () => { + it('BeginDialog', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_BeginDialog'); }); - it('BindingCaptureValueWithinSameAdaptive', async () => { + it('BindingCaptureValueWithinSameAdaptive', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_BindingCaptureValueWithinSameAdaptive'); }); - it('BindingOptionsAcrossAdaptiveDialogs', async () => { + it('BindingOptionsAcrossAdaptiveDialogs', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_BindingOptionsAcrossAdaptiveDialogs'); }); - it('BindingReferValueInLaterAction', async () => { + it('BindingReferValueInLaterAction', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_BindingReferValueInLaterAction'); }); - it('BindingReferValueInNestedAction', async () => { + it('BindingReferValueInNestedAction', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_BindingReferValueInNestedAction'); }); - it('ConditionallyAllowInterruptions', async () => { + it('ConditionallyAllowInterruptions', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ConditionallyAllowInterruptions'); }); - it('DoActions', async () => { + it('DoActions', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_DoActions'); }); - it('EditArray', async () => { + it('EditArray', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_EditArray'); }); - it('EmitEventActivityReceived', async () => { + it('EmitEventActivityReceived', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_EmitEventActivityReceived'); }); - it('EndTurn', async () => { + it('EndTurn', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_EndTurn'); }); - it('IfProperty', async () => { + it('IfProperty', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_IfProperty'); }); - it('NestedInlineSequences', async () => { + it('NestedInlineSequences', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_NestedInlineSequences'); }); - it('NestedMemoryAccess', async () => { + it('NestedMemoryAccess', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_NestedMemoryAccess'); }); - it('NestedRecognizers', async () => { + it('NestedRecognizers', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_NestedRecognizers'); }); - it('PropertySetInInterruption', async () => { + it('PropertySetInInterruption', async function () { + await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ParentBotInterruption'); + }); + + it('PropertySetInInterruption', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_PropertySetInInterruption'); }); - it('ReplacePlan', async () => { + it('ReplacePlan', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ReplacePlan'); }); - it('ReProcessInputProperty', async () => { + it('ReProcessInputProperty', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ReProcessInputProperty'); }); - it('ReProcessInputPropertyValidOnlyOnce', async () => { + it('ReProcessInputPropertyValidOnlyOnce', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_ReProcessInputPropertyValidOnlyOnce'); }); - it('StringLiteralInExpression', async () => { + it('StringLiteralInExpression', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_StringLiteralInExpression'); }); - it('TextInput', async () => { + it('TextInput', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_TextInput'); }); - it('TextInputDefaultValueResponse', async () => { + it('TextInputDefaultValueResponse', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_TextInputDefaultValueResponse'); }); - it('TextInputNoMaxTurnCount', async () => { + it('TextInputNoMaxTurnCount', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_TextInputNoMaxTurnCount'); }); - it('TopLevelFallback', async () => { + it('TopLevelFallback', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_TopLevelFallback'); }); - it('TopLevelFallbackMultipleActivities', async () => { + it('TopLevelFallbackMultipleActivities', async function () { await TestUtils.runTestScript(resourceExplorer, 'AdaptiveDialog_TopLevelFallbackMultipleActivities'); }); - it('TestBindingTwoWayAcrossAdaptiveDialogs', async () => { + it('TestBindingTwoWayAcrossAdaptiveDialogs', async function () { await TestUtils.runTestScript(resourceExplorer, 'TestBindingTwoWayAcrossAdaptiveDialogs'); }); - it('TestBindingTwoWayAcrossAdaptiveDialogsDefaultResultProperty', async () => { + it('TestBindingTwoWayAcrossAdaptiveDialogsDefaultResultProperty', async function () { await TestUtils.runTestScript(resourceExplorer, 'TestBindingTwoWayAcrossAdaptiveDialogsDefaultResultProperty'); }); - it('TestForeachWithPrompt', async () => { + it('TestForeachWithPrompt', async function () { await TestUtils.runTestScript(resourceExplorer, 'TestForeachWithPrompt'); }); }); diff --git a/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/AdaptiveDialogTests/AdaptiveDialog_ParentBotInterruption.test.dialog b/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/AdaptiveDialogTests/AdaptiveDialog_ParentBotInterruption.test.dialog new file mode 100644 index 0000000000..128929262d --- /dev/null +++ b/libraries/botbuilder-dialogs-adaptive-testing/tests/resources/AdaptiveDialogTests/AdaptiveDialog_ParentBotInterruption.test.dialog @@ -0,0 +1,148 @@ +{ + "$schema": "../../../tests.schema", + "$kind": "Microsoft.Test.Script", + "dialog": { + "$kind": "Microsoft.AdaptiveDialog", + "id": "AdaptiveDialog", + "recognizer": { + "$kind": "Microsoft.RegexRecognizer", + "intents": [ + { + "intent": "dialoga", + "pattern": "(?i)dialoga" + }, + { + "intent": "dialogb", + "pattern": "(?i)dialogb" + } + ] + }, + "triggers": [ + { + "$kind": "Microsoft.OnIntent", + "intent": "dialoga", + "actions": [ + { + "$kind": "Microsoft.BeginDialog", + "dialog": { + "id": "dialogA", + "$kind": "Microsoft.AdaptiveDialog", + "recognizer": { + "$kind": "Microsoft.RegexRecognizer" + }, + "triggers": [ + { + "$kind": "Microsoft.OnBeginDialog", + "actions": [ + { + "$kind": "Microsoft.TextInput", + "allowInterruptions": true, + "property": "dialog.value", + "prompt": "DialogA Prompt" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "DialogA: ${dialog.value}" + } + ] + } + ] + } + } + ] + }, + { + "$kind": "Microsoft.OnIntent", + "intent": "dialogb", + "actions": [ + { + "$kind": "Microsoft.BeginDialog", + "dialog": { + "$kind": "Microsoft.AdaptiveDialog", + "id": "dialogB", + "recognizer": { + "$kind": "Microsoft.RegexRecognizer" + }, + "triggers": [ + { + "$kind": "Microsoft.OnBeginDialog", + "actions": [ + { + "$kind": "Microsoft.TextInput", + "allowInterruptions": true, + "property": "dialog.value", + "prompt": "DialogB Prompt" + }, + { + "$kind": "Microsoft.SendActivity", + "activity": "DialogB: ${dialog.value}" + } + ] + } + ] + } + } + ] + }, + { + "$kind": "Microsoft.OnIntent", + "intent": "Santa", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "I love you santa." + } + ] + }, + { + "$kind": "Microsoft.OnUnknownIntent", + "actions": [ + { + "$kind": "Microsoft.SendActivity", + "activity": "In None..." + } + ] + } + ], + "autoEndDialog": false, + "defaultResultProperty": "dialog.result" + }, + "script": [ + { + "$kind": "Microsoft.Test.UserSays", + "text": "dialoga" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "DialogA Prompt" + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "dialogb" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "DialogB Prompt" + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "testb" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "DialogB: testb" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "DialogA Prompt" + }, + { + "$kind": "Microsoft.Test.UserSays", + "text": "testa" + }, + { + "$kind": "Microsoft.Test.AssertReply", + "text": "DialogA: testa" + } + ] +} diff --git a/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts index 5d418c6fdb..ac7c200517 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/adaptiveDialog.ts @@ -687,16 +687,27 @@ export class AdaptiveDialog extends DialogContainer im // from the stack and we want to detect this so we can stop processing actions. const instanceId = this.getUniqueInstanceId(actionContext); + // Initialize local interruption detection + // Any steps containing a dialog stack after the first step indicates the action was interrupted. + // We want to force a re-prompt and then end the turn when we encounter an interrupted step. + let interrupted = false; + // Create context for active action let actionDC = this.createChildContext(actionContext); while (actionDC) { - // Continue current action - let result = await actionDC.continueDialog(); - - // Start action if not continued - if (result.status == DialogTurnStatus.empty && this.getUniqueInstanceId(actionContext) == instanceId) { + let result: DialogTurnResult; + if (actionDC.stack.length === 0) { + // Start step const nextAction = actionContext.actions[0]; result = await actionDC.beginDialog(nextAction.dialogId, nextAction.options); + } else { + // Set interrupted flag only if it is undefined + if (interrupted && actionDC.state.getValue(TurnPath.interrupted) === undefined) { + actionDC.state.setValue(TurnPath.interrupted, true); + } + + // Continue step execution + result = await actionDC.continueDialog(); } // Is the step waiting for input or were we cancelled? @@ -734,6 +745,7 @@ export class AdaptiveDialog extends DialogContainer im // Apply any locale changes and fetch next action await actionContext.applyChanges(); actionDC = this.createChildContext(actionContext); + interrupted = true; } return await this.onEndOfActions(actionContext); diff --git a/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts b/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts index 018b168568..02ec4068b4 100644 --- a/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts +++ b/libraries/botbuilder-dialogs-adaptive/src/input/inputDialog.ts @@ -225,14 +225,15 @@ export abstract class InputDialog extends Dialog implements InputDialogConfigura * @returns A [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation. */ public async continueDialog(dc: DialogContext): Promise { - // Filter to only message activities const activity = dc.context.activity; - if (activity.type !== ActivityTypes.Message) { + + // Interrupted dialogs reprompt so we can ignore the incoming activity. + const interrupted = dc.state.getValue(TurnPath.interrupted, false); + if (!interrupted && activity.type !== ActivityTypes.Message) { return Dialog.EndOfTurn; } // Are we continuing after an interruption? - const interrupted = dc.state.getValue(TurnPath.interrupted, false); const turnCount = dc.state.getValue(InputDialog.TURN_COUNT_PROPERTY, 0); const state = await this.recognizeInput(dc, interrupted ? 0 : turnCount); if (state === InputState.valid) { diff --git a/libraries/botbuilder-dialogs/src/skillDialog.ts b/libraries/botbuilder-dialogs/src/skillDialog.ts index 153e36c7c0..b13328ff63 100644 --- a/libraries/botbuilder-dialogs/src/skillDialog.ts +++ b/libraries/botbuilder-dialogs/src/skillDialog.ts @@ -9,6 +9,7 @@ import { Activity, ActivityTypes, + Attachment, CardFactory, ConversationReference, DeliveryModes, @@ -16,18 +17,19 @@ import { ExtendedUserTokenProvider, OAuthCard, SkillConversationIdFactoryOptions, + StatusCodes, TokenExchangeInvokeRequest, - tokenExchangeOperationName, TokenResponse, TurnContext, - Attachment, - StatusCodes, + tokenExchangeOperationName, } from 'botbuilder-core'; + import { BeginSkillDialogOptions } from './beginSkillDialogOptions'; import { Dialog, DialogInstance, DialogReason, DialogTurnResult } from './dialog'; import { DialogContext } from './dialogContext'; import { DialogEvents } from './dialogEvents'; import { SkillDialogOptions } from './skillDialogOptions'; +import { TurnPath } from './memory/turnPath'; /** * A specialized Dialog that can wrap remote calls to a skill. @@ -107,13 +109,21 @@ export class SkillDialog extends Dialog> { * return value. */ public async continueDialog(dc: DialogContext): Promise { + // 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.interrupted)) { + // resume dialog execution + dc.state.setValue(TurnPath.interrupted, false); + return this.resumeDialog(dc, DialogReason.endCalled); + } + if (!this.onValidateActivity(dc.context.activity)) { return 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 === ActivityTypes.EndOfConversation) { - return await dc.endDialog(dc.context.activity.value); + return dc.endDialog(dc.context.activity.value); } // Create deep clone of the original activity to avoid altering it before forwarding it. @@ -126,7 +136,7 @@ export class SkillDialog extends Dialog> { // Just forward to the remote skill const eocActivity = await this.sendToSkill(dc.context, skillActivity, skillConversationId); if (eocActivity) { - return await dc.endDialog(eocActivity.value); + return dc.endDialog(eocActivity.value); } return Dialog.EndOfTurn;