diff --git a/Composer/packages/client/__tests__/recognizer.test.ts b/Composer/packages/client/__tests__/recognizer.test.ts new file mode 100644 index 0000000000..f7aaffa7d4 --- /dev/null +++ b/Composer/packages/client/__tests__/recognizer.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { LuFile, SDKKinds, QnAFile } from '@bfc/shared'; + +import { + getCrossTrainedRecognizerDialog, + getLuisRecognizerDialogs, + getMultiLanguagueRecognizerDialog, + getQnAMakerRecognizerDialogs, + preserveRecognizer, +} from '../src/recoilModel/Recognizers'; + +describe('Test the generated recognizer dialogs', () => { + it('should get MultiLanguagueRecognizer', () => { + const result = getMultiLanguagueRecognizerDialog( + 'test', + [ + { id: 'test.en-us', empty: false }, + { id: 'test.fr-fr', empty: false }, + ], + 'qna' + ); + + expect(result.id).toBe('test.qna.dialog'); + expect(result.content.recognizers['en-us']).toBe('test.en-us.qna'); + expect(result.content.recognizers['']).toBe('test.en-us.qna'); + expect(result.content.recognizers['fr-fr']).toBe('test.fr-fr.qna'); + }); + + it('should get CrossTrainedRecognizerDialog', () => { + const result = getCrossTrainedRecognizerDialog( + 'test', + [{ id: 'test.en-us', empty: false }] as LuFile[], + [{ id: 'test.en-us', empty: false }] as QnAFile[] + ); + expect(result.id).toBe('test.lu.qna.dialog'); + expect(result.content.recognizers[0]).toBe('test.lu'); + expect(result.content.recognizers[1]).toBe('test.qna'); + }); + + it('should get LuisRecognizerDialogs', () => { + const result = getLuisRecognizerDialogs('test', [ + { id: 'test.en-us', empty: false }, + { id: 'test.fr-fr', empty: false }, + ] as LuFile[]); + expect(result.length).toBe(2); + expect(result[0].id).toBe('test.en-us.lu.dialog'); + expect(result[1].content).toStrictEqual({ + $kind: SDKKinds.LuisRecognizer, + id: `LUIS_test`, + applicationId: `=settings.luis.test_fr_fr_lu.appId`, + version: `=settings.luis.test_fr_fr_lu.version`, + endpoint: '=settings.luis.endpoint', + endpointKey: '=settings.luis.endpointKey', + }); + }); + + it('should get QnaMakerRecognizer', () => { + const result = getQnAMakerRecognizerDialogs('test', [ + { id: 'test.en-us', empty: false }, + { id: 'test.fr-fr', empty: false }, + ] as QnAFile[]); + expect(result.length).toBe(2); + expect(result[0].id).toBe('test.en-us.qna.dialog'); + expect(result[1].content).toStrictEqual({ + $kind: SDKKinds.QnAMakerRecognizer, + id: `QnA_test`, + knowledgeBaseId: `=settings.qna.test_fr_fr_qna`, + hostname: '=settings.qna.hostname', + endpointKey: '=settings.qna.endpointKey', + }); + }); + + it('should preserve Recogniozer', () => { + const result = preserveRecognizer( + [{ id: 'test.en-us', content: '' }], + [ + { id: 'test.en-us', content: 'test' }, + { id: 'test.fr-fr', empty: false }, + ] + ); + + expect(result.length).toBe(1); + expect(result[0].content).toBe('test'); + }); +}); diff --git a/Composer/packages/client/__tests__/utils/buildUtil.test.ts b/Composer/packages/client/__tests__/utils/buildUtil.test.ts index 1a19bf9e16..e37518d3d5 100644 --- a/Composer/packages/client/__tests__/utils/buildUtil.test.ts +++ b/Composer/packages/client/__tests__/utils/buildUtil.test.ts @@ -81,10 +81,10 @@ describe('createCrossTrainConfig', () => { { id: 'dia6.en-us' }, ]; const config = createCrossTrainConfig(dialogs as DialogInfo[], luFiles as LuFile[], ['en-us']); - expect(config['main.en-us.lu'].rootDialog).toBeTruthy(); - expect(config['main.en-us.lu'].triggers.dia1_trigger[0]).toEqual('dia1.en-us.lu'); - expect(config['main.en-us.lu'].triggers.no_dialog.length).toEqual(0); - expect(config['main.en-us.lu'].triggers.dia2_trigger[0]).toEqual('dia2.en-us.lu'); - expect(config['main.en-us.lu'].triggers.dias_trigger.length).toBe(2); + expect(config['main.en-us'].rootDialog).toBeTruthy(); + expect(config['main.en-us'].triggers.dia1_trigger[0]).toEqual('dia1.en-us'); + expect(config['main.en-us'].triggers.no_dialog.length).toEqual(0); + expect(config['main.en-us'].triggers.dia2_trigger[0]).toEqual('dia2.en-us'); + expect(config['main.en-us'].triggers.dias_trigger.length).toBe(2); }); }); diff --git a/Composer/packages/client/src/components/TestController/TestController.tsx b/Composer/packages/client/src/components/TestController/TestController.tsx index 636da59093..d9a91d529c 100644 --- a/Composer/packages/client/src/components/TestController/TestController.tsx +++ b/Composer/packages/client/src/components/TestController/TestController.tsx @@ -9,7 +9,6 @@ import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; import formatMessage from 'format-message'; import { useRecoilValue } from 'recoil'; import { IConfig, IPublishConfig, defaultPublishConfig } from '@bfc/shared'; -import { useRecognizerConfig } from '@bfc/extension-client'; import { botEndpointsState, @@ -68,7 +67,6 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { const settings = useRecoilValue(settingsState(projectId)); const qnaFiles = useRecoilValue(qnaFilesState(projectId)); const botLoadErrorMsg = useRecoilValue(botLoadErrorState(projectId)); - const { recognizers } = useRecognizerConfig(); const botEndpoints = useRecoilValue(botEndpointsState); const { @@ -158,18 +156,13 @@ export const TestController: React.FC<{ projectId: string }> = (props) => { setBotStatus(BotStatus.publishing, projectId); dismissDialog(); const { luis, qna } = config; - const recognizerTypes = dialogs.reduce((result, file) => { - const recognizer = recognizers.filter((r) => r.isSelected && r.isSelected(file.content.recognizer)); - result[file.id] = recognizer[0]?.id || ''; - return result; - }, {}); await setSettings(projectId, { ...settings, luis: luis, qna: Object.assign({}, settings.qna, qna), }); - await build(luis, qna, recognizerTypes, projectId); + await build(luis, qna, projectId); } async function handleLoadBot() { diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index bc3dbd08c6..08997f0e14 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -10,6 +10,7 @@ import { useRecoilValue } from 'recoil'; import { botDiagnosticsState, botProjectFileState, + crossTrainConfigState, dialogSchemasState, formDialogSchemasSelectorFamily, jsonSchemaFilesState, @@ -20,6 +21,7 @@ import { skillManifestsState, validateDialogsSelectorFamily, } from '../../recoilModel'; +import { recognizersSelectorFamily } from '../../recoilModel/selectors/recognizers'; import { getReferredLuFiles } from './../../utils/luUtil'; import { @@ -45,7 +47,8 @@ export default function useNotifications(projectId: string, filter?: string) { const formDialogSchemas = useRecoilValue(formDialogSchemasSelectorFamily(projectId)); const botProjectFile = useRecoilValue(botProjectFileState(projectId)); const jsonSchemaFiles = useRecoilValue(jsonSchemaFilesState(projectId)); - + const recognizers = useRecoilValue(recognizersSelectorFamily(projectId)); + const crossTrainConfig = useRecoilValue(crossTrainConfigState(projectId)); const botAssets: BotAssets = { projectId, dialogs, @@ -58,6 +61,8 @@ export default function useNotifications(projectId: string, filter?: string) { formDialogSchemas, botProjectFile, jsonSchemaFiles, + recognizers, + crossTrainConfig, }; const memoized = useMemo(() => { diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index 5480f16d67..2dc625e09b 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -24,8 +24,11 @@ import { filePersistenceState, botProjectFileState, jsonSchemaFilesState, + crossTrainConfigState, } from './atoms'; import { botsForFilePersistenceSelector, formDialogSchemasSelectorFamily } from './selectors'; +import { Recognizer } from './Recognizers'; +import { recognizersSelectorFamily } from './selectors/recognizers'; const getBotAssets = async (projectId, snapshot: Snapshot): Promise => { const result = await Promise.all([ @@ -39,6 +42,8 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise = snapshot.getPromise(botProjectFileState(projectId)), snapshot.getPromise(formDialogSchemasSelectorFamily(projectId)), snapshot.getPromise(jsonSchemaFilesState(projectId)), + snapshot.getPromise(recognizersSelectorFamily(projectId)), + snapshot.getPromise(crossTrainConfigState(projectId)), ]); return { projectId, @@ -52,6 +57,8 @@ const getBotAssets = async (projectId, snapshot: Snapshot): Promise = botProjectFile: result[7], formDialogSchemas: result[8], jsonSchemaFiles: result[9], + recognizers: result[10], + crossTrainConfig: result[11], }; }; @@ -110,7 +117,10 @@ export const DispatcherWrapper = ({ children }) => { return ( {botProjects.map((projectId) => ( - + + + + ))} {loaded ? children : null} diff --git a/Composer/packages/client/src/recoilModel/Recognizers.tsx b/Composer/packages/client/src/recoilModel/Recognizers.tsx new file mode 100644 index 0000000000..051958dcb3 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/Recognizers.tsx @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DialogInfo, LuFile, QnAFile, SDKKinds, RecognizerFile } from '@bfc/shared'; +import React, { useEffect } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; +import isEqual from 'lodash/isEqual'; + +import { getExtension } from '../utils/fileUtil'; + +import * as luUtil from './../utils/luUtil'; +import * as buildUtil from './../utils/buildUtil'; +import { crossTrainConfigState, luFilesState, qnaFilesState, settingsState } from './atoms'; +import { dialogsSelectorFamily } from './selectors'; +import { recognizersSelectorFamily } from './selectors/recognizers'; + +const LuisRecognizerTemplate = (target: string, fileName: string) => ({ + $kind: SDKKinds.LuisRecognizer, + id: `LUIS_${target}`, + applicationId: `=settings.luis.${fileName.replace(/[.-]/g, '_')}_lu.appId`, + version: `=settings.luis.${fileName.replace(/[.-]/g, '_')}_lu.version`, + endpoint: '=settings.luis.endpoint', + endpointKey: '=settings.luis.endpointKey', +}); + +const QnAMakerRecognizerTemplate = (target: string, fileName: string) => ({ + $kind: SDKKinds.QnAMakerRecognizer, + id: `QnA_${target}`, + knowledgeBaseId: `=settings.qna.${fileName.replace(/[.-]/g, '_')}_qna`, + hostname: '=settings.qna.hostname', + endpointKey: '=settings.qna.endpointKey', +}); + +const MultiLanguageRecognizerTemplate = (target: string, fileType: 'lu' | 'qna') => ({ + $kind: SDKKinds.MultiLanguageRecognizer, + id: `${fileType === 'lu' ? 'LUIS' : 'QnA'}_${target}`, + recognizers: {}, +}); + +const CrossTrainedRecognizerTemplate = (): { + $kind: string; + recognizers: string[]; +} => ({ + $kind: SDKKinds.CrossTrainedRecognizerSet, + recognizers: [], +}); + +export const getMultiLanguagueRecognizerDialog = ( + target: string, + files: { empty: boolean; id: string }[], + fileType: 'lu' | 'qna', + defalutLanguage = 'en-us' +) => { + const multiLanguageRecognizer = MultiLanguageRecognizerTemplate(target, fileType); + + files.forEach((item) => { + if (item.empty || !item.id.startsWith(target)) return; + const locale = getExtension(item.id); + const fileName = `${item.id}.${fileType}`; + multiLanguageRecognizer.recognizers[locale] = fileName; + if (locale === defalutLanguage) { + multiLanguageRecognizer.recognizers[''] = fileName; + } + }); + + return { id: `${target}.${fileType}.dialog`, content: multiLanguageRecognizer }; +}; + +export const getLuisRecognizerDialogs = (target: string, luFiles: LuFile[]) => { + return luFiles + .filter((item) => !item.empty && item.id.startsWith(target)) + .map((item) => ({ id: `${item.id}.lu.dialog`, content: LuisRecognizerTemplate(target, item.id) })); +}; + +export const getQnAMakerRecognizerDialogs = (target: string, qnaFiles: QnAFile[]) => { + return qnaFiles + .filter((item) => !item.empty) + .map((item) => ({ id: `${item.id}.qna.dialog`, content: QnAMakerRecognizerTemplate(target, item.id) })); +}; + +export const getCrossTrainedRecognizerDialog = (target: string, luFiles: LuFile[], qnaFiles: QnAFile[]) => { + const crossTrainedRecognizer = CrossTrainedRecognizerTemplate(); + + if (luFiles.some((item) => !item.empty)) { + crossTrainedRecognizer.recognizers.push(`${target}.lu`); + } + + if (qnaFiles.some((item) => !item.empty)) { + crossTrainedRecognizer.recognizers.push(`${target}.qna`); + } + + return { + id: `${target}.lu.qna.dialog`, + content: crossTrainedRecognizer, + }; +}; + +export const isCrossTrainedRecognizerSet = (dialog: DialogInfo) => + typeof dialog.content.recognizer === 'string' && dialog.content.recognizer.endsWith('qna'); + +export const isLuisRecognizer = (dialog: DialogInfo) => + typeof dialog.content.recognizer === 'string' && dialog.content.recognizer.endsWith('lu'); + +export const generateRecognizers = (dialog: DialogInfo, luFiles: LuFile[], qnaFiles: QnAFile[]) => { + const isCrossTrain = isCrossTrainedRecognizerSet(dialog); + const luisRecognizers = getLuisRecognizerDialogs(dialog.id, luFiles); + const luMultiLanguagueRecognizer = getMultiLanguagueRecognizerDialog(dialog.id, luFiles, 'lu'); + + const crossTrainedRecognizer = getCrossTrainedRecognizerDialog(dialog.id, luFiles, qnaFiles); + const qnaMultiLanguagueRecognizer = getMultiLanguagueRecognizerDialog(dialog.id, qnaFiles, 'qna'); + const qnaMakeRecognizers = getQnAMakerRecognizerDialogs(dialog.id, qnaFiles); + + return { + isCrossTrain, + luisRecognizers, + luMultiLanguagueRecognizer, + crossTrainedRecognizer, + qnaMultiLanguagueRecognizer, + qnaMakeRecognizers, + }; +}; + +export const preserveRecognizer = (recognizers: { id: string; content: any }[], previousRecognizers: any[]) => { + return recognizers.map((recognizer) => { + const previous = previousRecognizers.find((item) => item.id === recognizer.id); + if (previous) { + recognizer.content = previous.content; + } + + return recognizer; + }); +}; + +export const Recognizer = React.memo((props: { projectId: string }) => { + const { projectId } = props; + const setRecognizers = useSetRecoilState(recognizersSelectorFamily(projectId)); + const [crossTrainConfig, setCrossTrainConfig] = useRecoilState(crossTrainConfigState(projectId)); + const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); + const luFiles = useRecoilValue(luFilesState(projectId)); + const qnaFiles = useRecoilValue(qnaFilesState(projectId)); + const settings = useRecoilValue(settingsState(projectId)); + + useEffect(() => { + let recognizers: RecognizerFile[] = []; + dialogs + .filter((dialog) => isCrossTrainedRecognizerSet(dialog) || isLuisRecognizer(dialog)) + .forEach((dialog) => { + const filtedLus = luFiles.filter((item) => item.id.startsWith(dialog.id)); + const filtedQnas = qnaFiles.filter((item) => item.id.startsWith(dialog.id)); + const { + isCrossTrain, + luisRecognizers, + luMultiLanguagueRecognizer, + crossTrainedRecognizer, + qnaMultiLanguagueRecognizer, + qnaMakeRecognizers, + } = generateRecognizers(dialog, filtedLus, filtedQnas); + + if (luisRecognizers.length) { + recognizers.push(luMultiLanguagueRecognizer); + recognizers = [...recognizers, ...preserveRecognizer(luisRecognizers, [])]; + } + if (isCrossTrain) { + recognizers.push(crossTrainedRecognizer); + } + if (isCrossTrain && qnaMakeRecognizers.length) { + recognizers.push(qnaMultiLanguagueRecognizer); + recognizers = [...recognizers, ...preserveRecognizer(qnaMakeRecognizers, [])]; + } + }); + setRecognizers(recognizers); + }, [dialogs, luFiles, qnaFiles]); + + useEffect(() => { + try { + const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs); + + const curCrossTrainConfig = buildUtil.createCrossTrainConfig(dialogs, referredLuFiles, settings.languages); + if (!isEqual(crossTrainConfig, curCrossTrainConfig)) { + setCrossTrainConfig(curCrossTrainConfig); + } + } catch (error) { + setCrossTrainConfig(crossTrainConfig); + } + }, [dialogs, luFiles, settings]); + + return null; +}); diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index db2582625d..cfb2c95555 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -5,6 +5,7 @@ import { BotProjectFile, BotProjectSpace, BotSchemas, + CrosstrainConfig, Diagnostic, DialogInfo, DialogSchemaFile, @@ -14,6 +15,7 @@ import { LgFile, LuFile, QnAFile, + RecognizerFile, Skill, } from '@bfc/shared'; import { atomFamily } from 'recoil'; @@ -142,6 +144,27 @@ export const skillsState = atomFamily({ }, }); +export const recognizerIdsState = atomFamily({ + key: getFullyQualifiedKey('recognizerIds'), + default: (id) => { + return []; + }, +}); + +export const recognizerState = atomFamily({ + key: getFullyQualifiedKey('recognizer'), + default: () => { + return { id: '', content: {}, lastModified: '' }; + }, +}); + +export const crossTrainConfigState = atomFamily({ + key: getFullyQualifiedKey('crossTrainConfig'), + default: () => { + return {}; + }, +}); + export const actionsSeedState = atomFamily({ key: getFullyQualifiedKey('actionsSeed'), default: (id) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts index d78a9a07c7..3c9f7888bf 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/builder.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/builder.ts @@ -6,14 +6,12 @@ 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'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import { luFilesState, qnaFilesState, botStatusState, botLoadErrorState } from '../atoms'; import { dialogsSelectorFamily } from '../selectors'; -import { settingsState } from '../atoms/botState'; const checkEmptyQuestionOrAnswerInQnAFile = (sections) => { return sections.some((s) => !s.Answer || s.Questions.some((q) => !q.content)); @@ -24,13 +22,11 @@ export const builderDispatcher = () => { ({ set, snapshot }: CallbackInterface) => async ( luisConfig: ILuisConfig, qnaConfig: IQnAConfig, - recognizerTypes: { [fileName: string]: string }, projectId: string ) => { const dialogs = await snapshot.getPromise(dialogsSelectorFamily(projectId)); const luFiles = await snapshot.getPromise(luFilesState(projectId)); const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId)); - const settings = await snapshot.getPromise(settingsState(projectId)); const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs); const errorMsg = qnaFiles.reduce( (result, file) => { @@ -51,13 +47,10 @@ export const builderDispatcher = () => { return; } try { - const crossTrainConfig = buildUtil.createCrossTrainConfig(dialogs, referredLuFiles, settings.languages); await httpClient.post(`/projects/${projectId}/build`, { luisConfig, qnaConfig, projectId, - crossTrainConfig, - recognizerTypes, luFiles: referredLuFiles.map((file) => ({ id: file.id, isEmpty: file.empty })), qnaFiles: qnaFiles.map((file) => ({ id: file.id, isEmpty: file.empty })), }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/index.ts b/Composer/packages/client/src/recoilModel/dispatchers/index.ts index da12d18bc6..4830bdfd3d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/index.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/index.ts @@ -23,6 +23,7 @@ import { extensionsDispatcher } from './extensions'; import { formDialogsDispatcher } from './formDialogs'; import { botProjectFileDispatcher } from './botProjectFile'; import { zoomDispatcher } from './zoom'; +import { recognizerDispatcher } from './recognizers'; const createDispatchers = () => { return { @@ -48,6 +49,7 @@ const createDispatchers = () => { ...formDialogsDispatcher(), ...botProjectFileDispatcher(), ...zoomDispatcher(), + ...recognizerDispatcher(), }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/recognizers.ts b/Composer/packages/client/src/recoilModel/dispatchers/recognizers.ts new file mode 100644 index 0000000000..ba52192843 --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/recognizers.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/* eslint-disable react-hooks/rules-of-hooks */ +import { CallbackInterface, useRecoilCallback } from 'recoil'; + +import { recognizerState } from '../atoms'; + +export const recognizerDispatcher = () => { + const updateRecognizer = useRecoilCallback( + ({ set }: CallbackInterface) => async (projectId: string, recognizerId: string, content: any) => { + set(recognizerState({ projectId, id: recognizerId }), content); + } + ); + + return { + updateRecognizer, + }; +}; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 8ce490ee03..eba4cb23da 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -75,6 +75,9 @@ import { undoHistoryState } from '../../undo/history'; import UndoHistory from '../../undo/undoHistory'; import { logMessage, setError } from '../shared'; +import { crossTrainConfigState } from './../../atoms/botState'; +import { recognizersSelectorFamily } from './../../selectors/recognizers'; + export const resetBotStates = async ({ reset }: CallbackInterface, projectId: string) => { const botStates = Object.keys(botstates); botStates.forEach((state) => { @@ -272,6 +275,8 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any skillManifestFiles, skills, mergedSettings, + recognizers, + crossTrainConfig, } = botFiles; const curLocation = await snapshot.getPromise(locationState(projectId)); const storedLocale = languageStorage.get(botName)?.locale; @@ -297,6 +302,8 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any }); set(dialogIdsState(projectId), dialogIds); + set(recognizersSelectorFamily(projectId), recognizers); + set(crossTrainConfigState(projectId), crossTrainConfig); await lgWorker.addProject(projectId, lgFiles); diff --git a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts index 378842a540..edc826e0e7 100644 --- a/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts +++ b/Composer/packages/client/src/recoilModel/persistence/FilePersistence.ts @@ -14,6 +14,8 @@ import { LgFile, QnAFile, FormDialogSchema, + RecognizerFile, + CrosstrainConfig, } from '@bfc/shared'; import keys from 'lodash/keys'; @@ -125,6 +127,8 @@ class FilePersistence { FileExtensions.DialogSchema, FileExtensions.Manifest, FileExtensions.Setting, + FileExtensions.Recognizer, + FileExtensions.CrossTrainConfig, ].includes(fileExtension); if (isJson) { content = JSON.stringify(content, null, 2) + '\n'; @@ -195,6 +199,24 @@ class FilePersistence { return changes; } + private getRecognizerChanges(current: RecognizerFile[], previous: RecognizerFile[]) { + const changeItems = this.getDifferenceItems(current, previous); + const changes = this.getFileChanges(FileExtensions.Recognizer, changeItems); + return changes; + } + + private getCrossTrainConfigChanges(current: CrosstrainConfig, previous: CrosstrainConfig) { + if (isEqual(current, previous)) return []; + let changeType = ChangeType.UPDATE; + if (!keys(previous).length) { + changeType = ChangeType.CREATE; + } + if (!keys(current).length) { + changeType = ChangeType.DELETE; + } + return [this.createChange({ id: '', content: current }, FileExtensions.CrossTrainConfig, changeType)]; + } + private getBotProjectFileChanges(current: BotProjectFile, previous: BotProjectFile) { if (!isEqual(current, previous)) { return [ @@ -251,6 +273,13 @@ class FilePersistence { previousAssets.botProjectFile ); + const recognizerFileChanges = this.getRecognizerChanges(currentAssets.recognizers, previousAssets.recognizers); + + const crossTrainFileChanges = this.getCrossTrainConfigChanges( + currentAssets.crossTrainConfig, + previousAssets.crossTrainConfig + ); + const fileChanges: IFileChange[] = [ ...dialogChanges, ...dialogSchemaChanges, @@ -261,6 +290,8 @@ class FilePersistence { ...settingChanges, ...formDialogChanges, ...botProjectFileChanges, + ...recognizerFileChanges, + ...crossTrainFileChanges, ]; return fileChanges; } diff --git a/Composer/packages/client/src/recoilModel/persistence/types.ts b/Composer/packages/client/src/recoilModel/persistence/types.ts index 941f6a119d..72d30086b9 100644 --- a/Composer/packages/client/src/recoilModel/persistence/types.ts +++ b/Composer/packages/client/src/recoilModel/persistence/types.ts @@ -18,6 +18,8 @@ export enum FileExtensions { SourceQnA = '.source.qna', Setting = 'appsettings.json', BotProject = '.botproj', + Recognizer = '', + CrossTrainConfig = 'cross-train.config.json', } export type FileErrorHandler = (error) => void; diff --git a/Composer/packages/client/src/recoilModel/selectors/recognizers.ts b/Composer/packages/client/src/recoilModel/selectors/recognizers.ts new file mode 100644 index 0000000000..8e772bdbdb --- /dev/null +++ b/Composer/packages/client/src/recoilModel/selectors/recognizers.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { RecognizerFile } from '@bfc/shared'; +import { selectorFamily } from 'recoil'; + +import { recognizerIdsState, recognizerState } from '../atoms'; + +export const recognizersSelectorFamily = selectorFamily({ + key: 'recognizers', + get: (projectId: string) => ({ get }) => { + const recognizerIds = get(recognizerIdsState(projectId)); + + return recognizerIds.map((id) => { + return get(recognizerState({ projectId, id })); + }); + }, + set: (projectId: string) => ({ set }, newRecognizers) => { + const newRecognizerArray = newRecognizers as RecognizerFile[]; + set( + recognizerIdsState(projectId), + newRecognizerArray.map((file) => file.id) + ); + newRecognizerArray.forEach((file) => set(recognizerState({ projectId, id: file.id }), file)); + }, +}); diff --git a/Composer/packages/client/src/utils/buildUtil.ts b/Composer/packages/client/src/utils/buildUtil.ts index 8769f7ff50..e4470f9d30 100644 --- a/Composer/packages/client/src/utils/buildUtil.ts +++ b/Composer/packages/client/src/utils/buildUtil.ts @@ -10,7 +10,7 @@ import { getReferredQnaFiles } from './qnaUtil'; import { getBaseName } from './fileUtil'; function createConfigId(fileId: string, language: string) { - return `${fileId}.${language}.lu`; + return `${fileId}.${language}`; } export function createCrossTrainConfig(dialogs: DialogInfo[], luFiles: LuFile[], languages: string[]) { @@ -29,7 +29,7 @@ export function createCrossTrainConfig(dialogs: DialogInfo[], luFiles: LuFile[], const triggers = filtered.reduce((result, { intent, dialogs }) => { const ids = dialogs .map((dialog) => createConfigId(dialog, language)) - .filter((id) => luFiles.some((file) => `${file.id}.lu` === id)); + .filter((id) => luFiles.some((file) => `${file.id}` === id)); if (!ids.length && dialogs.length) return result; result[intent] = ids; return result; diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 6f866c01a0..7912878ed2 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -26,7 +26,7 @@ "rimraf": "^2.6.3" }, "dependencies": { - "@microsoft/bf-lu": "^4.11.0-dev.20201013.7ccb128", + "@microsoft/bf-lu": "^4.11.0-dev.20201025.69cf2b9", "adaptive-expressions": "^4.11.0-dev.20201013.d5458bf", "botbuilder-lg": "4.11.0-dev.20201010.6e4a99e", "lodash": "^4.17.19" diff --git a/Composer/packages/lib/indexers/src/crossTrainConfigIndexer.ts b/Composer/packages/lib/indexers/src/crossTrainConfigIndexer.ts new file mode 100644 index 0000000000..aafd35adbd --- /dev/null +++ b/Composer/packages/lib/indexers/src/crossTrainConfigIndexer.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileInfo } from '@bfc/shared'; + +const index = (files: FileInfo[]) => { + if (!files.length) return {}; + + const { content } = files[0]; + try { + return JSON.parse(content); + } catch (error) { + return {}; + } +}; + +export const crossTrainConfigIndexer = { + index, +}; diff --git a/Composer/packages/lib/indexers/src/index.ts b/Composer/packages/lib/indexers/src/index.ts index 497ef04958..3b159c1ed0 100644 --- a/Composer/packages/lib/indexers/src/index.ts +++ b/Composer/packages/lib/indexers/src/index.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { DialogSetting, FileInfo, lgImportResolverGenerator } from '@bfc/shared'; +import { recognizerIndexer } from './recognizerIndexer'; import { dialogIndexer } from './dialogIndexer'; import { dialogSchemaIndexer } from './dialogSchemaIndexer'; import { jsonSchemaFileIndexer } from './jsonSchemaFileIndexer'; @@ -14,6 +15,7 @@ import { botProjectSpaceIndexer } from './botProjectSpaceIndexer'; import { FileExtensions } from './utils/fileExtensions'; import { getExtension, getBaseName } from './utils/help'; import { formDialogSchemaIndexer } from './formDialogSchemaIndexer'; +import { crossTrainConfigIndexer } from './crossTrainConfigIndexer'; class Indexer { private classifyFile(files: FileInfo[]) { @@ -34,10 +36,39 @@ class Indexer { [FileExtensions.DialogSchema]: [], [FileExtensions.Manifest]: [], [FileExtensions.BotProjectSpace]: [], + [FileExtensions.CrossTrainConfig]: [], } ); } + private separateDialogsAndRecognizers = (files: FileInfo[]) => { + return files.reduce( + (result: { dialogs: FileInfo[]; recognizers: FileInfo[] }, file) => { + if (file.name.endsWith('.lu.dialog') || file.name.endsWith('.qna.dialog')) { + result.recognizers.push(file); + } else { + result.dialogs.push(file); + } + return result; + }, + { dialogs: [], recognizers: [] } + ); + }; + + private separateConfigAndManifests = (files: FileInfo[]) => { + return files.reduce( + (result: { crossTrainConfigs: FileInfo[]; skillManifestFiles: FileInfo[] }, file) => { + if (file.name.endsWith('.config.json')) { + result.crossTrainConfigs.push(file); + } else { + result.skillManifestFiles.push(file); + } + return result; + }, + { crossTrainConfigs: [], skillManifestFiles: [] } + ); + }; + private getLgImportResolver = (files: FileInfo[], locale: string) => { const lgFiles = files.map(({ name, content }) => { return { @@ -52,17 +83,21 @@ class Indexer { public index(files: FileInfo[], botName: string, locale: string, skillContent: any, settings: DialogSetting) { const result = this.classifyFile(files); const luFeatures = settings.luFeatures; + const { dialogs, recognizers } = this.separateDialogsAndRecognizers(result[FileExtensions.Dialog]); + const { skillManifestFiles, crossTrainConfigs } = this.separateConfigAndManifests(result[FileExtensions.Manifest]); return { - dialogs: dialogIndexer.index(result[FileExtensions.Dialog], botName), + dialogs: dialogIndexer.index(dialogs, botName), dialogSchemas: dialogSchemaIndexer.index(result[FileExtensions.DialogSchema]), lgFiles: lgIndexer.index(result[FileExtensions.lg], this.getLgImportResolver(result[FileExtensions.lg], locale)), luFiles: luIndexer.index(result[FileExtensions.Lu], luFeatures), qnaFiles: qnaIndexer.index(result[FileExtensions.QnA]), - skillManifestFiles: skillManifestIndexer.index(result[FileExtensions.Manifest]), + skillManifestFiles: skillManifestIndexer.index(skillManifestFiles), skills: skillIndexer.index(skillContent, settings.skill), botProjectSpaceFiles: botProjectSpaceIndexer.index(result[FileExtensions.BotProjectSpace]), jsonSchemaFiles: jsonSchemaFileIndexer.index(result[FileExtensions.Json]), formDialogSchemas: formDialogSchemaIndexer.index(result[FileExtensions.FormDialog]), + recognizers: recognizerIndexer.index(recognizers), + crossTrainConfig: crossTrainConfigIndexer.index(crossTrainConfigs), }; } } diff --git a/Composer/packages/lib/indexers/src/recognizerIndexer.ts b/Composer/packages/lib/indexers/src/recognizerIndexer.ts new file mode 100644 index 0000000000..bd065588dd --- /dev/null +++ b/Composer/packages/lib/indexers/src/recognizerIndexer.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileInfo, RecognizerFile } from '@bfc/shared'; + +const index = (recognizerFiles: FileInfo[]) => { + return recognizerFiles.reduce((recognizers: RecognizerFile[], { content, name, lastModified }) => { + try { + const jsonContent = JSON.parse(content); + recognizers.push({ content: jsonContent, id: name }); + return recognizers; + } catch (error) { + return recognizers; + } + }, [] as RecognizerFile[]); +}; + +export const recognizerIndexer = { + index, +}; diff --git a/Composer/packages/lib/indexers/src/utils/fileExtensions.ts b/Composer/packages/lib/indexers/src/utils/fileExtensions.ts index 50301f905f..ab08fc5259 100644 --- a/Composer/packages/lib/indexers/src/utils/fileExtensions.ts +++ b/Composer/packages/lib/indexers/src/utils/fileExtensions.ts @@ -11,4 +11,5 @@ export enum FileExtensions { Manifest = '.json', BotProjectSpace = '.botproj', Json = '.json', + CrossTrainConfig = '.config.json', } diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 9f9f875b52..5ec00996cf 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -63,8 +63,9 @@ "@bfc/lu-languageserver": "*", "@bfc/shared": "*", "@microsoft/bf-dispatcher": "^4.11.0-beta.20201016.393c6b2", + "@microsoft/bf-lu": "^4.11.0-rc.20201028.6f4722f", "@microsoft/bf-generate-library": "^4.10.0-daily.20201026.178799", - "@microsoft/bf-lu": "^4.11.0-dev.20201013.7ccb128", + "@microsoft/bf-orchestrator": "4.11.0-beta.20201013.20d7917", "archiver": "^5.0.2", "axios": "^0.19.2", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/src/__mocks__/samplebots/bot1/recognizers/cross-train.config.json b/Composer/packages/server/src/__mocks__/samplebots/bot1/settings/cross-train.config.json similarity index 100% rename from Composer/packages/server/src/__mocks__/samplebots/bot1/recognizers/cross-train.config.json rename to Composer/packages/server/src/__mocks__/samplebots/bot1/settings/cross-train.config.json diff --git a/Composer/packages/server/src/controllers/project.ts b/Composer/packages/server/src/controllers/project.ts index 8ec4cb8daf..33c1410db3 100644 --- a/Composer/packages/server/src/controllers/project.ts +++ b/Composer/packages/server/src/controllers/project.ts @@ -327,14 +327,12 @@ async function build(req: Request, res: Response) { const currentProject = await BotProjectService.getProjectById(projectId, user); if (currentProject !== undefined) { try { - const { luisConfig, qnaConfig, luFiles, qnaFiles, crossTrainConfig, recognizerTypes } = req.body; + const { luisConfig, qnaConfig, luFiles, qnaFiles } = req.body; const files = await currentProject.buildFiles({ luisConfig, qnaConfig, luResource: luFiles, qnaResource: qnaFiles, - crossTrainConfig, - recognizerTypes, }); res.status(200).json(files); } catch (error) { diff --git a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts index b3f5ceb57a..baeebe3c52 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botProject.test.ts @@ -12,7 +12,6 @@ import { BotProject } from '../botProject'; import { LocationRef } from '../interface'; import { Resource } from './../interface'; -import { RecognizerTypes } from './../recognizer'; jest.mock('azure-storage', () => { return {}; @@ -46,7 +45,7 @@ beforeEach(async () => { describe('init', () => { it('should get project successfully', () => { const project: { [key: string]: any } = proj.getProject(); - expect(project.files.length).toBe(15); + expect(project.files.length).toBe(16); }); it('should always have a default bot project file', () => { @@ -125,7 +124,7 @@ describe('copyTo', () => { const newBotProject = await proj.copyTo(locationRef); await newBotProject.init(); const project: { [key: string]: any } = newBotProject.getProject(); - expect(project.files.length).toBe(15); + expect(project.files.length).toBe(16); }); }); @@ -299,9 +298,7 @@ describe('buildFiles', () => { { id: 'b.en-us', isEmpty: false }, { id: 'bot1.en-us', isEmpty: false }, ]; - const crossTrainConfig = {}; - const recognizerTypes: RecognizerTypes = { a: 'DefaultRecognizer', b: 'DefaultRecognizer', c: 'DefaultRecognizer' }; - await proj.buildFiles({ luisConfig, qnaConfig, luResource, qnaResource, crossTrainConfig, recognizerTypes }); + await proj.buildFiles({ luisConfig, qnaConfig, luResource, qnaResource }); try { if (fs.existsSync(path)) { @@ -410,7 +407,7 @@ describe('deleteAllFiles', () => { const newBotProject = await proj.copyTo(locationRef); await newBotProject.init(); const project: { [key: string]: any } = newBotProject.getProject(); - expect(project.files.length).toBe(15); + expect(project.files.length).toBe(16); await newBotProject.deleteAllFiles(); expect(fs.existsSync(copyDir)).toBe(false); }); diff --git a/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts b/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts index 4ff16c3aa0..4a703382de 100644 --- a/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts +++ b/Composer/packages/server/src/models/bot/__tests__/botStructure.test.ts @@ -7,6 +7,18 @@ const botName = 'Mybot'; const defaultLocale = 'en-us'; describe('Bot structure file path', () => { + // cross-train config + it('should get entry cross-train config file path', async () => { + const targetPath = defaultFilePath(botName, defaultLocale, 'cross-train.config.json'); + expect(targetPath).toEqual('settings/cross-train.config.json'); + }); + + // recognizer + it('should get entry recognizer file path', async () => { + const targetPath = defaultFilePath(botName, defaultLocale, 'test.lu.qna.dialog'); + expect(targetPath).toEqual('dialogs/test/recognizers/test.lu.qna.dialog'); + }); + // entry dialog it('should get entry .dialog file path', async () => { const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.dialog'); diff --git a/Composer/packages/server/src/models/bot/__tests__/preBuilder.test.ts b/Composer/packages/server/src/models/bot/__tests__/preBuilder.test.ts deleted file mode 100644 index c39013199d..0000000000 --- a/Composer/packages/server/src/models/bot/__tests__/preBuilder.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { FileInfo } from '@bfc/shared'; - -import { PreBuilder } from './../preBuilder'; -import { CrossTrainConfig } from './../builder'; - -const storage: any = {}; -const builder = new PreBuilder('c:/bot', storage); - -describe('Test the crossTrain config change', () => { - it('should convert the cross train config from id to relativePath', () => { - const configObject: CrossTrainConfig = { - 'main.en-us.lu': { - rootDialog: true, - triggers: { - dia1Trigger: ['dia1.en-us.lu'], - dia2Trigger: ['dia2.en-us.lu'], - }, - }, - 'dia2.en-us.lu': { - rootDialog: false, - triggers: { - dia3Trigger: ['dia3.en-us.lu'], - dia4Trigger: ['dia4.en-us.lu'], - }, - }, - 'main.fr-fr.lu': { - rootDialog: true, - triggers: { - dia1Trigger: ['dia1.fr-fr.lu'], - }, - }, - }; - - const files: FileInfo[] = [ - { - name: 'main.en-us.lu', - content: '', - path: 'c:/bot/lu/en-us/main.en-us.lu', - relativePath: '', - lastModified: '', - }, - { - name: 'dia1.en-us.lu', - content: '', - path: 'c:/bot/dia1/lu/en-us/dia1.en-us.lu', - relativePath: '', - lastModified: '', - }, - { - name: 'dia2.en-us.lu', - content: '', - path: 'c:/bot/dia2/lu/en-us/dia2.en-us.lu', - relativePath: '', - lastModified: '', - }, - { - name: 'dia3.en-us.lu', - content: '', - path: 'c:/bot/dia3/lu/en-us/dia3.en-us.lu', - relativePath: '', - lastModified: '', - }, - { - name: 'dia4.en-us.lu', - content: '', - path: 'c:/bot/dia4/lu/en-us/dia4.en-us.lu', - relativePath: '', - lastModified: '', - }, - { name: 'main.fr-fr.lu', content: '', path: 'c:/bot/lu/fr-fr/main.fr-fr.lu', relativePath: '', lastModified: '' }, - { - name: 'dia1.fr-fr.lu', - content: '', - path: 'c:/bot/dia1/lu/fr-fr/dia1.fr-fr.lu', - relativePath: '', - lastModified: '', - }, - ]; - const result = builder.generateCrossTrainConfig(configObject, files); - expect(result['../dia2/lu/en-us/dia2.en-us.lu']).not.toBeUndefined(); - expect(result['../dia2/lu/en-us/dia2.en-us.lu'].triggers.dia3Trigger[0]).toBe('../dia3/lu/en-us/dia3.en-us.lu'); - }); -}); diff --git a/Composer/packages/server/src/models/bot/__tests__/recognizer.test.ts b/Composer/packages/server/src/models/bot/__tests__/recognizer.test.ts deleted file mode 100644 index 63b60a2497..0000000000 --- a/Composer/packages/server/src/models/bot/__tests__/recognizer.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { SDKKinds } from '@bfc/shared'; - -import { - getCrossTrainedRecognizerDialog, - getLuFileLocale, - getLuisRecognizerDialogs, - getMultiLanguagueRecognizerDialog, - getQnaMakerRecognizerDialogs, - updateRecognizers, -} from '../recognizer'; - -describe('Test the generated recognizer dialogs', () => { - it('should get luis file locale', () => { - expect(getLuFileLocale('a.en-us.lu')).toBe('en-us'); - expect(getLuFileLocale('a.en-us.qna')).toBe('en-us'); - }); - - it('should get MultiLanguagueRecognizer', () => { - const result = getMultiLanguagueRecognizerDialog('test', ['test.en-us.qna', 'test.fr-fr.qna'], 'qna'); - expect(result.name).toBe('test.qna.dialog'); - expect(JSON.parse(result.content).recognizers['en-us']).toBe('test.en-us.qna'); - expect(JSON.parse(result.content).recognizers['']).toBe('test.en-us.qna'); - expect(JSON.parse(result.content).recognizers['fr-fr']).toBe('test.fr-fr.qna'); - }); - - it('should get CrossTrainedRecognizerDialog', () => { - const result = getCrossTrainedRecognizerDialog('test', ['test.en-us.qna', 'test.en-us.lu']); - expect(result.name).toBe('test.lu.qna.dialog'); - expect(JSON.parse(result.content).recognizers[0]).toBe('test.qna'); - expect(JSON.parse(result.content).recognizers[1]).toBe('test.lu'); - }); - - it('should get LuisRecognizerDialogs', () => { - const result = getLuisRecognizerDialogs('test', ['test.en-us.lu', 'test.fr-fr.lu']); - expect(result.length).toBe(2); - expect(result[0].name).toBe('test.en-us.lu.dialog'); - expect(JSON.parse(result[1].content)).toStrictEqual({ - $kind: SDKKinds.LuisRecognizer, - id: `LUIS_test`, - applicationId: `=settings.luis.test_fr_fr_lu.appId`, - version: `=settings.luis.test_fr_fr_lu.version`, - endpoint: '=settings.luis.endpoint', - endpointKey: '=settings.luis.endpointKey', - }); - }); - - it('should get QnaMakerRecognizer', () => { - const result = getQnaMakerRecognizerDialogs('test', ['test.en-us.qna', 'test.fr-fr.qna']); - expect(result.length).toBe(2); - expect(result[0].name).toBe('test.en-us.qna.dialog'); - expect(JSON.parse(result[1].content)).toStrictEqual({ - $kind: SDKKinds.QnAMakerRecognizer, - id: `QnA_test`, - knowledgeBaseId: `=settings.qna.test_fr_fr_qna`, - hostname: '=settings.qna.hostname', - endpointKey: '=settings.qna.endpointKey', - }); - }); - - it('should update recognizer', async () => { - const globMock = jest.fn(() => []); - const writeFileMock = jest.fn(); - await updateRecognizers(true)( - 'test', - ['test.en-us.qna', 'test.fr-fr.qna', 'test.en-us.lu', 'test.fr-fr.lu'], - { glob: globMock, writeFile: writeFileMock } as any, - { defalutLanguage: 'en-us', folderPath: '' } - ); - expect(globMock).toBeCalledWith('test.*', ''); - expect(writeFileMock).toBeCalledTimes(7); - }); -}); diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 431b70d12c..766deba83c 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -30,12 +30,12 @@ import log from '../../logger'; import { BotProjectService } from '../../services/project'; import AssetService from '../../services/asset'; +import { isCrossTrainConfig } from './botStructure'; import { Builder } from './builder'; import { IFileStorage } from './../storage/interface'; import { LocationRef, IBuildConfig } from './interface'; import { retrieveSkillManifests } from './skillManager'; -import { defaultFilePath, serializeFiles, parseFileName } from './botStructure'; -import { PreBuilder } from './preBuilder'; +import { defaultFilePath, serializeFiles, parseFileName, isRecognizer } from './botStructure'; const debug = log.extend('bot-project'); const mkDirAsync = promisify(fs.mkdir); @@ -56,7 +56,6 @@ export class BotProject implements IBotProject { public dataDir: string; public fileStorage: IFileStorage; public builder: Builder; - public preBuilder: PreBuilder; public defaultSDKSchema: { [key: string]: string; }; @@ -82,7 +81,6 @@ export class BotProject implements IBotProject { this.settingManager = new DefaultSettingManager(this.dir); this.fileStorage = StorageService.getStorageClient(this.ref.storageId, user); this.builder = new Builder(this.dir, this.fileStorage, defaultLanguage); - this.preBuilder = new PreBuilder(this.dir, this.fileStorage); } public get dialogFiles() { @@ -418,6 +416,8 @@ export class BotProject implements IBotProject { }; public validateFileName = (name: string) => { + if (isRecognizer(name)) return; + if (isCrossTrainConfig(name)) return; const { fileId, fileType } = parseFileName(name, ''); let fileName = fileId; @@ -451,14 +451,7 @@ export class BotProject implements IBotProject { return createdFiles; }; - public buildFiles = async ({ - luisConfig, - qnaConfig, - luResource = [], - qnaResource = [], - crossTrainConfig, - recognizerTypes, - }: IBuildConfig) => { + public buildFiles = async ({ luisConfig, qnaConfig, luResource = [], qnaResource = [] }: IBuildConfig) => { if (this.settings) { const emptyFiles = {}; const luFiles: FileInfo[] = []; @@ -480,11 +473,8 @@ export class BotProject implements IBotProject { } }); - await this.preBuilder.prebuild(recognizerTypes, { crossTrainConfig, luFiles, qnaFiles, emptyFiles }); - this.builder.setBuildConfig( { ...luisConfig, subscriptionKey: qnaConfig.subscriptionKey, qnaRegion: qnaConfig.qnaRegion }, - crossTrainConfig, this.settings.downsampling ); await this.builder.build(luFiles, qnaFiles, Array.from(this.files.values()) as FileInfo[]); @@ -721,11 +711,20 @@ export class BotProject implements IBotProject { } }; + //migrate the recognizer folder + private removeRecognizers = async () => { + const paths = await this.fileStorage.glob('recognizers/cross-train.config.json', this.dataDir); + if (paths.length) { + await this.fileStorage.rmrfDir(Path.join(this.dataDir, 'recognizers')); + } + }; + private _getFiles = async () => { if (!(await this.exists())) { throw new Error(`${this.dir} is not a valid path`); } + await this.removeRecognizers(); const fileList = new Map(); const patterns = [ '**/*.dialog', @@ -744,13 +743,14 @@ export class BotProject implements IBotProject { 'app.schema', 'app.uischema', '*.botproj', + 'cross-train.config.json', ]; for (const pattern of patterns) { // load only from the data dir, otherwise may get "build" versions from // deployment process const root = this.dataDir; const paths = await this.fileStorage.glob( - [pattern, '!(generated/**)', '!(runtime/**)', '!(recognizers/**)', '!(scripts/**)', '!(settings/**)'], + [pattern, '!(generated/**)', '!(runtime/**)', '!(scripts/**)', '!(settings/appsettings.json)'], root ); diff --git a/Composer/packages/server/src/models/bot/botStructure.ts b/Composer/packages/server/src/models/bot/botStructure.ts index 562a6ddf6a..71518baf26 100644 --- a/Composer/packages/server/src/models/bot/botStructure.ts +++ b/Composer/packages/server/src/models/bot/botStructure.ts @@ -25,10 +25,13 @@ const BotStructureTemplate = { qna: 'dialogs/${DIALOGNAME}/knowledge-base/en-us/${DIALOGNAME}.en-us.qna', sourceQnA: 'dialogs/${DIALOGNAME}/knowledge-base/source/${FILENAME}.source.qna', dialogSchema: 'dialogs/${DIALOGNAME}/${DIALOGNAME}.dialog.schema', + recognizer: 'dialogs/${DIALOGNAME}/recognizers/${RECOGNIZERNAME}', }, formDialogs: 'form-dialogs/${FORMDIALOGNAME}', skillManifests: 'manifests/${MANIFESTFILENAME}', botProject: '${BOTNAME}.botproj', + recognizer: 'recognizers/${RECOGNIZERNAME}', + crossTrainConfig: 'settings/${CROSSTRAINCONFIGNAME}', }; const templateInterpolate = (str: string, obj: { [key: string]: string }) => @@ -71,6 +74,9 @@ export const parseFileName = (name: string, defaultLocale: string) => { return { dialogId, fileId, locale, fileType }; }; +export const isRecognizer = (fileName: string) => fileName.endsWith('.lu.dialog') || fileName.endsWith('.qna.dialog'); +export const isCrossTrainConfig = (fileName: string) => fileName.endsWith('cross-train.config.json'); + export const defaultFilePath = (botName: string, defaultLocale: string, filename: string): string => { const BOTNAME = botName.toLowerCase(); const CommonFileId = 'common'; @@ -78,6 +84,29 @@ export const defaultFilePath = (botName: string, defaultLocale: string, filename const { fileId, locale, fileType, dialogId } = parseFileName(filename, defaultLocale); const LOCALE = locale; + // now recognizer extension is .lu.dialog or .qna.dialog + if (isRecognizer(filename)) { + const isRoot = filename.startsWith(botName.toLowerCase()); + const dialogId = filename.slice(0, filename.indexOf('.')); + if (isRoot) { + return templateInterpolate(BotStructureTemplate.recognizer, { + RECOGNIZERNAME: filename, + }); + } else { + return templateInterpolate(BotStructureTemplate.dialogs.recognizer, { + RECOGNIZERNAME: filename, + DIALOGNAME: dialogId, + }); + } + } + + //crossTrain config's file name is cross-train.config + if (isCrossTrainConfig(filename)) { + return templateInterpolate(BotStructureTemplate.crossTrainConfig, { + CROSSTRAINCONFIGNAME: filename, + }); + } + // 1. Even appsettings.json hit FileExtensions.Manifest, but it never use this do created. // 2. When export bot as a skill, name is `EchoBot-4-2-1-preview-1-manifest.json` if (fileType === FileExtensions.Manifest) { diff --git a/Composer/packages/server/src/models/bot/builder.ts b/Composer/packages/server/src/models/bot/builder.ts index 8289919cdd..475913db1e 100644 --- a/Composer/packages/server/src/models/bot/builder.ts +++ b/Composer/packages/server/src/models/bot/builder.ts @@ -19,8 +19,10 @@ const LuisBuilder = require('@microsoft/bf-lu/lib/parser/luis/luisBuilder'); const luisToLuContent = require('@microsoft/bf-lu/lib/parser/luis/luConverter'); const GENERATEDFOLDER = 'generated'; -const INTERUPTION = 'interuption'; +const SETTINGS = 'settings'; +const INTERRUPTION = 'interruption'; const SAMPLE_SIZE_CONFIGURATION = 2; +const CrossTrainConfigName = 'cross-train.config'; export type SingleConfig = { rootDialog: boolean; @@ -46,7 +48,6 @@ export class Builder { public config: IConfig | null = null; public downSamplingConfig: DownSamplingConfig = { maxImbalanceRatio: 0, maxUtteranceAllowed: 0 }; private _locale: string; - public crossTrainConfig: CrossTrainConfig = {}; private luBuilder = new luBuild.Builder((message) => { log(message); @@ -58,7 +59,7 @@ export class Builder { constructor(path: string, storage: IFileStorage, locale: string) { this.botDir = path; this.generatedFolderPath = Path.join(this.botDir, GENERATEDFOLDER); - this.interruptionFolderPath = Path.join(this.generatedFolderPath, INTERUPTION); + this.interruptionFolderPath = Path.join(this.generatedFolderPath, INTERRUPTION); this.storage = storage; this._locale = locale; } @@ -86,9 +87,8 @@ export class Builder { } }; - public setBuildConfig(config: IConfig, crossTrainConfig: CrossTrainConfig, downSamplingConfig: DownSamplingConfig) { + public setBuildConfig(config: IConfig, downSamplingConfig: DownSamplingConfig) { this.config = config; - this.crossTrainConfig = crossTrainConfig; this.downSamplingConfig = downSamplingConfig; } @@ -109,19 +109,27 @@ export class Builder { } private async crossTrain(luFiles: FileInfo[], qnaFiles: FileInfo[], allFiles: FileInfo[]) { + const crossTrainConfigPath = Path.join(this.botDir, SETTINGS, CrossTrainConfigName); + let crossTrainConfig = {}; + if (await this.storage.exists(crossTrainConfigPath)) { + const crossTrainConfigStr = await this.storage.readFile(crossTrainConfigPath); + if (crossTrainConfigStr) { + crossTrainConfig = JSON.parse(crossTrainConfigStr); + } + } const luContents = luFiles.map((file) => { - return { content: file.content, id: file.name }; + return { content: file.content, id: Path.basename(file.name, '.lu') }; }); const qnaContents = qnaFiles.map((file) => { - return { content: file.content, id: file.name }; + return { content: file.content, id: Path.basename(file.name, '.qna') }; }); const importResolver = luImportResolverGenerator([...getLUFiles(allFiles), ...getQnAFiles(allFiles)]); - const result = await crossTrainer.crossTrain(luContents, qnaContents, this.crossTrainConfig, { importResolver }); + const result = await crossTrainer.crossTrain(luContents, qnaContents, crossTrainConfig, { importResolver }); - await this.writeFiles(result.luResult); - await this.writeFiles(result.qnaResult); + await this.writeFiles(result.luResult, 'lu'); + await this.writeFiles(result.qnaResult, 'qna'); } private async getInterruptionFiles() { @@ -181,13 +189,13 @@ export class Builder { ); } - private async writeFiles(crossTrainResult) { + private async writeFiles(crossTrainResult, fileExtension: 'lu' | 'qna') { if (!(await this.storage.exists(this.interruptionFolderPath))) { await this.storage.mkDir(this.interruptionFolderPath); } await Promise.all( [...crossTrainResult.keys()].map(async (key: string) => { - const fileName = Path.basename(key); + const fileName = `${key}.${fileExtension}`; const newFileId = Path.join(this.interruptionFolderPath, fileName); await this.storage.writeFile(newFileId, crossTrainResult.get(key).Content); }) diff --git a/Composer/packages/server/src/models/bot/interface.ts b/Composer/packages/server/src/models/bot/interface.ts index 4313fb01da..733fd38515 100644 --- a/Composer/packages/server/src/models/bot/interface.ts +++ b/Composer/packages/server/src/models/bot/interface.ts @@ -3,9 +3,6 @@ import { ILuisConfig, IQnAConfig } from '@bfc/shared'; -import { CrossTrainConfig } from './builder'; -import { RecognizerTypes } from './recognizer'; - export type Resource = { id: string; isEmpty: boolean }; export interface LocationRef { @@ -18,8 +15,6 @@ export interface IBuildConfig { qnaConfig: IQnAConfig; luResource: Resource[]; qnaResource: Resource[]; - crossTrainConfig: CrossTrainConfig; - recognizerTypes: RecognizerTypes; } export interface ILuisSettings { diff --git a/Composer/packages/server/src/models/bot/preBuilder.ts b/Composer/packages/server/src/models/bot/preBuilder.ts deleted file mode 100644 index a13492122e..0000000000 --- a/Composer/packages/server/src/models/bot/preBuilder.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { FileInfo } from '@bfc/shared'; -import keys from 'lodash/keys'; - -import { IFileStorage } from '../storage/interface'; - -import { Path } from './../../utility/path'; -import { CrossTrainConfig } from './builder'; -import recognizers, { RecognizerTypes } from './recognizer'; - -const RECOGNIZERS = 'recognizers'; - -export class PreBuilder { - folderPath: string; - storage: IFileStorage; - - constructor(botDir: string, storage: IFileStorage) { - this.folderPath = Path.join(botDir, RECOGNIZERS); - this.storage = storage; - } - - public async prebuild( - recognizerTypes: RecognizerTypes, - options: { - crossTrainConfig?: CrossTrainConfig; - luFiles: FileInfo[]; - qnaFiles: FileInfo[]; - emptyFiles: { [fileName: string]: boolean }; - } - ) { - await this.createRecognizersDir(); - - await this.updateCrossTrainConfig(options.luFiles, options.crossTrainConfig); - - await this.updateRecognizers(recognizerTypes, [...options.luFiles, ...options.qnaFiles], options.emptyFiles); - } - - async createRecognizersDir() { - if (!(await this.storage.exists(this.folderPath))) { - await this.storage.mkDir(this.folderPath); - } - } - - async updateCrossTrainConfig(luFiles: FileInfo[], crossTrainConfig?: CrossTrainConfig) { - if (crossTrainConfig && luFiles.length) { - const configWithPath = this.generateCrossTrainConfig(crossTrainConfig, luFiles); - await this.storage.writeFile( - `${this.folderPath}/cross-train.config.json`, - JSON.stringify(configWithPath, null, 2) - ); - } - } - - replaceCrossTrainId(id: string, files: FileInfo[]) { - if (!id) return id; - const luFile = files.find((item) => item.name === id); - return Path.relative(this.folderPath, luFile?.path ?? ''); - } - - /** - * convert the cross train config from id to relativePath. The cli use the config to find the files. - * config = { - * 'main.lu': { - * rootDialog: true, - * triggers: { - * 'intentA':'diaA.lu', - * 'intentB': 'diaB.lu' - * } - * } - * } - */ - generateCrossTrainConfig(crossTrainConfig: CrossTrainConfig, files: FileInfo[]) { - const pathCache = {}; - - const configWithPath = keys(crossTrainConfig).reduce((result: CrossTrainConfig, key: string) => { - const { triggers: preTriggers, rootDialog } = crossTrainConfig[key]; - // replace the key with path - if (!pathCache[key]) pathCache[key] = this.replaceCrossTrainId(key, files); - - const triggers = keys(preTriggers).reduce((result: { [key: string]: string[] }, key) => { - const ids = preTriggers[key]; - result[key] = ids.map((item) => { - // replace the trigger value with path - if (!pathCache[item]) pathCache[item] = this.replaceCrossTrainId(item, files); - - return pathCache[item]; - }); - return result; - }, {}); - - result[pathCache[key]] = { triggers, rootDialog }; - return result; - }, {}); - - return configWithPath; - } - - /** - * update the recoginzers before build - */ - async updateRecognizers(recognizerTypes: RecognizerTypes, files: FileInfo[], emptyFiles) { - await Promise.all( - keys(recognizerTypes).map(async (item) => { - const type = recognizerTypes[item]; - const targetFiles = files - .filter((file) => file.name.startsWith(item) && !emptyFiles[file.name]) - .map((item) => item.name); - - const updateFunc = recognizers[type] ?? recognizers.Default; - await updateFunc(item, targetFiles, this.storage, { - defalutLanguage: 'en-us', - folderPath: this.folderPath, - }); - }) - ); - } -} diff --git a/Composer/packages/server/src/models/bot/recognizer.ts b/Composer/packages/server/src/models/bot/recognizer.ts deleted file mode 100644 index 9a35abe929..0000000000 --- a/Composer/packages/server/src/models/bot/recognizer.ts +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { SDKKinds } from '@bfc/shared'; - -import { IFileStorage } from '../storage/interface'; - -export type UpdateRecognizer = ( - target: string, - fileNames: string[], - storage: IFileStorage, - options: { defalutLanguage?: string; folderPath?: string } -) => Promise | void; - -export type RecognizerType = SDKKinds.CrossTrainedRecognizerSet | SDKKinds.LuisRecognizer | string; -export type RecognizerTypes = { [fileName: string]: RecognizerType }; - -type GeneratedDialog = { name: string; content: string }; - -const LuisRecognizerTemplate = (target: string, fileName: string) => ({ - $kind: SDKKinds.LuisRecognizer, - id: `LUIS_${target}`, - applicationId: `=settings.luis.${fileName.replace(/[.-]/g, '_')}.appId`, - version: `=settings.luis.${fileName.replace(/[.-]/g, '_')}.version`, - endpoint: '=settings.luis.endpoint', - endpointKey: '=settings.luis.endpointKey', -}); - -const MultiLanguageRecognizerTemplate = (target: string, fileType: 'lu' | 'qna') => ({ - $kind: SDKKinds.MultiLanguageRecognizer, - id: `${fileType === 'lu' ? 'LUIS' : 'QnA'}_${target}`, - recognizers: {}, -}); - -const CrossTrainedRecognizerTemplate = (): { - $kind: string; - recognizers: string[]; -} => ({ - $kind: SDKKinds.CrossTrainedRecognizerSet, - recognizers: [], -}); - -const QnAMakerRecognizerTemplate = (target: string, fileName: string) => ({ - $kind: SDKKinds.QnAMakerRecognizer, - id: `QnA_${target}`, - knowledgeBaseId: `=settings.qna.${fileName.replace(/[.-]/g, '_')}`, - hostname: '=settings.qna.hostname', - endpointKey: '=settings.qna.endpointKey', -}); - -//in composer the luFile name is a.locale.lu -export const getLuFileLocale = (fileName: string) => { - const items = fileName.split('.'); - return items[items.length - 2]; -}; - -export const getMultiLanguagueRecognizerDialog = ( - target: string, - fileNames: string[], - fileType: 'lu' | 'qna', - defalutLanguage = 'en-us' -) => { - const multiLanguageRecognizer = MultiLanguageRecognizerTemplate(target, fileType); - - fileNames.forEach((name) => { - if (!name.startsWith(target)) return; - const locale = getLuFileLocale(name); - multiLanguageRecognizer.recognizers[locale] = name; - if (locale === defalutLanguage) { - multiLanguageRecognizer.recognizers[''] = name; - } - }); - - return { name: `${target}.${fileType}.dialog`, content: JSON.stringify(multiLanguageRecognizer, null, 2) }; -}; - -export const getCrossTrainedRecognizerDialog = (target: string, fileNames: string[]) => { - const crossTrainedRecognizer = CrossTrainedRecognizerTemplate(); - - if (fileNames.some((item) => item.endsWith('.qna'))) { - crossTrainedRecognizer.recognizers.push(`${target}.qna`); - } - - if (fileNames.some((item) => item.endsWith('.lu'))) { - crossTrainedRecognizer.recognizers.push(`${target}.lu`); - } - - return { - name: `${target}.lu.qna.dialog`, - content: JSON.stringify(crossTrainedRecognizer, null, 2), - }; -}; - -export const getLuisRecognizerDialogs = (target: string, luFileNames: string[]) => { - return luFileNames.map((item) => { - const locale = getLuFileLocale(item); - return { - name: `${target}.${locale}.lu.dialog`, - content: JSON.stringify(LuisRecognizerTemplate(target, item), null, 2), - }; - }); -}; - -export const getQnaMakerRecognizerDialogs = (target: string, qnaFileNames: string[]) => { - return qnaFileNames.map((item) => { - const locale = getLuFileLocale(item); - return { - name: `${target}.${locale}.qna.dialog`, - content: JSON.stringify(QnAMakerRecognizerTemplate(target, item), null, 2), - }; - }); -}; - -/** - * DefaultRecognizer: - * luisRecoginzers: create and preserve(exists) - * multiLanguageRecognizer: update the recognizers - * crossTrainedRecognizer: update the recognizers - * - * @param target the dialog Id - * @param fileNames the lu and qna files name list - * @param folderPath the recognizers folder's path - */ -export const updateRecognizers = (isCrosstrain: boolean): UpdateRecognizer => async ( - target: string, - fileNames: string[], - storage: IFileStorage, - { defalutLanguage, folderPath } -) => { - const luFileNames = fileNames.filter((item) => item.endsWith('.lu')); - const qnaFileNames = fileNames.filter((item) => item.endsWith('.qna') && !item.endsWith('.source.qna')); - const luMultiLanguageRecognizerDialog = getMultiLanguagueRecognizerDialog(target, luFileNames, 'lu', defalutLanguage); - const qnaMultiLanguageRecognizerDialog = getMultiLanguagueRecognizerDialog( - target, - qnaFileNames, - 'qna', - defalutLanguage - ); - const luisRecognizersDialogs = getLuisRecognizerDialogs(target, luFileNames); - const qnaMakeRecognizersDialogs = getQnaMakerRecognizerDialogs(target, qnaFileNames); - const needUpdateDialogs: GeneratedDialog[] = []; - let needPreserveDialogs: GeneratedDialog[] = []; - - if (isCrosstrain) { - const crossTrainedRecognizerDialog = getCrossTrainedRecognizerDialog(target, fileNames); - needUpdateDialogs.push(crossTrainedRecognizerDialog); - - if (qnaMakeRecognizersDialogs.length) { - needUpdateDialogs.push(qnaMultiLanguageRecognizerDialog); - } - - needPreserveDialogs = [...needPreserveDialogs, ...qnaMakeRecognizersDialogs]; - } - - needPreserveDialogs = [...needPreserveDialogs, ...luisRecognizersDialogs]; - - if (luisRecognizersDialogs.length) { - needUpdateDialogs.push(luMultiLanguageRecognizerDialog); - } - - const previousFilePaths = await storage.glob(`${target}.*`, folderPath ?? ''); - const currentFiles = [...needUpdateDialogs, ...needPreserveDialogs]; - - //if remove a local, need to delete these files - const needDeleteFiles = previousFilePaths.filter((item) => !currentFiles.some((file) => file.name === item)); - const needupdateFiles = needUpdateDialogs.concat( - needPreserveDialogs.filter((item) => !previousFilePaths.some((path) => path === item.name)) - ); - - await Promise.all( - needDeleteFiles.map(async (fileName) => { - return await storage.removeFile(`${folderPath}/${fileName}`); - }) - ); - - await Promise.all( - needupdateFiles.map(async (item) => { - return await storage.writeFile(`${folderPath}/${item.name}`, item.content); - }) - ); -}; - -export const updateCrossTrained: UpdateRecognizer = updateRecognizers(true); - -export const updateLuis: UpdateRecognizer = updateRecognizers(false); - -/** - * RegexRecognizer now remove all the files - * ToDo: CustomRecognizer now remove all the files - */ -export const removeRecognizers: UpdateRecognizer = async ( - target: string, - fileNames: string[], - storage: IFileStorage, - { folderPath } -) => { - const filePaths = await storage.glob(`${target}.*`, folderPath ?? ''); - - await Promise.all( - filePaths.map(async (fileName) => { - return await storage.removeFile(`${folderPath}/${fileName}`); - }) - ); -}; - -const recognizers: { [key in RecognizerType]: UpdateRecognizer } = { - 'Microsoft.CrossTrainedRecognizerSet': updateCrossTrained, - 'Microsoft.LuisRecognizer': updateLuis, - Default: removeRecognizers, -}; - -export default recognizers; diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index eb953303b9..a51401e16c 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -214,6 +214,8 @@ export type BotAssets = { formDialogSchemas: FormDialogSchema[]; botProjectFile: BotProjectFile; jsonSchemaFiles: JsonSchemaFile[]; + recognizers: RecognizerFile[]; + crossTrainConfig: CrosstrainConfig; }; export type BotInfo = { @@ -251,3 +253,12 @@ export type FormDialogSchemaTemplate = { name: string; isGlobal: boolean; }; + +export type RecognizerFile = { + id: string; + content: any; +}; + +export type CrosstrainConfig = { + [fileName: string]: { rootDialog: boolean; triggers: { [intentName: string]: string[] } }; +}; diff --git a/Composer/yarn.lock b/Composer/yarn.lock index dd5d0436c9..e70993efab 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -3055,10 +3055,54 @@ semver "^5.5.1" tslib "^2.0.3" +"@microsoft/bf-lu@^4.11.0-dev.20201025.69cf2b9": + version "4.11.0-rc.20201026.e06ad47" + resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.11.0-rc.20201026.e06ad47.tgz#a7664b94c9ec75d12e5d11205075a935928587b0" + integrity sha512-Ysjv6Nnr0caxsSNWCafO9Kngb7gqCP5tq8mhUBM3FzsyOcmvfg5VVwTLZSYmogjM/tcr6WxqXQw2y7QG7NKB1A== + dependencies: + "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" + "@azure/ms-rest-azure-js" "2.0.1" + "@types/node-fetch" "~2.5.5" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.19" + node-fetch "~2.6.0" + semver "^5.5.1" + tslib "^2.0.3" + +"@microsoft/bf-lu@^4.11.0-rc.20201028.6f4722f": + version "4.11.0-rc.20201028.6f4722f" + resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.11.0-rc.20201028.6f4722f.tgz#76e28d6737184f4915d11f6d59b3d2468c9ab947" + integrity sha512-KMVS6I/F6pMyTb5M65raBrYmglRGkcGeKRmxld1XErTKtdz7aNF007BGug/ISJn3JIDZJ5FExIqPy27ns438oQ== + dependencies: + "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" + "@azure/ms-rest-azure-js" "2.0.1" + "@types/node-fetch" "~2.5.5" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.19" + node-fetch "~2.6.0" + semver "^5.5.1" + tslib "^2.0.3" + "@microsoft/bf-lu@next": - version "4.11.0-dev.20201015.a41c691" - resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.11.0-dev.20201015.a41c691.tgz#1002eb10b7625fead68274c007ac857de79f4446" - integrity sha512-Bq/4NJ8FpJV/wOOdjxLPMHmZoT08qSKozFbnukjdh5L0UNDDDyhTSSLmG1hdGc337VmtQF6YBsLu9sEmATSJUA== + version "4.12.0-dev.20201028.999bc32" + resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.12.0-dev.20201028.999bc32.tgz#be2d651dd0932d3d77c7fbca959837e61b4d5a92" + integrity sha512-GQWaH6IXNOC2qAFmURAT3sl5YD1MaBgpLAy4nS3r5zUcLPNJjE8taksueqtQwltvCWJGbZdPLgPVTdwI+iBR3g== dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -3077,6 +3121,23 @@ semver "^5.5.1" tslib "^2.0.3" +"@microsoft/bf-orchestrator@4.11.0-beta.20201013.20d7917": + version "4.11.0-beta.20201013.20d7917" + resolved "https://registry.yarnpkg.com/@microsoft/bf-orchestrator/-/bf-orchestrator-4.11.0-beta.20201013.20d7917.tgz#525e167625ce2c64f079d937aeb7495a07a7a1d0" + integrity sha512-J5yobTheQuE+gme1R30xIIlCU9xVaiHLUrXyI6xZnT922EYEGsn7qAWfeuP0AM6b5JgD4exAOwHf02ggOIZmGQ== + dependencies: + "@microsoft/bf-dispatcher" "4.11.0-beta.20201013.20d7917" + "@microsoft/bf-lu" next + "@types/fs-extra" "~8.1.0" + "@types/node-fetch" "~2.5.5" + fast-text-encoding "^1.0.3" + fs-extra "~9.0.0" + node-7z-forall "~1.0.5" + node-fetch "~2.6.0" + orchestrator-core beta + read-text-file "~1.1.0" + tslib "^1.10.0" + "@microsoft/load-themed-styles@^1.10.26": version "1.10.39" resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.39.tgz#23024bfa264a01ab2f05a9fda9c97c1f90ef80d8" diff --git a/extensions/azurePublish/src/luisAndQnA.ts b/extensions/azurePublish/src/luisAndQnA.ts index 1c5a2254e3..8afb9db7a5 100644 --- a/extensions/azurePublish/src/luisAndQnA.ts +++ b/extensions/azurePublish/src/luisAndQnA.ts @@ -23,7 +23,7 @@ export interface PublishConfig { [key: string]: any; } -const INTERUPTION = 'interuption'; +const INTERRUPTION = 'interruption'; export class LuisAndQnaPublish { private logger: (string) => any; @@ -37,7 +37,7 @@ export class LuisAndQnaPublish { // path to the ready to deploy generated folder this.remoteBotPath = path.join(config.projPath, 'ComposerDialogs'); this.generatedFolder = path.join(this.remoteBotPath, 'generated'); - this.interruptionFolderPath = path.join(this.generatedFolder, INTERUPTION); + this.interruptionFolderPath = path.join(this.generatedFolder, INTERRUPTION); // Cross Train config this.crossTrainConfig = {