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 API #1243

Merged
merged 5 commits into from
Apr 30, 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: 0 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
branches:
- main
pull_request:
types: [ opened, synchronize, edited ]
branches:
- main

Expand Down
65 changes: 65 additions & 0 deletions doc/ai.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
AI
==

AI allows to send an intelligence request to supported large language models and returns
an answer based on the provided prompt and items.

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Send AI request](#send-ai-request)
- [Send AI text generation request](#send-ai-text-generation-request)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Send AI request
--------------------------

To send an AI request, call static
[`sendAIRequest(String prompt, List<BoxAIItem> items, Mode mode)`][send-ai-request] method.
In the request you have to provide a prompt, a list of items that your prompt refers to and a mode of the request.
There are two modes available: `SINGLE_ITEM_QA` and `MULTI_ITEM_QA`, which specifies if this request refers to
for a single or multiple items.

<!-- sample post_ai_ask -->
```java
BoxAIResponse response = BoxAI.sendAIRequest(
api,
"What is the content of the file?",
Collections.singletonList("123456", BoxAIItem.Type.FILE)),
BoxAI.Mode.SINGLE_ITEM_QA
);
```

NOTE: The AI endpoint may return a 412 status code if you use for your request a file which has just been updated to the box.
It usually takes a few seconds for the file to be indexed and available for the AI endpoint.

[send-ai-request]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#sendAIRequest-com.box.sdk.BoxAPIConnection-java.lang.String-

Send AI text generation request
--------------

To send an AI request specifically focused on the creation of new text, call static
[`sendAITextGenRequest(String prompt, List<BoxAIItem> items, List<BoxAIDialogueEntry> dialogueHistory)`][send-ai-text-gen-request] method.
In the request you have to provide a prompt, a list of items that your prompt refers to and optionally a dialogue history,
which provides additional context to the LLM in generating the response.

<!-- sample post_ai_text_gen -->
```java
List<BoxAIDialogueEntry> dialogueHistory = new ArrayList<>();
dialogueHistory.add(
new BoxAIDialogueEntry(
"Make my email about public APIs sound more professional",
"Here is the first draft of your professional email about public APIs.",
BoxDateFormat.parse("2013-05-16T15:26:57-07:00")
)
);
BoxAIResponse response = BoxAI.sendAITextGenRequest(
api,
"Write an email to a client about the importance of public APIs.",
Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)),
dialogueHistory
);
```

[send-ai-text-gen-request]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#sendAITextGenRequest-com.box.sdk.BoxAPIConnection-java.lang.String-
144 changes: 144 additions & 0 deletions src/intTest/java/com/box/sdk/BoxAIIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.box.sdk;

import static com.box.sdk.BoxApiProvider.jwtApiForServiceAccount;
import static com.box.sdk.CleanupTools.deleteFile;
import static com.box.sdk.Retry.retry;
import static com.box.sdk.UniqueTestFolder.removeUniqueFolder;
import static com.box.sdk.UniqueTestFolder.setupUniqeFolder;
import static com.box.sdk.UniqueTestFolder.uploadFileToUniqueFolder;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;


/**
* {@link BoxGroup} related integration tests.
*/
public class BoxAIIT {

@BeforeClass
public static void setup() {
setupUniqeFolder();
}

@AfterClass
public static void afterClass() {
removeUniqueFolder();
}


@Test
public void askAISingleItem() throws InterruptedException {
BoxAPIConnection api = jwtApiForServiceAccount();
String fileName = "[askAISingleItem] Test File.txt";
BoxFile uploadedFile = uploadFileToUniqueFolder(api, fileName, "Test file");

try {
BoxFile.Info uploadedFileInfo = uploadedFile.getInfo();
// When a file has been just uploaded, AI service may not be ready to return text response
// and 412 is returned
retry(() -> {
BoxAIResponse response = BoxAI.sendAIRequest(
api,
"What is the name of the file?",
Collections.singletonList(new BoxAIItem(uploadedFileInfo.getID(), BoxAIItem.Type.FILE)),
BoxAI.Mode.SINGLE_ITEM_QA
);
assertThat(response.getAnswer(), containsString("Test file"));
assert response.getCreatedAt().before(new Date(System.currentTimeMillis()));
assertThat(response.getCompletionReason(), equalTo("done"));
}, 2, 2000);

} finally {
deleteFile(uploadedFile);
}
}

@Test
public void askAIMultipleItems() throws InterruptedException {
BoxAPIConnection api = jwtApiForServiceAccount();
String fileName1 = "[askAIMultipleItems] Test File.txt";
BoxFile uploadedFile1 = uploadFileToUniqueFolder(api, fileName1, "Test file");

try {
String fileName2 = "[askAIMultipleItems] Weather forecast.txt";
BoxFile uploadedFile2 = uploadFileToUniqueFolder(api, fileName2, "Test file");

try {
BoxFile.Info uploadedFileInfo1 = uploadedFile1.getInfo();
BoxFile.Info uploadedFileInfo2 = uploadedFile2.getInfo();

List<BoxAIItem> items = new ArrayList<>();
items.add(new BoxAIItem(uploadedFileInfo1.getID(), BoxAIItem.Type.FILE));
items.add(new BoxAIItem(uploadedFileInfo2.getID(), BoxAIItem.Type.FILE));

// When a file has been just uploaded, AI service may not be ready to return text response
// and 412 is returned
retry(() -> {
BoxAIResponse response = BoxAI.sendAIRequest(
api,
"What is the content of these files?",
items,
BoxAI.Mode.MULTIPLE_ITEM_QA
);
assertThat(response.getAnswer(), containsString("Test file"));
assert response.getCreatedAt().before(new Date(System.currentTimeMillis()));
assertThat(response.getCompletionReason(), equalTo("done"));
}, 2, 2000);
} finally {
deleteFile(uploadedFile2);
}

} finally {
deleteFile(uploadedFile1);
}
}

@Test
public void askAITextGenItemWithDialogueHistory() throws ParseException, InterruptedException {
BoxAPIConnection api = jwtApiForServiceAccount();
String fileName = "[askAITextGenItemWithDialogueHistory] Test File.txt";
Date date1 = BoxDateFormat.parse("2013-05-16T15:27:57-07:00");
Date date2 = BoxDateFormat.parse("2013-05-16T15:26:57-07:00");

BoxFile uploadedFile = uploadFileToUniqueFolder(api, fileName, "Test file");
try {
// When a file has been just uploaded, AI service may not be ready to return text response
// and 412 is returned
retry(() -> {
BoxFile.Info uploadedFileInfo = uploadedFile.getInfo();
assertThat(uploadedFileInfo.getName(), is(equalTo(fileName)));

List<BoxAIDialogueEntry> dialogueHistory = new ArrayList<>();
dialogueHistory.add(
new BoxAIDialogueEntry("What is the name of the file?", "Test file", date1)
);
dialogueHistory.add(
new BoxAIDialogueEntry("What is the size of the file?", "10kb", date2)
);
BoxAIResponse response = BoxAI.sendAITextGenRequest(
api,
"What is the name of the file?",
Collections.singletonList(new BoxAIItem(uploadedFileInfo.getID(), BoxAIItem.Type.FILE)),
dialogueHistory
);
assertThat(response.getAnswer(), containsString("name"));
assert response.getCreatedAt().before(new Date(System.currentTimeMillis()));
assertThat(response.getCompletionReason(), equalTo("done"));
}, 2, 2000);

} finally {
deleteFile(uploadedFile);
}
}
}
3 changes: 3 additions & 0 deletions src/intTest/java/com/box/sdk/Retry.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public static void retry(Runnable toExecute, int retries, int sleep) throws Inte
break;
} catch (Exception e) {
retriesExecuted++;
if (retriesExecuted >= retries) {
throw e;
}
LOGGER.debug(
format("Retrying [%d/%d] becasue of Exception '%s'", retriesExecuted, retries, e.getMessage())
);
Expand Down
141 changes: 141 additions & 0 deletions src/main/java/com/box/sdk/BoxAI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.box.sdk;

import com.box.sdk.http.HttpMethod;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import java.net.URL;
import java.util.List;


public final class BoxAI {

/**
* Ask AI url.
*/
public static final URLTemplate SEND_AI_REQUEST_URL = new URLTemplate("ai/ask");
/**
* Text gen AI url.
*/
public static final URLTemplate SEND_AI_TEXT_GEN_REQUEST_URL = new URLTemplate("ai/text_gen");

private BoxAI() {
}

/**
* Sends an AI request to supported LLMs and returns an answer specifically focused
* on the user's question given the provided items.
* @param api the API connection to be used by the created user.
* @param prompt The prompt provided by the client to be answered by the LLM.
* @param items The items to be processed by the LLM, currently only files are supported.
* @param mode The mode specifies if this request is for a single or multiple items.
* @return The response from the AI.
*/
public static BoxAIResponse sendAIRequest(BoxAPIConnection api, String prompt, List<BoxAIItem> items, Mode mode) {
URL url = SEND_AI_REQUEST_URL.build(api.getBaseURL());
JsonObject requestJSON = new JsonObject();
requestJSON.add("mode", mode.toString());
requestJSON.add("prompt", prompt);

JsonArray itemsJSON = new JsonArray();
for (BoxAIItem item : items) {
itemsJSON.add(item.getJSONObject());
}
requestJSON.add("items", itemsJSON);

BoxJSONRequest req = new BoxJSONRequest(api, url, HttpMethod.POST);
req.setBody(requestJSON.toString());

try (BoxJSONResponse response = req.send()) {
JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
return new BoxAIResponse(responseJSON);
}
}

/**
* Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text.
* @param api the API connection to be used by the created user.
* @param prompt The prompt provided by the client to be answered by the LLM.
* @param items The items to be processed by the LLM, currently only files are supported.
* @return The response from the AI.
*/
public static BoxAIResponse sendAITextGenRequest(BoxAPIConnection api, String prompt, List<BoxAIItem> items) {
return sendAITextGenRequest(api, prompt, items, null);
}

/**
* Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text.
* @param api the API connection to be used by the created user.
* @param prompt The prompt provided by the client to be answered by the LLM.
* @param items The items to be processed by the LLM, currently only files are supported.
* @param dialogueHistory The history of prompts and answers previously passed to the LLM.
* This provides additional context to the LLM in generating the response.
* @return The response from the AI.
*/
public static BoxAIResponse sendAITextGenRequest(
BoxAPIConnection api, String prompt, List<BoxAIItem> items, List<BoxAIDialogueEntry> dialogueHistory
) {
URL url = SEND_AI_TEXT_GEN_REQUEST_URL.build(api.getBaseURL());
JsonObject requestJSON = new JsonObject();
requestJSON.add("prompt", prompt);

JsonArray itemsJSON = new JsonArray();
for (BoxAIItem item : items) {
itemsJSON.add(item.getJSONObject());
}
requestJSON.add("items", itemsJSON);

if (dialogueHistory != null) {
JsonArray dialogueHistoryJSON = new JsonArray();
for (BoxAIDialogueEntry dialogueEntry : dialogueHistory) {
dialogueHistoryJSON.add(dialogueEntry.getJSONObject());
}
requestJSON.add("dialogue_history", dialogueHistoryJSON);
}

BoxJSONRequest req = new BoxJSONRequest(api, url, HttpMethod.POST);
req.setBody(requestJSON.toString());

try (BoxJSONResponse response = req.send()) {
JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
return new BoxAIResponse(responseJSON);
}
}

public enum Mode {
/**
* Multiple items
*/
MULTIPLE_ITEM_QA("multiple_item_qa"),

/**
* Single item
*/
SINGLE_ITEM_QA("single_item_qa");

private final String mode;

Mode(String mode) {
this.mode = mode;
}

static BoxAI.Mode fromJSONValue(String jsonValue) {
if (jsonValue.equals("multiple_item_qa")) {
return BoxAI.Mode.MULTIPLE_ITEM_QA;
} else if (jsonValue.equals("single_item_qa")) {
return BoxAI.Mode.SINGLE_ITEM_QA;
} else {
System.out.print("Invalid AI mode.");
return null;
}
}

String toJSONValue() {
return this.mode;
}

public String toString() {
return this.mode;
}
}
}
Loading
Loading