Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit 053b221

Browse files
committed
Merge branch 'master' into emilio/luis
2 parents 533a947 + 8f4567f commit 053b221

File tree

14 files changed

+960
-428
lines changed

14 files changed

+960
-428
lines changed

packages/luis/src/parser/converters/luistoluconverter.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ module.exports = {
7979
if(updatedText) fileContent += '- ' + updatedText + NEWLINE;
8080
});
8181
fileContent += NEWLINE + NEWLINE;
82+
if (intent.intent.features) {
83+
fileContent += `@ intent ${intent.intent.name}`;
84+
fileContent += addRolesAndFeatures(intent.intent);
85+
fileContent += NEWLINE + NEWLINE;
86+
}
8287
});
8388
}
8489

packages/luis/src/parser/lufile/classes/hclasses.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,15 @@ const readerObj = {
131131
return this.roles.includes(value);
132132
}
133133
},
134-
intentFeature: class {
134+
featureToModel: class {
135135
constructor(name) {
136136
this.featureName = name ? name : '';
137137
}
138+
},
139+
modelToFeature: class {
140+
constructor(name) {
141+
this.modelName = name ? name : '';
142+
}
138143
}
139144
};
140145

packages/luis/src/parser/lufile/enums/lusiEntityTypes.js renamed to packages/luis/src/parser/lufile/enums/luisEntityTypes.js

File renamed without changes.

packages/luis/src/parser/lufile/enums/luisobjenum.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ module.exports = {
1111
UTTERANCE: "utterances",
1212
PATTERNS: "patterns",
1313
REGEX: "regex_entities",
14-
COMPOSITES: "composites"
14+
COMPOSITES: "composites",
15+
MACHINELEARNED: "machine-learned"
1516
};

packages/luis/src/parser/lufile/parseFileContents.js

