Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support AI APIs using Box Node SDK #539

Merged
merged 9 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading