This repository was archived by the owner on Jan 15, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 123
Expand file tree
/
Copy pathbuilder.ts
More file actions
500 lines (413 loc) · 20.1 KB
/
Copy pathbuilder.ts
File metadata and controls
500 lines (413 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {LuBuildCore} from './core'
import {Settings} from './settings'
import {MultiLanguageRecognizer} from './multi-language-recognizer'
import {Recognizer} from './recognizer'
import {CrossTrainedRecognizer} from './cross-trained-recognizer'
const path = require('path')
const fs = require('fs-extra')
const delay = require('delay')
const fileHelper = require('./../../utils/filehelper')
const fileExtEnum = require('./../utils/helpers').FileExtTypeEnum
const retCode = require('./../utils/enums/CLI-errors')
const exception = require('./../utils/exception')
const LuisBuilderVerbose = require('./../luis/luisCollate')
const LuisBuilder = require('./../luis/luisBuilder')
const Luis = require('./../luis/luis')
const LUOptions = require('./../lu/luOptions')
const Content = require('./../lu/lu')
const recognizerType = require('./../utils/enums/recognizertypes')
const maxVersionCount = 100
export class Builder {
private readonly handler: (input: string) => any
constructor(handler: any) {
this.handler = handler
}
async loadContents(files: string[], options: any = {}) {
let culture = options.culture || "en-us"
let importResolver = options.importResolver
let log = options.log || false
// lu contents that will be returned
let luContents: Array<any> = []
for (const file of files) {
let fileCulture: string
let fileName: string
let cultureFromPath = fileHelper.getCultureFromPath(file)
if (cultureFromPath) {
fileCulture = cultureFromPath
let fileNameWithCulture = path.basename(file, path.extname(file))
fileName = fileNameWithCulture.substring(0, fileNameWithCulture.length - fileCulture.length - 1)
} else {
fileCulture = culture
fileName = path.basename(file, path.extname(file))
}
let fileContent = ''
let result
let luisObj
let luFiles = await fileHelper.getLuObjects(undefined, file, true, fileExtEnum.LUFile)
this.handler(`${file} loaded\n`)
// filter empty lu files
luFiles = luFiles.filter((file: any) => file.content !== '')
if (luFiles.length <= 0) {
const content = new Content(fileContent, new LUOptions(fileName, true, fileCulture, file))
luContents.push(content)
continue
}
try {
result = await LuisBuilderVerbose.build(luFiles, log, fileCulture, importResolver)
luisObj = new Luis(result)
fileContent = luisObj.parseToLuContent()
} catch (err) {
if (err.source) {
err.text = `Invalid LU file ${err.source}: ${err.text}`
} else {
err.text = `Invalid LU file ${file}: ${err.text}`
}
throw (new exception(retCode.errorCode.INVALID_INPUT_FILE, err.text))
}
const content = new Content(fileContent, new LUOptions(fileName, true, fileCulture, file))
luContents.push(content)
}
// validate if there are duplicated files with same name and locale
let setOfContents = new Set()
const hasDuplicates = luContents.some(function (currentObj) {
return setOfContents.size === setOfContents.add(currentObj.name).size
})
if (hasDuplicates) {
throw(new exception(retCode.errorCode.INVALID_INPUT_FILE, 'Files with same name and locale are found.'))
}
return luContents
}
async generateDialogs(luContents: any[], options: any = {}) {
let fallbackLocale = options.fallbackLocale || 'en-us'
let dialog = options.dialog || recognizerType.MULTILANGUAGE
let schema = options.schema
let assets: any[] = []
let multiRecognizers = new Map<string, MultiLanguageRecognizer>()
let recognizers = new Map<string, Recognizer>()
let crosstrainedRecognizers = new Map<string, CrossTrainedRecognizer>()
// concurrently handle applications
await Promise.all(luContents.map(async content => {
const fileFolder = path.dirname(content.path)
// init crosstrainedRecognizer asset even for empty content
if (!crosstrainedRecognizers.has(content.id)) {
const crossTrainedRecognizerPath = path.join(fileFolder, content.id + '.lu.qna.dialog')
crosstrainedRecognizers.set(content.id, new CrossTrainedRecognizer(crossTrainedRecognizerPath, [], schema))
}
// content is not empty
if (!fileHelper.isFileSectionEmpty(content)) {
// update crosstrainedRecognizer if not empty
let crosstrainedRecognizer = crosstrainedRecognizers.get(content.id) as CrossTrainedRecognizer
if (!crosstrainedRecognizer.recognizers.includes(content.id + '.lu')) {
crosstrainedRecognizer.recognizers.push(content.id + '.lu')
}
// init recognizer asset
const dialogFile = path.join(fileFolder, `${content.name}.dialog`)
let recognizer = new Recognizer(content.path, content.name, dialogFile, schema)
recognizers.set(content.name, recognizer)
// get culture from content or default to content.language
let luisObj = await LuisBuilder.fromLUAsync([content])
luisObj.culture = luisObj.culture && luisObj.culture !== '' && luisObj.culture !== 'en-us' ? luisObj.culture : content.language as string
// init or update multiLanguageRecognizer asset
if (!multiRecognizers.has(content.id)) {
const multiRecognizerPath = path.join(fileFolder, `${content.id}.lu.dialog`)
multiRecognizers.set(content.id, new MultiLanguageRecognizer(multiRecognizerPath, {}, schema))
}
let multiRecognizer = multiRecognizers.get(content.id) as MultiLanguageRecognizer
multiRecognizer.recognizers[luisObj.culture] = path.basename(recognizer.getDialogPath(), '.dialog')
if (luisObj.culture.toLowerCase() === fallbackLocale.toLowerCase()) {
multiRecognizer.recognizers[''] = path.basename(recognizer.getDialogPath(), '.dialog')
}
}
}))
// write dialog assets
assets.push(...Array.from(recognizers.values()))
assets.push(...Array.from(multiRecognizers.values()))
if (dialog === recognizerType.CROSSTRAINED) {
assets.push(...Array.from(crosstrainedRecognizers.values()))
}
return this.generateDeclarativeAssets(assets)
}
async build(
luContents: any[],
authoringKey: string,
botName: string,
options: any = {}) {
// set luis api call endpoint
let endpoint = options.endpoint || "https://westus.api.cognitive.microsoft.com"
// set suffix default to development
let suffix = options.suffix || 'development'
// set region default to westus
let region = options.region || 'westus'
// set kept version count which means how many versions would be kept in luis service
let keptVersionCount = options.keptVersionCount && options.keptVersionCount > 0 ? options.keptVersionCount : maxVersionCount
// set if publish this application to staging or production slot
// default to production
let isStaging = options.isStaging || false
// set schema
let schema = options.schema
// luis api TPS which means 5 concurrent transactions to luis api in 1 second
// can set to other value if switched to a higher TPS(transaction per second) key
let luisAPITPS = options.luisAPITPS || 5
// set luis call delay duration to 1100 millisecond because 1000 can hit corner case of rate limit
let timeBucketOfRequests = options.timeBucketOfRequests || 1100
// set retry count for rate limit luis API failure
let retryCount = options.retryCount || 1
// set retry duration for rate limit luis API failure
let retryDuration = options.retryDuration || 1000
// settings assets like app id and version returned from luis api call
let settingsAssets: any[] = []
// deep clone luContents
let clonedLuContents = [...luContents]
// filter if all lu contents are emtty
const filesSectionEmptyStatus = fileHelper.filesSectionEmptyStatus(clonedLuContents)
let isAllLuEmpty = [...filesSectionEmptyStatus.values()].every((isEmpty) => isEmpty)
try {
if (!isAllLuEmpty) {
const luBuildCore = new LuBuildCore(authoringKey, endpoint, retryCount, retryDuration)
const apps = await luBuildCore.getApplicationList()
let settings: any
// here we do a while loop to make full use of luis tps capacity
while (clonedLuContents.length > 0) {
// get a number(set by luisApiTps) of contents for each loop
const subLuContents = clonedLuContents.splice(0, luisAPITPS)
// concurrently handle applications
await Promise.all(subLuContents.map(async content => {
if (!filesSectionEmptyStatus.get(content.path)) {
// init current application object from lu content
let currentApp = await this.initApplicationFromLuContent(content, botName, suffix)
// init recognizer asset
const dialogFile = path.join(path.dirname(content.path), `${content.name}.dialog`)
let recognizer = new Recognizer(content.path, content.name, dialogFile, schema)
// find if there is a matched name with current app under current authoring key
if (!recognizer.getAppId()) {
for (let app of apps) {
if (app.name.toLowerCase() === currentApp.name.toLowerCase()) {
recognizer.setAppId(app.id)
break
}
}
}
let needTrainAndPublish = false
// compare models to update the model if a match found
// otherwise create a new application
if (recognizer.getAppId() && recognizer.getAppId() !== '') {
// To see if need update the model
needTrainAndPublish = await this.updateApplication(currentApp, luBuildCore, recognizer, timeBucketOfRequests, keptVersionCount)
} else {
// create a new application
needTrainAndPublish = await this.createApplication(currentApp, luBuildCore, recognizer, timeBucketOfRequests)
}
if (needTrainAndPublish) {
// train and publish application
await this.trainAndPublishApplication(luBuildCore, recognizer, timeBucketOfRequests, isStaging)
}
// init settings asset
if (settings === undefined) {
const settingsPath = path.join(path.dirname(content.path), `luis.settings.${suffix}.${region}.json`)
settings = new Settings(settingsPath, {})
}
// update settings asset
settings.luis[content.name.split('.').join('_').replace(/-/g, '_')] = {
"appId": recognizer.getAppId(),
"version": recognizer.versionId
}
}
}))
}
// write dialog assets
settingsAssets.push(settings)
}
} catch (error) {
throw(new exception(retCode.errorCode.LUIS_BUILD_FAILED, `Luis build failed: ${error.message}`))
}
const settingsContent = this.generateDeclarativeAssets(settingsAssets)
return settingsContent
}
async writeDialogAssets(contents: any[], options: any = {}) {
let force = options.force || false
let out = options.out
let luConfig = options.luConfig
let writeDone = false
let writeContents = contents.filter(c => c.id.endsWith('.dialog'))
let settingsContents = contents.filter(c => c.id.endsWith('.json'))
if (settingsContents && settingsContents.length > 0) {
let outPath
if (luConfig) {
outPath = path.join(path.resolve(path.dirname(luConfig)), settingsContents[0].id)
} else if (out) {
outPath = path.join(path.resolve(out), settingsContents[0].id)
} else {
outPath = path.resolve(settingsContents[0].id)
}
writeContents.push(this.mergeSettingsContent(outPath, settingsContents))
}
for (const content of writeContents) {
let outFilePath
if (out) {
outFilePath = path.join(path.resolve(out), path.basename(content.path))
} else {
outFilePath = content.path
}
let fileExists = fs.existsSync(outFilePath)
if (fileExists && outFilePath.endsWith('.lu.qna.dialog')) {
let existingCTRecognizerObject = JSON.parse(await fileHelper.getContentFromFile(outFilePath))
let currentCTRecognizerObject = JSON.parse(content.content)
let ctRecognizerToBeMerged = currentCTRecognizerObject.recognizers.filter((r: string) => !existingCTRecognizerObject.recognizers.includes(r))
existingCTRecognizerObject.recognizers = existingCTRecognizerObject.recognizers.concat(ctRecognizerToBeMerged)
content.content = JSON.stringify(existingCTRecognizerObject, null, 4)
}
if (force || !fs.existsSync(outFilePath)) {
if (!fs.existsSync(path.dirname(outFilePath))) {
fs.mkdirSync(path.dirname(outFilePath))
}
this.handler(`Writing to ${outFilePath}\n`)
await fs.writeFile(outFilePath, content.content, 'utf-8')
writeDone = true
}
}
return writeDone
}
async getActiveVersionIds(appNames: string[], authoringKey: string, region: string, retryCount?: number, retryDuration?: number) {
const luBuildCore = new LuBuildCore(authoringKey, `https://${region}.api.cognitive.microsoft.com`, retryCount || 1, retryDuration || 1000)
const apps = await luBuildCore.getApplicationList()
let appNameVersionMap = new Map<string, string>()
for (const appName of appNames) {
// find if there is a matched name with current app under current authoring key
appNameVersionMap.set(appName, '')
for (let app of apps) {
if (app.name === appName) {
const appInfo = await luBuildCore.getApplicationInfo(app.id)
appNameVersionMap.set(appName, appInfo.activeVersion)
break
}
}
}
return appNameVersionMap
}
async initApplicationFromLuContent(content: any, botName: string, suffix: string) {
let currentApp = await LuisBuilder.fromLUAsync([content]) // content.parseToLuis(true, content.language)
currentApp.culture = currentApp.culture && currentApp.culture !== '' && currentApp.culture !== 'en-us' ? currentApp.culture : content.language as string
currentApp.desc = currentApp.desc && currentApp.desc !== '' ? currentApp.desc : `Model for ${botName} app, targetting ${suffix}`
if (currentApp.name === undefined || currentApp.name === '') {
currentApp.name = `${botName}(${suffix})-${content.name}`
}
// remove empty intents from current app to avoid fewLabels error when training
this.filterEmptyIntents(currentApp)
return currentApp
}
async updateApplication(currentApp: any, luBuildCore: LuBuildCore, recognizer: Recognizer, timeBucket: number, keptVersionCount: number) {
await delay(timeBucket)
const appInfo = await luBuildCore.getApplicationInfo(recognizer.getAppId())
recognizer.versionId = appInfo.activeVersion || appInfo.endpoints.PRODUCTION.versionId
await delay(timeBucket)
const existingApp = await luBuildCore.exportApplication(recognizer.getAppId(), recognizer.versionId)
// compare models
const needUpdate = luBuildCore.compareApplications(currentApp, existingApp)
if (needUpdate) {
const newVersionId = luBuildCore.updateVersion(currentApp, existingApp)
recognizer.versionId = newVersionId
const options: any = {
versionId: newVersionId
}
this.handler(`${recognizer.getLuPath()} creating version=${newVersionId}\n`)
await delay(timeBucket)
await luBuildCore.importNewVersion(recognizer.getAppId(), currentApp, options)
// get all available versions
await delay(timeBucket)
const versionObjs = await luBuildCore.listApplicationVersions(recognizer.getAppId())
if (keptVersionCount < versionObjs.length) {
const versionObjsToDelete = versionObjs.reverse().splice(0, versionObjs.length - keptVersionCount)
for (const versionObj of versionObjsToDelete) {
if (versionObj.version !== newVersionId) {
this.handler(`${recognizer.getLuPath()} deleting old version=${versionObj.version}`)
await delay(timeBucket)
await luBuildCore.deleteVersion(recognizer.getAppId(), versionObj.version)
}
}
}
return true
} else {
this.handler(`${recognizer.getLuPath()} no changes\n`)
return false
}
}
async createApplication(currentApp: any, luBuildCore: LuBuildCore, recognizer: Recognizer, timeBucket: number) {
currentApp.versionId = currentApp.versionId && currentApp.versionId !== '' ? currentApp.versionId : '0.1'
recognizer.versionId = currentApp.versionId
this.handler(`Creating LUIS.ai application: ${currentApp.name} version:${currentApp.versionId}\n`)
await delay(timeBucket)
const response = await luBuildCore.importApplication(currentApp)
recognizer.setAppId(typeof response === 'string' ? response : response[Object.keys(response)[0]])
return true
}
async trainAndPublishApplication(luBuildCore: LuBuildCore, recognizer: Recognizer, timeBucket: number, isStaging: boolean) {
// send train application request
this.handler(`${recognizer.getLuPath()} training version=${recognizer.versionId}\n`)
await delay(timeBucket)
await luBuildCore.trainApplication(recognizer.getAppId(), recognizer.versionId)
this.handler(`${recognizer.getLuPath()} waiting for training for version=${recognizer.versionId}...\n`)
let done = true
do {
await delay(timeBucket)
// get training status to see if training completed
let trainingStatus = await luBuildCore.getTrainingStatus(recognizer.getAppId(), recognizer.versionId)
done = true
for (let status of trainingStatus) {
if (status.details) {
if (status.details.status === 'InProgress' || status.details.status === 'Queued') {
done = false
break
}
}
}
} while (!done)
this.handler('done\n')
// publish applications
this.handler(`${recognizer.getLuPath()} publishing version=${recognizer.versionId}\n`)
await delay(timeBucket)
await luBuildCore.publishApplication(recognizer.getAppId(), recognizer.versionId, isStaging)
this.handler(`${recognizer.getLuPath()} publishing finished for ${isStaging ? 'Staging' : 'Production'} slot\n`)
}
generateDeclarativeAssets(assets: Array<any>): Array<any> {
let contents = new Array<any>()
for (const asset of assets) {
let content: any
if (asset instanceof Settings) {
content = new Content(asset.save(), new LUOptions(path.basename(asset.getSettingsPath()), true, '', asset.getSettingsPath()))
} else {
content = new Content(asset.save(), new LUOptions(path.basename(asset.getDialogPath()), true, '', asset.getDialogPath()))
}
contents.push(content)
}
return contents
}
mergeSettingsContent(settingsPath: string, contents: any[]) {
let settings = new Settings(settingsPath, {})
for (const content of contents) {
const luisAppsMap = JSON.parse(content.content).luis
for (const appName of Object.keys(luisAppsMap)) {
settings.luis[appName] = {
"appId": luisAppsMap[appName]["appId"],
"version": luisAppsMap[appName]["version"]
}
}
}
return new Content(settings.save(), new LUOptions(path.basename(settings.getSettingsPath()), true, '', settings.getSettingsPath()))
}
filterEmptyIntents(app: any) {
const intents = app.intents
const utterances = app.utterances
const patterns = app.patterns
const emptyIntents = intents.filter((intent: any) => !utterances.some((utterance: any) => utterance.intent === intent.name)
&& !patterns.some((pattern: any) => pattern.intent === intent.name))
if (emptyIntents && emptyIntents.length > 0) {
const filteredIntents = intents.filter((intent: any) => !emptyIntents.some((emptyIntent: any) => emptyIntent.name === intent.name))
this.handler(`[WARN]: empty intent(s) ${emptyIntents.map((intent: any) => '# ' + intent.name).join(', ')} are filtered when handling luis application`)
app.intents = filteredIntents
}
}
}