Name
sn_nlu_workbench.NLUExpertFeedbackUtil
Description
Utilities to get the Data Labeling (Expert Feedback Loop) information.
Script
var NLUExpertFeedbackUtil = Class.create();
(function() {
var tables = sn_nlu_workbench.NLUWorkbenchConstants.tables;
var coreTables = global.NLUConstants.tables;
var TODO_FURTHER_REVIEW_FIELDS = {
LABEL: 'lct_suggested_label',
UTTERANCE_REF: 'lft_utterance_reference'
};
var tabNames = {
TODO: 'todo',
DONE: 'done',
FURTHER_REVIEW: 'further_review'
};
var outcomes = {
MATCH: 'positive',
MISMATCH: 'negative',
UNSURE: 'notsure',
IRRELEVANT: 'irrelevant'
};
NLUExpertFeedbackUtil.getModelOptimizeInfo = function(modelId) {
var modelStatusGr = global.NLUModel.getModelStatusGr(modelId);
var trainedVersion = modelStatusGr.getValue('trained_version');
var latestTrainOptimizeInfo = NLUBatchTestExecution.getReportInfo(modelId, true, trainedVersion, true) || {};
var latestModelPublished = trainedVersion === modelStatusGr.getValue('published_version');
return {
executionId: latestTrainOptimizeInfo.executionId,
definitionId: latestTrainOptimizeInfo.definitionId,
status: latestTrainOptimizeInfo.status,
lastTrainedVersionOptimizedOn: latestTrainOptimizeInfo.lastTrainedVersionOptimizedOn,
latestModelPublished: latestModelPublished
};
};
NLUExpertFeedbackUtil.labeledCountSince = function(modelName, fromDate) {
var ga = new GlideAggregate(tables.ULT);
ga.addQuery('product', 'nlu');
if (fromDate) ga.addQuery('sys_updated_on', '>=', new GlideDateTime(fromDate));
ga.addEncodedQuery('labelSTARTSWITHintent:' + modelName + '.^ORlabelSTARTSWITHmodel:' + modelName);
ga.addAggregate('COUNT');
ga.query();
return ga.next() && parseInt(ga.getAggregate('COUNT')) || 0;
};
NLUExpertFeedbackUtil.getLabelFromReference = function(label_reference) {
var gr = new GlideRecord(coreTables.SYS_NLU_INTENT);
if (gr.get(label_reference))
return 'intent:' + gr.model.name + '.' + gr.name;
return '';
};
NLUExpertFeedbackUtil.getReferenceFromLabel = function(label) {
if (!label || !label.includes(":")) return null;
var labelTable = '';
var query = '';
var labelArr1 = label.split(":");
if (labelArr1[0] === 'intent' && labelArr1[1].includes(".")) {
labelTable = coreTables.SYS_NLU_INTENT;
var labelArr2 = labelArr1[1].split(".");
query = 'model.name=' + labelArr2[0] + '^name=' + labelArr2[1];
} else if (labelArr1[0] === 'model') {
labelTable = coreTables.SYS_NLU_MODEL;
query = 'name=' + labelArr1[1];
}
if (!query) return null;
var gr = new GlideRecord(labelTable);
gr.addEncodedQuery(query);
gr.setLimit(1);
gr.orderBy('sys_updated_on');
gr.query();
if (gr.next()) {
return {
label_table: labelTable,
label_reference: gr.getUniqueValue()
};
}
return null;
};
NLUExpertFeedbackUtil.prototype = {
initialize: function() {
this.modelToIntentMap = {};
this.lctDeletes = [];
this.ultDeletes = [];
this.uftDeletes = [];
this.uncategorizedUtterances = {};
this.uncategorizedUtterances[tabNames.TODO] = 0;
this.uncategorizedUtterances[tabNames.DONE] = 0;
},
getModelList: function() {
this._populateModelIntentMap(tables.LCT, 'suggested_label');
this._populateModelIntentMap(tables.ULT, 'label');
var modelList = [];
for (var modelName in this.modelToIntentMap) {
var aggr = new GlideAggregate(coreTables.SYS_NLU_INTENT);
aggr.addQuery('model.name', modelName);
aggr.addQuery('name', this.modelToIntentMap[modelName]);
aggr.groupBy('model');
aggr.addAggregate('COUNT');
aggr.query();
if (aggr.next()) {
var statusGr = new GlideRecord(coreTables.SYS_NLU_MODEL_STATUS);
statusGr.addQuery('model.name', modelName);
statusGr.query();
modelList.push({
modelId: aggr.getValue('model'),
displayName: aggr.getDisplayValue('model'),
intentCount: aggr.getAggregate('COUNT') || 0,
name: modelName,
language: aggr.model.language,
lastTunedOn: statusGr.next() ? statusGr.getValue('last_tuned_on') : null
});
}
}
var modelAgg = new GlideAggregate(coreTables.SYS_NLU_MODEL_STATUS);
modelAgg.addQuery('published_version', '>=', '1');
modelAgg.addAggregate('COUNT');
modelAgg.query();
return {
hasPublishedModels: modelAgg.next() && modelAgg.getAggregate('COUNT') > 0,
modelList: modelList,
uncategorizedUtterances: this.uncategorizedUtterances
};
},
getIntentList: function(modelId) {
var intentsData = [];
var intentGr = new GlideRecord(coreTables.SYS_NLU_INTENT);
intentGr.addQuery('model', modelId);
intentGr.query();
var modelName = null;
var intentDataMap = {};
while (intentGr.next()) {
if (!modelName) modelName = intentGr.model.name;
var label = 'intent:' + modelName + '.' + intentGr.name;
intentDataMap[label] = {
intentId: intentGr.getUniqueValue(),
name: intentGr.getValue('name'),
label: label
};
}
var reviewProgressMap = this.getReviewProgressMap(Object.keys(intentDataMap), modelId);
for (var _label in reviewProgressMap) {
intentDataMap[_label].reviewProgress = reviewProgressMap[_label];
intentsData.push(intentDataMap[_label]);
}
var optimizeData = NLUExpertFeedbackUtil.getModelOptimizeInfo(modelId);
return {
optimizeData: optimizeData,
labeledCountSinceLastOptimized: NLUExpertFeedbackUtil.labeledCountSince(modelName, optimizeData.lastTrainedVersionOptimizedOn),
intents: intentsData
};
},
getReviewProgressMap: function(labelList, modelId) {
var reviewProgressMap = {};
var doneData = this._getUtteranceCountForLabels(
tables.ULT,
'label',
labelList,
modelId
);
var todoFurtherReviewData = this._getTodoFurtherReviewCountForLabels(
labelList
);
var todoData = todoFurtherReviewData[tabNames.TODO] || {};
var furtherReviewData = todoFurtherReviewData[tabNames.FURTHER_REVIEW] || {};
labelList.forEach(function(_label) {
var todo = todoData[_label] || 0;
var done = doneData[_label] || 0;
var furtherReview = furtherReviewData[_label] || 0;
if (!_label) { // For uncategorised utterances:
todo += furtherReview;
furtherReview = 0;
}
if (todo + done + furtherReview > 0) {
reviewProgressMap[_label] = {};
reviewProgressMap[_label][tabNames.TODO] = todo;
reviewProgressMap[_label][tabNames.DONE] = done;
reviewProgressMap[_label][tabNames.FURTHER_REVIEW] = furtherReview;
}
});
return reviewProgressMap;
},
/**
*
* feedbackConfig example
* {
* "todo": {
* "sysId1": {
* "feedback": "positive"
* }
* },
* "done": {
* "sysId11": {
* "feedback": "negative",
* "correct_label_reference": "sysId3434"
* }
* },
* "review": {
* "sysId23": {
* "feedback": "notsure"
* }
* }
* }
*/
updateFeedback: function(selectedIntent, feedbackConfig) {
try {
if (Object.keys(feedbackConfig[tabNames.TODO]).length > 0) {
this._updateTodoFeedback(feedbackConfig[tabNames.TODO]);
}
if (Object.keys(feedbackConfig[tabNames.DONE]).length > 0) {
this._updateDoneFeedback(feedbackConfig[tabNames.DONE]);
}
if (Object.keys(feedbackConfig[tabNames.FURTHER_REVIEW]).length > 0) {
this._updateReviewFeedback(feedbackConfig[tabNames.FURTHER_REVIEW]);
}
var modelIntentStr = (selectedIntent && selectedIntent.substring(7)) || false;
var tgtModel = modelIntentStr && modelIntentStr.split('.')[0];
var modelGr = global.NLUModel.getGRByName(tgtModel);
var map = this.getReviewProgressMap([selectedIntent], modelGr ? modelGr.getValue('sys_id') : null);
if (map && map[selectedIntent]) {
return map[selectedIntent];
}
} catch (error) {
throw error;
}
return {};
},
_updateTodoFeedback: function(todoConfig) {
var sysIds = Object.keys(todoConfig);
for (var i = 0; i < sysIds.length; i++) {
var sysId = sysIds[i];
var correct_label_reference = todoConfig[sysId].correct_label_reference || '';
var feedback = todoConfig[sysId].feedback;
var lctData = this._getLCTRecord(sysId);
if (feedback === outcomes.MATCH) {
// 1. Create record in ULT (label_type: positive)
this._createULTRecord({
text: lctData.text,
product: lctData.product,
source: lctData.source,
label: lctData.label,
label_type: outcomes.MATCH
});
// 2. Remove record in LCT
this._markLCTDelete(sysId);
} else if (feedback === outcomes.MISMATCH || feedback === outcomes.IRRELEVANT) {
// 1. Create record in ULT (label_type: negative, correct_label_reference if given)
this._createULTRecord({
text: lctData.text,
product: lctData.product,
source: lctData.source,
label: lctData.label,
label_type: feedback,
correct_label_reference: correct_label_reference
});
// 2. Remove record in LCT
this._markLCTDelete(sysId);
} else if (feedback === outcomes.UNSURE) {
// 1. Create record in UFT (label_type: unsure)
this._createUFTRecord({
utterance_reference: sysId,
utterance_table: tables.LCT,
label_type: outcomes.UNSURE
});
}
}
this._deleteRecords(tables.LCT);
},
_updateDoneFeedback: function(doneConfig) {
var sysIds = Object.keys(doneConfig);
for (var i = 0; i < sysIds.length; i++) {
var sysId = sysIds[i];
var feedback = doneConfig[sysId].feedback;
var correct_label_reference = doneConfig[sysId].correct_label_reference || '';
var ultRecord = new GlideRecord(tables.ULT);
ultRecord.addQuery('sys_id', sysId);
ultRecord.query();
if (feedback === outcomes.MATCH) {
if (ultRecord.next()) {
// Update ULT record label_type
ultRecord.setValue('label_type', feedback);
ultRecord.setValue('label_table', coreTables.SYS_NLU_INTENT);
ultRecord.setValue('correct_label_reference', '');
ultRecord.update();
return ultRecord.getUniqueValue();
}
} else if (feedback === outcomes.MISMATCH || feedback === outcomes.IRRELEVANT) {
if (ultRecord.next()) {
ultRecord.setValue('label_type', feedback);
if (correct_label_reference) {
ultRecord.setValue('correct_label_reference', correct_label_reference);
if (gs.nil(ultRecord.getValue('label_table')))
ultRecord.setValue('label_table', coreTables.SYS_NLU_INTENT);
} else {
ultRecord.setValue('correct_label_reference', '');
}
ultRecord.update();
}
} else if (feedback === outcomes.UNSURE) {
// Create record in LCT - take reference
var ultData = this._getULTRecord(sysId);
var lctSysId = global.MLLabelCandidate.createRecord({
text: ultData.text,
suggested_label: ultData.label,
source: ultData.source,
product: ultData.product
});
// Create UFT record with label_type - unsure (notsure) with utterance_reference as the above LCT record sys_id
this._createUFTRecord({
utterance_reference: lctSysId,
utterance_table: tables.LCT,
label_type: feedback
});
// delete ULT record
this._markULTDelete(sysId);
}
}
this._deleteRecords(tables.ULT);
},
_updateReviewFeedback: function(furtherReviewConfig) {
var sysIds = Object.keys(furtherReviewConfig);
for (var i = 0; i < sysIds.length; i++) {
var sysId = sysIds[i];
var feedback = furtherReviewConfig[sysId].feedback;
var correct_label_reference = furtherReviewConfig[sysId].correct_label_reference || '';
var uftData = this._getUFTRecord(sysId);
var lctData = this._getLCTRecord(uftData.utterance_reference);
var ultSysId;
// 1. Create record in ULT - take note of sysId
if (feedback === outcomes.MATCH) {
ultSysId = this._createULTRecord({
text: lctData.text,
product: lctData.product,
source: lctData.source,
label: lctData.label,
label_type: outcomes.MATCH,
correct_label_reference: ''
});
} else if (feedback === outcomes.MISMATCH || feedback === outcomes.IRRELEVANT) {
ultSysId = this._createULTRecord({
text: lctData.text,
product: lctData.product,
source: lctData.source,
label: lctData.label,
label_type: feedback,
correct_label_reference: feedback === outcomes.IRRELEVANT ? '' : correct_label_reference
});
}
// 2. Update record of UFT (utterance_reference to sysId from point 1.)
this._updateUtteranceReference({
sys_id: sysId,
utterance_reference: ultSysId,
utterance_table: tables.ULT,
label_type: feedback,
correct_label_reference: correct_label_reference
});
// 3. Remove record from LCT
this._markLCTDelete(uftData.utterance_reference);
}
this._deleteRecords(tables.LCT);
},
_updateUtteranceReference: function(data) {
var uftRecord = new GlideRecord(tables.LABEL_USER_FEEDBACK);
uftRecord.addQuery('sys_id', data.sys_id);
uftRecord.query();
if (uftRecord.next()) {
uftRecord.setValue('utterance_reference', data.utterance_reference);
uftRecord.setValue('utterance_table', data.utterance_table);
uftRecord.setValue('label_type', data.label_type);
uftRecord.setValue('correct_label_reference', data.correct_label_reference);
uftRecord.setValue('utterance_table', tables.ULT);
uftRecord.update();
}
},
_getLCTRecord: function(sysId) {
var gr = new GlideRecord(tables.LCT);
gr.addQuery('sys_id', sysId);
gr.query();
if (gr.next()) {
return {
text: gr.getValue('text'),
product: gr.getValue('product'),
source: gr.getValue('source'),
label: gr.getValue('suggested_label')
};
}
return null;
},
_getULTRecord: function(sysId) {
var gr = new GlideRecord(tables.ULT);
gr.addQuery('sys_id', sysId);
gr.query();
if (gr.next()) {
return {
text: gr.getValue('text'),
label: gr.getValue('label'),
label_type: gr.getValue('label_type'),
correct_label_reference: gr.getValue('correct_label_reference'),
product: gr.getValue('product'),
source: gr.getValue('source')
};
}
return null;
},
_getUFTRecord: function(sysId) {
var gr = new GlideRecord(tables.LABEL_USER_FEEDBACK);
gr.addQuery('sys_id', sysId);
gr.query();
if (gr.next()) {
return {
utterance_reference: gr.getValue('utterance_reference'),
utterance_table: gr.getValue('utterance_table'),
label_type: gr.getValue('label_type'),
correct_label_reference: gr.getValue('correct_label_reference'),
user: gr.getValue('user')
};
}
return null;
},
/**
* To create a record in ml_labeled_data table (ULT)
*/
_createULTRecord: function(data) {
var gr = new GlideRecord(tables.ULT);
gr.initialize();
gr.setValue('text', data.text);
gr.setValue('label_type', data.label_type);
gr.setValue('source', data.source);
gr.setValue('product', data.product);
gr.setValue('label', data.label);
gr.setValue('usage', 'nlu_model_train');
var labelRef = NLUExpertFeedbackUtil.getReferenceFromLabel(data.label);
if (labelRef) {
gr.setValue('label_reference', labelRef.label_reference);
gr.setValue('label_table', labelRef.label_table);
}
if (data.correct_label_reference) {
gr.setValue('correct_label_reference', data.correct_label_reference);
gr.setValue('correct_label', NLUExpertFeedbackUtil.getLabelFromReference(data.correct_label_reference));
gr.setValue('label_table', coreTables.SYS_NLU_INTENT);
}
gr.insert();
return gr.getUniqueValue();
},
/**
* To create a record in ml_label_candidate table (LCT)
*/
_createLCTRecord: function() {},
/**
* To create a record in ml_label_user_feedback table (UFT)
*/
_createUFTRecord: function(data) {
var gr = new GlideRecord(tables.LABEL_USER_FEEDBACK);
gr.initialize();
gr.setValue('utterance_reference', data.utterance_reference);
gr.setValue('utterance_table', data.utterance_table);
gr.setValue('label_type', data.label_type);
gr.insert();
return gr.getUniqueValue();
},
_markLCTDelete: function(sysId) {
this.lctDeletes.push(sysId);
},
_markULTDelete: function(sysId) {
this.ultDeletes.push(sysId);
},
_markUFTDelete: function(sysId) {
this.uftDeletes.push(sysId);
},
_deleteRecords: function(table) {
var records = [];
if (table === tables.LCT) {
records = this.lctDeletes;
global.MLLabelCandidate.deleteRecords('sys_idIN' + records.join(','));
this.lctDeletes = [];
} else if (table === tables.ULT) {
records = this.ultDeletes;
global.MLLabeledData.deleteRecords('sys_idIN' + records.join(','));
this.ultDeletes = [];
}
},
_getLabelListQuery: function(field, labels) {
var query = field + 'IN' + labels.join(',');
var index = labels.indexOf('');
if (index > -1) {
query = field + 'ISEMPTY';
var other = labels.filter(function(x) {
return x != '';
});
if (other.length > 0)
query += '^OR' + field + 'IN' + other.join(',');
}
return query;
},
_getUtteranceCountForLabels: function(tableName, field, labels, modelId) {
var aggr = new GlideAggregate(tableName);
aggr.addQuery('product', 'nlu');
aggr.addQuery('source', 'virtual_agent');
aggr.addEncodedQuery(this._getLabelListQuery(field, labels));
aggr.addAggregate('COUNT', field);
var statusGr = new GlideRecord('sys_nlu_model_status');
statusGr.addQuery('model', modelId);
statusGr.query();
if (statusGr.next() && statusGr.getValue('last_tuned_on')) {
aggr.addQuery('sys_updated_on', '>=', statusGr.getValue('last_tuned_on'));
}
aggr.query();
var labelReviewCountMap = {};
while (aggr.next()) {
var label = aggr.getValue(field);
labelReviewCountMap[label] =
parseInt(aggr.getAggregate('COUNT', field)) || 0;
}
return labelReviewCountMap;
},
_getTodoFurtherReviewCountForLabels: function(labels) {
var result = {};
var todo = {};
var furtherReview = {};
var aggr = new GlideAggregate(tables.DBVIEW_TODO_FURTHER_REVIEW);
aggr.groupBy(TODO_FURTHER_REVIEW_FIELDS.LABEL);
aggr.groupBy(TODO_FURTHER_REVIEW_FIELDS.UTTERANCE_REF);
aggr.addEncodedQuery(this._getLabelListQuery(TODO_FURTHER_REVIEW_FIELDS.LABEL, labels));
aggr.addAggregate('COUNT');
aggr.query();
while (aggr.next()) {
var label = aggr.getValue(TODO_FURTHER_REVIEW_FIELDS.LABEL);
var isFurtherReview = aggr.getValue(
TODO_FURTHER_REVIEW_FIELDS.UTTERANCE_REF
);
if (isFurtherReview)
furtherReview[label] =
(furtherReview[label] || 0) +
(parseInt(aggr.getAggregate('COUNT')) || 0);
else todo[label] = parseInt(aggr.getAggregate('COUNT')) || 0;
}
result[tabNames.TODO] = todo;
result[tabNames.FURTHER_REVIEW] = furtherReview;
return result;
},
_populateModelIntentMap: function(tableName, field) {
var aggr = new GlideAggregate(tableName);
aggr.addQuery('product', 'nlu');
aggr.addQuery('source', 'virtual_agent');
aggr.addAggregate('COUNT', field);
aggr.query();
while (aggr.next()) {
var label = aggr.getValue(field);
if (!label) {
var key = tableName === tables.LCT ? tabNames.TODO : tabNames.DONE;
this.uncategorizedUtterances[key] = aggr.getAggregate('COUNT', field) || 0;
} else if (label.indexOf('intent:') === 0) {
var modelIntentStr = label.substring('intent:'.length);
var model = modelIntentStr.split('.')[0];
var intent = modelIntentStr.substring(model.length + 1);
if (!this.modelToIntentMap.hasOwnProperty(model))
this.modelToIntentMap[model] = [];
if (this.modelToIntentMap[model].indexOf(intent) === -1)
this.modelToIntentMap[model].push(intent);
}
}
},
type: 'NLUExpertFeedbackUtil'
};
})();
Sys ID
8c1168780723301028ef0a701ad3002c