UNPKG

14.9 kBPlain TextView Raw
1#!/usr/bin/env node
2/**
3 * Copyright (c) Microsoft Corporation. All rights reserved.
4 * Licensed under the MIT License.
5 */
6const pkg = require('../package.json');
7const semver = require('semver');
8let requiredVersion = pkg.engines.node;
9if (!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
14global.fetch = require('node-fetch'); // Browser compatibility
15const assert = require('assert');
16const fs = require('fs-extra');
17const path = require('path');
18const readline = require('readline');
19const readlineSync = require('readline-sync');
20const minimist = require('minimist');
21const chalk = require('chalk');
22const help = require('../lib/help');
23const qnamaker = require('../lib');
24const manifest = require('../lib/api/qnamaker');
25const { getServiceManifest } = require('../lib/utils/argsUtil');
26const request = require('request-promise-native');
27const Knowledgebase = require('../lib/api/knowledgebase');
28const Endpointkeys = require('../lib/api/endpointkeys');
29const Operations = require('../lib/api/operations');
30const Delay = require('await-delay');
31
32let args;
33
34/**
35 * Entry for the app
36 *
37 * @returns {Promise<void>}
38 */
39async 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 */
183async 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
233async 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 */
251async 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 */
265async 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 */
294function 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 */
309async 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 */
375async 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
383async 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
423async 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
435runProgram()
436 .then(process.exit)
437 .catch(handleError)
438 .then(process.exit);