Skip to content

Commit

Permalink
feat: Support AI API (#1243)
Browse files Browse the repository at this point in the history
Closes: SDK-3736
  • Loading branch information
lukaszsocha2 authored Apr 30, 2024
1 parent 0c86487 commit 4e64f27
Show file tree
Hide file tree
Showing 12 changed files with 811 additions and 1 deletion.
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

0 comments on commit 4e64f27

Please sign in to comment.