Lines changed: 178 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ const fileToParse = require('./classes/filesToParse');
2424
const luParser = require('./luParser');
2525
const DiagnosticSeverity = require('./diagnostic').DiagnosticSeverity;
2626
const BuildDiagnostic = require('./diagnostic').BuildDiagnostic;
27-
const EntityTypeEnum = require('./enums/lusiEntityTypes');
27+
const EntityTypeEnum = require('./enums/luisEntityTypes');
2828
const luisEntityTypeMap = require('./enums/luisEntityTypeNameMap');
29+
const plAllowedTypes = ["simple", "composite", "machine-learned"];
30+
const featureTypeEnum = {
31+
featureToModel: 'modelName',
32+
modelToFeature: 'featureName'
33+
};
2934
const INTENTTYPE = 'intent';
3035
const parseFileContentsModule = {
3136
/**
@@ -148,7 +153,86 @@ const parseLuAndQnaWithAntlr = async function (parsedContent, fileContent, log,
148153
parseFeatureSections(parsedContent, featuresToProcess);
149154
}
150155
}
151-
156+
/**
157+
* Helper function to validate if the requested feature addition is valid.
158+
* @param {String} srcItemType
159+
* @param {String} srcItemName
160+
* @param {String} tgtFeatureType
161+
* @param {String} tgtFeatureName
162+
* @param {String} line
163+
*/
164+
const validateFeatureAssignment = function(srcItemType, srcItemName, tgtFeatureType, tgtFeatureName, line) {
165+
switch(srcItemType) {
166+
case INTENTTYPE:
167+
case EntityTypeEnum.SIMPLE:
168+
case EntityTypeEnum.ML:
169+
case EntityTypeEnum.COMPOSITE:
170+
// can use everything as a feature except pattern.any
171+
if (tgtFeatureType === EntityTypeEnum.PATTERNANY) {
172+
let errorMsg = `'patternany' entity cannot be added as a feature. Invalid definition found for "@ ${srcItemType} ${srcItemName} usesFeature ${tgtFeatureName}"`;
173+
let error = BuildDiagnostic({
174+
message: errorMsg,
175+
context: line
176+
})
177+
throw (new exception(retCode.errorCode.INVALID_INPUT, error.toString()));
178+
}
179+
break;
180+
default:
181+
// cannot have any features assigned
182+
let errorMsg = `Invalid definition found for "@ ${srcItemType} ${srcItemName} usesFeature ${tgtFeatureName}". usesFeature is only available for intent, ${plAllowedTypes.join(', ')}`;
183+
let error = BuildDiagnostic({
184+
message: errorMsg,
185+
context: line
186+
})
187+
throw (new exception(retCode.errorCode.INVALID_INPUT, error.toString()));
188+
break;
189+
}
190+
}
191+
/**
192+
* Helper function to add features to the parsed content scope.
193+
* @param {Object} tgtItem
194+
* @param {String} feature
195+
* @param {String} featureType
196+
* @param {Object} line
197+
*/
198+
const addFeatures = function(tgtItem, feature, featureType, line) {
199+
// target item cannot have the same name as the feature name
200+
if (tgtItem.name === feature) {
201+
// Item must be defined before being added as a feature.
202+
let errorMsg = `Source and target cannot be the same for usesFeature. e.g. x usesFeature x is invalid. "${tgtItem.name}" usesFeature "${feature}" is invalid.`;
203+
let error = BuildDiagnostic({
204+
message: errorMsg,
205+
context: line
206+
})
207+
throw (new exception(retCode.errorCode.INVALID_INPUT, error.toString()));
208+
}
209+
let featureAlreadyDefined = (tgtItem.features || []).find(item => item.modelName == feature || item.featureName == feature);
210+
switch (featureType) {
211+
case featureTypeEnum.featureToModel: {
212+
if (tgtItem.features) {
213+
if (!featureAlreadyDefined) tgtItem.features.push(new helperClass.featureToModel(feature));
214+
} else {
215+
tgtItem.features = new Array(new helperClass.featureToModel(feature));
216+
}
217+
break;
218+
}
219+
case featureTypeEnum.modelToFeature: {
220+
if (tgtItem.features) {
221+
if (!featureAlreadyDefined) tgtItem.features.push(new helperClass.modelToFeature(feature));
222+
} else {
223+
tgtItem.features = new Array(new helperClass.modelToFeature(feature));
224+
}
225+
break;
226+
}
227+
default:
228+
break;
229+
}
230+
}
231+
/**
232+
* Helper function to handle usesFeature definitions
233+
* @param {Object} parsedContent
234+
* @param {Object} featuresToProcess
235+
*/
152236
const parseFeatureSections = function(parsedContent, featuresToProcess) {
153237
// We are only interested in extracting features and setting things up here.
154238
(featuresToProcess || []).forEach(section => {
@@ -170,21 +254,24 @@ const parseFeatureSections = function(parsedContent, featuresToProcess) {
170254
let featuresList = section.Features.split(/[,;]/g).map(item => item.trim());
171255
(featuresList || []).forEach(feature => {
172256
let entityExists = (parsedContent.LUISJsonStructure.flatListOfEntityAndRoles || []).find(item => item.name == feature || item.name == `${feature}(interchangeable)`);
257+
let featureIntentExists = (parsedContent.LUISJsonStructure.intents || []).find(item => item.name == feature);
173258
if (entityExists) {
174259
if (entityExists.type === EntityTypeEnum.PHRASELIST) {
175260
// de-dupe and add features to intent.
176-
if (intentExists.features) {
177-
let featureAlreadyDefined = (intentExists.features || []).find(item => item.featureName == feature);
178-
if (!featureAlreadyDefined) intentExists.features.push(new helperClass.intentFeature(feature));
179-
} else {
180-
intentExists.features = [new helperClass.intentFeature(feature)];
181-
}
261+
validateFeatureAssignment(section.Type, section.Name, entityExists.type, feature, section.ParseTree.newEntityLine());
262+
addFeatures(intentExists, feature, featureTypeEnum.featureToModel, section.ParseTree.newEntityLine());
182263
// set enabledForAllModels on this phrase list
183264
let plEnity = parsedContent.LUISJsonStructure.model_features.find(item => item.name == feature);
184265
plEnity.enabledForAllModels = false;
185266
} else {
186267
// de-dupe and add model to intent.
268+
validateFeatureAssignment(section.Type, section.Name, entityExists.type, feature, section.ParseTree.newEntityLine());
269+
addFeatures(intentExists, feature, featureTypeEnum.modelToFeature, section.ParseTree.newEntityLine());
187270
}
271+
} else if (featureIntentExists) {
272+
// Add intent as a feature to another intent
273+
validateFeatureAssignment(section.Type, section.Name, INTENTTYPE, feature, section.ParseTree.newEntityLine());
274+
addFeatures(intentExists, feature, featureTypeEnum.modelToFeature, section.ParseTree.newEntityLine());
188275
} else {
189276
// Item must be defined before being added as a feature.
190277
let errorMsg = `Features must be defined before assigned to an intent. No definition found for feature "${feature}" in usesFeature definition for intent "${section.Name}"`;
@@ -207,28 +294,31 @@ const parseFeatureSections = function(parsedContent, featuresToProcess) {
207294
// handle as entity
208295
if (section.Features) {
209296
let featuresList = section.Features.split(/[,;]/g).map(item => item.trim());
297+
// Find the source entity from the collection and get its type
298+
let srcEntityInFlatList = (parsedContent.LUISJsonStructure.flatListOfEntityAndRoles || []).find(item => item.name == section.Name);
299+
let entityType = srcEntityInFlatList ? srcEntityInFlatList.type : undefined;
210300
(featuresList || []).forEach(feature => {
211-
let entityExists = (parsedContent.LUISJsonStructure.flatListOfEntityAndRoles || []).find(item => item.name == section.Name);
212-
let entityType = undefined;
213-
if (entityExists) entityType = entityExists.type;
214301
let featureExists = (parsedContent.LUISJsonStructure.flatListOfEntityAndRoles || []).find(item => item.name == feature || item.name == `${feature}(interchangeable)`);
302+
let featureIntentExists = (parsedContent.LUISJsonStructure.intents || []).find(item => item.name == feature);
303+
// find the entity based on its type.
304+
let srcEntity = (parsedContent.LUISJsonStructure[luisEntityTypeMap[entityType]] || []).find(item => item.name == section.Name);
215305
if (featureExists) {
216-
// find the entity based on its type.
217-
let entityFound = (parsedContent.LUISJsonStructure[luisEntityTypeMap[entityType]] || []).find(item => item.name == section.Name);
218306
if (featureExists.type === EntityTypeEnum.PHRASELIST) {
219307
// de-dupe and add features to intent.
220-
if (entityFound.features) {
221-
let featureAlreadyDefined = (entityFound.features || []).find(item => item.featureName == feature);
222-
if (!featureAlreadyDefined) entityFound.features.push(new helperClass.intentFeature(feature));
223-
} else {
224-
entityFound.features = [new helperClass.intentFeature(feature)];
225-
}
308+
validateFeatureAssignment(entityType, section.Name, featureExists.type, feature, section.ParseTree.newEntityLine());
309+
addFeatures(srcEntity, feature, featureTypeEnum.featureToModel, section.ParseTree.newEntityLine());
226310
// set enabledForAllModels on this phrase list
227311
let plEnity = parsedContent.LUISJsonStructure.model_features.find(item => item.name == feature);
228312
plEnity.enabledForAllModels = false;
229313
} else {
230314
// de-dupe and add model to intent.
315+
validateFeatureAssignment(entityType, section.Name, featureExists.type, feature, section.ParseTree.newEntityLine());
316+
addFeatures(srcEntity, feature, featureTypeEnum.modelToFeature, section.ParseTree.newEntityLine());
231317
}
318+
} else if (featureIntentExists) {
319+
// Add intent as a feature to another intent
320+
validateFeatureAssignment(entityType, section.Name, INTENTTYPE, feature, section.ParseTree.newEntityLine());
321+
addFeatures(srcEntity, feature, featureTypeEnum.modelToFeature, section.ParseTree.newEntityLine());
232322
} else {
233323
// Item must be defined before being added as a feature.
234324
let errorMsg = `Features must be defined before assigned to an entity. No definition found for feature "${feature}" in usesFeature definition for entity "${section.Name}"`;
@@ -241,8 +331,76 @@ const parseFeatureSections = function(parsedContent, featuresToProcess) {
241331
});
242332
}
243333
}
244-
})
334+
});
335+
336+
// Circular dependency for features is not allowed. E.g. A usesFeature B usesFeature A is not valid.
337+
verifyNoCircularDependencyForFeatures(parsedContent);
245338
}
339+
340+
/**
341+
* Helper function to update a list of dependencies for usesFeature
342+
* @param {String} type
343+
* @param {Object} parsedContent
344+
* @param {Object} dependencyList
345+
*/
346+
const updateDependencyList = function(type, parsedContent, dependencyList) {
347+
// go through intents and capture dependency list
348+
(parsedContent.LUISJsonStructure[type] || []).forEach(itemOfType => {
349+
let srcName = itemOfType.name;
350+
let copySrc, copyValue;
351+
if (itemOfType.features) {
352+
(itemOfType.features || []).forEach(feature => {
353+
if (feature.modelName) feature = feature.modelName;
354+
if (feature.featureName) feature = feature.featureName;
355+
// find any items where this feature is the target
356+
let featureDependencyEx = dependencyList.filter(item => srcName == (item.value ? item.value.slice(-1)[0] : undefined));
357+
(featureDependencyEx || []).forEach(item => {
358+
item.key = `${item.key.split('::')[0]}::${feature}`;
359+
item.value.push(feature);
360+
})
361+
// find any items where this feature is the source
362+
featureDependencyEx = dependencyList.find(item => feature == (item.value ? item.value.slice(0)[0] : undefined));
363+
if (featureDependencyEx) {
364+
copySrc = featureDependencyEx.key.split('::')[1];
365+
copyValue = featureDependencyEx.value.slice(1);
366+
}
367+
let dependencyExists = dependencyList.find(item => item.key == `${srcName}::${feature}`);
368+
if (!dependencyExists) {
369+
let lKey = copySrc ? `${srcName}::${copySrc}` : `${srcName}::${feature}`;
370+
let lValue = [srcName, feature];
371+
if (copyValue) copyValue.forEach(item => lValue.push(item));
372+
dependencyList.push({
373+
key : lKey,
374+
value : lValue
375+
})
376+
} else {
377+
dependencyExists.key = `${dependencyExists.key.split('::')[0]}::${feature}`;
378+
dependencyExists.value.push(feature);
379+
}
380+
let circularItemFound = dependencyList.find(item => item.value && item.value.slice(0)[0] == item.value.slice(-1)[0]);
381+
if (circularItemFound) {
382+
throw (new exception(retCode.errorCode.INVALID_INPUT, `Circular dependency found for usesFeature. ${circularItemFound.value.join(' -> ')}`));
383+
}
384+
385+
})
386+
}
387+
});
388+
}
389+
/**
390+
* Helper function to verify there are no circular dependencies in the parsed content.
391+
* @param {Object} parsedContent
392+
*/
393+
const verifyNoCircularDependencyForFeatures = function(parsedContent) {
394+
let dependencyList = [];
395+
updateDependencyList(LUISObjNameEnum.INTENT, parsedContent, dependencyList);
396+
updateDependencyList(LUISObjNameEnum.ENTITIES, parsedContent, dependencyList);
397+
updateDependencyList(LUISObjNameEnum.CLOSEDLISTS, parsedContent, dependencyList);
398+
updateDependencyList(LUISObjNameEnum.COMPOSITES, parsedContent, dependencyList);
399+
updateDependencyList(LUISObjNameEnum.PATTERNANYENTITY, parsedContent, dependencyList);
400+
updateDependencyList(LUISObjNameEnum.PREBUILT, parsedContent, dependencyList);
401+
updateDependencyList(LUISObjNameEnum.REGEX, parsedContent, dependencyList);
402+
}
403+
246404
/**
247405
* Reference parser code to parse reference section.
248406
* @param {parserObj} Object with that contains list of additional files to parse, parsed LUIS object and parsed QnA object

packages/luis/test/commands/luis/convert.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,17 @@ describe('luis:convert', () => {
4949
test
5050
.stdout()
5151
.command(['luis:convert', '--in', `${path.join(__dirname, './../../fixtures/verified/plFeatures.json')}`, '--out', 'root.lu'])
52-
.it('luis:convert successfully reconstructs a markdown file from a LUIS input file', async () => {
52+
.it('luis:convert successfully reconstructs a markdown file from a LUIS input file (phrase list as feature)', async () => {
5353
expect(await compareLuFiles('./../../../root.lu', './../../fixtures/verified/plFeatures.lu')).to.be.true
5454
})
5555

56+
test
57+
.stdout()
58+
.command(['luis:convert', '--in', `${path.join(__dirname, './../../fixtures/verified/modelAsFeatures.json')}`, '--out', 'root.lu'])
59+
.it('luis:convert successfully reconstructs a markdown file from a LUIS input file (model as features)', async () => {
60+
expect(await compareLuFiles('./../../../root.lu', './../../fixtures/verified/modelAsFeatureGen.lu')).to.be.true
61+
})
62+
5663
test
5764
.stdout()
5865
.command(['luis:convert', '--in', `${path.join(__dirname, './../../fixtures/examples/1.lu')}`, '--out', 'root.json', '--name', '1'])
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
> # Intent definitions
3+
4+
## test1
5+
- one
6+
7+
8+
@ intent test1 usesFeature test3
9+
10+
## test2
11+
- two
12+
13+
14+
@ intent test2 usesFeature simple1
15+
16+
## test3
17+
- three
18+
19+
20+
@ intent test3 usesFeature age
21+
22+
> # Entity definitions
23+
24+
@ simple simple1 usesFeatures phraselist1,age
25+
26+
27+
> # PREBUILT Entity definitions
28+
29+
@ prebuilt age
30+
31+
32+
> # Phrase list definitions
33+
34+
@ phraselist phraselist1(interchangeable) =
35+
- who,why,where,what
36+
37+
38+
> # List entities
39+
40+
@ list list1
41+
42+
> # RegEx entities
43+
44+
45+
> # Composite entities
46+
47+
@ composite composite1 usesFeatures phraselist1,list1 = [simple1, age]

0 commit comments

Comments
 (0)