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

Commit 8f4567f

Browse files
authored
Merge pull request #177 from microsoft/vishwac/model-as-feature
Model as feature to other models - support in lu format
2 parents 7837b71 + 0f77ec9 commit 8f4567f

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