Skip to content

Commit 4e64f27

Browse files
authored
feat: Support AI API (#1243)
Closes: SDK-3736
1 parent 0c86487 commit 4e64f27

File tree

12 files changed

+811
-1
lines changed

12 files changed

+811
-1
lines changed

.github/workflows/integration-tests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ on:
55
branches:
66
- main
77
pull_request:
8-
types: [ opened, synchronize, edited ]
98
branches:
109
- main
1110

doc/ai.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
AI
2+
==
3+
4+
AI allows to send an intelligence request to supported large language models and returns
5+
an answer based on the provided prompt and items.
6+
7+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
8+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
9+
10+
- [Send AI request](#send-ai-request)
11+
- [Send AI text generation request](#send-ai-text-generation-request)
12+
13+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
14+
15+
Send AI request
16+
--------------------------
17+
18+
To send an AI request, call static
19+
[`sendAIRequest(String prompt, List<BoxAIItem> items, Mode mode)`][send-ai-request] method.
20+
In the request you have to provide a prompt, a list of items that your prompt refers to and a mode of the request.
21+
There are two modes available: `SINGLE_ITEM_QA` and `MULTI_ITEM_QA`, which specifies if this request refers to
22+
for a single or multiple items.
23+
24+
<!-- sample post_ai_ask -->
25+
```java
26+
BoxAIResponse response = BoxAI.sendAIRequest(
27+
api,
28+
"What is the content of the file?",
29+
Collections.singletonList("123456", BoxAIItem.Type.FILE)),
30+
BoxAI.Mode.SINGLE_ITEM_QA
31+
);
32+
```
33+
34+
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.
35+
It usually takes a few seconds for the file to be indexed and available for the AI endpoint.
36+
37+
[send-ai-request]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#sendAIRequest-com.box.sdk.BoxAPIConnection-java.lang.String-
38+
39+
Send AI text generation request
40+
--------------
41+
42+
To send an AI request specifically focused on the creation of new text, call static
43+
[`sendAITextGenRequest(String prompt, List<BoxAIItem> items, List<BoxAIDialogueEntry> dialogueHistory)`][send-ai-text-gen-request] method.
44+
In the request you have to provide a prompt, a list of items that your prompt refers to and optionally a dialogue history,
45+
which provides additional context to the LLM in generating the response.
46+
47+
<!-- sample post_ai_text_gen -->
48+
```java
49+
List<BoxAIDialogueEntry> dialogueHistory = new ArrayList<>();
50+
dialogueHistory.add(
51+
new BoxAIDialogueEntry(
52+
"Make my email about public APIs sound more professional",
53+
"Here is the first draft of your professional email about public APIs.",
54+
BoxDateFormat.parse("2013-05-16T15:26:57-07:00")
55+
)
56+
);
57+
BoxAIResponse response = BoxAI.sendAITextGenRequest(
58+
api,
59+
"Write an email to a client about the importance of public APIs.",
60+
Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)),
61+
dialogueHistory
62+
);
63+
```
64+
65+
[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-
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.box.sdk;
2+
3+
import static com.box.sdk.BoxApiProvider.jwtApiForServiceAccount;
4+
import static com.box.sdk.CleanupTools.deleteFile;
5+
import static com.box.sdk.Retry.retry;
6+
import static com.box.sdk.UniqueTestFolder.removeUniqueFolder;
7+
import static com.box.sdk.UniqueTestFolder.setupUniqeFolder;
8+
import static com.box.sdk.UniqueTestFolder.uploadFileToUniqueFolder;
9+
import static org.hamcrest.MatcherAssert.assertThat;
10+
import static org.hamcrest.Matchers.containsString;
11+
import static org.hamcrest.Matchers.equalTo;
12+
import static org.hamcrest.Matchers.is;
13+
14+
import java.text.ParseException;
15+
import java.util.ArrayList;
16+
import java.util.Collections;
17+
import java.util.Date;
18+
import java.util.List;
19+
import org.junit.AfterClass;
20+
import org.junit.BeforeClass;
21+
import org.junit.Test;
22+
23+
24+
/**
25+
* {@link BoxGroup} related integration tests.
26+
*/
27+
public class BoxAIIT {
28+
29+
@BeforeClass
30+
public static void setup() {
31+
setupUniqeFolder();
32+
}
33+
34+
@AfterClass
35+
public static void afterClass() {
36+
removeUniqueFolder();
37+
}
38+
39+
40+
@Test
41+
public void askAISingleItem() throws InterruptedException {
42+
BoxAPIConnection api = jwtApiForServiceAccount();
43+
String fileName = "[askAISingleItem] Test File.txt";
44+
BoxFile uploadedFile = uploadFileToUniqueFolder(api, fileName, "Test file");
45+
46+
try {
47+
BoxFile.Info uploadedFileInfo = uploadedFile.getInfo();
48+
// When a file has been just uploaded, AI service may not be ready to return text response
49+
// and 412 is returned
50+
retry(() -> {
51+
BoxAIResponse response = BoxAI.sendAIRequest(
52+
api,
53+
"What is the name of the file?",
54+
Collections.singletonList(new BoxAIItem(uploadedFileInfo.getID(), BoxAIItem.Type.FILE)),
55+
BoxAI.Mode.SINGLE_ITEM_QA
56+
);
57+
assertThat(response.getAnswer(), containsString("Test file"));
58+
assert response.getCreatedAt().before(new Date(System.currentTimeMillis()));
59+
assertThat(response.getCompletionReason(), equalTo("done"));
60+
}, 2, 2000);
61+
62+
} finally {
63+
deleteFile(uploadedFile);
64+
}
65+
}
66+
67+
@Test
68+
public void askAIMultipleItems() throws InterruptedException {
69+
BoxAPIConnection api = jwtApiForServiceAccount();
70+
String fileName1 = "[askAIMultipleItems] Test File.txt";
71+
BoxFile uploadedFile1 = uploadFileToUniqueFolder(api, fileName1, "Test file");
72+
73+
try {
74+
String fileName2 = "[askAIMultipleItems] Weather forecast.txt";
75+
BoxFile uploadedFile2 = uploadFileToUniqueFolder(api, fileName2, "Test file");
76+
77+
try {
78+
BoxFile.Info uploadedFileInfo1 = uploadedFile1.getInfo();
79+
BoxFile.Info uploadedFileInfo2 = uploadedFile2.getInfo();
80+
81+
List<BoxAIItem> items = new ArrayList<>();
82+
items.add(new BoxAIItem(uploadedFileInfo1.getID(), BoxAIItem.Type.FILE));
83+
items.add(new BoxAIItem(uploadedFileInfo2.getID(), BoxAIItem.Type.FILE));
84+
85+
// When a file has been just uploaded, AI service may not be ready to return text response
86+
// and 412 is returned
87+
retry(() -> {
88+
BoxAIResponse response = BoxAI.sendAIRequest(
89+
api,
90+
"What is the content of these files?",
91+
items,
92+
BoxAI.Mode.MULTIPLE_ITEM_QA
93+
);
94+
assertThat(response.getAnswer(), containsString("Test file"));
95+
assert response.getCreatedAt().before(new Date(System.currentTimeMillis()));
96+
assertThat(response.getCompletionReason(), equalTo("done"));
97+
}, 2, 2000);
98+
} finally {
99+
deleteFile(uploadedFile2);
100+
}
101+
102+
} finally {
103+
deleteFile(uploadedFile1);
104+
}
105+
}
106+
107+
@Test
108+
public void askAITextGenItemWithDialogueHistory() throws ParseException, InterruptedException {
109+
BoxAPIConnection api = jwtApiForServiceAccount();
110+
String fileName = "[askAITextGenItemWithDialogueHistory] Test File.txt";
111+
Date date1 = BoxDateFormat.parse("2013-05-16T15:27:57-07:00");
112+
Date date2 = BoxDateFormat.parse("2013-05-16T15:26:57-07:00");
113+
114+
BoxFile uploadedFile = uploadFileToUniqueFolder(api, fileName, "Test file");
115+
try {
116+
// When a file has been just uploaded, AI service may not be ready to return text response
117+
// and 412 is returned
118+
retry(() -> {
119+
BoxFile.Info uploadedFileInfo = uploadedFile.getInfo();
120+
assertThat(uploadedFileInfo.getName(), is(equalTo(fileName)));
121+
122+
List<BoxAIDialogueEntry> dialogueHistory = new ArrayList<>();
123+
dialogueHistory.add(
124+
new BoxAIDialogueEntry("What is the name of the file?", "Test file", date1)
125+
);
126+
dialogueHistory.add(
127+
new BoxAIDialogueEntry("What is the size of the file?", "10kb", date2)
128+
);
129+
BoxAIResponse response = BoxAI.sendAITextGenRequest(
130+
api,
131+
"What is the name of the file?",
132+
Collections.singletonList(new BoxAIItem(uploadedFileInfo.getID(), BoxAIItem.Type.FILE)),
133+
dialogueHistory
134+
);
135+
assertThat(response.getAnswer(), containsString("name"));
136+
assert response.getCreatedAt().before(new Date(System.currentTimeMillis()));
137+
assertThat(response.getCompletionReason(), equalTo("done"));
138+
}, 2, 2000);
139+
140+
} finally {
141+
deleteFile(uploadedFile);
142+
}
143+
}
144+
}

src/intTest/java/com/box/sdk/Retry.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public static void retry(Runnable toExecute, int retries, int sleep) throws Inte
2727
break;
2828
} catch (Exception e) {
2929
retriesExecuted++;
30+
if (retriesExecuted >= retries) {
31+
throw e;
32+
}
3033
LOGGER.debug(
3134
format("Retrying [%d/%d] becasue of Exception '%s'", retriesExecuted, retries, e.getMessage())
3235
);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.box.sdk;
2+
3+
import com.box.sdk.http.HttpMethod;
4+
import com.eclipsesource.json.Json;
5+
import com.eclipsesource.json.JsonArray;
6+
import com.eclipsesource.json.JsonObject;
7+
import java.net.URL;
8+
import java.util.List;
9+
10+
11+
public final class BoxAI {
12+
13+
/**
14+
* Ask AI url.
15+
*/
16+
public static final URLTemplate SEND_AI_REQUEST_URL = new URLTemplate("ai/ask");
17+
/**
18+
* Text gen AI url.
19+
*/
20+
public static final URLTemplate SEND_AI_TEXT_GEN_REQUEST_URL = new URLTemplate("ai/text_gen");
21+
22+
private BoxAI() {
23+
}
24+
25+
/**
26+
* Sends an AI request to supported LLMs and returns an answer specifically focused
27+
* on the user's question given the provided items.
28+
* @param api the API connection to be used by the created user.
29+
* @param prompt The prompt provided by the client to be answered by the LLM.
30+
* @param items The items to be processed by the LLM, currently only files are supported.
31+
* @param mode The mode specifies if this request is for a single or multiple items.
32+
* @return The response from the AI.
33+
*/
34+
public static BoxAIResponse sendAIRequest(BoxAPIConnection api, String prompt, List<BoxAIItem> items, Mode mode) {
35+
URL url = SEND_AI_REQUEST_URL.build(api.getBaseURL());
36+
JsonObject requestJSON = new JsonObject();
37+
requestJSON.add("mode", mode.toString());
38+
requestJSON.add("prompt", prompt);
39+
40+
JsonArray itemsJSON = new JsonArray();
41+
for (BoxAIItem item : items) {
42+
itemsJSON.add(item.getJSONObject());
43+
}
44+
requestJSON.add("items", itemsJSON);
45+
46+
BoxJSONRequest req = new BoxJSONRequest(api, url, HttpMethod.POST);
47+
req.setBody(requestJSON.toString());
48+
49+
try (BoxJSONResponse response = req.send()) {
50+
JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
51+
return new BoxAIResponse(responseJSON);
52+
}
53+
}
54+
55+
/**
56+
* Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text.
57+
* @param api the API connection to be used by the created user.
58+
* @param prompt The prompt provided by the client to be answered by the LLM.
59+
* @param items The items to be processed by the LLM, currently only files are supported.
60+
* @return The response from the AI.
61+
*/
62+
public static BoxAIResponse sendAITextGenRequest(BoxAPIConnection api, String prompt, List<BoxAIItem> items) {
63+
return sendAITextGenRequest(api, prompt, items, null);
64+
}
65+
66+
/**
67+
* Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text.
68+
* @param api the API connection to be used by the created user.
69+
* @param prompt The prompt provided by the client to be answered by the LLM.
70+
* @param items The items to be processed by the LLM, currently only files are supported.
71+
* @param dialogueHistory The history of prompts and answers previously passed to the LLM.
72+
* This provides additional context to the LLM in generating the response.
73+
* @return The response from the AI.
74+
*/
75+
public static BoxAIResponse sendAITextGenRequest(
76+
BoxAPIConnection api, String prompt, List<BoxAIItem> items, List<BoxAIDialogueEntry> dialogueHistory
77+
) {
78+
URL url = SEND_AI_TEXT_GEN_REQUEST_URL.build(api.getBaseURL());
79+
JsonObject requestJSON = new JsonObject();
80+
requestJSON.add("prompt", prompt);
81+
82+
JsonArray itemsJSON = new JsonArray();
83+
for (BoxAIItem item : items) {
84+
itemsJSON.add(item.getJSONObject());
85+
}
86+
requestJSON.add("items", itemsJSON);
87+
88+
if (dialogueHistory != null) {
89+
JsonArray dialogueHistoryJSON = new JsonArray();
90+
for (BoxAIDialogueEntry dialogueEntry : dialogueHistory) {
91+
dialogueHistoryJSON.add(dialogueEntry.getJSONObject());
92+
}
93+
requestJSON.add("dialogue_history", dialogueHistoryJSON);
94+
}
95+
96+
BoxJSONRequest req = new BoxJSONRequest(api, url, HttpMethod.POST);
97+
req.setBody(requestJSON.toString());
98+
99+
try (BoxJSONResponse response = req.send()) {
100+
JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
101+
return new BoxAIResponse(responseJSON);
102+
}
103+
}
104+
105+
public enum Mode {
106+
/**
107+
* Multiple items
108+
*/
109+
MULTIPLE_ITEM_QA("multiple_item_qa"),
110+
111+
/**
112+
* Single item
113+
*/
114+
SINGLE_ITEM_QA("single_item_qa");
115+
116+
private final String mode;
117+
118+
Mode(String mode) {
119+
this.mode = mode;
120+
}
121+
122+
static BoxAI.Mode fromJSONValue(String jsonValue) {
123+
if (jsonValue.equals("multiple_item_qa")) {
124+
return BoxAI.Mode.MULTIPLE_ITEM_QA;
125+
} else if (jsonValue.equals("single_item_qa")) {
126+
return BoxAI.Mode.SINGLE_ITEM_QA;
127+
} else {
128+
System.out.print("Invalid AI mode.");
129+
return null;
130+
}
131+
}
132+
133+
String toJSONValue() {
134+
return this.mode;
135+
}
136+
137+
public String toString() {
138+
return this.mode;
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)