@telefonica/luis-cli
Advanced tools
Comparing version 4.0.0 to 5.0.0
/// <reference types="node" /> | ||
import { EventEmitter } from 'events'; | ||
declare module 'request' { | ||
interface RequestResponse { | ||
body?: any; | ||
} | ||
} | ||
export declare namespace LuisApi { | ||
interface RecognizedEntity { | ||
entity: string; | ||
type: string; | ||
startIndex: number; | ||
endIndex: number; | ||
} | ||
interface RecognitionResult { | ||
sentence: string; | ||
intent: string; | ||
entities: RecognizedEntity[]; | ||
} | ||
interface AppInfo { | ||
interface AppInfoGET { | ||
id: string; | ||
@@ -25,59 +9,99 @@ name: string; | ||
culture: string; | ||
active: boolean; | ||
numberOfIntents: number; | ||
numberOfEntities: number; | ||
isTrained: boolean; | ||
} | ||
interface IntentClassifier { | ||
interface IntentGET { | ||
id: string; | ||
name: string; | ||
typeId: number; | ||
readableType: string; | ||
} | ||
interface EntityExtractor { | ||
interface IntentPOST { | ||
name: string; | ||
} | ||
interface IntentDELETE { | ||
id: string; | ||
} | ||
interface EntityGET { | ||
id: string; | ||
name: string; | ||
typeId: number; | ||
readableType: string; | ||
} | ||
enum PhraseListModes { | ||
Exchangeable = 0, | ||
NonExchangeable = 1, | ||
interface EntityPOST { | ||
name: string; | ||
} | ||
interface PhraseList { | ||
id?: string; | ||
interface EntityDELETE { | ||
id: string; | ||
} | ||
interface PhraseListGET { | ||
id: number; | ||
name: string; | ||
mode: PhraseListModes; | ||
isActive?: boolean; | ||
editable?: boolean; | ||
isActive: boolean; | ||
isExchangeable: boolean; | ||
phrases: string; | ||
} | ||
interface LabeledEntity { | ||
interface PhraseListPOST { | ||
name: string; | ||
startToken: number; | ||
endToken: number; | ||
word: string; | ||
isBuiltInExtractor: boolean; | ||
phrases: string; | ||
isExchangeable: boolean; | ||
} | ||
interface LabeledUtterance { | ||
interface PhraseListDELETE { | ||
id: string; | ||
utteranceText: string; | ||
} | ||
interface EntityLabelExampleGET { | ||
entityName: string; | ||
startTokenIndex: number; | ||
endTokenIndex: number; | ||
} | ||
interface EntityLabelExamplePOST { | ||
entityName: string; | ||
startCharIndex: number; | ||
endCharIndex: number; | ||
} | ||
interface IntentPrediction { | ||
name: string; | ||
score: number; | ||
} | ||
interface EntityPrediction { | ||
entityName: string; | ||
startIndex: number; | ||
endIndex: number; | ||
phrase: string; | ||
} | ||
interface ExampleGET { | ||
id: number; | ||
text: string; | ||
tokenizedText: string[]; | ||
intentLabel: string; | ||
entityLabels: EntityLabelExampleGET[]; | ||
intentPredictions: IntentPrediction[]; | ||
entityPredictions: EntityPrediction[]; | ||
} | ||
interface ExamplePOST { | ||
text: string; | ||
intentName: string; | ||
entityLabels: EntityLabelExamplePOST[]; | ||
} | ||
interface ExampleDELETE { | ||
id: number; | ||
} | ||
interface RecognizedIntent { | ||
intent: string; | ||
predictedIntents: [{ | ||
name: string; | ||
score: number; | ||
}]; | ||
entities: LabeledEntity[]; | ||
predictedEntities: LabeledEntity[]; | ||
score: number; | ||
} | ||
interface Entity { | ||
entityType: string; | ||
startToken: number; | ||
endToken: number; | ||
interface RecognizedEntity { | ||
entity: string; | ||
type: string; | ||
startIndex: number; | ||
endIndex: number; | ||
score: number; | ||
} | ||
interface Example { | ||
exampleText: string; | ||
selectedIntentName: string; | ||
entityLabels?: Entity[]; | ||
interface RecognitionResult { | ||
query: string; | ||
topScoringIntent: RecognizedIntent; | ||
intents: RecognizedIntent[]; | ||
entities: RecognizedEntity[]; | ||
} | ||
enum TrainingStatuses { | ||
Success = 0, | ||
Failed = 1, | ||
Fail = 1, | ||
UpToDate = 2, | ||
@@ -94,5 +118,6 @@ InProgress = 3, | ||
interface PublishResult { | ||
url: string; | ||
endpointUrl: string; | ||
subscriptionKey: string; | ||
publishDate: Date; | ||
endpointRegion: string; | ||
isStaging: boolean; | ||
} | ||
@@ -113,31 +138,31 @@ } | ||
private retryRequest(opts, expectedStatusCode); | ||
private throttler(fn, items); | ||
private throttler(fn, appVersion, items); | ||
recognizeSentence(sentence: string): Promise<LuisApi.RecognitionResult>; | ||
recognizeSentences(sentences: string[]): Promise<LuisApi.RecognitionResult[]>; | ||
getApp(): Promise<LuisApi.AppInfo>; | ||
getIntents(): Promise<LuisApi.IntentClassifier[]>; | ||
createIntent(intentName: string): Promise<string>; | ||
createIntents(intentNames: string[]): Promise<string[]>; | ||
deleteIntent(intentId: string): Promise<void>; | ||
deleteIntents(intentIds: string[]): Promise<void>; | ||
getEntities(): Promise<LuisApi.EntityExtractor[]>; | ||
createEntity(entityName: string): Promise<string>; | ||
createEntities(entityNames: string[]): Promise<string[]>; | ||
deleteEntity(entityId: string): Promise<void>; | ||
deleteEntities(entityIds: string[]): Promise<void>; | ||
getPhraseLists(): Promise<LuisApi.PhraseList[]>; | ||
createPhraseList(phraseList: LuisApi.PhraseList): Promise<string>; | ||
createPhraseLists(phraseLists: LuisApi.PhraseList[]): Promise<string[]>; | ||
deletePhraseList(phraseListId: string): Promise<void>; | ||
deletePhraseLists(phraseListIds: string[]): Promise<void>; | ||
getExamples(skip: number, count: number): Promise<LuisApi.LabeledUtterance[]>; | ||
getAllExamples(): Promise<LuisApi.LabeledUtterance[]>; | ||
createExamples(examples: LuisApi.Example[]): Promise<string[]>; | ||
deleteExample(exampleId: string): Promise<void>; | ||
deleteExamples(exampleIds: string[]): Promise<void>; | ||
recognizeSentences(queries: string[]): Promise<LuisApi.RecognitionResult[]>; | ||
getApp(): Promise<LuisApi.AppInfoGET>; | ||
getIntents(appVersion: string): Promise<LuisApi.IntentGET[]>; | ||
createIntent(appVersion: string, intent: LuisApi.IntentPOST): Promise<string>; | ||
createIntents(appVersion: string, intents: LuisApi.IntentPOST[]): Promise<string[]>; | ||
deleteIntent(appVersion: string, intent: LuisApi.IntentDELETE): Promise<void>; | ||
deleteIntents(appVersion: string, intents: LuisApi.IntentDELETE[]): Promise<void>; | ||
getEntities(appVersion: string): Promise<LuisApi.EntityGET[]>; | ||
createEntity(appVersion: string, entity: LuisApi.EntityPOST): Promise<string>; | ||
createEntities(appVersion: string, entities: LuisApi.EntityPOST[]): Promise<string[]>; | ||
deleteEntity(appVersion: string, entity: LuisApi.EntityDELETE): Promise<void>; | ||
deleteEntities(appVersion: string, entities: LuisApi.EntityDELETE[]): Promise<void>; | ||
getPhraseLists(appVersion: string): Promise<LuisApi.PhraseListGET[]>; | ||
createPhraseList(appVersion: string, phraseList: LuisApi.PhraseListPOST): Promise<string>; | ||
createPhraseLists(appVersion: string, phraseLists: LuisApi.PhraseListPOST[]): Promise<string[]>; | ||
deletePhraseList(appVersion: string, phraseList: LuisApi.PhraseListDELETE): Promise<void>; | ||
deletePhraseLists(appVersion: string, phraseLists: LuisApi.PhraseListDELETE[]): Promise<void>; | ||
getExamples(appVersion: string, skip: number, count: number): Promise<LuisApi.ExampleGET[]>; | ||
getAllExamples(appVersion: string): Promise<LuisApi.ExampleGET[]>; | ||
createExamples(appVersion: string, examples: LuisApi.ExamplePOST[]): Promise<string[]>; | ||
deleteExample(appVersion: string, example: LuisApi.ExampleDELETE): Promise<void>; | ||
deleteExamples(appVersion: string, examples: LuisApi.ExampleDELETE[]): Promise<void>; | ||
private convertTrainingStatus(apiTrainingStatus); | ||
startTraining(): Promise<LuisApi.TrainingStatus>; | ||
getTrainingStatus(): Promise<LuisApi.TrainingStatus>; | ||
publish(): Promise<LuisApi.PublishResult>; | ||
export(): Promise<any>; | ||
startTraining(appVersion: string): Promise<LuisApi.TrainingStatus>; | ||
getTrainingStatus(appVersion: string): Promise<LuisApi.TrainingStatus>; | ||
publish(appVersion: string): Promise<LuisApi.PublishResult>; | ||
export(appVersion: string): Promise<any>; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const request = require("request-promise-native"); | ||
@@ -9,11 +10,6 @@ const _ = require("lodash"); | ||
(function (LuisApi) { | ||
var PhraseListModes; | ||
(function (PhraseListModes) { | ||
PhraseListModes[PhraseListModes["Exchangeable"] = 0] = "Exchangeable"; | ||
PhraseListModes[PhraseListModes["NonExchangeable"] = 1] = "NonExchangeable"; | ||
})(PhraseListModes = LuisApi.PhraseListModes || (LuisApi.PhraseListModes = {})); | ||
var TrainingStatuses; | ||
let TrainingStatuses; | ||
(function (TrainingStatuses) { | ||
TrainingStatuses[TrainingStatuses["Success"] = 0] = "Success"; | ||
TrainingStatuses[TrainingStatuses["Failed"] = 1] = "Failed"; | ||
TrainingStatuses[TrainingStatuses["Fail"] = 1] = "Fail"; | ||
TrainingStatuses[TrainingStatuses["UpToDate"] = 2] = "UpToDate"; | ||
@@ -24,3 +20,2 @@ TrainingStatuses[TrainingStatuses["InProgress"] = 3] = "InProgress"; | ||
const LUIS_API_BASE_URL = 'https://westus.api.cognitive.microsoft.com'; | ||
const LUIS_SERVICE_API_BASE_URL = 'http://luis-europe-west-endpoint.cloudapp.net'; | ||
const RETRY_OPTS = { | ||
@@ -43,7 +38,5 @@ retries: 10, | ||
this.serviceReq = request.defaults({ | ||
baseUrl: `${LUIS_SERVICE_API_BASE_URL}/api/v2.0/apps/${this.applicationId}`, | ||
qs: { | ||
'subscription-key': config.subscriptionKey, | ||
verbose: false, | ||
spellCheck: false | ||
baseUrl: `${baseUrl}/luis/v2.0/apps/`, | ||
headers: { | ||
'Ocp-Apim-Subscription-Key': config.subscriptionKey | ||
}, | ||
@@ -55,3 +48,3 @@ json: true, | ||
this.provisionReq = request.defaults({ | ||
baseUrl: `${baseUrl}/luis/v1.0/prog/apps`, | ||
baseUrl: `${baseUrl}/luis/api/v2.0/apps/`, | ||
headers: { | ||
@@ -85,4 +78,4 @@ 'Ocp-Apim-Subscription-Key': config.subscriptionKey | ||
} | ||
throttler(fn, items) { | ||
let promises = items.map(item => this.promiseThrottle.add(fn.bind(this, item))); | ||
throttler(fn, appVersion, items) { | ||
let promises = items.map(item => this.promiseThrottle.add(fn.bind(this, appVersion, item))); | ||
return Promise.all(promises); | ||
@@ -93,3 +86,3 @@ } | ||
method: 'GET', | ||
uri: '', | ||
uri: `/${this.applicationId}`, | ||
qs: { q: sentence } | ||
@@ -105,4 +98,5 @@ }; | ||
return { | ||
sentence: res.body.query, | ||
intent: res.body.topScoringIntent.intent, | ||
query: res.body.query, | ||
topScoringIntent: res.body.topScoringIntent, | ||
intents: res.body.intents, | ||
entities: res.body.entities.map((entity) => { | ||
@@ -113,3 +107,4 @@ return { | ||
startIndex: entity.startIndex, | ||
endIndex: entity.endIndex | ||
endIndex: entity.endIndex, | ||
score: entity.score | ||
}; | ||
@@ -120,3 +115,3 @@ }) | ||
} | ||
recognizeSentences(sentences) { | ||
recognizeSentences(queries) { | ||
let promiseThrottle = new PromiseThrottle({ | ||
@@ -126,3 +121,3 @@ requestsPerSecond: SERVICE_API_REQUESTS_PER_SECOND, | ||
}); | ||
let promises = sentences.map(sentence => promiseThrottle.add(this.recognizeSentence.bind(this, sentence))); | ||
let promises = queries.map(query => promiseThrottle.add(this.recognizeSentence.bind(this, query))); | ||
return Promise.all(promises); | ||
@@ -133,3 +128,3 @@ } | ||
method: 'GET', | ||
uri: this.applicationId | ||
uri: `/${this.applicationId}` | ||
}; | ||
@@ -140,181 +135,134 @@ return this.retryRequest(opts, 200) | ||
return { | ||
id: body.ID, | ||
name: body.Name, | ||
description: body.Description, | ||
culture: body.Culture, | ||
active: body.Active, | ||
numberOfIntents: body.NumberOfIntents, | ||
numberOfEntities: body.NumberOfEntities, | ||
isTrained: body.IsTrained | ||
id: body.id, | ||
name: body.name, | ||
description: body.description, | ||
culture: body.culture | ||
}; | ||
}); | ||
} | ||
getIntents() { | ||
getIntents(appVersion) { | ||
let opts = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/intents` | ||
uri: `/${this.applicationId}/versions/${appVersion}/intents` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then((res) => res.body.Result) | ||
.then((intents) => intents.map((intent) => { | ||
return { | ||
id: intent.id, | ||
name: intent.name | ||
}; | ||
})); | ||
.then((res) => res.body); | ||
} | ||
createIntent(intentName) { | ||
createIntent(appVersion, intent) { | ||
let opts = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/intents`, | ||
body: { name: intentName } | ||
uri: `/${this.applicationId}/versions/${appVersion}/intents`, | ||
body: intent | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res) => res.body) | ||
.then(intentId => { | ||
.then((res) => { | ||
this.emit('createIntent', { | ||
id: intentId, | ||
name: intentName | ||
id: res.body, | ||
name: intent.name | ||
}); | ||
return intentId; | ||
return res.body; | ||
}); | ||
} | ||
createIntents(intentNames) { | ||
return this.throttler(this.createIntent, intentNames); | ||
createIntents(appVersion, intents) { | ||
return this.throttler(this.createIntent, appVersion, intents); | ||
} | ||
deleteIntent(intentId) { | ||
deleteIntent(appVersion, intent) { | ||
let opts = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/intents/${intentId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/intents/${intent.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deleteIntent', intentId); | ||
this.emit('deleteIntent', intent.id); | ||
}); | ||
} | ||
deleteIntents(intentIds) { | ||
return this.throttler(this.deleteIntent, intentIds) | ||
deleteIntents(appVersion, intents) { | ||
return this.throttler(this.deleteIntent, appVersion, intents) | ||
.then(() => Promise.resolve()); | ||
} | ||
getEntities() { | ||
getEntities(appVersion) { | ||
let opts = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/entities` | ||
uri: `/${this.applicationId}/versions/${appVersion}/entities` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then((res) => res.body.Result) | ||
.then((entities) => entities.map((entity) => { | ||
return { | ||
id: entity.id, | ||
name: entity.name | ||
}; | ||
})); | ||
.then((res) => res.body); | ||
} | ||
createEntity(entityName) { | ||
createEntity(appVersion, entity) { | ||
let opts = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/entities`, | ||
body: { name: entityName } | ||
uri: `/${this.applicationId}/versions/${appVersion}/entities`, | ||
body: entity | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res) => res.body) | ||
.then(entityId => { | ||
.then((res) => { | ||
this.emit('createEntity', { | ||
id: entityId, | ||
name: entityName | ||
id: res.body, | ||
name: entity.name | ||
}); | ||
return entityId; | ||
}); | ||
return res.body; | ||
}) | ||
.catch(err => console.log(JSON.stringify(err))); | ||
} | ||
createEntities(entityNames) { | ||
return this.throttler(this.createEntity, entityNames); | ||
createEntities(appVersion, entities) { | ||
return this.throttler(this.createEntity, appVersion, entities); | ||
} | ||
deleteEntity(entityId) { | ||
deleteEntity(appVersion, entity) { | ||
let opts = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/entities/${entityId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/entities/${entity.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deleteEntity', entityId); | ||
this.emit('deleteEntity', entity.id); | ||
}); | ||
} | ||
deleteEntities(entityIds) { | ||
return this.throttler(this.deleteEntity, entityIds) | ||
deleteEntities(appVersion, entities) { | ||
return this.throttler(this.deleteEntity, appVersion, entities) | ||
.then(() => Promise.resolve()); | ||
} | ||
getPhraseLists() { | ||
getPhraseLists(appVersion) { | ||
let opts = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/phraselists` | ||
uri: `/${this.applicationId}/versions/${appVersion}/phraselists` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then((res) => res.body) | ||
.then((phraseLists) => phraseLists.map((phraseList) => { | ||
return { | ||
id: phraseList.Id.toString(), | ||
name: phraseList.Name, | ||
mode: phraseList.Mode.toLowerCase() === 'non-exchangeable' ? | ||
LuisApi.PhraseListModes.NonExchangeable : LuisApi.PhraseListModes.Exchangeable, | ||
isActive: phraseList.IsActive, | ||
editable: phraseList.Editable, | ||
phrases: phraseList.Phrases | ||
}; | ||
})); | ||
.then((res) => res.body); | ||
} | ||
createPhraseList(phraseList) { | ||
createPhraseList(appVersion, phraseList) { | ||
let opts = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/phraselists`, | ||
body: { | ||
Name: phraseList.name, | ||
Mode: phraseList.mode === LuisApi.PhraseListModes.NonExchangeable ? | ||
'Non-exchangeable' : 'Exchangeable', | ||
IsActive: phraseList.isActive !== undefined ? phraseList.isActive : true, | ||
Editable: phraseList.editable !== undefined ? phraseList.editable : true, | ||
Phrases: phraseList.phrases | ||
} | ||
uri: `/${this.applicationId}/versions/${appVersion}/phraselists`, | ||
body: phraseList | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res) => res.body) | ||
.then(phraseListId => { | ||
.then((res) => { | ||
this.emit('createPhraseList', { | ||
id: phraseListId, | ||
id: res.body, | ||
name: phraseList.name | ||
}); | ||
return phraseListId; | ||
return res.body; | ||
}); | ||
} | ||
createPhraseLists(phraseLists) { | ||
return this.throttler(this.createPhraseList, phraseLists); | ||
createPhraseLists(appVersion, phraseLists) { | ||
return this.throttler(this.createPhraseList, appVersion, phraseLists); | ||
} | ||
deletePhraseList(phraseListId) { | ||
deletePhraseList(appVersion, phraseList) { | ||
let opts = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/phraselists/${phraseListId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/phraselists/${phraseList.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deletePhraseList', phraseListId); | ||
this.emit('deletePhraseList', phraseList.id); | ||
}); | ||
} | ||
deletePhraseLists(phraseListIds) { | ||
return this.throttler(this.deletePhraseList, phraseListIds) | ||
deletePhraseLists(appVersion, phraseLists) { | ||
return this.throttler(this.deletePhraseList, appVersion, phraseLists) | ||
.then(() => Promise.resolve()); | ||
} | ||
getExamples(skip, count) { | ||
function mapEntities(entities) { | ||
return entities.map((entity) => { | ||
return { | ||
name: entity.name, | ||
startToken: entity.indeces.startToken, | ||
endToken: entity.indeces.endToken, | ||
word: entity.word, | ||
isBuiltInExtractor: entity.isBuiltInExtractor | ||
}; | ||
}); | ||
} | ||
getExamples(appVersion, skip, count) { | ||
let opts = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/examples`, | ||
uri: `/${this.applicationId}/versions/${appVersion}/examples`, | ||
qs: { skip, count } | ||
@@ -324,26 +272,9 @@ }; | ||
.then((res) => { | ||
let examples = res.body; | ||
if (examples.length) { | ||
this.emit('getExamples', skip, skip + examples.length - 1); | ||
if (res.body.length) { | ||
this.emit('getExamples', skip, skip + res.body.length - 1); | ||
} | ||
return examples; | ||
}) | ||
.then(examples => examples.map((example) => { | ||
return { | ||
id: example.exampleId, | ||
utteranceText: example.utteranceText, | ||
tokenizedText: example.tokenizedText, | ||
intent: example.IntentsResults.Name, | ||
predictedIntents: example.PredictedIntentResults.map((intent) => { | ||
return { | ||
name: intent.Name, | ||
score: intent.score | ||
}; | ||
}), | ||
entities: mapEntities(example.EntitiesResults), | ||
predictedEntities: mapEntities(example.PredictedEntitiesResults) | ||
}; | ||
})); | ||
return res.body; | ||
}); | ||
} | ||
getAllExamples() { | ||
getAllExamples(appVersion) { | ||
let examplesBunches = []; | ||
@@ -353,3 +284,3 @@ const getExamplesBunch = (skip = 0) => { | ||
.map((e, i) => skip + i * MAX_EXAMPLES_COUNT); | ||
let promises = skipList.map(skip => this.promiseThrottle.add(this.getExamples.bind(this, skip, MAX_EXAMPLES_COUNT))); | ||
let promises = skipList.map(skip => this.promiseThrottle.add(this.getExamples.bind(this, appVersion, skip, MAX_EXAMPLES_COUNT))); | ||
return Promise.all(promises) | ||
@@ -370,13 +301,12 @@ .then((examplesBunch) => { | ||
} | ||
createExamples(examples) { | ||
let createLimitedExamples = (examples) => { | ||
createExamples(appVersion, examples) { | ||
let createLimitedExamples = (appVersion, examples) => { | ||
let opts = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/examples`, | ||
uri: `/${this.applicationId}/versions/${appVersion}/examples`, | ||
body: examples | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res) => res.body) | ||
.then((body) => { | ||
let errors = body.filter(r => r.hasError); | ||
.then((res) => { | ||
let errors = res.body.filter((r) => r.hasError); | ||
if (errors.length) { | ||
@@ -387,21 +317,21 @@ return Promise.reject(new Error('LuisApiClient: The following examples have errors:\n' | ||
this.emit('createExampleBunch', examples.length); | ||
return body.map(r => r.value.ExampleId.toString()); | ||
return res.body.map((r) => r.value.ExampleId.toString()); | ||
}); | ||
}; | ||
let examplesBunches = _.chunk(examples, MAX_EXAMPLES_UPLOAD); | ||
return this.throttler(createLimitedExamples, examplesBunches) | ||
return this.throttler(createLimitedExamples, appVersion, examplesBunches) | ||
.then(_.flatten); | ||
} | ||
deleteExample(exampleId) { | ||
deleteExample(appVersion, example) { | ||
let opts = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/examples/${exampleId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/examples/${example.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deleteExample', exampleId); | ||
this.emit('deleteExample', example.id); | ||
}); | ||
} | ||
deleteExamples(exampleIds) { | ||
return this.throttler(this.deleteExample, exampleIds) | ||
deleteExamples(appVersion, examples) { | ||
return this.throttler(this.deleteExample, appVersion, examples) | ||
.then(() => Promise.resolve()); | ||
@@ -412,8 +342,8 @@ } | ||
let modelTrainingStatus = { | ||
modelId: modelStatus.ModelId, | ||
status: modelStatus.Details.StatusId, | ||
exampleCount: modelStatus.Details.ExampleCount | ||
modelId: modelStatus.modelId, | ||
status: modelStatus.details.statusId, | ||
exampleCount: modelStatus.details.exampleCount | ||
}; | ||
if (modelTrainingStatus.status === LuisApi.TrainingStatuses.Failed) { | ||
modelTrainingStatus.failureReason = modelStatus.Details.FailureReason; | ||
if (modelTrainingStatus.status === LuisApi.TrainingStatuses.Fail) { | ||
modelTrainingStatus.failureReason = modelStatus.details.failureReason; | ||
} | ||
@@ -423,15 +353,14 @@ return modelTrainingStatus; | ||
} | ||
startTraining() { | ||
startTraining(appVersion) { | ||
let opts = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/train` | ||
uri: `/${this.applicationId}/versions/${appVersion}/train` | ||
}; | ||
return this.retryRequest(opts, 202) | ||
.then((res) => res.body) | ||
.then(this.convertTrainingStatus); | ||
.then((res) => res.body); | ||
} | ||
getTrainingStatus() { | ||
getTrainingStatus(appVersion) { | ||
let opts = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/train` | ||
uri: `/${this.applicationId}/versions/${appVersion}/train` | ||
}; | ||
@@ -442,11 +371,10 @@ return this.retryRequest(opts, 200) | ||
} | ||
publish() { | ||
publish(appVersion) { | ||
let opts = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/publish`, | ||
uri: `/${this.applicationId}/publish`, | ||
body: { | ||
BotFramework: { Enabled: false, AppId: '', SubscriptionKey: '', Endpoint: '' }, | ||
Slack: { Enabled: false, ClientId: '', ClientSecret: '', RedirectUri: '' }, | ||
PrivacyStatement: '', | ||
TermsOfUse: '' | ||
versionId: appVersion, | ||
isStaging: false, | ||
region: 'westus' | ||
} | ||
@@ -458,12 +386,13 @@ }; | ||
return { | ||
url: body.URL, | ||
subscriptionKey: body.SubscriptionKey, | ||
publishDate: new Date(body.PublishDate) | ||
endpointUrl: body.endpointUrl, | ||
subscriptionKey: body['subscription-key'], | ||
endpointRegion: body.endpointRegion, | ||
isStaging: body.isStaging | ||
}; | ||
}); | ||
} | ||
export() { | ||
export(appVersion) { | ||
let opts = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/export` | ||
uri: `/${this.applicationId}/versions/${appVersion}/export` | ||
}; | ||
@@ -470,0 +399,0 @@ return this.retryRequest(opts, 200) |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs = require("fs"); | ||
@@ -15,4 +16,2 @@ const commander = require("commander"); | ||
})(Commands || (Commands = {})); | ||
; | ||
let command; | ||
let runner = null; | ||
@@ -28,2 +27,3 @@ const program = commander | ||
.option('-m, --model <filename>', 'JSON file containing the model to upload to the LUIS application') | ||
.option('-v, --app-version <app-version>', 'The version of the application to update') | ||
.action(options => selectRunner(Commands.Update, options)) | ||
@@ -70,3 +70,4 @@ .on('--help', function () { | ||
console.log(); | ||
console.log(` Check whether a set of examples are correctly recognized by an application in terms of intent and entities and save differences to 'errors.json':`); | ||
console.log(' Check whether a set of examples are correctly recognized by an application in terms of intent and entities ' + | ||
'and save differences to \'errors.json\':'); | ||
console.log(' $ luis-cli check -m model.json -a XXX -f errors.json -s YYY'); | ||
@@ -104,18 +105,27 @@ }); | ||
case Commands.Update: | ||
if (!options.appVersion) { | ||
printError('missing version of the model to update. Provide one through the `-v, --version` option.'); | ||
} | ||
if (!options.model) { | ||
printError('missing JSON file from which the model will be read. Provide one through the `-m, --model` option.'); | ||
} | ||
runner = updateApp(luisTrainer, applicationId, options.model); | ||
runner = updateApp(luisTrainer, applicationId, options.appVersion, options.model); | ||
break; | ||
case Commands.Export: | ||
if (!options.appVersion) { | ||
printError('missing version of the model to update. Provide one through the `-v, --version` option.'); | ||
} | ||
if (!options.model) { | ||
printError('missing JSON file to which the application will be exported. Provide one through the `-m, --model` option.'); | ||
} | ||
runner = exportApp(luisTrainer, applicationId, options.model); | ||
runner = exportApp(luisTrainer, applicationId, options.appVersion, options.model); | ||
break; | ||
case Commands.CheckPredictions: | ||
if (!options.appVersion) { | ||
printError('missing version of the model to update. Provide one through the `-v, --version` option.'); | ||
} | ||
if (!options.errors) { | ||
printError('missing JSON file to which the differences will be saved. Provide one through the `-r, --errors` option.'); | ||
} | ||
runner = checkPrediction(luisTrainer, applicationId, options.errors); | ||
runner = checkPrediction(luisTrainer, applicationId, options.appVersion, options.errors); | ||
break; | ||
@@ -133,3 +143,3 @@ case Commands.TestExamples: | ||
} | ||
function updateApp(luisTrainer, applicationId, modelFilename) { | ||
function updateApp(luisTrainer, applicationId, appVersion, modelFilename) { | ||
let model; | ||
@@ -147,3 +157,3 @@ try { | ||
} | ||
console.log(`Updating the application ${applicationId} with the model from "${modelFilename}"...`); | ||
console.log(`Updating the version ${appVersion} of the application ${applicationId} with the model from "${modelFilename}"...`); | ||
console.log(); | ||
@@ -224,3 +234,3 @@ luisTrainer.on('startUpdateIntents', (stats) => { | ||
}); | ||
return luisTrainer.update(model) | ||
return luisTrainer.update(appVersion, model) | ||
.then(() => { | ||
@@ -232,5 +242,5 @@ console.log('The application has been successfully updated'); | ||
} | ||
function exportApp(luisTrainer, applicationId, modelFilename) { | ||
function exportApp(luisTrainer, applicationId, appVersion, modelFilename) { | ||
console.log(`Exporting the application ${applicationId} to "${modelFilename}"...`); | ||
return luisTrainer.export() | ||
return luisTrainer.export(appVersion) | ||
.then(model => { | ||
@@ -243,3 +253,3 @@ fs.writeFileSync(modelFilename, JSON.stringify(model, null, 2)); | ||
} | ||
function checkPrediction(luisTrainer, applicationId, errorsFilename) { | ||
function checkPrediction(luisTrainer, applicationId, appVersion, errorsFilename) { | ||
console.log(`Checking predictions for the application ${applicationId}...`); | ||
@@ -253,3 +263,3 @@ luisTrainer.on('startGetAllExamples', () => { | ||
}); | ||
return luisTrainer.checkPredictions() | ||
return luisTrainer.checkPredictions(appVersion) | ||
.then(predictionResult => { | ||
@@ -304,4 +314,4 @@ if (predictionResult.errors.length) { | ||
function processPredictionResult(predictionResult, errorsFilename) { | ||
let intentErrors = predictionResult.errors.filter(error => error.predictedIntents && !error.ambiguousPredictedIntent).length; | ||
let ambiguousIntentErrors = predictionResult.errors.filter(error => error.predictedIntents && error.ambiguousPredictedIntent).length; | ||
let intentErrors = predictionResult.errors.filter(error => error.intentPredictions && !error.ambiguousPredictedIntent).length; | ||
let ambiguousIntentErrors = predictionResult.errors.filter(error => error.intentPredictions && error.ambiguousPredictedIntent).length; | ||
let entityErrors = predictionResult.errors.filter(error => error.predictedEntities).length; | ||
@@ -308,0 +318,0 @@ let tokenizationErrors = predictionResult.errors.filter(error => error.tokenizedText).length; |
@@ -18,6 +18,6 @@ /// <reference types="node" /> | ||
intent: string; | ||
predictedIntents?: string[]; | ||
intentPredictions?: LuisApi.IntentPrediction[]; | ||
ambiguousPredictedIntent?: boolean; | ||
entities?: LuisApi.LabeledEntity[]; | ||
predictedEntities?: LuisApi.LabeledEntity[]; | ||
entities?: (LuisApi.EntityLabelExampleGET | LuisApi.EntityLabelExamplePOST)[]; | ||
predictedEntities?: LuisApi.EntityLabelExamplePOST[]; | ||
} | ||
@@ -34,14 +34,14 @@ export interface PredictionResult { | ||
constructor(config: LuisTrainerConfig); | ||
export(): Promise<LuisModel.Model>; | ||
update(model: LuisModel.Model): Promise<void>; | ||
checkPredictions(): Promise<PredictionResult>; | ||
export(appVersion: string): Promise<LuisModel.Model>; | ||
update(appVersion: string, model: LuisModel.Model): Promise<void>; | ||
checkPredictions(appVersion: string): Promise<PredictionResult>; | ||
testExamples(examples: LuisModel.Utterance[]): Promise<PredictionResult>; | ||
private static wrapError(error, message); | ||
private checkCulture(culture); | ||
private updateIntents(newIntents); | ||
private updateEntities(newEntities); | ||
private updatePhraseLists(newModelPhraseLists); | ||
private updateExamples(newExamples); | ||
private train(); | ||
private publish(); | ||
private checkCulture(appVersion, culture); | ||
private updateIntents(appVersion, intents); | ||
private updateEntities(appVersion, entities); | ||
private updatePhraseLists(appVersion, modelPhraseLists); | ||
private updateExamples(appVersion, modelExamples); | ||
private train(appVersion); | ||
private publish(appVersion); | ||
private static splitSentenceByTokens(sentence); | ||
@@ -48,0 +48,0 @@ private static tokenizeSentence(sentence); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const _ = require("lodash"); | ||
@@ -11,4 +12,4 @@ const events_1 = require("events"); | ||
} | ||
export() { | ||
return this.luisApiClient.export() | ||
export(appVersion) { | ||
return this.luisApiClient.export(appVersion) | ||
.catch((reason) => { | ||
@@ -20,28 +21,41 @@ let err = new Error('Error trying to export the app'); | ||
} | ||
update(model) { | ||
return this.checkCulture(model.culture) | ||
.then(() => this.updateIntents(model.intents.map(intent => intent.name))) | ||
.then(() => this.updateEntities(model.entities.map(entity => entity.name))) | ||
.then(() => this.updatePhraseLists(model.model_features)) | ||
.then(() => this.updateExamples(model.utterances)) | ||
.then(() => this.train()) | ||
.then(() => this.publish()) | ||
update(appVersion, model) { | ||
return this.checkCulture(appVersion, model.culture) | ||
.then(() => this.updateIntents(appVersion, model.intents.map(intent => { | ||
return { | ||
name: intent.name | ||
}; | ||
}))) | ||
.then(() => this.updateEntities(appVersion, model.entities.map(entity => { | ||
return { | ||
name: entity.name | ||
}; | ||
}))) | ||
.then(() => this.updatePhraseLists(appVersion, model.model_features)) | ||
.then(() => this.updateExamples(appVersion, model.utterances)) | ||
.then(() => this.train(appVersion)) | ||
.then(() => this.publish(appVersion)) | ||
.then(() => Promise.resolve()); | ||
} | ||
checkPredictions() { | ||
checkPredictions(appVersion) { | ||
function getTopPredictedIntents(example) { | ||
return example.predictedIntents | ||
return example.intentPredictions | ||
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)) | ||
.filter((predictedIntent, i, sortedPredictedIntents) => predictedIntent.score === sortedPredictedIntents[0].score) | ||
.map(predictedIntent => predictedIntent.name); | ||
.map(predictedIntent => { | ||
return { | ||
name: predictedIntent.name, | ||
score: predictedIntent.score | ||
}; | ||
}); | ||
} | ||
function matchPredictedEntities(example) { | ||
return _.differenceWith(example.entities, example.predictedEntities, _.isEqual).length === 0; | ||
return _.differenceWith(example.entityLabels, example.entityPredictions, _.isEqual).length === 0; | ||
} | ||
function matchTokenizedText(example) { | ||
return _.isEqual(example.tokenizedText, LuisTrainer.tokenizeSentence(example.utteranceText)); | ||
return _.isEqual(example.tokenizedText, LuisTrainer.tokenizeSentence(example.text)); | ||
} | ||
this.emit('startGetAllExamples'); | ||
this.luisApiClient.on('getExamples', (first, last) => this.emit('getExamples', first, last)); | ||
return this.luisApiClient.getAllExamples() | ||
return this.luisApiClient.getAllExamples(appVersion) | ||
.then(examples => { | ||
@@ -52,13 +66,13 @@ this.emit('endGetAllExamples', examples.length); | ||
let error = { | ||
text: example.utteranceText, | ||
intent: example.intent | ||
text: example.text, | ||
intent: example.intentLabel | ||
}; | ||
let topPredictedIntents = getTopPredictedIntents(example); | ||
if (topPredictedIntents.indexOf(example.intent) === -1) { | ||
if (topPredictedIntents.findIndex(element => element.name === example.intentLabel) === -1) { | ||
error.ambiguousPredictedIntent = false; | ||
error.predictedIntents = topPredictedIntents; | ||
error.intentPredictions = topPredictedIntents; | ||
} | ||
else if (topPredictedIntents.length > 1) { | ||
error.ambiguousPredictedIntent = true; | ||
error.predictedIntents = topPredictedIntents; | ||
error.intentPredictions = topPredictedIntents; | ||
} | ||
@@ -68,4 +82,10 @@ else { | ||
if (!matchPredictedEntities(example)) { | ||
error.entities = example.entities; | ||
error.predictedEntities = example.predictedEntities; | ||
error.entities = example.entityLabels; | ||
error.predictedEntities = example.entityPredictions.map(entityPrediction => { | ||
return { | ||
entityName: entityPrediction.entityName, | ||
startCharIndex: entityPrediction.startIndex, | ||
endCharIndex: entityPrediction.endIndex | ||
}; | ||
}); | ||
} | ||
@@ -75,3 +95,3 @@ if (!matchTokenizedText(example)) { | ||
} | ||
if (error.predictedIntents || error.predictedEntities || error.tokenizedText) { | ||
if (error.intentPredictions || error.predictedEntities || error.tokenizedText) { | ||
return error; | ||
@@ -84,5 +104,5 @@ } | ||
.filter(example => example !== null); | ||
let exampleCounter = _.countBy(examples, example => example.intent); | ||
let intentErrorCounter = _.countBy(errors.filter(error => error.predictedIntents && !error.ambiguousPredictedIntent), error => error.intent); | ||
let ambiguousIntentCounter = _.countBy(errors.filter(error => error.predictedIntents && error.ambiguousPredictedIntent), error => error.intent); | ||
let exampleCounter = _.countBy(examples, example => example.intentLabel); | ||
let intentErrorCounter = _.countBy(errors.filter(error => error.intentPredictions && !error.ambiguousPredictedIntent), error => error.intent); | ||
let ambiguousIntentCounter = _.countBy(errors.filter(error => error.intentPredictions && error.ambiguousPredictedIntent), error => error.intent); | ||
let stats = new Map(); | ||
@@ -104,6 +124,5 @@ _.forEach(exampleCounter, (counter, intent) => { | ||
function matchRecognizedEntities(labeledEntities, recognizedEntities) { | ||
return _.differenceWith(labeledEntities, recognizedEntities, (labeled, recognized) => labeled.name === recognized.type && | ||
labeled.word === recognized.entity && | ||
labeled.startToken === recognized.startIndex && | ||
labeled.endToken === recognized.endIndex).length === 0; | ||
return _.differenceWith(labeledEntities, recognizedEntities, (labeled, recognized) => labeled.entityName === recognized.entity && | ||
labeled.startCharIndex === recognized.startIndex && | ||
labeled.endCharIndex === recognized.endIndex).length === 0; | ||
} | ||
@@ -120,5 +139,10 @@ this.luisApiClient.on('recognizeSentence', (sentence) => this.emit('recognizeSentence', sentence)); | ||
}; | ||
if (example.intent !== recognitionResult.intent) { | ||
if (example.intent !== recognitionResult.topScoringIntent.intent) { | ||
error.ambiguousPredictedIntent = false; | ||
error.predictedIntents = [recognitionResult.intent]; | ||
error.intentPredictions = [ | ||
{ | ||
name: recognitionResult.topScoringIntent.intent, | ||
score: recognitionResult.topScoringIntent.score | ||
} | ||
]; | ||
} | ||
@@ -128,7 +152,5 @@ let labeledEntities = example.entities.map(entity => { | ||
return { | ||
name: entity.entity, | ||
startToken: foundEntity.startChar, | ||
endToken: foundEntity.endChar, | ||
word: foundEntity.word, | ||
isBuiltInExtractor: false | ||
entityName: foundEntity.word, | ||
startCharIndex: foundEntity.startChar, | ||
endCharIndex: foundEntity.endChar | ||
}; | ||
@@ -140,11 +162,9 @@ }); | ||
return { | ||
name: entity.type, | ||
startToken: entity.startIndex, | ||
endToken: entity.endIndex, | ||
word: entity.entity, | ||
isBuiltInExtractor: false | ||
entityName: entity.entity, | ||
startCharIndex: entity.startIndex, | ||
endCharIndex: entity.endIndex | ||
}; | ||
}); | ||
} | ||
if (error.predictedIntents || error.predictedEntities) { | ||
if (error.intentPredictions || error.predictedEntities) { | ||
return error; | ||
@@ -158,3 +178,3 @@ } | ||
let exampleCounter = _.countBy(examples, example => example.intent); | ||
let intentErrorCounter = _.countBy(errors.filter(error => error.predictedIntents), error => error.intent); | ||
let intentErrorCounter = _.countBy(errors.filter(error => error.intentPredictions), error => error.intent); | ||
let stats = new Map(); | ||
@@ -182,3 +202,3 @@ _.forEach(exampleCounter, (counter, intent) => { | ||
} | ||
checkCulture(culture) { | ||
checkCulture(appVersion, culture) { | ||
return this.luisApiClient.getApp() | ||
@@ -198,14 +218,14 @@ .then(appInfo => { | ||
} | ||
updateIntents(newIntents) { | ||
return this.luisApiClient.getIntents() | ||
updateIntents(appVersion, intents) { | ||
return this.luisApiClient.getIntents(appVersion) | ||
.then(oldIntents => { | ||
let intentIdsToBeDeleted = _.differenceWith(oldIntents, newIntents, (a, b) => a.name === b).map(intent => intent.id); | ||
let intentsToBeCreated = _.differenceWith(newIntents, oldIntents, (a, b) => a === b.name); | ||
let intentsToBeDeleted = _.differenceWith(oldIntents, intents, (a, b) => a.name === b.name); | ||
let intentsToBeCreated = _.differenceWith(intents, oldIntents, (a, b) => a.name === b.name); | ||
let stats = { | ||
create: intentsToBeCreated.length, | ||
delete: intentIdsToBeDeleted.length | ||
delete: intentsToBeDeleted.length | ||
}; | ||
this.emit('startUpdateIntents', stats); | ||
return this.luisApiClient.deleteIntents(intentIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createIntents(intentsToBeCreated)) | ||
return this.luisApiClient.deleteIntents(appVersion, intentsToBeDeleted) | ||
.then(() => this.luisApiClient.createIntents(appVersion, intentsToBeCreated)) | ||
.then(() => { | ||
@@ -217,14 +237,14 @@ this.emit('endUpdateIntents', stats); | ||
} | ||
updateEntities(newEntities) { | ||
return this.luisApiClient.getEntities() | ||
updateEntities(appVersion, entities) { | ||
return this.luisApiClient.getEntities(appVersion) | ||
.then(oldEntities => { | ||
let entityIdsToBeDeleted = _.differenceWith(oldEntities, newEntities, (a, b) => a.name === b).map(entity => entity.id); | ||
let entitiesToBeCreated = _.differenceWith(newEntities, oldEntities, (a, b) => a === b.name); | ||
let entitiesToBeDeleted = _.differenceWith(oldEntities, entities, (a, b) => a.name === b.name); | ||
let entitiesToBeCreated = _.differenceWith(entities, oldEntities, (a, b) => a.name === b.name); | ||
let stats = { | ||
create: entitiesToBeCreated.length, | ||
delete: entityIdsToBeDeleted.length | ||
delete: entitiesToBeDeleted.length | ||
}; | ||
this.emit('startUpdateEntities', stats); | ||
return this.luisApiClient.deleteEntities(entityIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createEntities(entitiesToBeCreated)) | ||
return this.luisApiClient.deleteEntities(appVersion, entitiesToBeDeleted) | ||
.then(() => this.luisApiClient.createEntities(appVersion, entitiesToBeCreated)) | ||
.then(() => { | ||
@@ -236,30 +256,28 @@ this.emit('endUpdateEntities', stats); | ||
} | ||
updatePhraseLists(newModelPhraseLists) { | ||
updatePhraseLists(appVersion, modelPhraseLists) { | ||
const comparePhraseLists = (a, b) => { | ||
return a.name === b.name && | ||
a.mode === b.mode && | ||
a.isActive === b.isActive && | ||
a.isExchangeable === b.isExchangeable && | ||
a.phrases === b.phrases; | ||
}; | ||
return this.luisApiClient.getPhraseLists() | ||
return this.luisApiClient.getPhraseLists(appVersion) | ||
.then(oldPhraseLists => { | ||
let newPhraseLists = newModelPhraseLists.map((phraseList) => { | ||
let phraseLists = modelPhraseLists.map((phraseList) => { | ||
return { | ||
name: phraseList.name, | ||
mode: phraseList.mode === false ? | ||
luis_api_client_1.LuisApi.PhraseListModes.NonExchangeable : luis_api_client_1.LuisApi.PhraseListModes.Exchangeable, | ||
isActive: phraseList.activated, | ||
isExchangeable: true, | ||
phrases: phraseList.words | ||
}; | ||
}); | ||
let phraseListIdsToBeDeleted = _.differenceWith(oldPhraseLists, newPhraseLists, comparePhraseLists) | ||
.map(phraseList => phraseList.id); | ||
let phraseListToBeCreated = _.differenceWith(newPhraseLists, oldPhraseLists, comparePhraseLists); | ||
let phraseListsToBeDeleted = _.differenceWith(oldPhraseLists, phraseLists, comparePhraseLists); | ||
let phraseListToBeCreated = _.differenceWith(phraseLists, oldPhraseLists, comparePhraseLists); | ||
let stats = { | ||
create: phraseListToBeCreated.length, | ||
delete: phraseListIdsToBeDeleted.length | ||
delete: phraseListsToBeDeleted.length | ||
}; | ||
this.emit('startUpdatePhraseLists', stats); | ||
return this.luisApiClient.deletePhraseLists(phraseListIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createPhraseLists(phraseListToBeCreated)) | ||
return this.luisApiClient.deletePhraseLists(appVersion, phraseListsToBeDeleted) | ||
.then(() => this.luisApiClient.createPhraseLists(appVersion, phraseListToBeCreated)) | ||
.then(() => { | ||
@@ -271,28 +289,30 @@ this.emit('endUpdatePhraseLists', stats); | ||
} | ||
updateExamples(newExamples) { | ||
updateExamples(appVersion, modelExamples) { | ||
this.emit('startGetAllExamples'); | ||
this.luisApiClient.on('getExamples', (first, last) => this.emit('getExamples', first, last)); | ||
return this.luisApiClient.getAllExamples() | ||
return this.luisApiClient.getAllExamples(appVersion) | ||
.then(oldExamples => { | ||
this.emit('endGetAllExamples', oldExamples.length); | ||
let exampleIdsToBeDeleted = _.differenceWith(oldExamples, newExamples, (a, b) => a.utteranceText === b.text).map((example) => example.id); | ||
let examplesToBeCreated = _.differenceWith(newExamples, oldExamples, (a, b) => { | ||
let eq = a.text === b.utteranceText && | ||
a.intent === b.intent && | ||
a.entities.length === b.entities.length && | ||
a.entities.length === _.intersectionWith(a.entities, b.entities, (ae, be) => ae.entity === be.name && ae.startPos === be.startToken && ae.endPos === be.endToken).length; | ||
let examplesToBeDeleted = _.differenceWith(oldExamples, modelExamples, (a, b) => a.text === b.text); | ||
let examplesToBeCreated = _.differenceWith(modelExamples, oldExamples, (a, b) => { | ||
let eq = a.text === b.text && | ||
a.intent === b.intentLabel && | ||
a.entities.length === b.entityLabels.length && | ||
a.entities.length === _.intersectionWith(a.entities, b.entityLabels, (ae, be) => ae.entity === be.entityName && ae.startPos === be.startCharIndex && ae.endPos === be.endCharIndex).length; | ||
return eq; | ||
}) | ||
.map((example) => { | ||
let entityLabels = example.entities.map(entity => { | ||
let foundEntity = LuisTrainer.findEntity(example.text, entity.startPos, entity.endPos); | ||
return { | ||
entityName: entity.entity, | ||
startCharIndex: foundEntity.startChar, | ||
endCharIndex: foundEntity.endChar | ||
}; | ||
}); | ||
entityLabels = (entityLabels.length === 0) ? null : entityLabels; | ||
return { | ||
exampleText: example.text, | ||
selectedIntentName: example.intent, | ||
entityLabels: example.entities.map(entity => { | ||
let foundEntity = LuisTrainer.findEntity(example.text, entity.startPos, entity.endPos); | ||
return { | ||
entityType: entity.entity, | ||
startToken: foundEntity.startChar, | ||
endToken: foundEntity.endChar | ||
}; | ||
}) | ||
text: example.text, | ||
intentName: example.intent, | ||
entityLabels: entityLabels | ||
}; | ||
@@ -302,3 +322,3 @@ }); | ||
create: examplesToBeCreated.length, | ||
delete: exampleIdsToBeDeleted.length | ||
delete: examplesToBeDeleted.length | ||
}; | ||
@@ -308,4 +328,4 @@ this.emit('startUpdateExamples', stats); | ||
this.luisApiClient.on('createExampleBunch', (bunchLength) => this.emit('createExampleBunch', bunchLength)); | ||
return this.luisApiClient.deleteExamples(exampleIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createExamples(examplesToBeCreated)) | ||
return this.luisApiClient.deleteExamples(appVersion, examplesToBeDeleted) | ||
.then(() => this.luisApiClient.createExamples(appVersion, examplesToBeCreated)) | ||
.then(() => { | ||
@@ -317,3 +337,3 @@ this.emit('endUpdateExamples', stats); | ||
} | ||
train() { | ||
train(appVersion) { | ||
const delay = (t) => { | ||
@@ -326,7 +346,7 @@ return new Promise(resolve => { | ||
return delay(TRAINING_STATUS_POLLING_INTERVAL) | ||
.then(() => this.luisApiClient.getTrainingStatus()) | ||
.then(() => this.luisApiClient.getTrainingStatus(appVersion)) | ||
.then((trainingStatus) => { | ||
let finishedModels = trainingStatus.filter(modelStatus => modelStatus.status === luis_api_client_1.LuisApi.TrainingStatuses.Success || | ||
modelStatus.status === luis_api_client_1.LuisApi.TrainingStatuses.UpToDate || | ||
modelStatus.status === luis_api_client_1.LuisApi.TrainingStatuses.Failed); | ||
modelStatus.status === luis_api_client_1.LuisApi.TrainingStatuses.Fail); | ||
this.emit('trainingProgress', finishedModels.length, trainingStatus.length); | ||
@@ -337,3 +357,3 @@ if (finishedModels.length < trainingStatus.length) { | ||
let failedModels = trainingStatus | ||
.filter(modelStatus => modelStatus.status === luis_api_client_1.LuisApi.TrainingStatuses.Failed); | ||
.filter(modelStatus => modelStatus.status === luis_api_client_1.LuisApi.TrainingStatuses.Fail); | ||
if (failedModels.length) { | ||
@@ -352,3 +372,3 @@ let err = new Error(`${failedModels.length} model(s) have failed with the following reasons:\n` + | ||
this.emit('startTraining'); | ||
return this.luisApiClient.startTraining() | ||
return this.luisApiClient.startTraining(appVersion) | ||
.then((trainingStatus) => { | ||
@@ -359,5 +379,5 @@ return waitForTraining(); | ||
} | ||
publish() { | ||
publish(appVersion) { | ||
this.emit('startPublish'); | ||
return this.luisApiClient.publish() | ||
return this.luisApiClient.publish(appVersion) | ||
.then(publishResult => { | ||
@@ -364,0 +384,0 @@ this.emit('endPublish'); |
{ | ||
"name": "@telefonica/luis-cli", | ||
"version": "4.0.0", | ||
"version": "5.0.0", | ||
"description": "CLI for Microsoft LUIS API", | ||
"license": "Apache-2.0", | ||
"repository": "https://github.com/Telefonica/yot-bot", | ||
"repository": "https://github.com/Telefonica/luis-cli", | ||
"author": "TDAF <tdaf@tid.es>", | ||
@@ -21,28 +21,29 @@ "bin": { | ||
"lint": "tslint './src/**/*.ts'", | ||
"prepublish": "npm run build", | ||
"security": "nsp check", | ||
"test": "npm run build && mocha -R spec 'lib/**/*.spec.js'" | ||
"test": "npm run build && mocha -R spec lib/**/*.spec.js" | ||
}, | ||
"dependencies": { | ||
"colors": "^1.1.2", | ||
"commander": "^2.9.0", | ||
"commander": "^2.11.0", | ||
"lodash": "^4.17.4", | ||
"promise-retry": "^1.1.1", | ||
"promise-throttle": "^0.3.1", | ||
"request": "^2.79.0", | ||
"request-promise-native": "^1.0.3", | ||
"sprintf-js": "^1.0.3" | ||
"request": "^2.83.0", | ||
"request-promise-native": "^1.0.5", | ||
"sprintf-js": "^1.1.1" | ||
}, | ||
"devDependencies": { | ||
"@telefonica/language-model-converter": "^2.5.0", | ||
"@types/colors": "^1.1.1", | ||
"@types/commander": "^2.3.31", | ||
"@types/lodash": "^4.14.52", | ||
"@types/node": "^6.0.40", | ||
"@types/request-promise-native": "^1.0.2", | ||
"@types/sprintf-js": "0.0.27", | ||
"mocha": "^3.2.0", | ||
"nsp": "^2.6.2", | ||
"@telefonica/language-model-converter": "^2.7.0", | ||
"@types/colors": "^1.1.3", | ||
"@types/commander": "^2.11.0", | ||
"@types/lodash": "^4.14.78", | ||
"@types/node": "^6", | ||
"@types/request-promise-native": "^1.0.8", | ||
"@types/sprintf-js": "1.1.0", | ||
"mocha": "^4.0.1", | ||
"nsp": "^2.8.1", | ||
"shx": "^0.2.2", | ||
"tslint": "^4.4.2", | ||
"typescript": "^2.1.5" | ||
"tslint": "^5.8.0", | ||
"typescript": "^2.5.3" | ||
}, | ||
@@ -49,0 +50,0 @@ "engines": { |
@@ -22,6 +22,11 @@ # Luis Command-line Interface | ||
Type `luis-cli -h` to get more info about the available commands and general options. | ||
To know more about one command, type `luis-cli <command> -h`. | ||
Type `./bin/luis-cli -h` to get more info about the available commands and general options. | ||
To know more about one command, type `./bin/luis-cli <command> -h`. | ||
For example: to update, to train and to publish a concrete LUIS application version based on a new LUIS model: | ||
`./bin/luis-cli update -s aac4012305e94ffebe2fa8408954aab3 -a b073cd3d-3b86-4367-813a-638840c83e3c -m ./training_model.json -v 0.3` | ||
For additional information about LUIS training models, please visit: https://github.com/Telefonica/language-model-converter | ||
## LICENSE | ||
@@ -28,0 +33,0 @@ |
@@ -24,24 +24,6 @@ /** | ||
import { RequestResponse } from 'request'; | ||
// Fix the `RequestResponse` interface exported by the `request` module which is missing the `body` property | ||
declare module 'request' { | ||
interface RequestResponse { | ||
body?: any; | ||
} | ||
} | ||
export namespace LuisApi { | ||
export interface RecognizedEntity { | ||
entity: string; | ||
type: string; | ||
startIndex: number; | ||
endIndex: number; | ||
} | ||
export interface RecognitionResult { | ||
sentence: string; | ||
intent: string; | ||
entities: RecognizedEntity[]; | ||
} | ||
export interface AppInfo { | ||
/* See: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c37 */ | ||
export interface AppInfoGET { | ||
id: string; | ||
@@ -51,63 +33,131 @@ name: string; | ||
culture: string; | ||
active: boolean; | ||
numberOfIntents: number; | ||
numberOfEntities: number; | ||
isTrained: boolean; | ||
} | ||
export interface IntentClassifier { | ||
/* | ||
See: | ||
- GET: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c0d | ||
- POST: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c0c | ||
- DELETE: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c1c | ||
*/ | ||
export interface IntentGET { | ||
id: string; | ||
name: string; | ||
typeId: number; | ||
readableType: string; | ||
} | ||
export interface IntentPOST { | ||
name: string; | ||
} | ||
export interface IntentDELETE { | ||
id: string; | ||
} | ||
export interface EntityExtractor { | ||
/* | ||
See: | ||
- GET: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c0f | ||
- POST: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c0e | ||
- DELETE: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c1f | ||
*/ | ||
export interface EntityGET { | ||
id: string; | ||
name: string; | ||
typeId: number; | ||
readableType: string; | ||
} | ||
export interface EntityPOST { | ||
name: string; | ||
} | ||
export interface EntityDELETE { | ||
id: string; | ||
} | ||
export enum PhraseListModes { Exchangeable, NonExchangeable } | ||
export interface PhraseList { | ||
id?: string; | ||
/* | ||
See: | ||
- GET: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c00 | ||
- POST: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9bff | ||
- DELETE: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c07 | ||
*/ | ||
export interface PhraseListGET { | ||
id: number; | ||
name: string; | ||
mode: PhraseListModes; | ||
isActive?: boolean; | ||
editable?: boolean; | ||
isActive: boolean; | ||
isExchangeable: boolean; | ||
phrases: string; | ||
} | ||
export interface LabeledEntity { | ||
export interface PhraseListPOST { | ||
name: string; | ||
startToken: number; | ||
endToken: number; | ||
word: string; | ||
isBuiltInExtractor: boolean; | ||
phrases: string; | ||
isExchangeable: boolean; | ||
} | ||
export interface LabeledUtterance { | ||
export interface PhraseListDELETE { | ||
id: string; | ||
utteranceText: string; | ||
tokenizedText: string[]; | ||
intent: string; | ||
predictedIntents: [{ | ||
name: string; | ||
score: number; | ||
}]; | ||
entities: LabeledEntity[]; | ||
predictedEntities: LabeledEntity[]; | ||
} | ||
export interface Entity { | ||
entityType: string; | ||
startToken: number; | ||
endToken: number; | ||
/* | ||
See: | ||
- GET: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c0a | ||
- POST: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c08 | ||
- DELETE: https://westus.dev.cognitive.microsoft.com/docs/services/5890b47c39e2bb17b84a55ff/operations/5890b47c39e2bb052c5b9c0b | ||
*/ | ||
export interface EntityLabelExampleGET { | ||
entityName: string; | ||
startTokenIndex: number; | ||
endTokenIndex: number; | ||
} | ||
export interface EntityLabelExamplePOST { | ||
entityName: string; | ||
startCharIndex: number; | ||
endCharIndex: number; | ||
} | ||
export interface IntentPrediction { | ||
name: string; | ||
score: number; | ||
} | ||
export interface EntityPrediction { | ||
entityName: string; | ||
startIndex: number; | ||
endIndex: number; | ||
phrase: string; | ||
} | ||
export interface ExampleGET { | ||
id: number; | ||
text: string; | ||
tokenizedText: string[]; | ||
intentLabel: string; | ||
entityLabels: EntityLabelExampleGET[]; | ||
intentPredictions: IntentPrediction[]; | ||
entityPredictions: EntityPrediction[]; | ||
} | ||
export interface ExamplePOST { | ||
text: string; | ||
intentName: string; | ||
entityLabels: EntityLabelExamplePOST[]; | ||
} | ||
export interface ExampleDELETE { | ||
id: number; | ||
} | ||
export interface Example { | ||
exampleText: string; | ||
selectedIntentName: string; | ||
entityLabels?: Entity[]; | ||
/* | ||
See: | ||
- GET: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 | ||
*/ | ||
export interface RecognizedIntent { | ||
intent: string; | ||
score: number; | ||
} | ||
export interface RecognizedEntity { | ||
entity: string; | ||
type: string; | ||
startIndex: number; | ||
endIndex: number; | ||
score: number; | ||
} | ||
export interface RecognitionResult { | ||
query: string; | ||
topScoringIntent: RecognizedIntent; | ||
intents: RecognizedIntent[]; | ||
entities: RecognizedEntity[]; | ||
} | ||
export enum TrainingStatuses { Success = 0, Failed = 1, UpToDate = 2, InProgress = 3 } | ||
export enum TrainingStatuses { Success = 0, Fail = 1, UpToDate = 2, InProgress = 3 } | ||
@@ -124,5 +174,6 @@ export interface ModelTrainingStatus { | ||
export interface PublishResult { | ||
url: string; | ||
endpointUrl: string; | ||
subscriptionKey: string; | ||
publishDate: Date; | ||
endpointRegion: string; | ||
isStaging: boolean; | ||
} | ||
@@ -132,4 +183,2 @@ } | ||
const LUIS_API_BASE_URL = 'https://westus.api.cognitive.microsoft.com'; | ||
// Alternative endpoint only for the service API in order to use the private endpoint deployed in EU | ||
const LUIS_SERVICE_API_BASE_URL = 'http://luis-europe-west-endpoint.cloudapp.net'; | ||
const RETRY_OPTS = { | ||
@@ -168,8 +217,5 @@ retries: 10, | ||
this.serviceReq = request.defaults({ | ||
// XXX: the public service API use a slightly different path, so this should be changed in the future | ||
baseUrl: `${LUIS_SERVICE_API_BASE_URL}/api/v2.0/apps/${this.applicationId}`, | ||
qs: { | ||
'subscription-key': config.subscriptionKey, | ||
verbose: false, | ||
spellCheck: false | ||
baseUrl: `${baseUrl}/luis/v2.0/apps/`, | ||
headers: { | ||
'Ocp-Apim-Subscription-Key': config.subscriptionKey | ||
}, | ||
@@ -181,3 +227,3 @@ json: true, | ||
this.provisionReq = request.defaults({ | ||
baseUrl: `${baseUrl}/luis/v1.0/prog/apps`, | ||
baseUrl: `${baseUrl}/luis/api/v2.0/apps/`, | ||
headers: { | ||
@@ -219,4 +265,4 @@ 'Ocp-Apim-Subscription-Key': config.subscriptionKey | ||
*/ | ||
private throttler(fn: Function, items: any[]): Promise<any> { | ||
let promises = items.map(item => this.promiseThrottle.add(fn.bind(this, item))); | ||
private throttler(fn: Function, appVersion: string, items: any[]): Promise<any> { | ||
let promises = items.map(item => this.promiseThrottle.add(fn.bind(this, appVersion, item))); | ||
return Promise.all(promises); | ||
@@ -228,3 +274,3 @@ } | ||
method: 'GET', | ||
uri: '', | ||
uri: `/${this.applicationId}`, | ||
qs: { q: sentence } | ||
@@ -241,4 +287,5 @@ }; | ||
return { | ||
sentence: res.body.query, | ||
intent: res.body.topScoringIntent.intent, | ||
query: res.body.query, | ||
topScoringIntent: res.body.topScoringIntent, | ||
intents: res.body.intents, | ||
entities: res.body.entities.map((entity: any) => { | ||
@@ -249,3 +296,4 @@ return { | ||
startIndex: entity.startIndex, | ||
endIndex: entity.endIndex | ||
endIndex: entity.endIndex, | ||
score: entity.score | ||
} as LuisApi.RecognizedEntity; | ||
@@ -257,3 +305,3 @@ }) | ||
recognizeSentences(sentences: string[]): Promise<LuisApi.RecognitionResult[]> { | ||
recognizeSentences(queries: string[]): Promise<LuisApi.RecognitionResult[]> { | ||
let promiseThrottle = new PromiseThrottle({ | ||
@@ -263,10 +311,10 @@ requestsPerSecond: SERVICE_API_REQUESTS_PER_SECOND, | ||
}); | ||
let promises = sentences.map(sentence => promiseThrottle.add(this.recognizeSentence.bind(this, sentence))); | ||
let promises = queries.map(query => promiseThrottle.add(this.recognizeSentence.bind(this, query))); | ||
return Promise.all(promises) as Promise<LuisApi.RecognitionResult[]>; | ||
} | ||
getApp(): Promise<LuisApi.AppInfo> { | ||
getApp(): Promise<LuisApi.AppInfoGET> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: this.applicationId | ||
uri: `/${this.applicationId}` | ||
}; | ||
@@ -277,63 +325,52 @@ return this.retryRequest(opts, 200) | ||
return { | ||
id: body.ID, | ||
name: body.Name, | ||
description: body.Description, | ||
culture: body.Culture, | ||
active: body.Active, | ||
numberOfIntents: body.NumberOfIntents, | ||
numberOfEntities: body.NumberOfEntities, | ||
isTrained: body.IsTrained | ||
} as LuisApi.AppInfo; | ||
id: body.id, | ||
name: body.name, | ||
description: body.description, | ||
culture: body.culture | ||
} as LuisApi.AppInfoGET; | ||
}); | ||
} | ||
getIntents(): Promise<LuisApi.IntentClassifier[]> { | ||
getIntents(appVersion: string): Promise<LuisApi.IntentGET[]> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/intents` | ||
uri: `/${this.applicationId}/versions/${appVersion}/intents` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then((res: RequestResponse) => res.body.Result) | ||
.then((intents) => intents.map((intent: any) => { | ||
return { | ||
id: intent.id, | ||
name: intent.name | ||
} as LuisApi.IntentClassifier; | ||
})); | ||
.then((res: RequestResponse) => res.body as LuisApi.IntentGET[]); | ||
} | ||
createIntent(intentName: string): Promise<string> { | ||
createIntent(appVersion: string, intent: LuisApi.IntentPOST): Promise<string> { | ||
let opts: request.Options = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/intents`, | ||
body: { name: intentName } | ||
uri: `/${this.applicationId}/versions/${appVersion}/intents`, | ||
body: intent | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res: RequestResponse) => res.body) | ||
.then(intentId => { | ||
.then((res: RequestResponse) => { | ||
this.emit('createIntent', { | ||
id: intentId, | ||
name: intentName | ||
id: res.body, | ||
name: intent.name | ||
}); | ||
return intentId; | ||
return res.body; | ||
}); | ||
} | ||
createIntents(intentNames: string[]): Promise<string[]> { | ||
return this.throttler(this.createIntent, intentNames) as Promise<string[]>; | ||
createIntents(appVersion: string, intents: LuisApi.IntentPOST[]): Promise<string[]> { | ||
return this.throttler(this.createIntent, appVersion, intents) as Promise<string[]>; | ||
} | ||
deleteIntent(intentId: string): Promise<void> { | ||
deleteIntent(appVersion: string, intent: LuisApi.IntentDELETE): Promise<void> { | ||
let opts: request.Options = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/intents/${intentId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/intents/${intent.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deleteIntent', intentId); | ||
this.emit('deleteIntent', intent.id); | ||
}); | ||
} | ||
deleteIntents(intentIds: string[]): Promise<void> { | ||
return this.throttler(this.deleteIntent, intentIds) | ||
deleteIntents(appVersion: string, intents: LuisApi.IntentDELETE[]): Promise<void> { | ||
return this.throttler(this.deleteIntent, appVersion, intents) | ||
.then(() => Promise.resolve()); | ||
@@ -343,134 +380,97 @@ | ||
getEntities(): Promise<LuisApi.EntityExtractor[]> { | ||
getEntities(appVersion: string): Promise<LuisApi.EntityGET[]> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/entities` | ||
uri: `/${this.applicationId}/versions/${appVersion}/entities` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then((res: RequestResponse) => res.body.Result) | ||
.then((entities) => entities.map((entity: any) => { | ||
return { | ||
id: entity.id, | ||
name: entity.name | ||
} as LuisApi.EntityExtractor; | ||
})); | ||
.then((res: RequestResponse) => res.body as LuisApi.EntityGET[]); | ||
} | ||
createEntity(entityName: string): Promise<string> { | ||
createEntity(appVersion: string, entity: LuisApi.EntityPOST): Promise<string> { | ||
let opts: request.Options = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/entities`, | ||
body: { name: entityName } | ||
uri: `/${this.applicationId}/versions/${appVersion}/entities`, | ||
body: entity | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res: RequestResponse) => res.body) | ||
.then(entityId => { | ||
.then((res: RequestResponse) => { | ||
this.emit('createEntity', { | ||
id: entityId, | ||
name: entityName | ||
id: res.body, | ||
name: entity.name | ||
}); | ||
return entityId; | ||
}); | ||
return res.body; | ||
}) | ||
.catch(err => console.log(JSON.stringify(err))); | ||
} | ||
createEntities(entityNames: string[]): Promise<string[]> { | ||
return this.throttler(this.createEntity, entityNames) as Promise<string[]>; | ||
createEntities(appVersion: string, entities: LuisApi.EntityPOST[]): Promise<string[]> { | ||
return this.throttler(this.createEntity, appVersion, entities) as Promise<string[]>; | ||
} | ||
deleteEntity(entityId: string): Promise<void> { | ||
deleteEntity(appVersion: string, entity: LuisApi.EntityDELETE): Promise<void> { | ||
let opts: request.Options = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/entities/${entityId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/entities/${entity.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deleteEntity', entityId); | ||
this.emit('deleteEntity', entity.id); | ||
}); | ||
} | ||
deleteEntities(entityIds: string[]): Promise<void> { | ||
return this.throttler(this.deleteEntity, entityIds) | ||
deleteEntities(appVersion: string, entities: LuisApi.EntityDELETE[]): Promise<void> { | ||
return this.throttler(this.deleteEntity, appVersion, entities) | ||
.then(() => Promise.resolve()); | ||
} | ||
getPhraseLists(): Promise<LuisApi.PhraseList[]> { | ||
getPhraseLists(appVersion: string): Promise<LuisApi.PhraseListGET[]> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/phraselists` | ||
uri: `/${this.applicationId}/versions/${appVersion}/phraselists` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then((res: RequestResponse) => res.body) | ||
.then((phraseLists) => phraseLists.map((phraseList: any) => { | ||
return { | ||
id: phraseList.Id.toString(), | ||
name: phraseList.Name, | ||
mode: phraseList.Mode.toLowerCase() === 'non-exchangeable' ? | ||
LuisApi.PhraseListModes.NonExchangeable : LuisApi.PhraseListModes.Exchangeable, | ||
isActive: phraseList.IsActive, | ||
editable: phraseList.Editable, | ||
phrases: phraseList.Phrases | ||
} as LuisApi.PhraseList; | ||
})); | ||
.then((res: RequestResponse) => res.body as LuisApi.PhraseListGET[]); | ||
} | ||
createPhraseList(phraseList: LuisApi.PhraseList): Promise<string> { | ||
createPhraseList(appVersion: string, phraseList: LuisApi.PhraseListPOST): Promise<string> { | ||
let opts: request.Options = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/phraselists`, | ||
body: { | ||
Name: phraseList.name, | ||
Mode: phraseList.mode === LuisApi.PhraseListModes.NonExchangeable ? | ||
'Non-exchangeable' : 'Exchangeable', | ||
IsActive: phraseList.isActive !== undefined ? phraseList.isActive : true, | ||
Editable: phraseList.editable !== undefined ? phraseList.editable : true, | ||
Phrases: phraseList.phrases | ||
} | ||
uri: `/${this.applicationId}/versions/${appVersion}/phraselists`, | ||
body: phraseList | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res: RequestResponse) => res.body) | ||
.then(phraseListId => { | ||
.then((res: RequestResponse) => { | ||
this.emit('createPhraseList', { | ||
id: phraseListId, | ||
id: res.body, | ||
name: phraseList.name | ||
}); | ||
return phraseListId; | ||
return res.body; | ||
}); | ||
} | ||
createPhraseLists(phraseLists: LuisApi.PhraseList[]): Promise<string[]> { | ||
return this.throttler(this.createPhraseList, phraseLists) as Promise<string[]>; | ||
createPhraseLists(appVersion: string, phraseLists: LuisApi.PhraseListPOST[]): Promise<string[]> { | ||
return this.throttler(this.createPhraseList, appVersion, phraseLists) as Promise<string[]>; | ||
} | ||
deletePhraseList(phraseListId: string): Promise<void> { | ||
deletePhraseList(appVersion: string, phraseList: LuisApi.PhraseListDELETE): Promise<void> { | ||
let opts: request.Options = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/phraselists/${phraseListId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/phraselists/${phraseList.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deletePhraseList', phraseListId); | ||
this.emit('deletePhraseList', phraseList.id); | ||
}); | ||
} | ||
deletePhraseLists(phraseListIds: string[]): Promise<void> { | ||
return this.throttler(this.deletePhraseList, phraseListIds) | ||
deletePhraseLists(appVersion: string, phraseLists: LuisApi.PhraseListDELETE[]): Promise<void> { | ||
return this.throttler(this.deletePhraseList, appVersion, phraseLists) | ||
.then(() => Promise.resolve()); | ||
} | ||
getExamples(skip: number, count: number): Promise<LuisApi.LabeledUtterance[]> { | ||
function mapEntities(entities: any[]): LuisApi.LabeledEntity[] { | ||
return entities.map((entity: any) => { | ||
return { | ||
name: entity.name, | ||
startToken: entity.indeces.startToken, | ||
endToken: entity.indeces.endToken, | ||
word: entity.word, | ||
isBuiltInExtractor: entity.isBuiltInExtractor | ||
} as LuisApi.LabeledEntity; | ||
}); | ||
} | ||
getExamples(appVersion: string, skip: number, count: number): Promise<LuisApi.ExampleGET[]> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/examples`, | ||
uri: `/${this.applicationId}/versions/${appVersion}/examples`, | ||
qs: { skip, count } | ||
@@ -480,25 +480,8 @@ }; | ||
.then((res: RequestResponse) => { | ||
let examples = res.body; | ||
if (examples.length) { | ||
if (res.body.length) { | ||
// Don't emit events once the last example has been reached | ||
this.emit('getExamples', skip, skip + examples.length - 1); | ||
this.emit('getExamples', skip, skip + res.body.length - 1); | ||
} | ||
return examples; | ||
}) | ||
.then(examples => examples.map((example: any) => { | ||
return { | ||
id: example.exampleId, | ||
utteranceText: example.utteranceText, | ||
tokenizedText: example.tokenizedText, | ||
intent: example.IntentsResults.Name, | ||
predictedIntents: example.PredictedIntentResults.map((intent: any) => { | ||
return { | ||
name: intent.Name, | ||
score: intent.score | ||
}; | ||
}), | ||
entities: mapEntities(example.EntitiesResults), | ||
predictedEntities: mapEntities(example.PredictedEntitiesResults) | ||
} as LuisApi.LabeledUtterance; | ||
})); | ||
return res.body as LuisApi.ExampleGET[]; | ||
}); | ||
} | ||
@@ -509,4 +492,4 @@ | ||
*/ | ||
getAllExamples(): Promise<LuisApi.LabeledUtterance[]> { | ||
let examplesBunches: LuisApi.LabeledUtterance[][] = []; | ||
getAllExamples(appVersion: string): Promise<LuisApi.ExampleGET[]> { | ||
let examplesBunches: LuisApi.ExampleGET[][] = []; | ||
@@ -518,5 +501,6 @@ // Recursively get examples in bunches of MAX_PARALLEL_EXAMPLES_REQUESTS parallel requests | ||
.map((e, i) => skip + i * MAX_EXAMPLES_COUNT); | ||
let promises = skipList.map(skip => this.promiseThrottle.add(this.getExamples.bind(this, skip, MAX_EXAMPLES_COUNT))); | ||
let promises = skipList.map(skip => this.promiseThrottle.add( | ||
this.getExamples.bind(this, appVersion, skip, MAX_EXAMPLES_COUNT))); | ||
return Promise.all(promises) | ||
.then((examplesBunch: LuisApi.LabeledUtterance[][]) => { | ||
.then((examplesBunch: LuisApi.ExampleGET[][]) => { | ||
let lastBunchFound = examplesBunch.some(bunch => { | ||
@@ -538,14 +522,13 @@ if (bunch.length) { | ||
createExamples(examples: LuisApi.Example[]): Promise<string[]> { | ||
createExamples(appVersion: string, examples: LuisApi.ExamplePOST[]): Promise<string[]> { | ||
// Create examples up to MAX_EXAMPLES_UPLOAD | ||
let createLimitedExamples = (examples: LuisApi.Example[]) => { | ||
let createLimitedExamples = (appVersion: string, examples: LuisApi.ExamplePOST[]) => { | ||
let opts: request.Options = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/examples`, | ||
uri: `/${this.applicationId}/versions/${appVersion}/examples`, | ||
body: examples | ||
}; | ||
return this.retryRequest(opts, 201) | ||
.then((res: RequestResponse) => res.body) | ||
.then((body: any[]) => { | ||
let errors = body.filter(r => r.hasError); | ||
.then((res: RequestResponse) => { | ||
let errors = res.body.filter((r: any) => r.hasError); | ||
if (errors.length) { | ||
@@ -556,3 +539,3 @@ return Promise.reject(new Error('LuisApiClient: The following examples have errors:\n' | ||
this.emit('createExampleBunch', examples.length); | ||
return body.map(r => r.value.ExampleId.toString()); | ||
return res.body.map((r: any) => r.value.ExampleId.toString()); | ||
}); | ||
@@ -563,19 +546,19 @@ }; | ||
let examplesBunches = _.chunk(examples, MAX_EXAMPLES_UPLOAD); | ||
return this.throttler(createLimitedExamples, examplesBunches) | ||
return this.throttler(createLimitedExamples, appVersion, examplesBunches) | ||
.then(_.flatten); | ||
} | ||
deleteExample(exampleId: string): Promise<void> { | ||
deleteExample(appVersion: string, example: LuisApi.ExampleDELETE): Promise<void> { | ||
let opts: request.Options = { | ||
method: 'DELETE', | ||
uri: `${this.applicationId}/examples/${exampleId}` | ||
uri: `/${this.applicationId}/versions/${appVersion}/examples/${example.id}` | ||
}; | ||
return this.retryRequest(opts, 200) | ||
.then(() => { | ||
this.emit('deleteExample', exampleId); | ||
this.emit('deleteExample', example.id); | ||
}); | ||
} | ||
deleteExamples(exampleIds: string[]): Promise<void> { | ||
return this.throttler(this.deleteExample, exampleIds) | ||
deleteExamples(appVersion: string, examples: LuisApi.ExampleDELETE[]): Promise<void> { | ||
return this.throttler(this.deleteExample, appVersion, examples) | ||
.then(() => Promise.resolve()); | ||
@@ -587,8 +570,8 @@ } | ||
let modelTrainingStatus: LuisApi.ModelTrainingStatus = { | ||
modelId: modelStatus.ModelId, | ||
status: modelStatus.Details.StatusId as LuisApi.TrainingStatuses, | ||
exampleCount: modelStatus.Details.ExampleCount | ||
modelId: modelStatus.modelId, | ||
status: modelStatus.details.statusId as LuisApi.TrainingStatuses, | ||
exampleCount: modelStatus.details.exampleCount | ||
}; | ||
if (modelTrainingStatus.status === LuisApi.TrainingStatuses.Failed) { | ||
modelTrainingStatus.failureReason = modelStatus.Details.FailureReason; | ||
if (modelTrainingStatus.status === LuisApi.TrainingStatuses.Fail) { | ||
modelTrainingStatus.failureReason = modelStatus.details.failureReason; | ||
} | ||
@@ -599,16 +582,15 @@ return modelTrainingStatus; | ||
startTraining(): Promise<LuisApi.TrainingStatus> { | ||
startTraining(appVersion: string): Promise<LuisApi.TrainingStatus> { | ||
let opts: request.Options = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/train` | ||
uri: `/${this.applicationId}/versions/${appVersion}/train` | ||
}; | ||
return this.retryRequest(opts, 202) | ||
.then((res: RequestResponse) => res.body) | ||
.then(this.convertTrainingStatus); | ||
.then((res: RequestResponse) => res.body); | ||
} | ||
getTrainingStatus(): Promise<LuisApi.TrainingStatus> { | ||
getTrainingStatus(appVersion: string): Promise<LuisApi.TrainingStatus> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/train` | ||
uri: `/${this.applicationId}/versions/${appVersion}/train` | ||
}; | ||
@@ -620,12 +602,11 @@ return this.retryRequest(opts, 200) | ||
publish(): Promise<LuisApi.PublishResult> { | ||
publish(appVersion: string): Promise<LuisApi.PublishResult> { | ||
let opts: request.Options = { | ||
method: 'POST', | ||
uri: `${this.applicationId}/publish`, | ||
uri: `/${this.applicationId}/publish`, | ||
// We don't really know what this body is for but it must be included for the API to work | ||
body: { | ||
BotFramework: { Enabled: false, AppId: '', SubscriptionKey: '', Endpoint: '' }, | ||
Slack: { Enabled: false, ClientId: '', ClientSecret: '', RedirectUri: '' }, | ||
PrivacyStatement: '', | ||
TermsOfUse: '' | ||
versionId: appVersion, | ||
isStaging: false, | ||
region: 'westus' | ||
} | ||
@@ -637,5 +618,6 @@ }; | ||
return { | ||
url: body.URL, | ||
subscriptionKey: body.SubscriptionKey, | ||
publishDate: new Date(body.PublishDate) | ||
endpointUrl: body.endpointUrl, | ||
subscriptionKey: body['subscription-key'], | ||
endpointRegion: body.endpointRegion, | ||
isStaging: body.isStaging | ||
} as LuisApi.PublishResult; | ||
@@ -645,6 +627,6 @@ }); | ||
export(): Promise<any> { | ||
export(appVersion: string): Promise<any> { | ||
let opts: request.Options = { | ||
method: 'GET', | ||
uri: `${this.applicationId}/export` | ||
uri: `/${this.applicationId}/versions/${appVersion}/export` | ||
}; | ||
@@ -651,0 +633,0 @@ return this.retryRequest(opts, 200) |
@@ -28,20 +28,10 @@ /** | ||
enum Commands { Update, Export, CheckPredictions, TestExamples }; | ||
let command: Commands; | ||
enum Commands { Update, Export, CheckPredictions, TestExamples } | ||
let runner: Promise<number> = null; | ||
let runner: Promise<number | void> = null; | ||
interface ProgramOptions extends commander.ICommand { | ||
parent?: ProgramOptions; // When using commands, `parent` holds options from the main program | ||
endpoint?: string; | ||
applicationId?: string; | ||
model?: string; | ||
errors?: string; | ||
subscriptionKey?: string; | ||
} | ||
const program: ProgramOptions = commander | ||
const program = commander | ||
.usage('command [options]') | ||
.option('-e, --endpoint <endpoint>', `LUIS endpoint (also got from the LUIS_ENDPOINT env var) [${DEFAULT_LUIS_ENDPOINT}]`, | ||
DEFAULT_LUIS_ENDPOINT) | ||
DEFAULT_LUIS_ENDPOINT) | ||
.option('-s, --subscription-key <subscription-key>', 'LUIS subscription key (also got from the LUIS_SUBSCRIPTION_KEY env var)'); | ||
@@ -54,4 +44,5 @@ | ||
.option('-m, --model <filename>', 'JSON file containing the model to upload to the LUIS application') | ||
.option('-v, --app-version <app-version>', 'The version of the application to update') | ||
.action(options => selectRunner(Commands.Update, options)) | ||
.on('--help', function() { | ||
.on('--help', function () { | ||
console.log(' Example:'); | ||
@@ -69,3 +60,3 @@ console.log(); | ||
.action(options => selectRunner(Commands.Export, options)) | ||
.on('--help', function() { | ||
.on('--help', function () { | ||
console.log(' Example:'); | ||
@@ -83,3 +74,3 @@ console.log(); | ||
.action(options => selectRunner(Commands.CheckPredictions, options)) | ||
.on('--help', function() { | ||
.on('--help', function () { | ||
console.log(' Example:'); | ||
@@ -98,6 +89,7 @@ console.log(); | ||
.action(options => selectRunner(Commands.TestExamples, options)) | ||
.on('--help', function() { | ||
.on('--help', function () { | ||
console.log(' Example:'); | ||
console.log(); | ||
console.log(` Check whether a set of examples are correctly recognized by an application in terms of intent and entities and save differences to 'errors.json':`); | ||
console.log(' Check whether a set of examples are correctly recognized by an application in terms of intent and entities ' + | ||
'and save differences to \'errors.json\':'); | ||
console.log(' $ luis-cli check -m model.json -a XXX -f errors.json -s YYY'); | ||
@@ -118,3 +110,3 @@ }); | ||
function selectRunner(command: Commands, options: ProgramOptions) { | ||
function selectRunner(command: Commands, options: any) { | ||
// Get values from env vars if not provided via options | ||
@@ -131,3 +123,3 @@ let endpoint = options.parent.endpoint || process.env.LUIS_ENDPOINT; | ||
if ((command === Commands.Update || command === Commands.Export || command === Commands.CheckPredictions || | ||
command === Commands.TestExamples) && !applicationId) { | ||
command === Commands.TestExamples) && !applicationId) { | ||
printError('unknown LUIS application id. Provide one through the `-a, --application-id` option ' + | ||
@@ -148,20 +140,29 @@ 'or the `LUIS_APPLICATION_ID` env var.'); | ||
case Commands.Update: | ||
if (!options.appVersion) { | ||
printError('missing version of the model to update. Provide one through the `-v, --version` option.'); | ||
} | ||
if (!options.model) { | ||
printError('missing JSON file from which the model will be read. Provide one through the `-m, --model` option.'); | ||
} | ||
runner = updateApp(luisTrainer, applicationId, options.model); | ||
runner = updateApp(luisTrainer, applicationId, options.appVersion, options.model); | ||
break; | ||
case Commands.Export: | ||
if (!options.appVersion) { | ||
printError('missing version of the model to update. Provide one through the `-v, --version` option.'); | ||
} | ||
if (!options.model) { | ||
printError('missing JSON file to which the application will be exported. Provide one through the `-m, --model` option.'); | ||
} | ||
runner = exportApp(luisTrainer, applicationId, options.model); | ||
runner = exportApp(luisTrainer, applicationId, options.appVersion, options.model); | ||
break; | ||
case Commands.CheckPredictions: | ||
if (!options.appVersion) { | ||
printError('missing version of the model to update. Provide one through the `-v, --version` option.'); | ||
} | ||
if (!options.errors) { | ||
printError('missing JSON file to which the differences will be saved. Provide one through the `-r, --errors` option.'); | ||
} | ||
runner = checkPrediction(luisTrainer, applicationId, options.errors); | ||
runner = checkPrediction(luisTrainer, applicationId, options.appVersion, options.errors); | ||
break; | ||
@@ -181,3 +182,3 @@ | ||
function updateApp(luisTrainer: LuisTrainer, applicationId: string, modelFilename: string): Promise<number> { | ||
function updateApp(luisTrainer: LuisTrainer, applicationId: string, appVersion: string, modelFilename: string): Promise<number | void> { | ||
let model: LuisModel.Model; | ||
@@ -194,3 +195,3 @@ try { | ||
console.log(`Updating the application ${applicationId} with the model from "${modelFilename}"...`); | ||
console.log(`Updating the version ${appVersion} of the application ${applicationId} with the model from "${modelFilename}"...`); | ||
console.log(); | ||
@@ -274,3 +275,3 @@ luisTrainer.on('startUpdateIntents', (stats: UpdateEvent) => { | ||
return luisTrainer.update(model) | ||
return luisTrainer.update(appVersion, model) | ||
.then(() => { | ||
@@ -283,5 +284,5 @@ console.log('The application has been successfully updated'); | ||
function exportApp(luisTrainer: LuisTrainer, applicationId: string, modelFilename: string): Promise<number> { | ||
function exportApp(luisTrainer: LuisTrainer, applicationId: string, appVersion: string, modelFilename: string): Promise<number | void> { | ||
console.log(`Exporting the application ${applicationId} to "${modelFilename}"...`); | ||
return luisTrainer.export() | ||
return luisTrainer.export(appVersion) | ||
.then(model => { | ||
@@ -295,3 +296,4 @@ fs.writeFileSync(modelFilename, JSON.stringify(model, null, 2)); | ||
function checkPrediction(luisTrainer: LuisTrainer, applicationId: string, errorsFilename: string): Promise<number> { | ||
function checkPrediction(luisTrainer: LuisTrainer, applicationId: string, appVersion: string, | ||
errorsFilename: string): Promise<number | void> { | ||
console.log(`Checking predictions for the application ${applicationId}...`); | ||
@@ -307,3 +309,3 @@ | ||
return luisTrainer.checkPredictions() | ||
return luisTrainer.checkPredictions(appVersion) | ||
.then(predictionResult => { | ||
@@ -321,3 +323,4 @@ if (predictionResult.errors.length) { | ||
function testExamples(luisTrainer: LuisTrainer, applicationId: string, modelFilename: string, errorsFilename: string): Promise<number> { | ||
function testExamples(luisTrainer: LuisTrainer, applicationId: string, modelFilename: string, | ||
errorsFilename: string): Promise<number | void> { | ||
let model: LuisModel.Model; | ||
@@ -362,5 +365,5 @@ try { | ||
let intentErrors = predictionResult.errors.filter(error => | ||
error.predictedIntents && !error.ambiguousPredictedIntent).length; | ||
error.intentPredictions && !error.ambiguousPredictedIntent).length; | ||
let ambiguousIntentErrors = predictionResult.errors.filter(error => | ||
error.predictedIntents && error.ambiguousPredictedIntent).length; | ||
error.intentPredictions && error.ambiguousPredictedIntent).length; | ||
let entityErrors = predictionResult.errors.filter(error => error.predictedEntities).length; | ||
@@ -391,3 +394,3 @@ let tokenizationErrors = predictionResult.errors.filter(error => error.tokenizedText).length; | ||
const HEADERS = {COL1: 'INTENTS', COL2: 'ERRORS', COL3: 'AMBIGUITIES'}; | ||
const HEADERS = { COL1: 'INTENTS', COL2: 'ERRORS', COL3: 'AMBIGUITIES' }; | ||
let longestIntentLen = 0; | ||
@@ -398,3 +401,3 @@ let total = 0; | ||
let strStats: {intent: string, errors: string, ambiguities: string, color: Function}[] = []; | ||
let strStats: { intent: string, errors: string, ambiguities: string, color: Function }[] = []; | ||
let sortedStats = new Map([...predictionResult.stats.entries()].sort()); | ||
@@ -401,0 +404,0 @@ sortedStats.forEach((stats, intent) => { |
@@ -36,6 +36,6 @@ /** | ||
intent: string; | ||
predictedIntents?: string[]; | ||
intentPredictions?: LuisApi.IntentPrediction[]; | ||
ambiguousPredictedIntent?: boolean; | ||
entities?: LuisApi.LabeledEntity[]; | ||
predictedEntities?: LuisApi.LabeledEntity[]; | ||
entities?: (LuisApi.EntityLabelExampleGET | LuisApi.EntityLabelExamplePOST)[]; | ||
predictedEntities?: LuisApi.EntityLabelExamplePOST[]; | ||
} | ||
@@ -72,4 +72,4 @@ | ||
export(): Promise<LuisModel.Model> { | ||
return this.luisApiClient.export() | ||
export(appVersion: string): Promise<LuisModel.Model> { | ||
return this.luisApiClient.export(appVersion) | ||
.catch((reason: Error) => { | ||
@@ -82,10 +82,21 @@ let err = new Error('Error trying to export the app') as any; | ||
update(model: LuisModel.Model): Promise<void> { | ||
return this.checkCulture(model.culture) | ||
.then(() => this.updateIntents(model.intents.map(intent => intent.name))) | ||
.then(() => this.updateEntities(model.entities.map(entity => entity.name))) | ||
.then(() => this.updatePhraseLists(model.model_features)) | ||
.then(() => this.updateExamples(model.utterances)) | ||
.then(() => this.train()) | ||
.then(() => this.publish()) | ||
update(appVersion: string, model: LuisModel.Model): Promise<void> { | ||
return this.checkCulture(appVersion, model.culture) | ||
.then(() => this.updateIntents(appVersion, model.intents.map( | ||
intent => { | ||
return { | ||
name: intent.name | ||
}; | ||
}))) | ||
.then(() => this.updateEntities(appVersion, model.entities.map( | ||
entity => { | ||
return { | ||
name: entity.name | ||
}; | ||
} | ||
))) | ||
.then(() => this.updatePhraseLists(appVersion, model.model_features)) | ||
.then(() => this.updateExamples(appVersion, model.utterances)) | ||
.then(() => this.train(appVersion)) | ||
.then(() => this.publish(appVersion)) | ||
.then(() => Promise.resolve()); | ||
@@ -103,20 +114,25 @@ } | ||
*/ | ||
checkPredictions(): Promise<PredictionResult> { | ||
checkPredictions(appVersion: string): Promise<PredictionResult> { | ||
// Return the top scoring predicted intents. In case of tie, all the top ones will be returned. | ||
function getTopPredictedIntents(example: LuisApi.LabeledUtterance): string[] { | ||
return example.predictedIntents | ||
function getTopPredictedIntents(example: LuisApi.ExampleGET): LuisApi.IntentPrediction[] { | ||
return example.intentPredictions | ||
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)) | ||
.filter((predictedIntent, i, sortedPredictedIntents) => | ||
predictedIntent.score === sortedPredictedIntents[0].score) | ||
.map(predictedIntent => predictedIntent.name); | ||
.map(predictedIntent => { | ||
return { | ||
name: predictedIntent.name, | ||
score: predictedIntent.score | ||
}; | ||
}); | ||
} | ||
function matchPredictedEntities(example: LuisApi.LabeledUtterance): boolean { | ||
function matchPredictedEntities(example: LuisApi.ExampleGET): boolean { | ||
// Predicted entities must have at least the labeled entities. | ||
// If there are some extra predicted entities, it is not an issue. | ||
return _.differenceWith(example.entities, example.predictedEntities, _.isEqual).length === 0; | ||
return _.differenceWith(example.entityLabels, example.entityPredictions, _.isEqual).length === 0; | ||
} | ||
function matchTokenizedText(example: LuisApi.LabeledUtterance): boolean { | ||
return _.isEqual(example.tokenizedText, LuisTrainer.tokenizeSentence(example.utteranceText)); | ||
function matchTokenizedText(example: LuisApi.ExampleGET): boolean { | ||
return _.isEqual(example.tokenizedText, LuisTrainer.tokenizeSentence(example.text)); | ||
} | ||
@@ -127,3 +143,3 @@ | ||
this.emit('getExamples', first, last)); | ||
return this.luisApiClient.getAllExamples() | ||
return this.luisApiClient.getAllExamples(appVersion) | ||
.then(examples => { | ||
@@ -134,3 +150,3 @@ this.emit('endGetAllExamples', examples.length); | ||
// sortedExamples = sortedExamples.map(example => { | ||
// example.predictedIntents = example.predictedIntents.sort((a, b) => a.name.localeCompare(b.name)); | ||
// example.intentPredictions = example.intentPredictions.sort((a, b) => a.name.localeCompare(b.name)); | ||
// return example; | ||
@@ -146,11 +162,11 @@ // }); | ||
let error: PredictionError = { | ||
text: example.utteranceText, | ||
intent: example.intent | ||
text: example.text, | ||
intent: example.intentLabel | ||
}; | ||
let topPredictedIntents = getTopPredictedIntents(example); | ||
if (topPredictedIntents.indexOf(example.intent) === -1) { | ||
if (topPredictedIntents.findIndex(element => element.name === example.intentLabel) === -1) { | ||
// None of the top scoring predicted intents matches the labeled one | ||
error.ambiguousPredictedIntent = false; | ||
error.predictedIntents = topPredictedIntents; | ||
error.intentPredictions = topPredictedIntents; | ||
} else if (topPredictedIntents.length > 1) { | ||
@@ -160,3 +176,3 @@ // The labeled intent is one of the top scoring predicted ones | ||
error.ambiguousPredictedIntent = true; | ||
error.predictedIntents = topPredictedIntents; | ||
error.intentPredictions = topPredictedIntents; | ||
} else { | ||
@@ -167,4 +183,12 @@ // The labeled intent matches the only one top scoring predicted intent | ||
if (!matchPredictedEntities(example)) { | ||
error.entities = example.entities; | ||
error.predictedEntities = example.predictedEntities; | ||
error.entities = example.entityLabels; | ||
error.predictedEntities = example.entityPredictions.map( | ||
entityPrediction => { | ||
return { | ||
entityName: entityPrediction.entityName, | ||
startCharIndex: entityPrediction.startIndex, | ||
endCharIndex: entityPrediction.endIndex | ||
}; | ||
} | ||
); | ||
} | ||
@@ -176,3 +200,3 @@ | ||
if (error.predictedIntents || error.predictedEntities || error.tokenizedText) { | ||
if (error.intentPredictions || error.predictedEntities || error.tokenizedText) { | ||
return error; | ||
@@ -186,8 +210,8 @@ } else { | ||
// Calculate stats | ||
let exampleCounter = _.countBy(examples, example => example.intent); | ||
let exampleCounter = _.countBy(examples, example => example.intentLabel); | ||
let intentErrorCounter = _.countBy( | ||
errors.filter(error => error.predictedIntents && !error.ambiguousPredictedIntent), | ||
errors.filter(error => error.intentPredictions && !error.ambiguousPredictedIntent), | ||
error => error.intent); | ||
let ambiguousIntentCounter = _.countBy( | ||
errors.filter(error => error.predictedIntents && error.ambiguousPredictedIntent), | ||
errors.filter(error => error.intentPredictions && error.ambiguousPredictedIntent), | ||
error => error.intent); | ||
@@ -217,12 +241,11 @@ let stats: PredictionStats = new Map(); | ||
function matchRecognizedEntities( | ||
labeledEntities: LuisApi.LabeledEntity[], | ||
labeledEntities: LuisApi.EntityLabelExamplePOST[], | ||
recognizedEntities: LuisApi.RecognizedEntity[]): boolean { | ||
// Recognized entities must have at least the labeled entities. | ||
// If there are some extra recognized entities, it is not an issue. | ||
return _.differenceWith(labeledEntities, recognizedEntities, | ||
(labeled: LuisApi.LabeledEntity, recognized: LuisApi.RecognizedEntity) => | ||
labeled.name === recognized.type && | ||
labeled.word === recognized.entity && | ||
labeled.startToken === recognized.startIndex && | ||
labeled.endToken === recognized.endIndex | ||
return _.differenceWith<any>(labeledEntities, recognizedEntities, | ||
(labeled: LuisApi.EntityLabelExamplePOST, recognized: LuisApi.RecognizedEntity) => | ||
labeled.entityName === recognized.entity && | ||
labeled.startCharIndex === recognized.startIndex && | ||
labeled.endCharIndex === recognized.endIndex | ||
).length === 0; | ||
@@ -246,5 +269,10 @@ } | ||
// Compare intents | ||
if (example.intent !== recognitionResult.intent) { | ||
if (example.intent !== recognitionResult.topScoringIntent.intent) { | ||
error.ambiguousPredictedIntent = false; | ||
error.predictedIntents = [recognitionResult.intent]; | ||
error.intentPredictions = [ | ||
{ | ||
name: recognitionResult.topScoringIntent.intent, | ||
score: recognitionResult.topScoringIntent.score | ||
} | ||
]; | ||
} | ||
@@ -256,8 +284,6 @@ | ||
return { | ||
name: entity.entity, | ||
startToken: foundEntity.startChar, | ||
endToken: foundEntity.endChar, | ||
word: foundEntity.word, | ||
isBuiltInExtractor: false | ||
} as LuisApi.LabeledEntity; | ||
entityName: foundEntity.word, | ||
startCharIndex: foundEntity.startChar, | ||
endCharIndex: foundEntity.endChar | ||
}; | ||
}); | ||
@@ -268,12 +294,10 @@ if (!matchRecognizedEntities(labeledEntities, recognitionResult.entities)) { | ||
return { | ||
name: entity.type, | ||
startToken: entity.startIndex, | ||
endToken: entity.endIndex, | ||
word: entity.entity, | ||
isBuiltInExtractor: false | ||
} as LuisApi.LabeledEntity; | ||
entityName: entity.entity, | ||
startCharIndex: entity.startIndex, | ||
endCharIndex: entity.endIndex | ||
}; | ||
}); | ||
} | ||
if (error.predictedIntents || error.predictedEntities) { | ||
if (error.intentPredictions || error.predictedEntities) { | ||
return error; | ||
@@ -288,3 +312,3 @@ } else { | ||
let exampleCounter = _.countBy(examples, example => example.intent); | ||
let intentErrorCounter = _.countBy(errors.filter(error => error.predictedIntents), error => error.intent); | ||
let intentErrorCounter = _.countBy(errors.filter(error => error.intentPredictions), error => error.intent); | ||
let stats: PredictionStats = new Map(); | ||
@@ -315,3 +339,3 @@ _.forEach(exampleCounter, (counter, intent) => { | ||
private checkCulture(culture: string): Promise<void> { | ||
private checkCulture(appVersion: string, culture: string): Promise<void> { | ||
return this.luisApiClient.getApp() | ||
@@ -331,18 +355,17 @@ .then(appInfo => { | ||
private updateIntents(newIntents: string[]): Promise<void> { | ||
return this.luisApiClient.getIntents() | ||
private updateIntents(appVersion: string, intents: LuisApi.IntentPOST[]): Promise<void> { | ||
return this.luisApiClient.getIntents(appVersion) | ||
.then(oldIntents => { | ||
let intentIdsToBeDeleted = _.differenceWith(oldIntents, newIntents, | ||
(a: LuisApi.IntentClassifier, b: string) => a.name === b | ||
).map(intent => intent.id); | ||
let intentsToBeCreated = _.differenceWith(newIntents, oldIntents, | ||
(a: string, b: LuisApi.IntentClassifier) => a === b.name | ||
let intentsToBeDeleted = _.differenceWith<any>(oldIntents, intents, | ||
(a: LuisApi.IntentGET, b: LuisApi.IntentPOST) => a.name === b.name); | ||
let intentsToBeCreated = _.differenceWith<LuisApi.IntentPOST>(intents, oldIntents, | ||
(a: LuisApi.IntentPOST, b: LuisApi.IntentGET) => a.name === b.name | ||
); | ||
let stats: UpdateEvent = { | ||
create: intentsToBeCreated.length, | ||
delete: intentIdsToBeDeleted.length | ||
delete: intentsToBeDeleted.length | ||
}; | ||
this.emit('startUpdateIntents', stats); | ||
return this.luisApiClient.deleteIntents(intentIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createIntents(intentsToBeCreated)) | ||
return this.luisApiClient.deleteIntents(appVersion, intentsToBeDeleted) | ||
.then(() => this.luisApiClient.createIntents(appVersion, intentsToBeCreated)) | ||
.then(() => { | ||
@@ -355,18 +378,18 @@ this.emit('endUpdateIntents', stats); | ||
private updateEntities(newEntities: string[]): Promise<void> { | ||
return this.luisApiClient.getEntities() | ||
private updateEntities(appVersion: string, entities: LuisApi.EntityPOST[]): Promise<void> { | ||
return this.luisApiClient.getEntities(appVersion) | ||
.then(oldEntities => { | ||
let entityIdsToBeDeleted = _.differenceWith(oldEntities, newEntities, | ||
(a: LuisApi.EntityExtractor, b: string) => a.name === b | ||
).map(entity => entity.id); | ||
let entitiesToBeCreated = _.differenceWith(newEntities, oldEntities, | ||
(a: string, b: LuisApi.EntityExtractor) => a === b.name | ||
let entitiesToBeDeleted = _.differenceWith<any>( | ||
oldEntities, entities, | ||
(a: LuisApi.EntityGET, b: LuisApi.EntityPOST) => a.name === b.name); | ||
let entitiesToBeCreated = _.differenceWith<LuisApi.EntityPOST>(entities, oldEntities, | ||
(a: LuisApi.EntityPOST, b: LuisApi.EntityGET) => a.name === b.name | ||
); | ||
let stats: UpdateEvent = { | ||
create: entitiesToBeCreated.length, | ||
delete: entityIdsToBeDeleted.length | ||
delete: entitiesToBeDeleted.length | ||
}; | ||
this.emit('startUpdateEntities', stats); | ||
return this.luisApiClient.deleteEntities(entityIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createEntities(entitiesToBeCreated)) | ||
return this.luisApiClient.deleteEntities(appVersion, entitiesToBeDeleted) | ||
.then(() => this.luisApiClient.createEntities(appVersion, entitiesToBeCreated)) | ||
.then(() => { | ||
@@ -379,35 +402,33 @@ this.emit('endUpdateEntities', stats); | ||
private updatePhraseLists(newModelPhraseLists: LuisModel.ModelFeature[]): Promise<void> { | ||
private updatePhraseLists(appVersion: string, modelPhraseLists: LuisModel.ModelFeature[]): Promise<void> { | ||
/** | ||
* Compare phraseLists ignoring non-meaningful properties | ||
*/ | ||
const comparePhraseLists = (a: LuisApi.PhraseList, b: LuisApi.PhraseList) => { | ||
const comparePhraseLists = (a: any, b: any) => { | ||
return a.name === b.name && | ||
a.mode === b.mode && | ||
a.isActive === b.isActive && | ||
a.isExchangeable === b.isExchangeable && | ||
a.phrases === b.phrases; | ||
}; | ||
return this.luisApiClient.getPhraseLists() | ||
return this.luisApiClient.getPhraseLists(appVersion) | ||
.then(oldPhraseLists => { | ||
// Convert data from the model to the format used by the API | ||
let newPhraseLists = newModelPhraseLists.map((phraseList: LuisModel.ModelFeature) => { | ||
let phraseLists = modelPhraseLists.map((phraseList: LuisModel.ModelFeature) => { | ||
return { | ||
name: phraseList.name, | ||
mode: phraseList.mode === false ? | ||
LuisApi.PhraseListModes.NonExchangeable : LuisApi.PhraseListModes.Exchangeable, | ||
isActive: phraseList.activated, | ||
isExchangeable: true, | ||
phrases: phraseList.words | ||
} as LuisApi.PhraseList; | ||
} as LuisApi.PhraseListPOST; | ||
}); | ||
let phraseListIdsToBeDeleted = _.differenceWith(oldPhraseLists, newPhraseLists, comparePhraseLists) | ||
.map(phraseList => phraseList.id); | ||
let phraseListToBeCreated = _.differenceWith(newPhraseLists, oldPhraseLists, comparePhraseLists); | ||
let phraseListsToBeDeleted = _.differenceWith<any>(oldPhraseLists, phraseLists, comparePhraseLists); | ||
let phraseListToBeCreated = _.differenceWith<LuisApi.PhraseListPOST>(phraseLists, oldPhraseLists, comparePhraseLists); | ||
let stats: UpdateEvent = { | ||
create: phraseListToBeCreated.length, | ||
delete: phraseListIdsToBeDeleted.length | ||
delete: phraseListsToBeDeleted.length | ||
}; | ||
this.emit('startUpdatePhraseLists', stats); | ||
return this.luisApiClient.deletePhraseLists(phraseListIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createPhraseLists(phraseListToBeCreated)) | ||
return this.luisApiClient.deletePhraseLists(appVersion, phraseListsToBeDeleted) | ||
.then(() => this.luisApiClient.createPhraseLists(appVersion, phraseListToBeCreated)) | ||
.then(() => { | ||
@@ -420,7 +441,7 @@ this.emit('endUpdatePhraseLists', stats); | ||
private updateExamples(newExamples: LuisModel.Utterance[]): Promise<void> { | ||
private updateExamples(appVersion: string, modelExamples: LuisModel.Utterance[]): Promise<void> { | ||
this.emit('startGetAllExamples'); | ||
this.luisApiClient.on('getExamples', (first: number, last: number) => | ||
this.emit('getExamples', first, last)); | ||
return this.luisApiClient.getAllExamples() | ||
return this.luisApiClient.getAllExamples(appVersion) | ||
.then(oldExamples => { | ||
@@ -431,5 +452,4 @@ this.emit('endGetAllExamples', oldExamples.length); | ||
// but those will be overwritten when uploading the so called `examplesToBeCreated`. | ||
let exampleIdsToBeDeleted = _.differenceWith(oldExamples, newExamples, | ||
(a: LuisApi.LabeledUtterance, b: LuisModel.Utterance) => a.utteranceText === b.text | ||
).map((example: LuisApi.LabeledUtterance) => example.id); | ||
let examplesToBeDeleted = _.differenceWith<any>(oldExamples, modelExamples, | ||
(a: LuisApi.ExampleGET, b: LuisModel.Utterance) => a.text === b.text); | ||
@@ -439,11 +459,11 @@ // Examples to be uploaded (overwriting those that already exist with the same text although | ||
// differs from existing one by the intent or entities. | ||
let examplesToBeCreated = _.differenceWith(newExamples, oldExamples, | ||
(a: LuisModel.Utterance, b: LuisApi.LabeledUtterance) => { | ||
let eq = a.text === b.utteranceText && | ||
a.intent === b.intent && | ||
a.entities.length === b.entities.length && | ||
let examplesToBeCreated = _.differenceWith<any>(modelExamples, oldExamples, | ||
(a: LuisModel.Utterance, b: LuisApi.ExampleGET) => { | ||
let eq = a.text === b.text && | ||
a.intent === b.intentLabel && | ||
a.entities.length === b.entityLabels.length && | ||
// Compare array of entities w/o assuming the same order | ||
a.entities.length === _.intersectionWith(a.entities, b.entities, | ||
(ae: LuisModel.EntityPosition, be: LuisApi.LabeledEntity) => | ||
ae.entity === be.name && ae.startPos === be.startToken && ae.endPos === be.endToken | ||
a.entities.length === _.intersectionWith<any>(a.entities, b.entityLabels, | ||
(ae: LuisModel.EntityPosition, be: LuisApi.EntityLabelExamplePOST) => | ||
ae.entity === be.entityName && ae.startPos === be.startCharIndex && ae.endPos === be.endCharIndex | ||
).length; | ||
@@ -455,14 +475,16 @@ return eq; | ||
.map((example: LuisModel.Utterance) => { | ||
let entityLabels = example.entities.map(entity => { | ||
let foundEntity = LuisTrainer.findEntity(example.text, entity.startPos, entity.endPos); | ||
return { | ||
entityName: entity.entity, | ||
startCharIndex: foundEntity.startChar, | ||
endCharIndex: foundEntity.endChar | ||
}; | ||
}); | ||
entityLabels = (entityLabels.length === 0) ? null : entityLabels; | ||
return { | ||
exampleText: example.text, | ||
selectedIntentName: example.intent, | ||
entityLabels: example.entities.map(entity => { | ||
let foundEntity = LuisTrainer.findEntity(example.text, entity.startPos, entity.endPos); | ||
return { | ||
entityType: entity.entity, | ||
startToken: foundEntity.startChar, | ||
endToken: foundEntity.endChar | ||
} as LuisApi.Entity; | ||
}) | ||
} as LuisApi.Example; | ||
text: example.text, | ||
intentName: example.intent, | ||
entityLabels: entityLabels | ||
} as LuisApi.ExamplePOST; | ||
}); | ||
@@ -473,3 +495,4 @@ | ||
// require('fs').writeFileSync('old.txt', oldExamples.map(example => example.utteranceText).sort().join('\n'), 'utf-8'); | ||
// require('fs').writeFileSync('create.txt', examplesToBeCreated.map(example => example.exampleText).sort().join('\n'), 'utf-8'); | ||
// require('fs').writeFileSync('create.txt', examplesToBeCreated.map(example => | ||
// example.exampleText).sort().join('\n'), 'utf-8'); | ||
// let SENTENCE = ''; | ||
@@ -486,3 +509,3 @@ // console.log('='.repeat(50)); | ||
create: examplesToBeCreated.length, | ||
delete: exampleIdsToBeDeleted.length | ||
delete: examplesToBeDeleted.length | ||
}; | ||
@@ -494,4 +517,4 @@ this.emit('startUpdateExamples', stats); | ||
this.emit('createExampleBunch', bunchLength)); | ||
return this.luisApiClient.deleteExamples(exampleIdsToBeDeleted) | ||
.then(() => this.luisApiClient.createExamples(examplesToBeCreated)) | ||
return this.luisApiClient.deleteExamples(appVersion, examplesToBeDeleted) | ||
.then(() => this.luisApiClient.createExamples(appVersion, examplesToBeCreated)) | ||
.then(() => { | ||
@@ -504,3 +527,3 @@ this.emit('endUpdateExamples', stats); | ||
private train(): Promise<void> { | ||
private train(appVersion: string): Promise<void> { | ||
const delay = (t: number): Promise<void> => { | ||
@@ -514,3 +537,3 @@ return new Promise<void>(resolve => { | ||
return delay(TRAINING_STATUS_POLLING_INTERVAL) | ||
.then(() => this.luisApiClient.getTrainingStatus()) | ||
.then(() => this.luisApiClient.getTrainingStatus(appVersion)) | ||
.then((trainingStatus: LuisApi.TrainingStatus) => { | ||
@@ -523,3 +546,3 @@ // Debug stuff | ||
modelStatus.status === LuisApi.TrainingStatuses.UpToDate || | ||
modelStatus.status === LuisApi.TrainingStatuses.Failed); | ||
modelStatus.status === LuisApi.TrainingStatuses.Fail); | ||
this.emit('trainingProgress', finishedModels.length, trainingStatus.length); | ||
@@ -531,3 +554,3 @@ if (finishedModels.length < trainingStatus.length) { | ||
let failedModels = trainingStatus | ||
.filter(modelStatus => modelStatus.status === LuisApi.TrainingStatuses.Failed); | ||
.filter(modelStatus => modelStatus.status === LuisApi.TrainingStatuses.Fail); | ||
if (failedModels.length) { | ||
@@ -548,3 +571,3 @@ let err = new Error( | ||
this.emit('startTraining'); | ||
return this.luisApiClient.startTraining() | ||
return this.luisApiClient.startTraining(appVersion) | ||
.then((trainingStatus) => { | ||
@@ -559,5 +582,5 @@ // Debug stuff | ||
private publish(): Promise<void> { | ||
private publish(appVersion: string): Promise<void> { | ||
this.emit('startPublish'); | ||
return this.luisApiClient.publish() | ||
return this.luisApiClient.publish(appVersion) | ||
.then(publishResult => { | ||
@@ -564,0 +587,0 @@ this.emit('endPublish'); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
46
5
1
197721
17
2932
Updatedcommander@^2.11.0
Updatedrequest@^2.83.0
Updatedsprintf-js@^1.1.1