Skip to content

Commit

Permalink
feat: Support AI APIs using Box Node SDK (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
congminh1254 authored Aug 1, 2024
1 parent 035fd7a commit 59551d2
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Avatar URL: 'https://app.box.com/api/avatar/large/77777'
<!-- commands -->
# Command Topics

* [`box ai`](docs/ai.md) - Sends an AI request to supported LLMs and returns an answer
* [`box autocomplete`](docs/autocomplete.md) - Display autocomplete installation instructions
* [`box collaboration-allowlist`](docs/collaboration-allowlist.md) - List collaboration allowlist entries
* [`box collaborations`](docs/collaborations.md) - Manage collaborations
Expand Down
78 changes: 78 additions & 0 deletions docs/ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
`box ai`
========

Sends an AI request to supported LLMs and returns an answer

* [`box ai:ask`](#box-aiask)
* [`box ai:text-gen`](#box-aitext-gen)

## `box ai:ask`

Sends an AI request to supported LLMs and returns an answer

```
USAGE
$ box ai:ask
OPTIONS
-h, --help Show CLI help
-q, --quiet Suppress any non-error output to stderr
-s, --save Save report to default reports folder on disk
-t, --token=token Provide a token to perform this call
-v, --verbose Show verbose output, which can be helpful for debugging
-y, --yes Automatically respond yes to all confirmation prompts
--as-user=as-user Provide an ID for a user
--bulk-file-path=bulk-file-path File path to bulk .csv or .json objects
--csv Output formatted CSV
--fields=fields Comma separated list of fields to show
--items=items (required) The items for the AI request
--json Output formatted JSON
--no-color Turn off colors for logging
--prompt=prompt (required) The prompt for the AI request
--save-to-file-path=save-to-file-path Override default file path to save report
EXAMPLE
box ai:ask --items=id=12345,type=file --prompt "What is the status of this document?"
```

_See code: [src/commands/ai/ask.js](https://github.com/box/boxcli/blob/v3.14.1/src/commands/ai/ask.js)_

## `box ai:text-gen`

Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text.

```
USAGE
$ box ai:text-gen
OPTIONS
-h, --help Show CLI help
-q, --quiet Suppress any non-error output to stderr
-s, --save Save report to default reports folder on disk
-t, --token=token Provide a token to perform this call
-v, --verbose Show verbose output, which can be helpful for debugging
-y, --yes Automatically respond yes to all confirmation prompts
--as-user=as-user Provide an ID for a user
--bulk-file-path=bulk-file-path File path to bulk .csv or .json objects
--csv Output formatted CSV
--dialogue-history=dialogue-history The history of prompts and answers previously passed to the LLM.
--fields=fields Comma separated list of fields to show
--items=items (required) The items to be processed by the LLM, often files. The array can
include exactly one element.
--json Output formatted JSON
--no-color Turn off colors for logging
--prompt=prompt (required) The prompt for the AI request
--save-to-file-path=save-to-file-path Override default file path to save report
EXAMPLE
box ai:text-gen --dialogue-history=prompt="What is the status of this document?",answer="It is in
review",created-at="2024-07-09T11:29:46.835Z" --items=id=12345,type=file --prompt="What is the status of this
document?"
```

_See code: [src/commands/ai/text-gen.js](https://github.com/box/boxcli/blob/v3.14.1/src/commands/ai/text-gen.js)_
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@oclif/plugin-help": "^2.2.1",
"@oclif/plugin-not-found": "^1.2.0",
"archiver": "^3.0.0",
"box-node-sdk": "^3.5.0",
"box-node-sdk": "^3.7.0",
"chalk": "^2.4.1",
"cli-progress": "^2.1.0",
"csv": "^6.3.3",
Expand Down
66 changes: 66 additions & 0 deletions src/commands/ai/ask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const BoxCommand = require('../../box-command');
const { flags } = require('@oclif/command');
const utils = require('../../util');

class AiAskCommand extends BoxCommand {
async run() {
const { flags, args } = this.parse(AiAskCommand);
let options = {};
options.mode = flags.items.length > 1 ? 'multi_item_qa' : 'single_item_qa';

if (flags.prompt) {
options.prompt = flags.prompt;
}
if (flags.items) {
options.items = flags.items;
}

let answer = await this.client.ai.ask({
prompt: options.prompt,
items: options.items,
mode: options.mode
});
await this.output(answer);
}
}

AiAskCommand.description = 'Sends an AI request to supported LLMs and returns an answer';
AiAskCommand.examples = ['box ai:ask --items=id=12345,type=file --prompt "What is the status of this document?"'];
AiAskCommand._endpoint = 'post_ai_ask';

AiAskCommand.flags = {
...BoxCommand.flags,
prompt: flags.string({
required: true,
description: 'The prompt for the AI request',
}),
items: flags.string({
required: true,
description: 'The items for the AI request',
multiple: true,
parse(input) {
const item = {
id: '',
type: 'file'
};
const obj = utils.parseStringToObject(input, ['id', 'type', 'content']);
for (const key in obj) {
if (key === 'id') {
item.id = obj[key];
} else if (key === 'type') {
item.type = obj[key];
} else if (key === 'content') {
item.content = obj[key];
} else {
throw new Error(`Invalid item key ${key}`);
}
}

return item;
}
}),
};

module.exports = AiAskCommand;
86 changes: 86 additions & 0 deletions src/commands/ai/text-gen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

const BoxCommand = require('../../box-command');
const { flags } = require('@oclif/command');
const utils = require('../../util');

class AiTextGenCommand extends BoxCommand {
async run() {
const { flags, args } = this.parse(AiTextGenCommand);
let options = {};

if (flags['dialogue-history']) {
options.dialogueHistory = flags['dialogue-history'];
}
options.prompt = flags.prompt;
options.items = flags.items;
let answer = await this.client.ai.textGen({
prompt: options.prompt,
items: options.items,
dialogue_history: options.dialogueHistory,
});

await this.output(answer);
}
}

AiTextGenCommand.description = 'Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text.';
AiTextGenCommand.examples = ['box ai:text-gen --dialogue-history=prompt="What is the status of this document?",answer="It is in review",created-at="2024-07-09T11:29:46.835Z" --items=id=12345,type=file --prompt="What is the status of this document?"'];
AiTextGenCommand._endpoint = 'post_ai_text_gen';

AiTextGenCommand.flags = {
...BoxCommand.flags,

'dialogue-history': flags.string({
required: false,
description: 'The history of prompts and answers previously passed to the LLM.',
multiple: true,
parse(input) {
const record = {};
const obj = utils.parseStringToObject(input, ['prompt', 'answer', 'created-at']);
for (const key in obj) {
if (key === 'prompt') {
record.prompt = obj[key];
} else if (key === 'answer') {
record.answer = obj[key];
} else if (key === 'created-at') {
record.created_at = BoxCommand.normalizeDateString(obj[key]);
} else {
throw new Error(`Invalid record key ${key}`);
}
}

return record;
},
}),
items: flags.string({
required: true,
description: 'The items to be processed by the LLM, often files. The array can include exactly one element.',
multiple: true,
parse(input) {
const item = {
id: '',
type: 'file'
};
const obj = utils.parseStringToObject(input, ['id', 'type', 'content']);
for (const key in obj) {
if (key === 'id') {
item.id = obj[key];
} else if (key === 'type') {
item.type = obj[key];
} else if (key === 'content') {
item.content = obj[key];
} else {
throw new Error(`Invalid item key ${key}`);
}
}
return item;
}
}),
prompt: flags.string({
required: true,
description: 'The prompt for the AI request',
})
};

module.exports = AiTextGenCommand;
45 changes: 45 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,50 @@ function parseMetadataString(input) {
return op;
}

/**
* Parse a string into a JSON object
*
* @param {string} inputString The string to parse
* @param {string[]} keys The keys to parse from the string
* @returns {Object} The parsed object
*/
function parseStringToObject(inputString, keys) {
const result = {};

while (inputString.length > 0) {
inputString = inputString.trim();
let parsedKey = inputString.split('=')[0];
inputString = inputString.substring(inputString.indexOf('=') + 1);

// Find the next key or the end of the string
let nextKeyIndex = inputString.length;
for (let key of keys) {
let keyIndex = inputString.indexOf(key);
if (keyIndex !== -1 && keyIndex < nextKeyIndex) {
nextKeyIndex = keyIndex;
}
}

let parsedValue = inputString.substring(0, nextKeyIndex).trim();
if (parsedValue.endsWith(',') && nextKeyIndex !== inputString.length) {
parsedValue = parsedValue.substring(0, parsedValue.length - 1);
}
if (parsedValue.startsWith('"') && parsedValue.endsWith('"')) {
parsedValue = parsedValue.substring(1, parsedValue.length - 1);
}

if (!keys.includes(parsedKey)) {
throw new BoxCLIError(
`Invalid key '${parsedKey}'. Valid keys are ${keys.join(', ')}`
);
}

result[parsedKey] = parsedValue;
inputString = inputString.substring(nextKeyIndex);
}
return result;
}

/**
* Check if directory exists and creates it if shouldCreate flag was passed.
*
Expand Down Expand Up @@ -343,6 +387,7 @@ module.exports = {
parseMetadataOp(value) {
return parseMetadataString(value);
},
parseStringToObject,
checkDir,
readFileAsync,
writeFileAsync,
Expand Down
Loading

0 comments on commit 59551d2

Please sign in to comment.