Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Composer/packages/client/src/recoilModel/Recognizers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const Recognizer = React.memo((props: { projectId: string }) => {
useEffect(() => {
let recognizers: RecognizerFile[] = [];
dialogs
.filter((dialog) => !dialog.isFormDialog)
.filter((dialog) => isCrossTrainedRecognizerSet(dialog) || isLuisRecognizer(dialog))
.forEach((dialog) => {
const filtedLus = luFiles.filter((item) => item.id.startsWith(dialog.id));
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/client/src/recoilModel/atoms/botState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const emptyDialog: DialogInfo = {
triggers: [],
intentTriggers: [],
skills: [],
isFormDialog: false,
};
type dialogStateParams = { projectId: string; dialogId: string };
export const dialogState = atomFamily<DialogInfo, dialogStateParams>({
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/client/src/shell/useShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const stubDialog = (): DialogInfo => ({
triggers: [],
intentTriggers: [],
skills: [],
isFormDialog: false,
});

export function useShell(source: EventSource, projectId: string): Shell {
Expand Down
2 changes: 2 additions & 0 deletions Composer/packages/lib/indexers/src/dialogIndexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function parse(id: string, content: any) {
const luFile = typeof content.recognizer === 'string' ? content.recognizer : '';
const qnaFile = typeof content.recognizer === 'string' ? content.recognizer : '';
const lgFile = typeof content.generator === 'string' ? content.generator : '';
const isFormDialog = has(content, 'schema'); // mark as form generated dialog;
const diagnostics: Diagnostic[] = [];
return {
id,
Expand All @@ -194,6 +195,7 @@ function parse(id: string, content: any) {
triggers: extractTriggers(content),
intentTriggers: extractIntentTriggers(content),
skills: extractReferredSkills(content),
isFormDialog,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,105 +9,111 @@ 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');
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');
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 <botName>.dialog file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.dialog');
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.dialog', {});
expect(targetPath).toEqual('mybot.dialog');
});

// common.lg
it('should get common.lg file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'common.en-us.lg');
const targetPath = defaultFilePath(botName, defaultLocale, 'common.en-us.lg', {});
expect(targetPath).toEqual('language-generation/en-us/common.en-us.lg');
});

// common.zh-cn.lg
it('should get common.<locale>.lg file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'common.zh-cn.lg');
const targetPath = defaultFilePath(botName, defaultLocale, 'common.zh-cn.lg', {});
expect(targetPath).toEqual('language-generation/zh-cn/common.zh-cn.lg');
});

// skill manifest
it('should get exported skill manifest file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'EchoBot-4-2-1-preview-1-manifest.json');
const targetPath = defaultFilePath(botName, defaultLocale, 'EchoBot-4-2-1-preview-1-manifest.json', {});
expect(targetPath).toEqual('manifests/EchoBot-4-2-1-preview-1-manifest.json');
});

// mybot.en-us.lg
it('should get entry dialog.lg file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lg');
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lg', {});
expect(targetPath).toEqual('language-generation/en-us/mybot.en-us.lg');
});

// mybot.zh-cn.lg
it('should get entry dialog.lg file path', async () => {
const targetPath = defaultFilePath('my-bot', defaultLocale, 'my-bot.zh-cn.lg');
const targetPath = defaultFilePath('my-bot', defaultLocale, 'my-bot.zh-cn.lg', {});
expect(targetPath).toEqual('language-generation/zh-cn/my-bot.zh-cn.lg');
});

// entry dialog's lu
it('should get entry dialog.lu file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lu');
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.lu', {});
expect(targetPath).toEqual('language-understanding/en-us/mybot.en-us.lu');
});

// child dialog's entry
it('should get child dialog file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.dialog');
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.dialog', {});
expect(targetPath).toEqual('dialogs/greeting/greeting.dialog');
});

// entry dialog's qna
it('should get entry dialog.qna file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.qna');
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.en-us.qna', {});
expect(targetPath).toEqual('knowledge-base/en-us/mybot.en-us.qna');
});

