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

Commit f95fb98

Browse files
authored
qnamaker:build support --qnaConfig (#840)
* support --qnaConfig and extract subscriptionKey from system config * optimize typo
1 parent 2d78bb6 commit f95fb98

File tree

6 files changed

+201
-43
lines changed

6 files changed

+201
-43
lines changed

packages/lu/src/parser/qnabuild/builder.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export class Builder {
3232

3333
async loadContents(
3434
files: string[],
35-
inputFolder: string,
3635
botName: string,
3736
suffix: string,
3837
region: string,
@@ -43,10 +42,17 @@ export class Builder {
4342
let qnaContents = new Map<string, string>()
4443

4544
for (const file of files) {
46-
const qnaFiles = await fileHelper.getLuObjects(undefined, file, true, fileExtEnum.QnAFile)
45+
let fileCulture: string
46+
let cultureFromPath = fileHelper.getCultureFromPath(file)
47+
if (cultureFromPath) {
48+
fileCulture = cultureFromPath
49+
} else {
50+
fileCulture = culture
51+
}
4752

4853
let fileContent = ''
4954
let result
55+
const qnaFiles = await fileHelper.getLuObjects(undefined, file, true, fileExtEnum.QnAFile)
5056
try {
5157
result = await qnaBuilderVerbose.build(qnaFiles, true)
5258

@@ -62,16 +68,10 @@ export class Builder {
6268
}
6369

6470
this.handler(`${file} loaded\n`)
65-
let fileCulture: string
66-
let cultureFromPath = fileHelper.getCultureFromPath(file)
67-
if (cultureFromPath) {
68-
fileCulture = cultureFromPath
69-
} else {
70-
fileCulture = culture
71-
}
7271

72+
const fileFolder = path.dirname(file)
7373
if (multiRecognizer === undefined) {
74-
const multiRecognizerPath = path.join(inputFolder, `${botName}.qna.dialog`)
74+
const multiRecognizerPath = path.join(fileFolder, `${botName}.qna.dialog`)
7575
let multiRecognizerContent = {}
7676
if (fs.existsSync(multiRecognizerPath)) {
7777
multiRecognizerContent = JSON.parse(await fileHelper.getContentFromFile(multiRecognizerPath)).recognizers
@@ -82,7 +82,7 @@ export class Builder {
8282
}
8383

8484
if (settings === undefined) {
85-
const settingsPath = path.join(inputFolder, `qnamaker.settings.${suffix}.${region}.json`)
85+
const settingsPath = path.join(fileFolder, `qnamaker.settings.${suffix}.${region}.json`)
8686
let settingsContent = {}
8787
if (fs.existsSync(settingsPath)) {
8888
settingsContent = JSON.parse(await fileHelper.getContentFromFile(settingsPath)).qna
@@ -95,7 +95,7 @@ export class Builder {
9595
const content = new Content(fileContent, new qnaOptions(botName, true, fileCulture, file))
9696

9797
if (!recognizers.has(content.name)) {
98-
const dialogFile = path.join(inputFolder, `${content.name}.dialog`)
98+
const dialogFile = path.join(fileFolder, `${content.name}.dialog`)
9999
let existingDialogObj: any
100100
if (fs.existsSync(dialogFile)) {
101101
existingDialogObj = JSON.parse(await fileHelper.getContentFromFile(dialogFile))

packages/qnamaker/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ OPTIONS
140140
141141
--log write out log messages to console
142142
143+
--luConfig=luConfig Path to config for qnamaker build which can contain switches for arguments
144+
143145
--region=region [default: westus] Overrides public endpoint
144146
https://<region>.api.cognitive.microsoft.com/qnamaker/v4.0/
145147

packages/qnamaker/src/commands/qnamaker/build.ts

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
*/
55

66
import {CLIError, Command, flags} from '@microsoft/bf-cli-command'
7+
import {processFlags} from '../../utils/qnamakerbase'
8+
79
const path = require('path')
10+
const fs = require('fs-extra')
811
const username = require('username')
912
const exception = require('@microsoft/bf-lu/lib/parser/utils/exception')
1013
const file = require('@microsoft/bf-lu/lib/utils/filehelper')
@@ -27,59 +30,89 @@ export default class QnamakerBuild extends Command {
2730
static flags: any = {
2831
help: flags.help({char: 'h'}),
2932
in: flags.string({char: 'i', description: 'Source .qna file or folder'}),
30-
subscriptionKey: flags.string({char: 's', description: 'QnA maker subscription key', required: true}),
31-
botName: flags.string({char: 'b', description: 'Bot name', required: true}),
33+
subscriptionKey: flags.string({char: 's', description: 'QnA maker subscription key'}),
34+
botName: flags.string({char: 'b', description: 'Bot name'}),
3235
region: flags.string({description: 'Overrides public endpoint https://<region>.api.cognitive.microsoft.com/qnamaker/v4.0/', default: 'westus'}),
3336
out: flags.string({char: 'o', description: 'Output folder name to write out .dialog files. If not specified, knowledge base ids will be output to console'}),
3437
defaultCulture: flags.string({description: 'Culture code for the content. Infer from .qna if available. Defaults to en-us if not set'}),
3538
fallbackLocale: flags.string({description: 'Locale to be used at the fallback if no locale specific recognizer is found. Only valid if --out is set'}),
3639
suffix: flags.string({description: 'Environment name as a suffix identifier to include in qnamaker kb name. Defaults to current logged in user alias'}),
3740
dialog: flags.string({description: 'Dialog recognizer type [multiLanguage|crosstrained]', default: 'multiLanguage'}),
3841
force: flags.boolean({char: 'f', description: 'If --out flag is provided, overwrites relevant dialog file', default: false}),
42+
qnaConfig: flags.string({description: 'Path to config for qna build which can contain switches for arguments'}),
3943
log: flags.boolean({description: 'write out log messages to console', default: false}),
4044
}
4145

4246
async run() {
4347
try {
4448
const {flags}: any = this.parse(QnamakerBuild)
4549

50+
// Luconfig overrides flags
51+
let files: string[] = []
52+
if (flags.qnaConfig) {
53+
const configFilePath = path.resolve(flags.qnaConfig)
54+
if (await fs.exists(configFilePath)) {
55+
const configObj = JSON.parse(await file.getContentFromFile(configFilePath))
56+
for (let prop of Object.keys(configObj)) {
57+
if (prop === 'models') {
58+
files = configObj.models.map((m: string) => path.isAbsolute(m) ? m : path.join(path.dirname(configFilePath), m))
59+
} else if (prop === 'out') {
60+
flags.out = path.isAbsolute(configObj.out) ? configObj.out : path.join(path.dirname(configFilePath), configObj.out)
61+
} else {
62+
flags[prop] = configObj[prop]
63+
}
64+
}
65+
}
66+
}
67+
68+
// Flags override userConfig
69+
let qnamakerBuildFlags = Object.keys(QnamakerBuild.flags)
70+
qnamakerBuildFlags.push('endpoint')
71+
72+
let {inVal, subscriptionKey, botName, region, out, defaultCulture, fallbackLocale, suffix, dialog, force, log, endpoint}
73+
= await processFlags(flags, qnamakerBuildFlags, this.config.configDir)
74+
4675
flags.stdin = await this.readStdin()
4776

48-
if (!flags.stdin && !flags.in) {
77+
if (!flags.stdin && !inVal && files.length === 0) {
4978
throw new CLIError('Missing input. Please use stdin or pass a file or folder location with --in flag')
5079
}
5180

52-
if (flags.dialog && flags.dialog !== recognizerType.MULTILANGUAGE && flags.dialog !== recognizerType.CROSSTRAINED) {
81+
if (!subscriptionKey) {
82+
throw new CLIError('Missing qnamaker subscription key. Please pass subscription key with --subscriptionKey flag or specify via bf config:set:qnamaker --subscriptionKey.')
83+
}
84+
85+
if (!botName) {
86+
throw new CLIError('Missing bot name. Please pass bot name with --botName flag or specify via --qnaConfig.')
87+
}
88+
89+
if (dialog && dialog !== recognizerType.MULTILANGUAGE && dialog !== recognizerType.CROSSTRAINED) {
5390
throw new CLIError('Recognizer type specified by --dialog is not right. Please specify [multiLanguage|crosstrained]')
5491
}
5592

56-
flags.defaultCulture = flags.defaultCulture && flags.defaultCulture !== '' ? flags.defaultCulture : 'en-us'
57-
flags.region = flags.region && flags.region !== '' ? flags.region : 'westus'
58-
flags.suffix = flags.suffix && flags.suffix !== '' ? flags.suffix : await username() || 'development'
59-
flags.fallbackLocale = flags.fallbackLocale && flags.fallbackLocale !== '' ? flags.fallbackLocale : 'en-us'
93+
defaultCulture = defaultCulture && defaultCulture !== '' ? defaultCulture : 'en-us'
94+
region = region && region !== '' ? region : 'westus'
95+
suffix = suffix && suffix !== '' ? suffix : await username() || 'development'
96+
fallbackLocale = fallbackLocale && fallbackLocale !== '' ? fallbackLocale : 'en-us'
6097

61-
const endpoint = `https://${flags.region}.api.cognitive.microsoft.com/qnamaker/v4.0`
98+
endpoint = endpoint && endpoint !== '' ? endpoint : `https://${region}.api.cognitive.microsoft.com/qnamaker/v4.0`
6299

63100
// create builder class
64101
const builder = new Builder((input: string) => {
65-
if (flags.log) this.log(input)
102+
if (log) this.log(input)
66103
})
67104

68105
let qnaContents: any[] = []
69106
let recognizers = new Map<string, any>()
70107
let multiRecognizer: any
71108
let settings: any
72-
73-
const dialogFilePath = (flags.stdin || !flags.in) ? process.cwd() : flags.in.endsWith(fileExtEnum.QnAFile) ? path.dirname(path.resolve(flags.in)) : path.resolve(flags.in)
74109

75-
let files: string[] = []
76-
77-
if (flags.in && flags.in !== '') {
78-
if (flags.log) this.log('Loading files...\n')
110+
if ((inVal && inVal !== '') || files.length > 0) {
111+
if (log) this.log('Loading files...\n')
79112

80113
// get qna files from flags.in.
81-
if (flags.in && flags.in !== '') {
82-
const qnaFiles: string[] = await file.getLuFiles(flags.in, true, fileExtEnum.QnAFile)
114+
if (inVal && inVal !== '') {
115+
const qnaFiles: string[] = await file.getLuFiles(inVal, true, fileExtEnum.QnAFile)
83116
files.push(...qnaFiles)
84117
}
85118

@@ -88,37 +121,37 @@ export default class QnamakerBuild extends Command {
88121

89122
// load qna contents from qna files
90123
// load existing recognizers, multiRecogniers and settings or create default ones
91-
const loadedResources = await builder.loadContents(files, dialogFilePath, flags.botName, flags.suffix, flags.region, flags.defaultCulture)
124+
const loadedResources = await builder.loadContents(files, botName, suffix, region, defaultCulture)
92125
qnaContents = loadedResources.qnaContents
93126
recognizers = loadedResources.recognizers
94127
multiRecognizer = loadedResources.multiRecognizer
95128
settings = loadedResources.settings
96129
} else {
97130
// load qna content from stdin and create default recognizer, multiRecognier and settings
98-
if (flags.log) this.log('Load qna content from stdin\n')
99-
const content = new Content(flags.stdin, new qnaOptions(flags.botName, true, flags.defaultCulture, path.join(process.cwd(), 'stdin')))
131+
if (log) this.log('Load qna content from stdin\n')
132+
const content = new Content(flags.stdin, new qnaOptions(botName, true, defaultCulture, path.join(process.cwd(), 'stdin')))
100133
qnaContents.push(content)
101-
multiRecognizer = new MultiLanguageRecognizer(path.join(process.cwd(), `${flags.botName}.qna.dialog`), {})
102-
settings = new Settings(path.join(process.cwd(), `qnamaker.settings.${flags.suffix}.${flags.region}.json`), {})
134+
multiRecognizer = new MultiLanguageRecognizer(path.join(process.cwd(), `${botName}.qna.dialog`), {})
135+
settings = new Settings(path.join(process.cwd(), `qnamaker.settings.${suffix}.${region}.json`), {})
103136
const recognizer = Recognizer.load(content.path, content.name, path.join(process.cwd(), `${content.name}.dialog`), settings, {})
104137
recognizers.set(content.name, recognizer)
105138
}
106139

107140
// update or create and then publish qnamaker kb based on loaded resources
108-
if (flags.log) this.log('Handling qnamaker knowledge bases...')
109-
const dialogContents = await builder.build(qnaContents, recognizers, flags.subscriptionKey, endpoint, flags.botName, flags.suffix, flags.fallbackLocale, multiRecognizer, settings)
141+
if (log) this.log('Handling qnamaker knowledge bases...')
142+
const dialogContents = await builder.build(qnaContents, recognizers, subscriptionKey, endpoint, botName, suffix, fallbackLocale, multiRecognizer, settings)
110143

111144
// get endpointKeys
112-
const endpointKeysInfo = await builder.getEndpointKeys(flags.subscriptionKey, endpoint)
145+
const endpointKeysInfo = await builder.getEndpointKeys(subscriptionKey, endpoint)
113146
const endpointKeys: any = {
114147
"primaryEndpointKey": endpointKeysInfo.primaryEndpointKey,
115148
"secondaryEndpointKey": endpointKeysInfo.secondaryEndpointKey
116149
}
117150

118151
// write dialog assets based on config
119-
if (flags.out) {
120-
const outputFolder = path.resolve(flags.out)
121-
const writeDone = await builder.writeDialogAssets(dialogContents, flags.force, outputFolder, flags.dialog, files)
152+
if (out) {
153+
const outputFolder = path.resolve(out)
154+
const writeDone = await builder.writeDialogAssets(dialogContents, force, outputFolder, dialog, files)
122155
if (writeDone) {
123156
this.log(`Successfully wrote .dialog files to ${outputFolder}\n`)
124157
this.log('QnA knowledge base endpointKeys:')

packages/qnamaker/src/utils/qnamakerbase.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const srvMan = require('./../../utils/servicemanifest')
1111
const {ServiceBase} = require('./../../utils/api/serviceBase')
1212
const file = require('@microsoft/bf-lu/lib/utils/filehelper')
1313

14+
const configPrefix = 'qnamaker__'
1415

1516
export async function processInputs(flags: any, payload: any, configfile: string, stdin = '') {
1617
let result: Inputs = {}
@@ -65,3 +66,52 @@ export async function updateQnAMakerConfig(config: any , configfile: string) {
6566
export interface Inputs {
6667
[key: string]: any
6768
}
69+
70+
export async function processFlags(flags: any, flagLabels: string[], configDir: string) {
71+
let config = filterByAllowedConfigValues(await getUserConfig(configDir), configPrefix)
72+
config = config ? filterConfig(config, configPrefix) : config
73+
const input: any = {}
74+
flagLabels
75+
.filter(flag => flag !== 'help')
76+
.map((flag: string) => {
77+
if (flag === 'in') {
78+
// rename property since 'in' is a reserved keyword
79+
input[`${flag}Val`] = flags[flag]
80+
}
81+
82+
input[flag] = flags[flag] || (config ? config[configPrefix + flag] : null)
83+
})
84+
85+
return input
86+
}
87+
88+
async function getUserConfig (configPath: string) {
89+
if (fs.existsSync(path.join(configPath, 'config.json'))) {
90+
return fs.readJSON(path.join(configPath, 'config.json'), {throws: false})
91+
}
92+
93+
return {}
94+
}
95+
96+
function filterByAllowedConfigValues (configObj: any, prefix: string) {
97+
const allowedConfigValues = [`${prefix}kbId`, `${prefix}endpoint`, `${prefix}region`, `${prefix}subscriptionKey`]
98+
const filtered = Object.keys(configObj)
99+
.filter(key => allowedConfigValues.includes(key))
100+
.reduce((filteredConfigObj: any, key) => {
101+
filteredConfigObj[key] = configObj[key]
102+
103+
return filteredConfigObj
104+
}, {})
105+
106+
return filtered
107+
}
108+
109+
function filterConfig (config: any, prefix: string) {
110+
return Object.keys(config)
111+
.filter((key: string) => key.startsWith(prefix))
112+
.reduce((filteredConfig: any, key: string) => {
113+
filteredConfig[key] = config[key]
114+
115+
return filteredConfig
116+
}, {})
117+
}

packages/qnamaker/test/commands/qnamaker/build.test.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('qnamaker:build cli parameters test', () => {
2626
.stderr()
2727
.command(['qnamaker:build', '--in', `${path.join(__dirname, './../../fixtures/testcases/qnabuild')}`, '--botName', 'Contoso'])
2828
.it('displays an error if any required input parameters are missing', ctx => {
29-
expect(ctx.stderr).to.contain('Missing required flag:\n -s, --subscriptionKey SUBSCRIPTIONKEY QnA maker subscription key')
29+
expect(ctx.stderr).to.contain('Missing qnamaker subscription key. Please pass subscription key with --subscriptionKey flag or specify via bf config:set:qnamaker --subscriptionKey.')
3030
})
3131

3232
test
@@ -42,7 +42,7 @@ describe('qnamaker:build cli parameters test', () => {
4242
.stderr()
4343
.command(['qnamaker:build', '--subscriptionKey', uuidv1(), '--in', `${path.join(__dirname, './../../fixtures/testcases/qnabuild')}`])
4444
.it('displays an error if any required input parameters are missing', ctx => {
45-
expect(ctx.stderr).to.contain('Missing required flag:\n -b, --botName BOTNAME Bot name')
45+
expect(ctx.stderr).to.contain('Missing bot name. Please pass bot name with --botName flag or specify via --qnaConfig.')
4646
})
4747

4848
test
@@ -544,4 +544,66 @@ describe('qnamaker:build update knowledge base with multiturn successfully when
544544
expect(ctx.stdout).to.contain('Updating finished')
545545
expect(ctx.stdout).to.contain('Publishing kb')
546546
})
547+
})
548+
549+
describe('qnamaker:build update knowledge base successfully with parameters set from qna config', () => {
550+
before(async function () {
551+
await fs.ensureDir(path.join(__dirname, './../../../results/'))
552+
553+
nock('https://westus.api.cognitive.microsoft.com')
554+
.get(uri => uri.includes('qnamaker'))
555+
.reply(200, {
556+
knowledgebases:
557+
[{
558+
name: 'test(development).en-us.qna',
559+
id: 'f8c64e2a-1111-3a09-8f78-39d7adc76ec5',
560+
hostName: 'https://myqnamakerbot.azurewebsites.net'
561+
}]
562+
})
563+
564+
nock('https://westus.api.cognitive.microsoft.com')
565+
.get(uri => uri.includes('knowledgebases'))
566+
.reply(200, {
567+
qnaDocuments: [{
568+
id: 1,
569+
source: 'custom editorial',
570+
questions: ['how many sandwich types do you have'],
571+
answer: '25 types',
572+
metadata: []
573+
}]
574+
})
575+
576+
nock('https://westus.api.cognitive.microsoft.com')
577+
.put(uri => uri.includes('knowledgebases'))
578+
.reply(204)
579+
580+
nock('https://westus.api.cognitive.microsoft.com')
581+
.post(uri => uri.includes('knowledgebases'))
582+
.reply(204)
583+
584+
nock('https://westus.api.cognitive.microsoft.com')
585+
.get(uri => uri.includes('endpointkeys'))
586+
.reply(200, {
587+
primaryEndpointKey: 'xxxx',
588+
secondaryEndpointKey: 'yyyy'
589+
})
590+
})
591+
592+
after(async function () {
593+
await fs.remove(path.join(__dirname, './../../../results/'))
594+
})
595+
596+
test
597+
.stdout()
598+
.command(['qnamaker:build', '--qnaConfig', './test/fixtures/testcases/qnabuild/sandwich/qnafiles/qnaconfig.json', '--subscriptionKey', uuidv1()])
599+
.it('should update a knowledge base successfully with parameters set from qna config', async ctx => {
600+
expect(ctx.stdout).to.contain('Handling qnamaker knowledge bases...')
601+
expect(ctx.stdout).to.contain('Updating to new version for kb test(development).en-us.qna')
602+
expect(ctx.stdout).to.contain('Updating finished')
603+
expect(ctx.stdout).to.contain('Publishing kb')
604+
605+
expect(await compareFiles('./../../../results/qnamaker.settings.development.westus.json', './../../fixtures/testcases/qnabuild/sandwich/config/qnamaker.settings.development.westus.json')).to.be.true
606+
expect(await compareFiles('./../../../results/test.en-us.qna.dialog', './../../fixtures/testcases/qnabuild/sandwich/dialogs/test.en-us.qna.dialog')).to.be.true
607+
expect(await compareFiles('./../../../results/test.qna.dialog', './../../fixtures/testcases/qnabuild/sandwich/dialogs/test.qna.dialog')).to.be.true
608+
})
547609
})

0 commit comments

Comments
 (0)