diff --git a/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts b/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts index f0c17cffc0..4124f560b4 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/trigger.ts @@ -80,29 +80,24 @@ const getNewDialogWithTrigger = async ( ]; await createLgTemplates({ id: lgFile.id, templates: lgTemplates, projectId }); } else if (formData.$kind === onChooseIntentKey) { - const designerId1 = getDesignerIdFromDialogPath(newDialog, `content.triggers[${index}].actions[4].prompt`); + const designerId1 = getDesignerIdFromDialogPath(newDialog, `content.triggers[${index}].actions[2].prompt`); const designerId2 = getDesignerIdFromDialogPath( newDialog, - `content.triggers[${index}].actions[5].elseActions[0].activity` + `content.triggers[${index}].actions[3].elseActions[0].activity` ); - const lgTemplates1: LgTemplate[] = [ - LgTemplateSamples.TextInputPromptForOnChooseIntent(designerId1) as LgTemplate, + const lgTemplates: LgTemplate[] = [ + LgTemplateSamples.textInputPromptForOnChooseIntent(designerId1) as LgTemplate, + LgTemplateSamples.onChooseIntentAdaptiveCard(designerId1) as LgTemplate, + LgTemplateSamples.whichOneDidYouMean(designerId1) as LgTemplate, + LgTemplateSamples.pickOne(designerId1) as LgTemplate, + LgTemplateSamples.getAnswerReadBack(designerId1) as LgTemplate, + LgTemplateSamples.getIntentReadBack(designerId1) as LgTemplate, + LgTemplateSamples.generateChoices(designerId1) as LgTemplate, + LgTemplateSamples.choice(designerId1) as LgTemplate, LgTemplateSamples.SendActivityForOnChooseIntent(designerId2) as LgTemplate, ]; - let lgTemplates2: LgTemplate[] = [ - LgTemplateSamples.adaptiveCardJson as LgTemplate, - LgTemplateSamples.whichOneDidYouMean as LgTemplate, - LgTemplateSamples.pickOne as LgTemplate, - LgTemplateSamples.getAnswerReadBack as LgTemplate, - LgTemplateSamples.getIntentReadBack as LgTemplate, - ]; - const commonlgFile = lgFiles.find(({ id }) => id === `common.${locale}`); - - lgTemplates2 = lgTemplates2.filter((t) => commonlgFile?.templates.findIndex((clft) => clft.name === t.name) === -1); - - await createLgTemplates({ id: `common.${locale}`, templates: lgTemplates2, projectId }); - await createLgTemplates({ id: lgFile.id, templates: lgTemplates1, projectId }); + await createLgTemplates({ id: lgFile.id, templates: lgTemplates, projectId }); } return { id: newDialog.id, @@ -165,7 +160,7 @@ export const triggerDispatcher = () => { luFile && deleteActions( actions, - (templateNames: string[]) => removeLgTemplates({ id: dialogId, templateNames, projectId }), + (templateNames: string[]) => removeLgTemplates({ id: `${dialogId}.${locale}`, templateNames, projectId }), (intentNames: string[]) => Promise.all(intentNames.map((intentName) => removeLuIntent({ id: luFile.id, intentName, projectId }))) ); diff --git a/Composer/packages/lib/indexers/src/utils/lgUtil.ts b/Composer/packages/lib/indexers/src/utils/lgUtil.ts index a20e12f5aa..14c7c181eb 100644 --- a/Composer/packages/lib/indexers/src/utils/lgUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/lgUtil.ts @@ -17,6 +17,7 @@ import { Range, LgFile, DiagnosticSeverity, + LgTemplateRef, } from '@bfc/shared'; import formatMessage from 'format-message'; import isEmpty from 'lodash/isEmpty'; @@ -238,12 +239,43 @@ export function removeTemplates( ): LgFile { const { id } = lgFile; let resource = getLgResource(lgFile, importResolver); - templateNames.forEach((templateName) => { + + const normalizedLgTemplates = templateNames + .map((x) => { + const lgTemplateRef = LgTemplateRef.parse(x); + return lgTemplateRef ? lgTemplateRef.name : x; + }) + .filter((x) => !!x); + + const generatedLgTemplateNames = getGeneratedLgTemplateNames(lgFile, normalizedLgTemplates); + + [...normalizedLgTemplates, ...generatedLgTemplateNames].forEach((templateName) => { resource = resource.deleteTemplate(templateName); }); return convertTemplatesToLgFile(id, resource.toString(), resource); } +/** + * This util function returns the names of all auto generated templates associated with the templates being removed. + * @param file Lg file that contains the templates. + * @param toBeRemovedLgTemplateNames Names of Lg templates that are being removed. + */ +const getGeneratedLgTemplateNames = (file: LgFile, toBeRemovedLgTemplateNames: string[]) => { + const generatedLgTemplateNames: string[] = []; + const lgTemplates = file.templates.filter((t) => toBeRemovedLgTemplateNames.includes(t.name) && !!t.properties); + for (const lgTemplate of lgTemplates) { + // Auto-generated templates in structured responses have the following pattern + // [name of the parent template]_text OR [name of the parent template]_speak OR [name of the parent template]_attachment_[random string] + const pattern = `${lgTemplate.name}_((text|speak)|(attachment_.+))$`; + // eslint-disable-next-line security/detect-non-literal-regexp + const regex = new RegExp(`^${pattern}`); + const generatedLgTemplates = file.templates.map((t) => t.name).filter((name) => regex.test(name)); + generatedLgTemplateNames.push(...generatedLgTemplates); + } + + return generatedLgTemplateNames; +}; + export function textFromTemplate(template: LgTemplate): string { const { name, parameters = [], body } = template; const textBuilder: string[] = []; diff --git a/Composer/packages/lib/shared/src/constant.ts b/Composer/packages/lib/shared/src/constant.ts index 75a8bae49b..8e8d53874d 100644 --- a/Composer/packages/lib/shared/src/constant.ts +++ b/Composer/packages/lib/shared/src/constant.ts @@ -349,60 +349,55 @@ export const QnALocales = [ 'vi-vn', ]; -const adaptiveCardJsonBody = - '-```\ -\n{\ -\n "$schema",\ -\n "version": "1.0",\ -\n "type": "AdaptiveCard",\ -\n "speak": "",\ -\n "body": [\ -\n {\ -\n "type": "TextBlock",\ -\n "text": "${whichOneDidYouMean()}",\ -\n "weight": "Bolder"\ -\n },\ -\n {\ -\n "type": "TextBlock",\ -\n "text": "${pickOne()}",\ -\n "separator": "true"\ -\n },\ -\n {\ -\n "type": "Input.ChoiceSet",\ -\n "placeholder": "Placeholder text",\ -\n "id": "userChosenIntent",\ -\n "choices": [\ -\n {\ -\n "title": "${getIntentReadBack()}",\ -\n "value": "luisResult"\ -\n },\ -\n {\ -\n "title": "${getAnswerReadBack()}",\ -\n "value": "qnaResult"\ -\n },\ -\n {\ -\n "title": "None of the above",\ -\n "value": "none"\ -\n }\ -\n ],\ -\n "style": "expanded",\ -\n "value": "luis"\ -\n },\ -\n {\ -\n "type": "ActionSet",\ -\n "actions": [\ -\n {\ -\n "type": "Action.Submit",\ -\n "title": "Submit",\ -\n "data": {\ -\n "intent": "chooseIntentCardResponse"\ -\n }\ -\n }\ -\n ]\ -\n }\ -\n ]\ -\n}\ -```'; +export const chooseIntentTemplatePrefix = 'ChooseIntent'; + +const adaptiveCardJsonBody = (designerId: string) => + `-\`\`\`{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "type": "AdaptiveCard", + "speak": "", + "body": [ + { + "type": "TextBlock", + "text": "\${${chooseIntentTemplatePrefix}_${designerId}_attachment_whichOneDidYouMean()}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "\${${chooseIntentTemplatePrefix}_${designerId}_attachment_pickOne()}", + "separator": "true" + }, + { + "type": "Input.ChoiceSet", + "placeholder": "Placeholder text", + "id": "userChosenIntent", + "choices": [ + \${${chooseIntentTemplatePrefix}_${designerId}_attachment_generateChoices()}, + { + "title": "None of the above", + "value": "none" + } + ], + "style": "expanded", + "value": "luis" + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.Submit", + "title": "Submit", + "data": { + "intent": "chooseIntentCardResponse" + } + } + ] + } + ] +} +\`\`\` +`; const whichOneDidYouMeanBody = `\ - I'm not sure which one you mean. @@ -414,50 +409,86 @@ const pickOne = `\ - Can you help clarify by choosing one ? `; -const getIntentReadBack = `\ -- SWITCH : \${toLower(dialog.luisResult.intent)} +const getIntentReadBack = (designerId: string) => `\ +- SWITCH : \${intent} +- CASE: \${'QnAMatch'} + - \${${chooseIntentTemplatePrefix}_${designerId}_attachment_getAnswerReadBack()} - CASE : \${'GetUserProfile'} - Start filling in your profile(GetUserProfile intent) - DEFAULT : - - \${dialog.luisResult.intent} + - \${intent} +`; + +const generateChoices = (designerId: string) => `\ +- \${join(foreach(indicesAndValues(candidates), c, ${chooseIntentTemplatePrefix}_${designerId}_attachment_choice(c.value.intent, c.index)), ',')} +`; + +const choice = (designerId: string) => `\ +- { "title": "\${${chooseIntentTemplatePrefix}_${designerId}_attachment_getIntentReadBack(title)}", "value": "\${value}" } `; const getAnswerReadBack = `- See an answer from the Knowledge Base `; export const LgTemplateSamples = { - ['adaptiveCardJson']: { - name: 'AdaptiveCardJson', - body: adaptiveCardJsonBody, + onChooseIntentAdaptiveCard: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_card`, + body: adaptiveCardJsonBody(designerId), + parameters: ['candidates'], + }; + }, + whichOneDidYouMean: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_whichOneDidYouMean`, + body: whichOneDidYouMeanBody, + }; + }, + pickOne: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_pickOne`, + body: pickOne, + }; }, - ['whichOneDidYouMean']: { - name: `whichOneDidYouMean`, - body: whichOneDidYouMeanBody, + getAnswerReadBack: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_getAnswerReadBack`, + body: getAnswerReadBack, + }; }, - ['pickOne']: { - name: 'pickOne', - body: pickOne, + getIntentReadBack: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_getIntentReadBack`, + parameters: ['intent'], + body: getIntentReadBack(designerId), + }; }, - ['getAnswerReadBack']: { - name: 'getAnswerReadBack', - body: getAnswerReadBack, + generateChoices: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_generateChoices`, + parameters: ['candidates'], + body: generateChoices(designerId), + }; }, - ['getIntentReadBack']: { - name: 'getIntentReadBack', - body: getIntentReadBack, + choice: (designerId: string) => { + return { + name: `${chooseIntentTemplatePrefix}_${designerId}_attachment_choice`, + parameters: ['title', 'value'], + body: choice(designerId), + }; }, - TextInputPromptForOnChooseIntent: (designerId) => { + textInputPromptForOnChooseIntent: (designerId) => { return { - name: `TextInput_Prompt_${designerId}`, + name: `${chooseIntentTemplatePrefix}_${designerId}`, body: `[Activity - Attachments = \${json(AdaptiveCardJson())} + Attachments = \${json(${chooseIntentTemplatePrefix}_${designerId}_attachment_card(dialog.candidates))} ] `, }; }, SendActivityForOnChooseIntent: (designerId) => { return { - name: `SendActivity_${designerId}`, + name: `${chooseIntentTemplatePrefix}_SendActivity_${designerId}`, body: '- Sure, no worries.\n', }; }, diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index 380ead5fa6..2bcf623d44 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -11,6 +11,7 @@ import { deleteAdaptiveAction, deleteAdaptiveActionList } from './deleteUtils'; import { FieldProcessorAsync } from './copyUtils/ExternalApi'; import { generateDesignerId } from './generateUniqueId'; import { conceptLabels } from './labelMap'; +import { chooseIntentTemplatePrefix } from './constant'; interface DesignerAttributes { name: string; @@ -187,86 +188,69 @@ const initialDialogShape = () => ({ }, assignments: [ { - value: '=turn.recognized.candidates[0]', - property: 'dialog.luisResult', + property: 'turn.minThreshold', + value: 0.5, }, { - property: 'dialog.qnaResult', - value: '=turn.recognized.candidates[1]', + property: 'turn.maxChoices', + value: 3, }, - ], - }, - { - $kind: SDKKinds.IfCondition, - $designer: { - id: generateDesignerId(), - }, - condition: 'dialog.luisResult.score >= 0.9 && dialog.qnaResult.score <= 0.5', - actions: [ { - $kind: SDKKinds.EmitEvent, - $designer: { - id: generateDesignerId(), - }, - eventName: 'recognizedIntent', - eventValue: '=dialog.luisResult.result', + property: 'conversation.lastAmbiguousUtterance', + value: '=turn.activity.text', }, { - $kind: SDKKinds.BreakLoop, - $designer: { - id: generateDesignerId(), - }, + property: 'dialog.candidates', + value: + '=take(sortByDescending(where(flatten(select(turn.recognized.candidates, x, if (x.intent=="ChooseIntent", x.result.candidates, x))), c, not(startsWith(c.intent, "DeferToRecognizer_QnA")) && c.score > turn.minThreshold), \'score\'), turn.maxChoices)', }, ], }, { - $kind: SDKKinds.IfCondition, - $designer: { - id: generateDesignerId(), - }, - condition: 'dialog.luisResult.score <= 0.5 && dialog.qnaResult.score >= 0.9', - actions: [ - { - $kind: SDKKinds.EmitEvent, - $designer: { - id: generateDesignerId(), - }, - eventName: 'recognizedIntent', - eventValue: '=dialog.qnaResult.result', - }, - { - $kind: SDKKinds.BreakLoop, - $designer: { - id: generateDesignerId(), - }, - }, - ], - }, - { - $kind: SDKKinds.IfCondition, + $kind: SDKKinds.SwitchCondition, $designer: { id: generateDesignerId(), }, - condition: 'dialog.qnaResult.score <= 0.05', - actions: [ + condition: '=count(dialog.candidates)', + cases: [ { - $kind: SDKKinds.EmitEvent, - $designer: { - id: generateDesignerId(), - }, - eventName: 'recognizedIntent', - eventValue: '=dialog.luisResult.result', + value: '0', + actions: [ + { + $kind: SDKKinds.EmitEvent, + $designer: { + id: generateDesignerId(), + }, + eventName: 'unknownIntent', + }, + { + $kind: SDKKinds.EndDialog, + $designer: { + id: generateDesignerId(), + }, + }, + ], }, { - $kind: SDKKinds.BreakLoop, - $designer: { - id: generateDesignerId(), - }, + value: '1', + actions: [ + { + $kind: SDKKinds.EmitEvent, + $designer: { + id: generateDesignerId(), + }, + eventName: 'recognizedIntent', + eventValue: '=first(dialog.candidates).result', + }, + { + $kind: SDKKinds.EndDialog, + $designer: { + id: generateDesignerId(), + }, + }, + ], }, ], - top: 3, - cardNoMatchResponse: 'Thanks for the feedback.', - cardNoMatchText: 'None of the above.', }, { $kind: SDKKinds.TextInput, @@ -276,7 +260,7 @@ const initialDialogShape = () => ({ maxTurnCount: 3, alwaysPrompt: true, allowInterruptions: false, - prompt: `\${TextInput_Prompt_${generateDesignerId()}()}`, + prompt: `\${${chooseIntentTemplatePrefix}_${generateDesignerId()}()}`, property: 'turn.intentChoice', value: '=@userChosenIntent', top: 3, @@ -302,7 +286,7 @@ const initialDialogShape = () => ({ id: generateDesignerId(), }, eventName: 'recognizedIntent', - eventValue: '=dialog[turn.intentChoice].result', + eventValue: '=dialog.candidates[int(turn.intentChoice)].result', }, ], elseActions: [ @@ -311,7 +295,7 @@ const initialDialogShape = () => ({ $designer: { id: generateDesignerId(), }, - activity: `\${SendActivity_${generateDesignerId()}()}`, + activity: `\${${chooseIntentTemplatePrefix}_SendActivity_${generateDesignerId()}()}`, }, ], top: 3,