// entry dialog's source qna
it('should get entry dialog.source.qna file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna');
const targetPath = defaultFilePath(botName, defaultLocale, 'mybot.myimport1.source.qna', {});
expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna');
});

// shared source qna
it('should get shared <name>.source.qna file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.source.qna');
const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.source.qna', {});
expect(targetPath).toEqual('knowledge-base/source/myimport1.source.qna');
});

// child dialog's lg
it('should get child dialog-lg file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lg');
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lg', {});
expect(targetPath).toEqual('dialogs/greeting/language-generation/en-us/greeting.en-us.lg');
});

// child dialog's lu
it('should get child dialog-lu file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lu');
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.lu', {});
expect(targetPath).toEqual('dialogs/greeting/language-understanding/en-us/greeting.en-us.lu');
});

// child dialog's qna
it('should get child dialog-qna file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.qna');
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.en-us.qna', {});
expect(targetPath).toEqual('dialogs/greeting/knowledge-base/en-us/greeting.en-us.qna');
});

// child dialog's source qna
it('should get child dialog.source.qna file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna');
const targetPath = defaultFilePath(botName, defaultLocale, 'greeting.myimport1.source.qna', {});
expect(targetPath).toEqual('dialogs/greeting/knowledge-base/source/myimport1.source.qna');
});

// customized endpoint
it('should get child dialog.source.qna file path', async () => {
const targetPath = defaultFilePath(botName, defaultLocale, 'myimport1.qna', { endpoint: 'dialogs/Welcome' });
expect(targetPath).toEqual('dialogs/Welcome/knowledge-base/en-us/myimport1.en-us.qna');
});
});

