diff --git a/Composer/packages/server/src/models/bot/__tests__/recognizer.test.ts b/Composer/packages/server/src/models/bot/__tests__/recognizer.test.ts new file mode 100644 index 0000000000..63b60a2497 --- /dev/null +++ b/Composer/packages/server/src/models/bot/__tests__/recognizer.test.ts @@ -0,0 +1,74 @@ +// 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/recognizer.ts b/Composer/packages/server/src/models/bot/recognizer.ts index 8bf8181f02..9a35abe929 100644 --- a/Composer/packages/server/src/models/bot/recognizer.ts +++ b/Composer/packages/server/src/models/bot/recognizer.ts @@ -25,9 +25,9 @@ const LuisRecognizerTemplate = (target: string, fileName: string) => ({ endpointKey: '=settings.luis.endpointKey', }); -const MultiLanguageRecognizerTemplate = (target: string) => ({ +const MultiLanguageRecognizerTemplate = (target: string, fileType: 'lu' | 'qna') => ({ $kind: SDKKinds.MultiLanguageRecognizer, - id: `LUIS_${target}`, + id: `${fileType === 'lu' ? 'LUIS' : 'QnA'}_${target}`, recognizers: {}, }); @@ -39,27 +39,41 @@ const CrossTrainedRecognizerTemplate = (): { recognizers: [], }); -//in composer the luFile name is a.local.lu -const getLuFileLocal = (fileName: string) => { +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]; }; -const getMultiLanguagueRecognizerDialog = (target: string, luFileNames: string[], defalutLanguage = 'en-us') => { - const multiLanguageRecognizer = MultiLanguageRecognizerTemplate(target); - - luFileNames.forEach((item) => { - const local = getLuFileLocal(item); - multiLanguageRecognizer.recognizers[local] = item; - if (local === defalutLanguage) { - multiLanguageRecognizer.recognizers[''] = item; +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}.lu.dialog`, content: JSON.stringify(multiLanguageRecognizer, null, 2) }; + return { name: `${target}.${fileType}.dialog`, content: JSON.stringify(multiLanguageRecognizer, null, 2) }; }; -const getCrossTrainedRecognizerDialog = (target: string, fileNames: string[]) => { +export const getCrossTrainedRecognizerDialog = (target: string, fileNames: string[]) => { const crossTrainedRecognizer = CrossTrainedRecognizerTemplate(); if (fileNames.some((item) => item.endsWith('.qna'))) { @@ -76,16 +90,26 @@ const getCrossTrainedRecognizerDialog = (target: string, fileNames: string[]) => }; }; -const getLuisRecognizerDialogs = (target: string, luFileNames: string[]) => { +export const getLuisRecognizerDialogs = (target: string, luFileNames: string[]) => { return luFileNames.map((item) => { - const local = getLuFileLocal(item); + const locale = getLuFileLocale(item); return { - name: `${target}.${local}.lu.dialog`, + 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) @@ -96,29 +120,41 @@ const getLuisRecognizerDialogs = (target: string, luFileNames: string[]) => { * @param fileNames the lu and qna files name list * @param folderPath the recognizers folder's path */ -const updateRecognizers = (isCrosstrain: boolean): UpdateRecognizer => async ( +export const updateRecognizers = (isCrosstrain: boolean): UpdateRecognizer => async ( target: string, fileNames: string[], storage: IFileStorage, { defalutLanguage, folderPath } ) => { const luFileNames = fileNames.filter((item) => item.endsWith('.lu')); - const multiLanguageRecognizerDialog = getMultiLanguagueRecognizerDialog(target, luFileNames, defalutLanguage); + 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[] = []; - const needPreserveDialogs: GeneratedDialog[] = []; + let needPreserveDialogs: GeneratedDialog[] = []; if (isCrosstrain) { const crossTrainedRecognizerDialog = getCrossTrainedRecognizerDialog(target, fileNames); needUpdateDialogs.push(crossTrainedRecognizerDialog); + + if (qnaMakeRecognizersDialogs.length) { + needUpdateDialogs.push(qnaMultiLanguageRecognizerDialog); + } + + needPreserveDialogs = [...needPreserveDialogs, ...qnaMakeRecognizersDialogs]; } - luisRecognizersDialogs.forEach((item) => { - needPreserveDialogs.push(item); - }); + needPreserveDialogs = [...needPreserveDialogs, ...luisRecognizersDialogs]; if (luisRecognizersDialogs.length) { - needUpdateDialogs.push(multiLanguageRecognizerDialog); + needUpdateDialogs.push(luMultiLanguageRecognizerDialog); } const previousFilePaths = await storage.glob(`${target}.*`, folderPath ?? '');