1 | #!/usr/bin/env node
|
2 | /**
|
3 | * Copyright (c) Microsoft Corporation. All rights reserved.
|
4 | * Licensed under the MIT License.
|
5 | */
|
6 | const pkg = require('../package.json');
|
7 | const semver = require('semver');
|
8 | let requiredVersion = pkg.engines.node;
|
9 | if (!semver.satisfies(process.version, requiredVersion)) {
|
10 | console.log(`Required node version ${requiredVersion} not satisfied with current version ${process.version}.`);
|
11 | process.exit(1);
|
12 | }
|
13 |
|
14 | global.fetch = require('node-fetch'); // Browser compatibility
|
15 | const assert = require('assert');
|
16 | const fs = require('fs-extra');
|
17 | const path = require('path');
|
18 | const readline = require('readline');
|
19 | const readlineSync = require('readline-sync');
|
20 | const minimist = require('minimist');
|
21 | const chalk = require('chalk');
|
22 | const help = require('../lib/help');
|
23 | const qnamaker = require('../lib');
|
24 | const manifest = require('../lib/api/qnamaker');
|
25 | const { getServiceManifest } = require('../lib/utils/argsUtil');
|
26 | const request = require('request-promise-native');
|
27 | const Knowledgebase = require('../lib/api/knowledgebase');
|
28 | const Endpointkeys = require('../lib/api/endpointkeys');
|
29 | const Operations = require('../lib/api/operations');
|
30 | const Delay = require('await-delay');
|
31 |
|
32 | let args;
|
33 |
|
34 | /**
|
35 | * Entry for the app
|
36 | *
|
37 | * @returns {Promise<void>}
|
38 | */
|
39 | async function runProgram() {
|
40 | let argvFragment = process.argv.slice(2);
|
41 | if (argvFragment.length === 0) {
|
42 | argvFragment = ['-h'];
|
43 | }
|
44 | args = minimist(argvFragment);
|
45 |
|
46 | if (args.init) {
|
47 | const result = await initializeConfig();
|
48 | if (result) {
|
49 | process.stdout.write(`Successfully wrote ${process.cwd()}/.qnamakerrc`);
|
50 | }
|
51 | return;
|
52 | }
|
53 |
|
54 | if (args['!'] ||
|
55 | args.help ||
|
56 | args.h ||
|
57 | args._.includes('help')) {
|
58 | return help(args, process.stdout);
|
59 | }
|
60 |
|
61 | if (args.version || args.v) {
|
62 | return process.stdout.write(require(path.join(__dirname, '../package.json')).version);
|
63 | }
|
64 |
|
65 | const config = await composeConfig(args);
|
66 | if (!args.kbId)
|
67 | args.kbId = config.kbId;
|
68 | if (!args.hostname)
|
69 | args.hostname = config.hostname;
|
70 |
|
71 | if (args._[0] == "set")
|
72 | return await handleSetCommand(args, config);
|
73 |
|
74 | const serviceManifest = getServiceManifest(args);
|
75 |
|
76 | requestBody = await validateArguments(serviceManifest);
|
77 |
|
78 | // special case operations
|
79 | switch (serviceManifest.operation.methodAlias) {
|
80 | case "delete":
|
81 | if (!args.f && !args.force) {
|
82 | let kbResult = await new Knowledgebase().getKnowledgebaseDetails(config);
|
83 | let kb = JSON.parse(await kbResult.text());
|
84 | let answer = readlineSync.question(`Are you sure you would like to delete ${kb.name} [${kb.id}]? [no] `, { defaultInput: 'no' });
|
85 | if (answer.trim()[0] == 'n') {
|
86 | process.stderr.write('operation canceled');
|
87 | return;
|
88 | }
|
89 | }
|
90 | break;
|
91 | }
|
92 |
|
93 | let result = await qnamaker(config, serviceManifest, args, requestBody);
|
94 | if (result.error) {
|
95 | throw new Error(JSON.stringify(result.error, null, 4));
|
96 | }
|
97 |
|
98 | // special case response
|
99 | switch (serviceManifest.operation.name) {
|
100 | case "getKnowledgebaseDetails": {
|
101 | config.kbId = result.id;
|
102 | let kb = await updateKbId(config);
|
103 | if (args.msbot) {
|
104 | process.stdout.write(JSON.stringify({
|
105 | type: "qna",
|
106 | name: kb.name,
|
107 | id: kb.id,
|
108 | kbId: kb.id,
|
109 | subscriptionKey: config.subscriptionKey,
|
110 | endpointKey: config.endpointKey,
|
111 | hostname: kb.hostName
|
112 | }, null, 2));
|
113 | } else {
|
114 | process.stdout.write(JSON.stringify(result, null, 2));
|
115 | }
|
116 | }
|
117 | break;
|
118 |
|
119 | case "createKnowledgebase": {
|
120 | result = await waitForOperationSucceeded(config, result);
|
121 | let kbId = result.resourceLocation.split('/')[2];
|
122 | config.kbId = kbId;
|
123 | let kb = await updateKbId(config);
|
124 | if (args.msbot) {
|
125 | process.stdout.write(JSON.stringify({
|
126 | type: "qna",
|
127 | name: kb.name,
|
128 | id: kb.id,
|
129 | kbId: kb.id,
|
130 | subscriptionKey: config.subscriptionKey,
|
131 | endpointKey: config.endpointKey,
|
132 | hostname: config.hostname
|
133 | }, null, 2));
|
134 |
|
135 | } else {
|
136 | process.stdout.write(JSON.stringify(result, null, 2));
|
137 | }
|
138 | if (!(args.q || args.quiet) && !args.msbot) {
|
139 | let answer = readlineSync.question(`\nWould you like to save ${kb.name} ${kb.id} in your .qnamakerrc so that future commands will be with this KB? [yes]`, { defaultInput: 'yes' });
|
140 | if (answer[0] == 'y') {
|
141 | await fs.writeJson(path.join(process.cwd(), '.qnamakerrc'), config, { spaces: 2 });
|
142 | process.stdout.write('.qnamakerrc updated');
|
143 | }
|
144 | }
|
145 | break;
|
146 | }
|
147 | case "downloadLegacyKnowledgebase":
|
148 | if (!result.name) {
|
149 | result.name = "unknown";
|
150 | if (args.name) {
|
151 | result.name = args.name;
|
152 | }
|
153 | else {
|
154 | if (!(args.q || args.quiet)) {
|
155 | let answer = readlineSync.question(`\nWhat is the name of your knowledgebase? `);
|
156 | if (answer && answer.length > 0)
|
157 | result.name = answer;
|
158 | }
|
159 | }
|
160 | }
|
161 | process.stdout.write(JSON.stringify(result, null, 2));
|
162 | break;
|
163 |
|
164 | default: {
|
165 | // dump json as json stringified
|
166 | if (typeof result == 'string')
|
167 | process.stdout.write(result);
|
168 | else
|
169 | process.stdout.write(JSON.stringify(result, null, 2));
|
170 | break;
|
171 | }
|
172 | }
|
173 | }
|
174 |
|
175 | /**
|
176 | * Walks the user though the creation of the .qnamakerrc
|
177 | * file and writes it to disk. The knowledge base ID and subscription key
|
178 | * are optional but if omitted, --\knowledgeBaseID and --subscriptionKey
|
179 | * flags may be required for some commands.
|
180 | *
|
181 | * @returns {Promise<*>}
|
182 | */
|
183 | async function initializeConfig() {
|
184 | process.stdout.write(chalk.cyan.bold('\nThis util will walk you through creating a .qnamakerrc file\n\nPress ^C at any time to quit.\n\n'));
|
185 | const questions = [
|
186 | 'What is your QnAMaker access/subscription key? (found on the Cognitive Services Azure portal page under "access keys") ',
|
187 | 'What would you like to use as your active knowledgebase ID? [none] '
|
188 | ];
|
189 |
|
190 | const prompt = readline.createInterface({
|
191 | input: process.stdin,
|
192 | output: process.stdout,
|
193 | });
|
194 |
|
195 | const answers = [];
|
196 | for (let i = 0; i < questions.length; i++) {
|
197 | const question = questions[i];
|
198 | const answer = await new Promise((resolve) => {
|
199 |
|
200 | function doPrompt(promptMessage) {
|
201 | prompt.question(promptMessage, response => {
|
202 | resolve(response);
|
203 | });
|
204 | }
|
205 |
|
206 | doPrompt(question);
|
207 | });
|
208 | answers.push(answer.trim());
|
209 | }
|
210 |
|
211 | let [subscriptionKey, kbId] = answers;
|
212 |
|
213 | const config = Object.assign({}, { subscriptionKey, kbId });
|
214 |
|
215 | if (subscriptionKey && kbId) {
|
216 | await updateKbId(config);
|
217 | }
|
218 |
|
219 | try {
|
220 | await new Promise((resolve, reject) => {
|
221 | const confirmation = `\n\nDoes this look ok?\n${JSON.stringify(config, null, 2)}\n[Yes]/No: `;
|
222 | prompt.question(confirmation, response => {
|
223 | /^(y|yes)$/.test((response || 'yes').toLowerCase()) ? resolve(response) : reject();
|
224 | });
|
225 | });
|
226 | } catch (e) {
|
227 | return false;
|
228 | }
|
229 | await fs.writeJson(path.join(process.cwd(), '.qnamakerrc'), config, { spaces: 2 });
|
230 | return true;
|
231 | }
|
232 |
|
233 | async function updateKbId(config) {
|
234 | let response = await new Endpointkeys().getEndpointKeys(config);
|
235 | config.endpointKey = JSON.parse(await response.text()).primaryEndpointKey;
|
236 |
|
237 | response = await new Knowledgebase().getKnowledgebaseDetails(config);
|
238 | let kb = JSON.parse(await response.text());
|
239 | config.hostname = kb.hostName;
|
240 |
|
241 | return kb;
|
242 | }
|
243 |
|
244 | /**
|
245 | * Retrieves the input file to send as
|
246 | * the body of the request.
|
247 | *
|
248 | * @param args
|
249 | * @returns {Promise<*>}
|
250 | */
|
251 | async function getFileInput(args) {
|
252 | if (typeof args.in !== 'string') {
|
253 | return null;
|
254 | }
|
255 | // Let any errors fall through to the runProgram() promise
|
256 | return await fs.readJson(path.resolve(args.in));
|
257 | }
|
258 |
|
259 | /**
|
260 | * Composes the config from the 3 sources that it may reside.
|
261 | * Precedence is 1. Arguments, 2. qnamakerrc and 3. env variables
|
262 | *
|
263 | * @returns {Promise<*>}
|
264 | */
|
265 | async function composeConfig() {
|
266 | const { QNA_MAKER_SUBSCRIPTION_KEY, QNAMAKER_HOSTNAME, QNAMAKER_ENDPOINTKEY, QNA_MAKER_KBID } = process.env;
|
267 | const { subscriptionKey, hostname, endpointKey, kbId } = args;
|
268 |
|
269 | let qnamakerrcJson = {};
|
270 | let config;
|
271 | try {
|
272 | await fs.access(path.join(process.cwd(), '.qnamakerrc'), fs.R_OK);
|
273 | qnamakerrcJson = await fs.readJson(path.join(process.cwd(), '.qnamakerrc'));
|
274 | } catch (e) {
|
275 | // Do nothing
|
276 | } finally {
|
277 | config = {
|
278 | subscriptionKey: (subscriptionKey || qnamakerrcJson.subscriptionKey || QNA_MAKER_SUBSCRIPTION_KEY),
|
279 | hostname: (hostname || qnamakerrcJson.hostname || QNAMAKER_HOSTNAME),
|
280 | endpointKey: (endpointKey || qnamakerrcJson.endpointKey || QNAMAKER_ENDPOINTKEY),
|
281 | kbId: (kbId || qnamakerrcJson.kbId || QNA_MAKER_KBID)
|
282 | };
|
283 | validateConfig(config);
|
284 | }
|
285 | return config;
|
286 | }
|
287 |
|
288 | /**
|
289 | * Validates the config object to contain the
|
290 | * fields necessary for endpoint calls.
|
291 | *
|
292 | * @param {*} config The config object to validate
|
293 | */
|
294 | function validateConfig(config) {
|
295 | // appId and versionId are not validated here since
|
296 | // not all operations require these to be present.
|
297 | // Validation of specific params are done in the
|
298 | // ServiceBase.js
|
299 | const { subscriptionKey, knowledgeBaseID, endpoint, endpointKey } = config;
|
300 | const messageTail = `is missing from the configuration.\n\nDid you run ${chalk.cyan.bold('qnamaker --init')} yet?`;
|
301 | assert(typeof subscriptionKey === 'string', `The subscriptionKey ${messageTail}`);
|
302 | }
|
303 |
|
304 | /**
|
305 | * Provides basic validation of the command arguments.
|
306 | *
|
307 | * @param serviceManifest
|
308 | */
|
309 | async function validateArguments(serviceManifest) {
|
310 | let error = new Error();
|
311 | let body = undefined;
|
312 | error.name = 'ArgumentError';
|
313 | if (!serviceManifest) {
|
314 | error.message = 'The operation does not exist';
|
315 | throw error;
|
316 | }
|
317 |
|
318 | const { operation } = serviceManifest;
|
319 | if (!operation) {
|
320 | error.message = 'The operation does not exist';
|
321 |
|
322 | throw error;
|
323 | }
|
324 |
|
325 | const entitySpecified = typeof args.in === 'string';
|
326 | const entityRequired = !!operation.entityName;
|
327 |
|
328 | if (!entityRequired && entitySpecified) {
|
329 | error.message = `The ${operation.name} operation does not accept an input`;
|
330 |
|
331 | throw error;
|
332 | }
|
333 |
|
334 | if (entityRequired) {
|
335 | if (entitySpecified) {
|
336 | body = await getFileInput(args);
|
337 | }
|
338 | else {
|
339 | switch (serviceManifest.operation.name) {
|
340 | case "generateAnswer":
|
341 | body = {
|
342 | question: args.question,
|
343 | top: args.top
|
344 | };
|
345 | break;
|
346 | default:
|
347 | error.message = `The ${operation.name} requires an input of type: ${operation.entityType}`;
|
348 | throw error;
|
349 | }
|
350 | }
|
351 | }
|
352 |
|
353 | if (serviceManifest.operation.params) {
|
354 | for (let param of serviceManifest.operation.params) {
|
355 | if (param.required) {
|
356 | if (!args[param.name] && !args[param.alias || param.name]) {
|
357 | error.message = `The --${param.name} argument is missing and required`;
|
358 | throw error;
|
359 | }
|
360 | }
|
361 | }
|
362 | }
|
363 |
|
364 | // Note that the ServiceBase will validate params that may be required.
|
365 | return body;
|
366 | }
|
367 |
|
368 |
|
369 | /**
|
370 | * Exits with a non-zero status and prints
|
371 | * the error if present or displays the help
|
372 | *
|
373 | * @param error
|
374 | */
|
375 | async function handleError(error) {
|
376 | process.stderr.write('\n' + chalk.red.bold(error + '\n\n'));
|
377 | // if (error.name === 'ArgumentError') {
|
378 | await help(args);
|
379 | // }
|
380 | return 1;
|
381 | }
|
382 |
|
383 | async function handleSetCommand(args, config) {
|
384 | let target = args._.length >= 2 ? args._[1].toLowerCase() : undefined;
|
385 | if (!target) {
|
386 | process.stdout.write(chalk.red.bold(`missing .qnamakerrc property name: [kbid|subscriptionkey]\n`));
|
387 | return help(args);
|
388 | }
|
389 |
|
390 | let targetName = (args._.length >= 3) ? args._.slice(2).join(' ').toLowerCase().trim() : undefined;
|
391 | if (!targetName) {
|
392 | process.stdout.write(chalk.red.bold(`missing the value\n`));
|
393 | return help(args);
|
394 | }
|
395 |
|
396 | switch (target) {
|
397 | case 'endpointkey':
|
398 | config.endpointKey = targetName;
|
399 | break;
|
400 |
|
401 | case 'hostname':
|
402 | config.hostname = targetName;
|
403 | break;
|
404 |
|
405 | case 'subscriptionkey':
|
406 | config.subscriptionKey = targetName;
|
407 | break;
|
408 |
|
409 | case 'kbid':
|
410 | config.kbId = targetName;
|
411 | await updateKbId(config);
|
412 | break;
|
413 |
|
414 | default:
|
415 | process.stdout.write(chalk.red.bold(`unknown .qnamakerrc property ${target}. Valid values are: [kbid|subscriptionkey]\n`));
|
416 | return help(args);
|
417 | }
|
418 | await fs.writeJson(path.join(process.cwd(), '.qnamakerrc'), config, { spaces: 2 });
|
419 | process.stdout.write(JSON.stringify(config, null, 4));
|
420 | return true;
|
421 | }
|
422 |
|
423 | async function waitForOperationSucceeded(config, result) {
|
424 | while (result.operationState != "Succeeded") {
|
425 | let opResult = await new Operations().getOperationDetails({ subscriptionKey: config.subscriptionKey, operationId: result.operationId });
|
426 | result = JSON.parse(await opResult.text());
|
427 |
|
428 | if (result.operationState == "Succeeded")
|
429 | break;
|
430 | await Delay(1000);
|
431 | }
|
432 | return result;
|
433 | }
|
434 |
|
435 | runProgram()
|
436 | .then(process.exit)
|
437 | .catch(handleError)
|
438 | .then(process.exit);
|