describe('Parse file name', () => {
Expand Down
29 changes: 24 additions & 5 deletions Composer/packages/server/src/models/bot/botProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { promisify } from 'util';
import fs from 'fs';

import has from 'lodash/has';
import axios from 'axios';
import { autofixReferInDialog } from '@bfc/indexers';
import {
Expand Down Expand Up @@ -436,7 +437,14 @@ export class BotProject implements IBotProject {
this._validateFileContent(name, content);
const botName = this.name;
const defaultLocale = this.settings?.defaultLanguage || defaultLanguage;
const relativePath = defaultFilePath(botName, defaultLocale, filename, this.rootDialogId);

// find created file belong to which dialog, all resources should be writed to <dialog>/
const dialogId = name.split('.')[0];
const dialogFile = this.files.get(`${dialogId}.dialog`);
const endpoint = dialogFile ? Path.dirname(dialogFile.relativePath) : '';
const rootDialogId = this.rootDialogId;

const relativePath = defaultFilePath(botName, defaultLocale, filename, { endpoint, rootDialogId });
const file = this.files.get(filename);
if (file) {
throw new Error(`${filename} dialog already exist`);
Expand Down Expand Up @@ -535,10 +543,10 @@ export class BotProject implements IBotProject {

public async generateDialog(name: string, templateDirs?: string[]) {
const defaultLocale = this.settings?.defaultLanguage || defaultLanguage;
const relativePath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.FormDialogSchema}`);
const relativePath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.FormDialogSchema}`, {});
const schemaPath = Path.resolve(this.dir, relativePath);

const dialogPath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.Dialog}`);
const dialogPath = defaultFilePath(this.name, defaultLocale, `${name}${FileExtensions.Dialog}`, {});
const outDir = Path.dirname(Path.resolve(this.dir, dialogPath));

const feedback = (type: FeedbackType, message: string): void => {
Expand Down Expand Up @@ -585,7 +593,7 @@ export class BotProject implements IBotProject {

public async deleteFormDialog(dialogId: string) {
const defaultLocale = this.settings?.defaultLanguage || defaultLanguage;
const dialogPath = defaultFilePath(this.name, defaultLocale, `${dialogId}${FileExtensions.Dialog}`);
const dialogPath = defaultFilePath(this.name, defaultLocale, `${dialogId}${FileExtensions.Dialog}`, {});
const dirToDelete = Path.dirname(Path.resolve(this.dir, dialogPath));

// I check that the path is longer 3 to avoid deleting a drive and all its contents.
Expand Down Expand Up @@ -790,11 +798,22 @@ export class BotProject implements IBotProject {

// migration: create qna files for old bots
private _createQnAFilesForOldBot = async (files: Map<string, FileInfo>) => {
// flowing migration scripts depends on files;
this.files = new Map<string, FileInfo>([...files]);
const dialogFiles: FileInfo[] = [];
const qnaFiles: FileInfo[] = [];
files.forEach((file) => {
if (file.name.endsWith('.dialog')) {
dialogFiles.push(file);
try {
// filter form dialog generated file.
const dialogJson = JSON.parse(file.content);
const isFormDialog = has(dialogJson, 'schema');
if (!isFormDialog) {
dialogFiles.push(file);
}
} catch (_e) {
// ignore
}
}
if (file.name.endsWith('.qna')) {
qnaFiles.push(file);
Expand Down
45 changes: 40 additions & 5 deletions Composer/packages/server/src/models/bot/botStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,27 @@ export const defaultFilePath = (
botName: string,
defaultLocale: string,
filename: string,
rootDialogId = ''
options: {
endpoint?: string; // <endpoint>/<file-path>
rootDialogId?: string;
}
): string => {
const BOTNAME = botName.toLowerCase();
const CommonFileId = 'common';

const { endpoint = '', rootDialogId = '' } = options;
const { fileId, locale, fileType, dialogId } = parseFileName(filename, defaultLocale);
const LOCALE = locale;

// now recognizer extension is .lu.dialog or .qna.dialog
if (isRecognizer(filename)) {
const dialogId = filename.split('.')[0];
const isRoot = filename.startsWith(botName) || (rootDialogId && filename.startsWith(rootDialogId));
if (isRoot) {
if (endpoint) {
return templateInterpolate(Path.join(endpoint, BotStructureTemplate.recognizer), {
RECOGNIZERNAME: filename,
});
} else if (isRoot) {
return templateInterpolate(BotStructureTemplate.recognizer, {
RECOGNIZERNAME: filename,
});
Expand Down Expand Up @@ -137,19 +145,46 @@ export const defaultFilePath = (
const isRootFile = BOTNAME === DIALOGNAME.toLowerCase();

if (fileType === FileExtensions.SourceQnA) {
if (endpoint) {
return templateInterpolate(Path.join(endpoint, BotStructureTemplate.sourceQnA), {
FILENAME: fileId,
DIALOGNAME,
});
}
const TemplatePath =
isRootFile || !dialogId ? BotStructureTemplate.sourceQnA : BotStructureTemplate.dialogs.sourceQnA;
return templateInterpolate(TemplatePath, {
FILENAME: fileId,
DIALOGNAME,
});
}

return templateInterpolate(BotStructureTemplate.skillManifests, {
MANIFESTFILENAME: filename,
let TemplatePath = '';

if (endpoint) {
switch (fileType) {
case FileExtensions.Dialog:
TemplatePath = BotStructureTemplate.entry;
break;
case FileExtensions.Lg:
TemplatePath = BotStructureTemplate.lg;
break;
case FileExtensions.Lu:
TemplatePath = BotStructureTemplate.lu;
break;
case FileExtensions.Qna:
TemplatePath = BotStructureTemplate.qna;
break;
case FileExtensions.DialogSchema:
TemplatePath = BotStructureTemplate.dialogSchema;
}
return templateInterpolate(Path.join(endpoint, TemplatePath), {
BOTNAME: fileId,
DIALOGNAME,
LOCALE,
});
}

let TemplatePath = '';
if (fileType === FileExtensions.Dialog) {
TemplatePath = isRootFile ? BotStructureTemplate.entry : BotStructureTemplate.dialogs.entry;
} else if (fileType === FileExtensions.Lg) {
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/types/src/indexers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type DialogInfo = {
triggers: ITrigger[];
intentTriggers: IIntentTrigger[];
skills: string[];
isFormDialog: boolean;
};

export type LgTemplateJsonPath = {
Expand Down