diff --git a/Composer/packages/client/__tests__/utils/buildUtil.test.ts b/Composer/packages/client/__tests__/utils/buildUtil.test.ts new file mode 100644 index 0000000000..ce3e9fb67d --- /dev/null +++ b/Composer/packages/client/__tests__/utils/buildUtil.test.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DialogInfo, LuFile } from '@bfc/shared'; + +import { createCrossTrainConfig } from '../../src/utils/buildUtil'; + +describe('createCrossTrainConfig', () => { + it('should create crosstrain config', () => { + const dialogs = [ + { + id: 'main', + luFile: 'main', + isRoot: true, + intentTriggers: [ + { intent: 'dia1_trigger', dialogs: ['dia1'] }, + { intent: 'dia2_trigger', dialogs: ['dia2'] }, + { intent: 'dias_trigger', dialogs: ['dia5', 'dia6'] }, + { intent: 'no_dialog', dialogs: [] }, + { intent: '', dialogs: ['start_dialog_without_intent'] }, + ], + }, + { + id: 'dia1', + luFile: 'dia1', + intentTriggers: [ + { intent: 'dia3_trigger', dialogs: ['dia3'] }, + { intent: 'dia4_trigger', dialogs: ['dia4'] }, + ], + }, + { + id: 'dia2', + luFile: 'dia2', + intentTriggers: [], + }, + { + id: 'dia3', + luFile: 'dia3', + intentTriggers: [], + }, + { + id: 'dia4', + luFile: 'dia4', + intentTriggers: [], + }, + { + id: 'dia5', + luFile: 'dia5', + intentTriggers: [], + }, + { + id: 'dia6', + luFile: 'dia6', + intentTriggers: [], + }, + { + id: 'start_dialog_without_intent', + luFile: 'start_dialog_without_intent', + intentTriggers: [], + }, + { + id: 'dialog_without_lu', + intentTriggers: [], + }, + ]; + const luFiles = [ + { + id: 'main.en-us', + intents: [ + { Name: 'dia1_trigger' }, + { Name: 'dia2_trigger' }, + { Name: 'dias_trigger' }, + { Name: 'no_dialog' }, + { Name: 'dialog_without_lu' }, + ], + }, + { id: 'dia1.en-us', intents: [{ Name: 'dia3_trigger' }, { Name: 'dia4_trigger' }] }, + { id: 'dia2.en-us' }, + { id: 'dia3.en-us' }, + { id: 'dia5.en-us' }, + { id: 'dia6.en-us' }, + ]; + const config = createCrossTrainConfig(dialogs as DialogInfo[], luFiles as LuFile[]); + expect(config.rootIds.length).toEqual(1); + expect(config.rootIds[0]).toEqual('main.en-us.lu'); + expect(config.triggerRules['main.en-us.lu'].dia1_trigger).toEqual('dia1.en-us.lu'); + expect(config.triggerRules['main.en-us.lu'].no_dialog).toEqual(''); + expect(config.triggerRules['main.en-us.lu'].dia1_trigger).toEqual('dia1.en-us.lu'); + expect(config.triggerRules['main.en-us.lu'].dias_trigger.length).toBe(2); + expect(config.triggerRules['dia1.en-us.lu'].dia3_trigger).toEqual('dia3.en-us.lu'); + expect(config.triggerRules['dia1.en-us.lu']['dia4.en-us.lu']).toBeUndefined(); + }); +}); diff --git a/Composer/packages/client/__tests__/utils/luUtil.test.ts b/Composer/packages/client/__tests__/utils/luUtil.test.ts index 58f27d7175..47301a5e16 100644 --- a/Composer/packages/client/__tests__/utils/luUtil.test.ts +++ b/Composer/packages/client/__tests__/utils/luUtil.test.ts @@ -3,7 +3,7 @@ import { LuFile, DialogInfo, Diagnostic, DiagnosticSeverity } from '@bfc/shared'; -import { getReferredLuFiles, createCrossTrainConfig, checkLuisBuild } from '../../src/utils/luUtil'; +import { getReferredLuFiles, checkLuisBuild } from '../../src/utils/luUtil'; describe('getReferredLuFiles', () => { it('returns referred luFiles from dialog', () => { @@ -13,91 +13,6 @@ describe('getReferredLuFiles', () => { expect(referred.length).toEqual(1); expect(referred[0].id).toEqual('a.en-us'); }); - - it('should create crosstrain config', () => { - const dialogs = [ - { - id: 'main', - luFile: 'main', - isRoot: true, - intentTriggers: [ - { intent: 'dia1_trigger', dialogs: ['dia1'] }, - { intent: 'dia2_trigger', dialogs: ['dia2'] }, - { intent: 'dias_trigger', dialogs: ['dia5', 'dia6'] }, - { intent: 'no_dialog', dialogs: [] }, - { intent: '', dialogs: ['start_dialog_without_intent'] }, - ], - }, - { - id: 'dia1', - luFile: 'dia1', - intentTriggers: [ - { intent: 'dia3_trigger', dialogs: ['dia3'] }, - { intent: 'dia4_trigger', dialogs: ['dia4'] }, - ], - }, - { - id: 'dia2', - luFile: 'dia2', - intentTriggers: [], - }, - { - id: 'dia3', - luFile: 'dia3', - intentTriggers: [], - }, - { - id: 'dia4', - luFile: 'dia4', - intentTriggers: [], - }, - { - id: 'dia5', - luFile: 'dia5', - intentTriggers: [], - }, - { - id: 'dia6', - luFile: 'dia6', - intentTriggers: [], - }, - { - id: 'start_dialog_without_intent', - luFile: 'start_dialog_without_intent', - intentTriggers: [], - }, - { - id: 'dialog_without_lu', - intentTriggers: [], - }, - ]; - const luFiles = [ - { - id: 'main.en-us', - intents: [ - { Name: 'dia1_trigger' }, - { Name: 'dia2_trigger' }, - { Name: 'dias_trigger' }, - { Name: 'no_dialog' }, - { Name: 'dialog_without_lu' }, - ], - }, - { id: 'dia1.en-us', intents: [{ Name: 'dia3_trigger' }, { Name: 'dia4_trigger' }] }, - { id: 'dia2.en-us' }, - { id: 'dia3.en-us' }, - { id: 'dia5.en-us' }, - { id: 'dia6.en-us' }, - ]; - const config = createCrossTrainConfig(dialogs as DialogInfo[], luFiles as LuFile[]); - expect(config.rootIds.length).toEqual(1); - expect(config.rootIds[0]).toEqual('main.en-us.lu'); - expect(config.triggerRules['main.en-us.lu'].dia1_trigger).toEqual('dia1.en-us.lu'); - expect(config.triggerRules['main.en-us.lu'].no_dialog).toEqual(''); - expect(config.triggerRules['main.en-us.lu'].dia1_trigger).toEqual('dia1.en-us.lu'); - expect(config.triggerRules['main.en-us.lu'].dias_trigger.length).toBe(2); - expect(config.triggerRules['dia1.en-us.lu'].dia3_trigger).toEqual('dia3.en-us.lu'); - expect(config.triggerRules['dia1.en-us.lu']['dia4.en-us.lu']).toBeUndefined(); - }); }); it('check the lu files before publish', () => { diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx index 0c7f4ed829..1866dd6d0f 100644 --- a/Composer/packages/client/src/components/TestController/TestController.tsx +++ b/Composer/packages/client/src/components/TestController/TestController.tsx @@ -22,13 +22,12 @@ import { botLoadErrorState, } from '../../recoilModel'; import settingsStorage from '../../utils/dialogSettingStorage'; -import { QnaConfig, BotStatus, LuisConfig } from '../../constants'; +import { BotStatus } from '../../constants'; import { isAbsHosted } from '../../utils/envUtil'; import useNotifications from '../../pages/notifications/useNotifications'; import { navigateTo, openInEmulator } from '../../utils/navigation'; -import { getReferredQnaFiles } from '../../utils/qnaUtil'; -import { getReferredLuFiles } from './../../utils/luUtil'; +import { isBuildConfigComplete, needsBuild } from './../../utils/buildUtil'; import { PublishDialog } from './publishDialog'; import { ErrorCallout } from './errorCallout'; import { EmulatorOpenButton } from './emulatorOpenButton'; @@ -153,15 +152,14 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { } } - async function handlePublish(config: IPublishConfig) { + async function handleBuild(config: IPublishConfig) { setBotStatus(BotStatus.publishing, projectId); dismissDialog(); const { luis, qna } = config; - const endpointKey = settings.qna?.endpointKey; await setSettings(projectId, { ...settings, luis: luis, - qna: Object.assign({}, settings.qna, qna, { endpointKey }), + qna: Object.assign({}, settings.qna, qna), }); await build(luis, qna, projectId); } @@ -175,48 +173,24 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { await publishToTarget(projectId, defaultPublishConfig, { comment: '' }, sensitiveSettings); } - function isConfigComplete(config) { - let complete = true; - if (getReferredLuFiles(luFiles, dialogs).length > 0) { - if (Object.values(LuisConfig).some((luisConfigKey) => config.luis[luisConfigKey] === '')) { - complete = false; - } - } - if (getReferredQnaFiles(qnaFiles, dialogs).length > 0) { - if (Object.values(QnaConfig).some((qnaConfigKey) => config.qna[qnaConfigKey] === '')) { - complete = false; - } - } - return complete; - } - - // return true if dialogs have one with default recognizer. - function needsPublish(dialogs) { - let isDefaultRecognizer = false; - if (dialogs.some((dialog) => typeof dialog.content.recognizer === 'string')) { - isDefaultRecognizer = true; - } - return isDefaultRecognizer; - } - async function handleStart() { dismissCallout(); const config = Object.assign( {}, { luis: settings.luis, - qna: { - subscriptionKey: settings.qna?.subscriptionKey, - qnaRegion: settings.qna?.qnaRegion, - endpointKey: settings.qna?.endpointKey, - }, + qna: settings.qna, } ); - if (!isAbsHosted() && needsPublish(dialogs)) { - if (botStatus === BotStatus.failed || botStatus === BotStatus.pending || !isConfigComplete(config)) { + if (!isAbsHosted() && needsBuild(dialogs)) { + if ( + botStatus === BotStatus.failed || + botStatus === BotStatus.pending || + !isBuildConfigComplete(config, dialogs, luFiles, qnaFiles) + ) { openDialog(); } else { - await handlePublish(config); + await handleBuild(config); } } else { await handleLoadBot(); @@ -278,7 +252,7 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { isOpen={modalOpen} projectId={projectId} onDismiss={dismissDialog} - onPublish={handlePublish} + onPublish={handleBuild} /> )} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts index 4bf6302f6f..81c92cca33 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts @@ -6,6 +6,7 @@ import { useRecoilCallback, CallbackInterface } from 'recoil'; import { ILuisConfig, IQnAConfig } from '@bfc/shared'; import * as luUtil from '../../utils/luUtil'; +import * as buildUtil from '../../utils/buildUtil'; import { Text, BotStatus } from '../../constants'; import httpClient from '../../utils/httpUtil'; import luFileStatusStorage from '../../utils/luFileStatusStorage'; @@ -48,7 +49,7 @@ export const builderDispatcher = () => { } try { //TODO crosstrain should add locale - const crossTrainConfig = luUtil.createCrossTrainConfig(dialogs, referredLuFiles); + const crossTrainConfig = buildUtil.createCrossTrainConfig(dialogs, referredLuFiles); await httpClient.post(`/projects/${projectId}/build`, { luisConfig, qnaConfig, diff --git a/Composer/packages/client/src/utils/buildUtil.ts b/Composer/packages/client/src/utils/buildUtil.ts new file mode 100644 index 0000000000..29958f92e0 --- /dev/null +++ b/Composer/packages/client/src/utils/buildUtil.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DialogInfo, LuFile } from '@bfc/shared'; +import keys from 'lodash/keys'; + +import { LuisConfig, QnaConfig } from '../constants'; + +import { getReferredLuFiles } from './luUtil'; +import { getReferredQnaFiles } from './qnaUtil'; +import { getBaseName, getExtension } from './fileUtil'; + +function createConfigId(fileId) { + return `${fileId}.lu`; +} + +function getLuFilesByDialogId(dialogId: string, luFiles: LuFile[]) { + return luFiles.filter((lu) => getBaseName(lu.id) === dialogId).map((lu) => createConfigId(lu.id)); +} + +function getFileLocale(fileName: string) { + //file name = 'a.en-us.lu' + return getExtension(getBaseName(fileName)); +} +//replace the dialogId with luFile's name +function addLocaleToConfig(config: ICrossTrainConfig, luFiles: LuFile[]) { + const { rootIds, triggerRules } = config; + config.rootIds = rootIds.reduce((result: string[], id: string) => { + return [...result, ...getLuFilesByDialogId(id, luFiles)]; + }, []); + config.triggerRules = keys(triggerRules).reduce((result, key) => { + const fileNames = getLuFilesByDialogId(key, luFiles); + return { + ...result, + ...fileNames.reduce((result, name) => { + const locale = getFileLocale(name); + const triggers = triggerRules[key]; + keys(triggers).forEach((trigger) => { + if (!result[name]) result[name] = {}; + const ids = triggers[trigger]; + if (Array.isArray(ids)) { + result[name][trigger] = ids.map((id) => (id ? `${id}.${locale}.lu` : id)); + } else { + result[name][trigger] = ids ? `${ids}.${locale}.lu` : ids; + } + }); + return result; + }, {}), + }; + }, {}); + return config; +} +interface ICrossTrainConfig { + rootIds: string[]; + triggerRules: { [key: string]: any }; + intentName: string; + verbose: boolean; +} + +//generate the cross-train config without locale +/* the config is like + { + rootIds: [ + 'main.en-us.lu', + 'main.fr-fr.lu' + ], + triggerRules: { + 'main.en-us.lu': { + 'dia1_trigger': 'dia1.en-us.lu', + 'dia2_trigger': 'dia2.en-us.lu' + }, + 'dia2.en-us.lu': { + 'dia3_trigger': 'dia3.en-us.lu', + 'dia4_trigger': 'dia4.en-us.lu' + }, + 'main.fr-fr.lu': { + 'dia1_trigger': 'dia1.fr-fr.lu' + } + }, + intentName: '_Interruption', + verbose: true + } + */ + +export function createCrossTrainConfig(dialogs: DialogInfo[], luFiles: LuFile[]): ICrossTrainConfig { + const triggerRules = {}; + const countMap = {}; + + //map all referred lu files + luFiles.forEach((file) => { + countMap[getBaseName(file.id)] = 1; + }); + + let rootId = ''; + dialogs.forEach((dialog) => { + if (dialog.isRoot) rootId = dialog.id; + const luFile = luFiles.find((luFile) => getBaseName(luFile.id) === dialog.luFile); + if (luFile) { + const fileId = dialog.id; + const { intentTriggers } = dialog; + // filter intenttrigger which be involved in lu file + //find the trigger's dialog that use a recognizer + intentTriggers + .filter((intentTrigger) => luFile.intents.find((intent) => intent.Name === intentTrigger.intent)) + .forEach((item) => { + //find all dialogs in trigger that has a luis recognizer + const used = item.dialogs.filter((dialog) => !!countMap[dialog]); + + const deduped = Array.from(new Set(used)); + + const result = {}; + if (deduped.length === 1) { + result[item.intent] = deduped[0]; + } else if (deduped.length) { + result[item.intent] = deduped; + } else { + result[item.intent] = ''; + } + + triggerRules[fileId] = { ...triggerRules[fileId], ...result }; + }); + } + }); + + const crossTrainConfig: ICrossTrainConfig = { + rootIds: [], + triggerRules: {}, + intentName: '_Interruption', + verbose: true, + }; + crossTrainConfig.rootIds = keys(countMap).filter( + (key) => (countMap[key] === 0 || key === rootId) && triggerRules[key] + ); + crossTrainConfig.triggerRules = triggerRules; + return addLocaleToConfig(crossTrainConfig, luFiles); +} + +export function isBuildConfigComplete(config, dialogs, luFiles, qnaFiles) { + let complete = true; + if (getReferredLuFiles(luFiles, dialogs).length > 0) { + if (Object.values(LuisConfig).some((luisConfigKey) => config.luis[luisConfigKey] === '')) { + complete = false; + } + } + if (getReferredQnaFiles(qnaFiles, dialogs).length > 0) { + if (Object.values(QnaConfig).some((qnaConfigKey) => config.qna[qnaConfigKey] === '')) { + complete = false; + } + } + return complete; +} + +// return true if dialogs have one with default recognizer. +export function needsBuild(dialogs) { + return dialogs.some((dialog) => typeof dialog.content.recognizer === 'string'); +} diff --git a/Composer/packages/client/src/utils/luUtil.ts b/Composer/packages/client/src/utils/luUtil.ts index 376c54a6b0..787171513e 100644 --- a/Composer/packages/client/src/utils/luUtil.ts +++ b/Composer/packages/client/src/utils/luUtil.ts @@ -6,12 +6,11 @@ * it's designed have no state, input text file, output text file. * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ -import keys from 'lodash/keys'; import { createSingleMessage, BotIndexer } from '@bfc/indexers'; import { LuFile, DialogInfo, DiagnosticSeverity } from '@bfc/shared'; import formatMessage from 'format-message'; -import { getBaseName, getExtension } from './fileUtil'; +import { getBaseName } from './fileUtil'; export * from '@bfc/indexers/lib/utils/luUtil'; @@ -26,132 +25,6 @@ export function getReferredLuFiles(luFiles: LuFile[], dialogs: DialogInfo[], che }); } -function createConfigId(fileId) { - return `${fileId}.lu`; -} - -function getLuFilesByDialogId(dialogId: string, luFiles: LuFile[]) { - return luFiles.filter((lu) => getBaseName(lu.id) === dialogId).map((lu) => createConfigId(lu.id)); -} - -function getFileLocale(fileName: string) { - //file name = 'a.en-us.lu' - return getExtension(getBaseName(fileName)); -} - -//replace the dialogId with luFile's name -function addLocaleToConfig(config: ICrossTrainConfig, luFiles: LuFile[]) { - const { rootIds, triggerRules } = config; - config.rootIds = rootIds.reduce((result: string[], id: string) => { - return [...result, ...getLuFilesByDialogId(id, luFiles)]; - }, []); - config.triggerRules = keys(triggerRules).reduce((result, key) => { - const fileNames = getLuFilesByDialogId(key, luFiles); - return { - ...result, - ...fileNames.reduce((result, name) => { - const locale = getFileLocale(name); - const triggers = triggerRules[key]; - keys(triggers).forEach((trigger) => { - if (!result[name]) result[name] = {}; - const ids = triggers[trigger]; - if (Array.isArray(ids)) { - result[name][trigger] = ids.map((id) => (id ? `${id}.${locale}.lu` : id)); - } else { - result[name][trigger] = ids ? `${ids}.${locale}.lu` : ids; - } - }); - return result; - }, {}), - }; - }, {}); - return config; -} - -interface ICrossTrainConfig { - rootIds: string[]; - triggerRules: { [key: string]: any }; - intentName: string; - verbose: boolean; -} - -//generate the cross-train config without locale -/* the config is like - { - rootIds: [ - 'main.en-us.lu', - 'main.fr-fr.lu' - ], - triggerRules: { - 'main.en-us.lu': { - 'dia1_trigger': 'dia1.en-us.lu', - 'dia2_trigger': 'dia2.en-us.lu' - }, - 'dia2.en-us.lu': { - 'dia3_trigger': 'dia3.en-us.lu', - 'dia4_trigger': 'dia4.en-us.lu' - }, - 'main.fr-fr.lu': { - 'dia1_trigger': 'dia1.fr-fr.lu' - } - }, - intentName: '_Interruption', - verbose: true - } - */ -export function createCrossTrainConfig(dialogs: DialogInfo[], luFiles: LuFile[]): ICrossTrainConfig { - const triggerRules = {}; - const countMap = {}; - - //map all referred lu files - luFiles.forEach((file) => { - countMap[getBaseName(file.id)] = 1; - }); - - let rootId = ''; - dialogs.forEach((dialog) => { - if (dialog.isRoot) rootId = dialog.id; - const luFile = luFiles.find((luFile) => getBaseName(luFile.id) === dialog.luFile); - if (luFile) { - const fileId = dialog.id; - const { intentTriggers } = dialog; - // filter intenttrigger which be involved in lu file - //find the trigger's dialog that use a recognizer - intentTriggers - .filter((intentTrigger) => luFile.intents.find((intent) => intent.Name === intentTrigger.intent)) - .forEach((item) => { - //find all dialogs in trigger that has a luis recognizer - const used = item.dialogs.filter((dialog) => !!countMap[dialog]); - - const deduped = Array.from(new Set(used)); - - const result = {}; - if (deduped.length === 1) { - result[item.intent] = deduped[0]; - } else if (deduped.length) { - result[item.intent] = deduped; - } else { - result[item.intent] = ''; - } - - triggerRules[fileId] = { ...triggerRules[fileId], ...result }; - }); - } - }); - - const crossTrainConfig: ICrossTrainConfig = { - rootIds: [], - triggerRules: {}, - intentName: '_Interruption', - verbose: true, - }; - crossTrainConfig.rootIds = keys(countMap).filter( - (key) => (countMap[key] === 0 || key === rootId) && triggerRules[key] - ); - crossTrainConfig.triggerRules = triggerRules; - return addLocaleToConfig(crossTrainConfig, luFiles); -} - function generateErrorMessage(invalidLuFile: LuFile[]) { return invalidLuFile.reduce((msg, file) => { const fileErrorText = file.diagnostics.reduce((text, diagnostic) => {