From 468c98ab5a580e7dcd5f0c19d167bf364ab93af0 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Mon, 7 Oct 2024 13:12:58 +0200 Subject: [PATCH 1/8] feat: Support AI extract and AI extract structured --- doc/ai.md | 48 ++++- src/intTest/java/com/box/sdk/BoxAIIT.java | 171 ++++++++++++++++-- src/main/java/com/box/sdk/BoxAI.java | 81 +++++++++ src/main/java/com/box/sdk/BoxAIAgent.java | 23 ++- .../java/com/box/sdk/BoxAIAgentExtract.java | 109 +++++++++++ .../box/sdk/BoxAIAgentExtractStructured.java | 109 +++++++++++ .../BoxAIAgentLLMEndpointParamsGoogle.java | 20 +- .../BoxAIAgentLLMEndpointParamsOpenAI.java | 32 ++-- .../java/com/box/sdk/BoxAIExtractField.java | 74 ++++++++ .../box/sdk/BoxAIExtractMetadataTemplate.java | 56 ++++++ .../BoxAI/ExtractMetadataFreeform200.json | 5 + .../BoxAI/ExtractMetadataStructured200.json | 9 + .../GetAIAgentDefaultConfigExtract200.json | 38 ++++ ...gentDefaultConfigExtractStructured200.json | 38 ++++ src/test/java/com/box/sdk/BoxAITest.java | 127 +++++++++++++ 15 files changed, 895 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/box/sdk/BoxAIAgentExtract.java create mode 100644 src/main/java/com/box/sdk/BoxAIAgentExtractStructured.java create mode 100644 src/main/java/com/box/sdk/BoxAIExtractField.java create mode 100644 src/main/java/com/box/sdk/BoxAIExtractMetadataTemplate.java create mode 100644 src/test/Fixtures/BoxAI/ExtractMetadataFreeform200.json create mode 100644 src/test/Fixtures/BoxAI/ExtractMetadataStructured200.json create mode 100644 src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtract200.json create mode 100644 src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtractStructured200.json diff --git a/doc/ai.md b/doc/ai.md index e51f93468..4ed501842 100644 --- a/doc/ai.md +++ b/doc/ai.md @@ -87,4 +87,50 @@ BoxAIAgentConfig config = BoxAI.getAiAgentDefaultConfig( ); ``` -[get-ai-agent-default-config]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#getAiAgentDefaultConfig-com.box.sdk.BoxAPIConnection-com.box.sdk.ai.BoxAIAgent.Mode-java.lang.String-java.lang.String- \ No newline at end of file +[get-ai-agent-default-config]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#getAiAgentDefaultConfig-com.box.sdk.BoxAPIConnection-com.box.sdk.ai.BoxAIAgent.Mode-java.lang.String-java.lang.String- + +Extract metadata freeform +-------------------------- + +To send an AI request to supported Large Language Models (LLMs) and extracts metadata in form of key-value pairs, call static +[` extractMetadataFreeform(BoxAPIConnection api, String prompt, List items, BoxAIAgentExtract agent)`][extract-metadata-freeform] method. +In the request you have to provide a prompt, a list of items that your prompt refers to and an optional agent configuration. + + +```java +BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT, "en-US", null); +BoxAIAgentExtract agentExtract = (BoxAIAgentExtract) agent; + +BoxAIResponse response = BoxAI.extractMetadataFreeform( + api, + "What is the content of the file?", + Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), + agentExtract +); +``` + +[extract-metadata-freeform]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataFreeform-com.box.sdk.BoxAPIConnection-java.lang.String-java.util.List-com.box.sdk.ai.BoxAIAgentExtract- + +Extract metadata structured +-------------------------- + +Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of key-value pairs. For this request, you need to use an already defined metadata template or a define a schema yourself. +To send an AI request to extract metadata from files, call static +[`extractMetadataStructured extractMetadataStructured(BoxAPIConnection api, List items, BoxAIExtractMetadataTemplate template, List fields, BoxAIAgentExtractStructured agent)`][extract-metadata-structured] method. + + +```java +BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT_STRUCTURED, "en-US", null); +BoxAIAgentExtractStructured agentExtractStructured = (BoxAIAgentExtractStructured) agent; +BoxAIExtractMetadataTemplate template = new BoxAIExtractMetadataTemplate("templateKey", "enterprise"); + +JsonObject result = BoxAI.extractMetadataStructured( + api, + Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), + template, + null, + agentExtractStructured +); +``` + +[extract-metadata-structured]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-com.box.sdk.ai.BoxAIExtractMetadataTemplate-java.util.List-com.box.sdk.ai.BoxAIAgentExtractStructured- \ No newline at end of file diff --git a/src/intTest/java/com/box/sdk/BoxAIIT.java b/src/intTest/java/com/box/sdk/BoxAIIT.java index b0c137aef..dd5afdfa2 100644 --- a/src/intTest/java/com/box/sdk/BoxAIIT.java +++ b/src/intTest/java/com/box/sdk/BoxAIIT.java @@ -11,6 +11,8 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; @@ -49,10 +51,10 @@ public void askAISingleItem() throws InterruptedException { // 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 + 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())); @@ -86,10 +88,10 @@ public void askAIMultipleItems() throws InterruptedException { // and 412 is returned retry(() -> { BoxAIResponse response = BoxAI.sendAIRequest( - api, - "What is the content of these files?", - items, - BoxAI.Mode.MULTIPLE_ITEM_QA + 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())); @@ -111,7 +113,7 @@ public void askAITextGenItemWithDialogueHistory() throws ParseException, Interru 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"); + 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 @@ -121,16 +123,16 @@ public void askAITextGenItemWithDialogueHistory() throws ParseException, Interru List dialogueHistory = new ArrayList<>(); dialogueHistory.add( - new BoxAIDialogueEntry("What is the name of the file?", "Test file", date1) + 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) + 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 + 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())); @@ -192,4 +194,143 @@ public void askAISingleItemWithAgent() throws InterruptedException { deleteFile(uploadedFile); } } + + @Test + public void aiExtract() throws InterruptedException { + BoxAPIConnection api = jwtApiForServiceAccount(); + BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT, "en-US", null); + BoxAIAgentExtract agentExtract = (BoxAIAgentExtract) agent; + + BoxFile uploadedFile = uploadFileToUniqueFolder(api, "[aiExtract] Test File.txt", + "My name is John Doe. I live in San Francisco. I was born in 1990. I work at Box."); + + try { + // 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.extractMetadataFreeform(api, + "firstName, lastName, location, yearOfBirth, company", + Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), + agentExtract); + assertThat(response.getAnswer(), containsString("John")); + assertThat(response.getCompletionReason(), equalTo("done")); + }, 2, 2000); + } finally { + deleteFile(uploadedFile); + } + } + + @Test + public void aiExtractStructuredWithFields() throws InterruptedException { + BoxAPIConnection api = jwtApiForServiceAccount(); + BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT_STRUCTURED, "en-US", null); + BoxAIAgentExtractStructured agentExtractStructured = (BoxAIAgentExtractStructured) agent; + + BoxFile uploadedFile = uploadFileToUniqueFolder(api, "[aiExtractStructuredWithFields] Test File.txt", + "My name is John Doe. I was born in 4th July 1990. I am 34 years old. My hobby is guitar and books."); + + try { + // When a file has been just uploaded, AI service may not be ready to return text response + // and 412 is returned + retry(() -> { + JsonObject response = BoxAI.extractMetadataStructured(api, + Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), + null, + new ArrayList() {{ + add(new BoxAIExtractField("string", + "Person first name", + "First name", + "firstName", + null, + "What is the your first name?")); + add(new BoxAIExtractField("string", + "Person last name", "Last name", "lastName", null, "What is the your last name?")); + add(new BoxAIExtractField("date", + "Person date of birth", + "Birth date", + "dateOfBirth", + null, + "What is the date of your birth?")); + add(new BoxAIExtractField("float", + "Person age", + "Age", + "age", + null, + "How old are you?")); + add(new BoxAIExtractField("multiSelect", + "Person hobby", + "Hobby", + "hobby", + new ArrayList() {{ + add("guitar"); + add("books"); + }}, + "What is your hobby?")); + }}, + agentExtractStructured); + assertThat(response.get("firstName").asString(), is(equalTo("John"))); + assertThat(response.get("lastName").asString(), is(equalTo("Doe"))); + assertThat(response.get("dateOfBirth").asString(), is(equalTo("1990-07-04"))); + assertThat(response.get("age").asInt(), is(equalTo(34))); + assertThat(response.get("hobby").asArray().get(0).asString(), is(equalTo("guitar"))); + assertThat(response.get("hobby").asArray().get(1).asString(), is(equalTo("books"))); + }, 2, 2000); + } finally { + deleteFile(uploadedFile); + } + } + + @Test + public void aiExtractStructuredWithMetadataTemplate() throws InterruptedException { + BoxAPIConnection api = jwtApiForServiceAccount(); + BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT_STRUCTURED, "en-US", null); + BoxAIAgentExtractStructured agentExtractStructured = (BoxAIAgentExtractStructured) agent; + + BoxFile uploadedFile = uploadFileToUniqueFolder(api, "[aiExtractStructuredWithMetadataTemplate] Test File.txt", + "My name is John Doe. I was born in 4th July 1990. I am 34 years old. My hobby is guitar and books."); + String templateKey = "key" + java.util.UUID.randomUUID().toString(); + MetadataTemplate template = MetadataTemplate.createMetadataTemplate(api, + "enterprise", + templateKey, + templateKey, + false, + new ArrayList() {{ + add(new MetadataTemplate.Field(Json.parse( + "{\"key\":\"firstName\",\"displayName\":\"First name\"," + + "\"description\":\"Person first name\",\"type\":\"string\"}").asObject())); + add(new MetadataTemplate.Field(Json.parse( + "{\"key\":\"lastName\",\"displayName\":\"Last name\"," + + "\"description\":\"Person last name\",\"type\":\"string\"}").asObject())); + add(new MetadataTemplate.Field(Json.parse( + "{\"key\":\"dateOfBirth\",\"displayName\":\"Birth date\"," + + "\"description\":\"Person date of birth\",\"type\":\"date\"}").asObject())); + add(new MetadataTemplate.Field(Json.parse( + "{\"key\":\"age\",\"displayName\":\"Age\"," + + "\"description\":\"Person age\",\"type\":\"float\"}").asObject())); + add(new MetadataTemplate.Field(Json.parse( + "{\"key\":\"hobby\",\"displayName\":\"Hobby\"," + + "\"description\":\"Person hobby\",\"type\":\"multiSelect\"}").asObject())); + }}); + + try { + // When a file has been just uploaded, AI service may not be ready to return text response + // and 412 is returned + retry(() -> { + JsonObject response = BoxAI.extractMetadataStructured(api, + Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), + new BoxAIExtractMetadataTemplate(templateKey, "enterprise"), + null, + agentExtractStructured); + assertThat(response.get("firstName").asString(), is(equalTo("John"))); + assertThat(response.get("lastName").asString(), is(equalTo("Doe"))); + assertThat(response.get("dateOfBirth").asString(), is(equalTo("1990-07-04"))); + assertThat(response.get("age").asInt(), is(equalTo(34))); + assertThat(response.get("hobby").asArray().get(0).asString(), is(equalTo("guitar"))); + assertThat(response.get("hobby").asArray().get(1).asString(), is(equalTo("books"))); + }, 2, 2000); + } finally { + deleteFile(uploadedFile); + MetadataTemplate.deleteMetadataTemplate(api, template.getScope(), template.getTemplateKey()); + } + } } diff --git a/src/main/java/com/box/sdk/BoxAI.java b/src/main/java/com/box/sdk/BoxAI.java index 0dc24fa53..32d1ac741 100644 --- a/src/main/java/com/box/sdk/BoxAI.java +++ b/src/main/java/com/box/sdk/BoxAI.java @@ -22,6 +22,14 @@ public final class BoxAI { * AI agent default config url. */ public static final URLTemplate AI_AGENT_DEFAULT_CONFIG_URL = new URLTemplate("ai_agent_default"); + /** + * AI extract metadata freeform url. + */ + public static final URLTemplate EXTRACT_METADATA_FREEFORM_URL = new URLTemplate("ai/extract"); + /** + * AI extract metadata structured url. + */ + public static final URLTemplate EXTRACT_METADATA_STRUCTURED_URL = new URLTemplate("ai/extract_structured"); private BoxAI() { } @@ -239,4 +247,77 @@ public String toString() { return this.mode; } } + + /** + * Sends an AI request to supported Large Language Models (LLMs) and extracts metadata in form of key-value pairs. + * Freeform metadata extraction does not require any metadata template setup before sending the request. + * + * @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 agent The AI agent configuration to be used for the request. + * @return The response from the AI. + */ + public static BoxAIResponse extractMetadataFreeform(BoxAPIConnection api, + String prompt, + List items, + BoxAIAgentExtract agent) { + URL url = EXTRACT_METADATA_FREEFORM_URL.build(api.getBaseURL()); + + JsonObject requestJSON = new JsonObject(); + JsonArray itemsJSON = new JsonArray(); + for (BoxAIItem item : items) { + itemsJSON.add(item.getJSONObject()); + } + requestJSON.add("items", itemsJSON); + requestJSON.add("prompt", prompt); + if (agent != null) { + requestJSON.add("ai_agent", agent.getJSONObject()); + } + + 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 static JsonObject extractMetadataStructured(BoxAPIConnection api, List items, + BoxAIExtractMetadataTemplate template, + List fields, + BoxAIAgentExtractStructured agent) { + URL url = EXTRACT_METADATA_STRUCTURED_URL.build(api.getBaseURL()); + + JsonObject requestJSON = new JsonObject(); + JsonArray itemsJSON = new JsonArray(); + for (BoxAIItem item : items) { + itemsJSON.add(item.getJSONObject()); + } + requestJSON.add("items", itemsJSON); + + if (template != null) { + requestJSON.add("metadata_template", template.getJSONObject()); + } + + if (fields != null) { + JsonArray fieldsJSON = new JsonArray(); + for (BoxAIExtractField field : fields) { + fieldsJSON.add(field.getJSONObject()); + } + requestJSON.add("fields", fieldsJSON); + } + + if (agent != null) { + requestJSON.add("ai_agent", agent.getJSONObject()); + } + + BoxJSONRequest req = new BoxJSONRequest(api, url, HttpMethod.POST); + req.setBody(requestJSON.toString()); + + try (BoxJSONResponse response = req.send()) { + return Json.parse(response.getJSON()).asObject(); + } + } } diff --git a/src/main/java/com/box/sdk/BoxAIAgent.java b/src/main/java/com/box/sdk/BoxAIAgent.java index 08315cefa..4d0c13ee4 100644 --- a/src/main/java/com/box/sdk/BoxAIAgent.java +++ b/src/main/java/com/box/sdk/BoxAIAgent.java @@ -6,14 +6,14 @@ public abstract class BoxAIAgent extends BoxJSONObject { /** * The type of the AI agent. - * Value can be "ai_agent_ask" or "ai_agent_text_gen". + * Value can be "ai_agent_ask" or "ai_agent_text_gen", "ai_agent_extract", "ai_agent_extract_structured". */ private String type; /** * Constructs an AI agent with default settings. * @param type The type of the AI agent. - * Value can be "ai_agent_ask" or "ai_agent_text_gen". + * Value can be "ai_agent_ask", "ai_agent_text_gen", "ai_agent_extract", "ai_agent_extract_structured". */ public BoxAIAgent(String type) { super(); @@ -38,6 +38,10 @@ public static BoxAIAgent parse(JsonObject jsonObject) { return new BoxAIAgentAsk(jsonObject); } else if (type.equals(BoxAIAgentTextGen.TYPE)) { return new BoxAIAgentTextGen(jsonObject); + } else if (type.equals(BoxAIAgentExtract.TYPE)) { + return new BoxAIAgentExtract(jsonObject); + } else if (type.equals(BoxAIAgentExtractStructured.TYPE)) { + return new BoxAIAgentExtractStructured(jsonObject); } else { throw new IllegalArgumentException("Invalid AI agent type: " + type); } @@ -86,7 +90,16 @@ public enum Mode { /** * The type of AI agent used for generating text. */ - TEXT_GEN("text_gen"); + TEXT_GEN("text_gen"), + /** + * The type of AI agent used for extracting metadata freeform. + */ + EXTRACT("extract"), + /** + * The type of AI agent used for extracting metadata structured. + */ + EXTRACT_STRUCTURED("extract_structured"); + private final String value; @@ -99,6 +112,10 @@ static BoxAIAgent.Mode fromJSONValue(String value) { return ASK; } else if (value.equals("text_gen")) { return TEXT_GEN; + } else if (value.equals("extract")) { + return EXTRACT; + } else if (value.equals("extract_structured")) { + return EXTRACT_STRUCTURED; } else { throw new IllegalArgumentException("Invalid AI agent mode: " + value); } diff --git a/src/main/java/com/box/sdk/BoxAIAgentExtract.java b/src/main/java/com/box/sdk/BoxAIAgentExtract.java new file mode 100644 index 000000000..04f0bf998 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxAIAgentExtract.java @@ -0,0 +1,109 @@ +package com.box.sdk; + +import com.box.sdk.internal.utils.JsonUtils; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +/** + * Represents an AI Agent used for extraction. + */ +@BoxResourceType("ai_agent_extract") +public class BoxAIAgentExtract extends BoxAIAgent { + + /** + * The type of the AI Ask agent. + */ + public static final String TYPE = "ai_agent_extract"; + + /** + * AI agent tool used to handle basic text. + */ + private BoxAIAgentAskBasicText basicText; + /** + * AI agent tool used to handle longer text. + */ + private BoxAIAgentAskLongText longText; + + /** + * Constructs an AI agent with default settings. + * @param basicText AI agent tool used to handle basic text. + * @param longText AI agent tool used to handle longer text. + */ + public BoxAIAgentExtract(BoxAIAgentAskBasicText basicText, + BoxAIAgentAskLongText longText) { + super(TYPE); + this.basicText = basicText; + this.longText = longText; + } + + /** + * Constructs an AI agent with default settings. + * @param jsonObject JSON object representing the AI agent. + */ + public BoxAIAgentExtract(JsonObject jsonObject) { + super(jsonObject); + } + + /** + * Gets the AI agent tool used to handle basic text. + * @return The AI agent tool used to handle basic text. + */ + public BoxAIAgentAskBasicText getBasicText() { + return basicText; + } + + /** + * Sets the AI agent tool used to handle basic text. + * @param basicText The AI agent tool used to handle basic text. + */ + public void setBasicText(BoxAIAgentAskBasicText basicText) { + this.basicText = basicText; + } + + /** + * Gets the AI agent tool used to handle longer text. + * @return The AI agent tool used to handle longer text. + */ + public BoxAIAgentAskLongText getLongText() { + return longText; + } + + /** + * Sets the AI agent tool used to handle longer text. + * @param longText The AI agent tool used to handle longer text. + */ + public void setLongText(BoxAIAgentAskLongText longText) { + this.longText = longText; + } + + @Override + void parseJSONMember(JsonObject.Member member) { + super.parseJSONMember(member); + String memberName = member.getName(); + JsonValue memberValue = member.getValue(); + try { + switch (memberName) { + case "basic_text": + this.basicText = new BoxAIAgentAskBasicText(memberValue.asObject()); + break; + case "long_text": + this.longText = new BoxAIAgentAskLongText(memberValue.asObject()); + break; + default: + break; + } + } catch (Exception e) { + throw new BoxAPIException("Could not parse JSON response.", e); + } + } + + @Override + public JsonObject getJSONObject() { + JsonObject jsonObject = new JsonObject(); + JsonUtils.addIfNotNull(jsonObject, "type", this.getType()); + JsonUtils.addIfNotNull(jsonObject, "basic_text", this.basicText.getJSONObject()); + JsonUtils.addIfNotNull(jsonObject, "long_text", this.longText.getJSONObject()); + return jsonObject; + } +} + diff --git a/src/main/java/com/box/sdk/BoxAIAgentExtractStructured.java b/src/main/java/com/box/sdk/BoxAIAgentExtractStructured.java new file mode 100644 index 000000000..94e5d6202 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxAIAgentExtractStructured.java @@ -0,0 +1,109 @@ +package com.box.sdk; + +import com.box.sdk.internal.utils.JsonUtils; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +/** + * Represents an AI Agent used for extraction. + */ +@BoxResourceType("ai_agent_extract_structured") +public class BoxAIAgentExtractStructured extends BoxAIAgent { + + /** + * The type of the AI Ask agent. + */ + public static final String TYPE = "ai_agent_extract_structured"; + + /** + * AI agent tool used to handle basic text. + */ + private BoxAIAgentAskBasicText basicText; + /** + * AI agent tool used to handle longer text. + */ + private BoxAIAgentAskLongText longText; + + /** + * Constructs an AI agent with default settings. + * @param basicText AI agent tool used to handle basic text. + * @param longText AI agent tool used to handle longer text. + */ + public BoxAIAgentExtractStructured(BoxAIAgentAskBasicText basicText, + BoxAIAgentAskLongText longText) { + super(TYPE); + this.basicText = basicText; + this.longText = longText; + } + + /** + * Constructs an AI agent with default settings. + * @param jsonObject JSON object representing the AI agent. + */ + public BoxAIAgentExtractStructured(JsonObject jsonObject) { + super(jsonObject); + } + + /** + * Gets the AI agent tool used to handle basic text. + * @return The AI agent tool used to handle basic text. + */ + public BoxAIAgentAskBasicText getBasicText() { + return basicText; + } + + /** + * Sets the AI agent tool used to handle basic text. + * @param basicText The AI agent tool used to handle basic text. + */ + public void setBasicText(BoxAIAgentAskBasicText basicText) { + this.basicText = basicText; + } + + /** + * Gets the AI agent tool used to handle longer text. + * @return The AI agent tool used to handle longer text. + */ + public BoxAIAgentAskLongText getLongText() { + return longText; + } + + /** + * Sets the AI agent tool used to handle longer text. + * @param longText The AI agent tool used to handle longer text. + */ + public void setLongText(BoxAIAgentAskLongText longText) { + this.longText = longText; + } + + @Override + void parseJSONMember(JsonObject.Member member) { + super.parseJSONMember(member); + String memberName = member.getName(); + JsonValue memberValue = member.getValue(); + try { + switch (memberName) { + case "basic_text": + this.basicText = new BoxAIAgentAskBasicText(memberValue.asObject()); + break; + case "long_text": + this.longText = new BoxAIAgentAskLongText(memberValue.asObject()); + break; + default: + break; + } + } catch (Exception e) { + throw new BoxAPIException("Could not parse JSON response.", e); + } + } + + @Override + public JsonObject getJSONObject() { + JsonObject jsonObject = new JsonObject(); + JsonUtils.addIfNotNull(jsonObject, "type", this.getType()); + JsonUtils.addIfNotNull(jsonObject, "basic_text", this.basicText.getJSONObject()); + JsonUtils.addIfNotNull(jsonObject, "long_text", this.longText.getJSONObject()); + return jsonObject; + } +} + diff --git a/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsGoogle.java b/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsGoogle.java index b7d28bfed..2a5cabe00 100644 --- a/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsGoogle.java +++ b/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsGoogle.java @@ -17,20 +17,20 @@ public class BoxAIAgentLLMEndpointParamsGoogle extends BoxAIAgentLLMEndpointPara * The temperature is used for sampling during response generation, which occurs when top-P and top-K are applied. * Temperature controls the degree of randomness in token selection. */ - private double temperature; + private Double temperature; /** * Top-K changes how the model selects tokens for output. * A top-K of 1 means the next selected token is the most probable among all tokens in the model's vocabulary * (also called greedy decoding), while a top-K of 3 means that the next token is selected from among the three * most probable tokens by using temperature. */ - private int topK; + private Integer topK; /** * Top-P changes how the model selects tokens for output. * Tokens are selected from the most (see top-K) to least probable until the sum of their probabilities equals the * top-P value. */ - private double topP; + private Double topP; /** * Constructs an AI agent with default settings. @@ -44,7 +44,7 @@ public class BoxAIAgentLLMEndpointParamsGoogle extends BoxAIAgentLLMEndpointPara * Tokens are selected from the most (see top-K) to least probable until the sum of their probabilities equals the * top-P value. */ - public BoxAIAgentLLMEndpointParamsGoogle(double temperature, int topK, double topP) { + public BoxAIAgentLLMEndpointParamsGoogle(Double temperature, Integer topK, Double topP) { super(TYPE); this.temperature = temperature; this.topK = topK; @@ -63,7 +63,7 @@ public BoxAIAgentLLMEndpointParamsGoogle(JsonObject jsonObject) { * Gets the temperature used for sampling during response generation, which occurs when top-P and top-K are applied. * @return The temperature used for sampling during response generation, which occurs when top-P and top-K are applied. */ - public double getTemperature() { + public Double getTemperature() { return temperature; } @@ -71,7 +71,7 @@ public double getTemperature() { * Sets the temperature used for sampling during response generation, which occurs when top-P and top-K are applied. * @param temperature The temperature used for sampling during response generation, which occurs when top-P and top-K are applied. */ - public void setTemperature(double temperature) { + public void setTemperature(Double temperature) { this.temperature = temperature; } @@ -79,7 +79,7 @@ public void setTemperature(double temperature) { * Gets the top-K value. * @return The top-K value. */ - public int getTopK() { + public Integer getTopK() { return topK; } @@ -87,7 +87,7 @@ public int getTopK() { * Sets the top-K value. * @param topK The top-K value. */ - public void setTopK(int topK) { + public void setTopK(Integer topK) { this.topK = topK; } @@ -95,7 +95,7 @@ public void setTopK(int topK) { * Gets the top-P value. * @return The top-P value. */ - public double getTopP() { + public Double getTopP() { return topP; } @@ -103,7 +103,7 @@ public double getTopP() { * Sets the top-P value. * @param topP The top-P value. */ - public void setTopP(double topP) { + public void setTopP(Double topP) { this.topP = topP; } diff --git a/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsOpenAI.java b/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsOpenAI.java index ec555ce99..00ecad81a 100644 --- a/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsOpenAI.java +++ b/src/main/java/com/box/sdk/BoxAIAgentLLMEndpointParamsOpenAI.java @@ -17,12 +17,12 @@ public class BoxAIAgentLLMEndpointParamsOpenAI extends BoxAIAgentLLMEndpointPara * Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text * so far, decreasing the model's likelihood to repeat the same line verbatim. */ - private double frequencyPenalty; + private Double frequencyPenalty; /** * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, * increasing the model's likelihood to talk about new topics. */ - private double presencePenalty; + private Double presencePenalty; /** * Up to 4 sequences where the API will stop generating further tokens. */ @@ -32,13 +32,13 @@ public class BoxAIAgentLLMEndpointParamsOpenAI extends BoxAIAgentLLMEndpointPara * while lower values like 0.2 will make it more focused and deterministic. * We generally recommend altering this or top_p but not both. */ - private double temperature; + private Double temperature; /** * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of * the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass * are considered. We generally recommend altering this or temperature but not both. */ - private double topP; + private Double topP; /** * Constructs an AI agent with default settings. @@ -54,11 +54,11 @@ public class BoxAIAgentLLMEndpointParamsOpenAI extends BoxAIAgentLLMEndpointPara * the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass * are considered. We generally recommend altering this or temperature but not both. */ - public BoxAIAgentLLMEndpointParamsOpenAI(double frequencyPenalty, - double presencePenalty, + public BoxAIAgentLLMEndpointParamsOpenAI(Double frequencyPenalty, + Double presencePenalty, String stop, - double temperature, - double topP) { + Double temperature, + Double topP) { super(TYPE); this.frequencyPenalty = frequencyPenalty; this.presencePenalty = presencePenalty; @@ -79,7 +79,7 @@ public BoxAIAgentLLMEndpointParamsOpenAI(JsonObject jsonObject) { * Gets the frequency penalty. * @return The frequency penalty. */ - public double getFrequencyPenalty() { + public Double getFrequencyPenalty() { return frequencyPenalty; } @@ -87,7 +87,7 @@ public double getFrequencyPenalty() { * Sets the frequency penalty. * @param frequencyPenalty The frequency penalty. */ - public void setFrequencyPenalty(double frequencyPenalty) { + public void setFrequencyPenalty(Double frequencyPenalty) { this.frequencyPenalty = frequencyPenalty; } @@ -95,7 +95,7 @@ public void setFrequencyPenalty(double frequencyPenalty) { * Gets the presence penalty. * @return The presence penalty. */ - public double getPresencePenalty() { + public Double getPresencePenalty() { return presencePenalty; } @@ -103,7 +103,7 @@ public double getPresencePenalty() { * Sets the presence penalty. * @param presencePenalty The presence penalty. */ - public void setPresencePenalty(double presencePenalty) { + public void setPresencePenalty(Double presencePenalty) { this.presencePenalty = presencePenalty; } @@ -127,7 +127,7 @@ public void setStop(String stop) { * Gets the temperature. * @return The temperature. */ - public double getTemperature() { + public Double getTemperature() { return temperature; } @@ -135,7 +135,7 @@ public double getTemperature() { * Sets the temperature. * @param temperature The temperature. */ - public void setTemperature(double temperature) { + public void setTemperature(Double temperature) { this.temperature = temperature; } @@ -143,7 +143,7 @@ public void setTemperature(double temperature) { * Gets the top-P. * @return The top-P. */ - public double getTopP() { + public Double getTopP() { return topP; } @@ -151,7 +151,7 @@ public double getTopP() { * Sets the top-P. * @param topP The top-P. */ - public void setTopP(double topP) { + public void setTopP(Double topP) { this.topP = topP; } diff --git a/src/main/java/com/box/sdk/BoxAIExtractField.java b/src/main/java/com/box/sdk/BoxAIExtractField.java new file mode 100644 index 000000000..8f0ca1ec3 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxAIExtractField.java @@ -0,0 +1,74 @@ +package com.box.sdk; + +import com.box.sdk.internal.utils.JsonUtils; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import java.util.List; + +public class BoxAIExtractField extends BoxJSONObject { + /** + * The type of the field. It include but is not limited to string, float, date, enum, and multiSelect. + */ + private String type; + /** + * The description of the field. + */ + private String description; + /** + * The display name of the field. + */ + private String displayName; + /** + * The key of the field. + */ + private String key; + /** + * A list of options for this field. + * This is most often used in combination with the enum and multiSelect field types. + */ + private List options; + /** + * The prompt of the field. + */ + private String prompt; + + /** + * Constructs a BoxAIExtractField object with a given type, description, display name, key, options, and prompt. + * + * @param type the type of the field. + * @param description the description of the field. + * @param displayName the display name of the field. + * @param key the key of the field. + * @param options a list of options for this field. + * @param prompt the prompt of the field. + */ + public BoxAIExtractField(String type, + String description, + String displayName, + String key, List options, + String prompt) { + this.type = type; + this.description = description; + this.displayName = displayName; + this.key = key; + this.options = options; + this.prompt = prompt; + } + + public JsonObject getJSONObject() { + JsonObject jsonObject = new JsonObject(); + JsonUtils.addIfNotNull(jsonObject, "type", this.type); + JsonUtils.addIfNotNull(jsonObject, "description", this.description); + JsonUtils.addIfNotNull(jsonObject, "displayName", this.displayName); + JsonUtils.addIfNotNull(jsonObject, "key", this.key); + if (this.options != null) { + JsonArray options = new JsonArray(); + for (String option : this.options) { + options.add(new JsonObject().add("key", option)); + } + jsonObject.add("options", options); + } + JsonUtils.addIfNotNull(jsonObject, "prompt", this.prompt); + return jsonObject; + } +} diff --git a/src/main/java/com/box/sdk/BoxAIExtractMetadataTemplate.java b/src/main/java/com/box/sdk/BoxAIExtractMetadataTemplate.java new file mode 100644 index 000000000..c7fb4c3da --- /dev/null +++ b/src/main/java/com/box/sdk/BoxAIExtractMetadataTemplate.java @@ -0,0 +1,56 @@ +package com.box.sdk; + +import com.box.sdk.internal.utils.JsonUtils; +import com.eclipsesource.json.JsonObject; + +public class BoxAIExtractMetadataTemplate extends BoxJSONObject { + /** + * The type of object this class represents. + */ + public static final String TYPE = "metadata_template"; + /** + * The scope of the metadata template that can either be global or enterprise. + */ + private String scope; + /** + * The template key of the metadata template. + */ + private String templateKey; + + /** + * Constructs a BoxAIExtractMetadataTemplate object with a given scope and template key. + * @param templateKey the template key of the metadata template. + * @param scope the scope of the metadata template. + */ + public BoxAIExtractMetadataTemplate(String templateKey, String scope) { + this.templateKey = templateKey; + this.scope = scope; + } + + @Override + void parseJSONMember(JsonObject.Member member) { + super.parseJSONMember(member); + String memberName = member.getName(); + if (memberName.equals("scope")) { + this.scope = member.getValue().asString(); + } else if (memberName.equals("template_key")) { + this.templateKey = member.getValue().asString(); + } + } + + public JsonObject getJSONObject() { + JsonObject jsonObject = new JsonObject(); + JsonUtils.addIfNotNull(jsonObject, "type", TYPE); + JsonUtils.addIfNotNull(jsonObject, "scope", this.scope); + JsonUtils.addIfNotNull(jsonObject, "template_key", this.templateKey); + return jsonObject; + } + + public String getScope() { + return this.scope; + } + + public String getTemplateKey() { + return this.templateKey; + } +} diff --git a/src/test/Fixtures/BoxAI/ExtractMetadataFreeform200.json b/src/test/Fixtures/BoxAI/ExtractMetadataFreeform200.json new file mode 100644 index 000000000..1ec314c6a --- /dev/null +++ b/src/test/Fixtures/BoxAI/ExtractMetadataFreeform200.json @@ -0,0 +1,5 @@ +{ + "answer": "Public APIs are important because of key and important reasons.", + "completion_reason": "done", + "created_at": "2012-12-12T10:53:43.123-08:00" +} \ No newline at end of file diff --git a/src/test/Fixtures/BoxAI/ExtractMetadataStructured200.json b/src/test/Fixtures/BoxAI/ExtractMetadataStructured200.json new file mode 100644 index 000000000..15e917b2b --- /dev/null +++ b/src/test/Fixtures/BoxAI/ExtractMetadataStructured200.json @@ -0,0 +1,9 @@ +{ + "firstName": "John", + "lastName": "Doe", + "age": 25, + "hobbies": [ + "reading", + "travelling" + ] +} \ No newline at end of file diff --git a/src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtract200.json b/src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtract200.json new file mode 100644 index 000000000..4089200ff --- /dev/null +++ b/src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtract200.json @@ -0,0 +1,38 @@ +{ + "type": "ai_agent_extract", + "basic_text": { + "llm_endpoint_params": { + "type": "openai_params", + "frequency_penalty": 1.5, + "presence_penalty": 1.5, + "stop": "<|im_end|>", + "temperature": 0, + "top_p": 1 + }, + "model": "azure__openai__gpt_3_5_turbo_16k", + "num_tokens_for_completion": 8400, + "prompt_template": "It is `{current_date}`, consider these travel options `{content}` and answer the `{user_question}`.", + "system_message": "You are a helpful travel assistant specialized in budget travel" + }, + "long_text": { + "embeddings": { + "model": "openai__text_embedding_ada_002", + "strategy": { + "id": "basic", + "num_tokens_per_chunk": 64 + } + }, + "llm_endpoint_params": { + "type": "openai_params", + "frequency_penalty": 1.5, + "presence_penalty": 1.5, + "stop": "<|im_end|>", + "temperature": 0, + "top_p": 1 + }, + "model": "azure__openai__gpt_3_5_turbo_16k", + "num_tokens_for_completion": 8400, + "prompt_template": "It is `{current_date}`, consider these travel options `{content}` and answer the `{user_question}`.", + "system_message": "You are a helpful travel assistant specialized in budget travel" + } +} \ No newline at end of file diff --git a/src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtractStructured200.json b/src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtractStructured200.json new file mode 100644 index 000000000..89555e825 --- /dev/null +++ b/src/test/Fixtures/BoxAI/GetAIAgentDefaultConfigExtractStructured200.json @@ -0,0 +1,38 @@ +{ + "type": "ai_agent_extract_structured", + "basic_text": { + "llm_endpoint_params": { + "type": "openai_params", + "frequency_penalty": 1.5, + "presence_penalty": 1.5, + "stop": "<|im_end|>", + "temperature": 0, + "top_p": 1 + }, + "model": "azure__openai__gpt_3_5_turbo_16k", + "num_tokens_for_completion": 8400, + "prompt_template": "It is `{current_date}`, consider these travel options `{content}` and answer the `{user_question}`.", + "system_message": "You are a helpful travel assistant specialized in budget travel" + }, + "long_text": { + "embeddings": { + "model": "openai__text_embedding_ada_002", + "strategy": { + "id": "basic", + "num_tokens_per_chunk": 64 + } + }, + "llm_endpoint_params": { + "type": "openai_params", + "frequency_penalty": 1.5, + "presence_penalty": 1.5, + "stop": "<|im_end|>", + "temperature": 0, + "top_p": 1 + }, + "model": "azure__openai__gpt_3_5_turbo_16k", + "num_tokens_for_completion": 8400, + "prompt_template": "It is `{current_date}`, consider these travel options `{content}` and answer the `{user_question}`.", + "system_message": "You are a helpful travel assistant specialized in budget travel" + } +} \ No newline at end of file diff --git a/src/test/java/com/box/sdk/BoxAITest.java b/src/test/java/com/box/sdk/BoxAITest.java index 11af93666..5302119f1 100644 --- a/src/test/java/com/box/sdk/BoxAITest.java +++ b/src/test/java/com/box/sdk/BoxAITest.java @@ -1,6 +1,7 @@ package com.box.sdk; import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockRule; @@ -236,4 +237,130 @@ public void testGetAIAgentDefaultConfigTextGenSuccess() { JsonObject jsonResult = Json.parse(result).asObject(); assertThat(agent.getJSONObject().toString(), equalTo(jsonResult.toString())); } + + @Test + public void testGetAIAgentDefaultConfigExtractSuccess() { + String result = TestUtils.getFixture("BoxAI/GetAIAgentDefaultConfigExtract200"); + String urlPath = "/2.0/ai_agent_default"; + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(urlPath)) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(result))); + + BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT, + "en", "openai__gpt_3_5_turbo"); + BoxAIAgentExtract agentExtract = (BoxAIAgentExtract) agent; + + assertThat(agent.getType(), equalTo("ai_agent_extract")); + assertThat(agentExtract.getBasicText().getModel(), equalTo("azure__openai__gpt_3_5_turbo_16k")); + + JsonObject jsonResult = Json.parse(result).asObject(); + assertThat(agent.getJSONObject().toString(), equalTo(jsonResult.toString())); + } + + @Test + public void testGetAIAgentDefaultConfigExtractStructuredSuccess() { + String result = TestUtils.getFixture("BoxAI/GetAIAgentDefaultConfigExtractStructured200"); + String urlPath = "/2.0/ai_agent_default"; + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(urlPath)) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(result))); + + BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT_STRUCTURED, + "en", "openai__gpt_3_5_turbo"); + BoxAIAgentExtractStructured agentExtract = (BoxAIAgentExtractStructured) agent; + + assertThat(agent.getType(), equalTo("ai_agent_extract_structured")); + assertThat(agentExtract.getBasicText().getModel(), equalTo("azure__openai__gpt_3_5_turbo_16k")); + + JsonObject jsonResult = Json.parse(result).asObject(); + assertThat(agent.getJSONObject().toString(), equalTo(jsonResult.toString())); + } + + @Test + public void testExtractMetadataFreeformSuccess() { + String agentString = TestUtils.getFixture("BoxAI/GetAIAgentDefaultConfigExtract200"); + final String fileId = "12345"; + final String prompt = "Extract data related to contract conditions"; + final BoxAIAgentExtract agent = new BoxAIAgentExtract(Json.parse(agentString).asObject()); + JsonObject expectedRequestBody = new JsonObject(); + expectedRequestBody.add("prompt", prompt); + expectedRequestBody.add("items", new JsonArray().add(new JsonObject() + .add("type", "file") + .add("id", fileId))); + expectedRequestBody.add("ai_agent", agent.getJSONObject()); + + String result = TestUtils.getFixture("BoxAI/ExtractMetadataFreeform200"); + wireMockRule.stubFor(WireMock.post(WireMock.urlPathEqualTo("/2.0/ai/extract")) + .withRequestBody(WireMock.equalToJson( + expectedRequestBody.toString() + )) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(result))); + + BoxAIResponse response = BoxAI.extractMetadataFreeform( + api, + prompt, + Collections.singletonList(new BoxAIItem(fileId, BoxAIItem.Type.FILE)), + agent + ); + + assertThat(response.getAnswer(), equalTo("Public APIs are important because of key and important reasons.")); + assertThat(response.getCreatedAt(), equalTo(new Date(1355338423123L))); + assertThat(response.getCompletionReason(), equalTo("done")); + } + + @Test + public void testExtractMetadataStructuredSuccess() { + String agentString = TestUtils.getFixture("BoxAI/GetAIAgentDefaultConfigExtractStructured200"); + final String fileId = "12345"; + final BoxAIExtractMetadataTemplate template = new BoxAIExtractMetadataTemplate("templateId", "enterprise"); + final List items = Collections.singletonList(new BoxAIItem(fileId, BoxAIItem.Type.FILE)); + final String result = TestUtils.getFixture("BoxAI/ExtractMetadataStructured200"); + final BoxAIAgentExtractStructured agent = new BoxAIAgentExtractStructured(Json.parse(agentString).asObject()); + final BoxAIExtractField field = new BoxAIExtractField( + "text", + "The name of the file", + "Name", + "name", + new ArrayList() {{ + add("option 1"); + add("option 2"); + }}, + "What is the name of the file?"); + + final JsonObject expectedRequestBody = new JsonObject() + .add("items", new JsonArray().add(new JsonObject() + .add("type", "file") + .add("id", fileId))) + .add("metadata_template", new JsonObject() + .add("type", "metadata_template") + .add("template_key", template.getTemplateKey()) + .add("scope", template.getScope())) + .add("ai_agent", agent.getJSONObject()) + .add("fields", new JsonArray().add(field.getJSONObject())); + + wireMockRule.stubFor(WireMock.post(WireMock.urlPathEqualTo("/2.0/ai/extract_structured")) + .withRequestBody(WireMock.equalToJson( + expectedRequestBody.toString() + )) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(result))); + + JsonObject response = BoxAI.extractMetadataStructured( + api, + items, + template, + Collections.singletonList(field), + agent + ); + + assertThat(response, equalTo(Json.parse(result).asObject())); + assertThat(response.get("firstName").asString(), equalTo("John")); + } } From c7a092f22b8b95388bf66f2eeba9257035767732 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 01:01:22 +0200 Subject: [PATCH 2/8] Update code --- doc/ai.md | 9 +++-- src/intTest/java/com/box/sdk/BoxAIIT.java | 36 +++++++++--------- src/main/java/com/box/sdk/BoxAI.java | 4 +- .../java/com/box/sdk/BoxAIExtractField.java | 8 ++-- .../com/box/sdk/BoxAIExtractFieldOption.java | 27 +++++++++++++ .../sdk/BoxAIExtractStructuredResponse.java | 38 +++++++++++++++++++ src/test/java/com/box/sdk/BoxAITest.java | 10 ++--- 7 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/box/sdk/BoxAIExtractFieldOption.java create mode 100644 src/main/java/com/box/sdk/BoxAIExtractStructuredResponse.java diff --git a/doc/ai.md b/doc/ai.md index 4ed501842..d457bf291 100644 --- a/doc/ai.md +++ b/doc/ai.md @@ -11,6 +11,8 @@ an answer based on the provided prompt and items. - [Send AI request](#send-ai-request) - [Send AI text generation request](#send-ai-text-generation-request) - [Get AI Agent default configuration](#get-ai-agent-default-configuration) + - [Extract metadata freeform](#extract-metadata-freeform) + - [Extract metadata structured](#extract-metadata-structured) @@ -92,7 +94,7 @@ BoxAIAgentConfig config = BoxAI.getAiAgentDefaultConfig( Extract metadata freeform -------------------------- -To send an AI request to supported Large Language Models (LLMs) and extracts metadata in form of key-value pairs, call static +To send an AI request to supported Large Language Models (LLMs) and extract metadata in form of key-value pairs, call static [` extractMetadataFreeform(BoxAPIConnection api, String prompt, List items, BoxAIAgentExtract agent)`][extract-metadata-freeform] method. In the request you have to provide a prompt, a list of items that your prompt refers to and an optional agent configuration. @@ -114,7 +116,7 @@ BoxAIResponse response = BoxAI.extractMetadataFreeform( Extract metadata structured -------------------------- -Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of key-value pairs. For this request, you need to use an already defined metadata template or a define a schema yourself. +Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of key-value pairs. For this request, you need to use an already defined metadata template or define a schema yourself. To send an AI request to extract metadata from files, call static [`extractMetadataStructured extractMetadataStructured(BoxAPIConnection api, List items, BoxAIExtractMetadataTemplate template, List fields, BoxAIAgentExtractStructured agent)`][extract-metadata-structured] method. @@ -124,13 +126,14 @@ BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT_ST BoxAIAgentExtractStructured agentExtractStructured = (BoxAIAgentExtractStructured) agent; BoxAIExtractMetadataTemplate template = new BoxAIExtractMetadataTemplate("templateKey", "enterprise"); -JsonObject result = BoxAI.extractMetadataStructured( +BoxAIExtractStructuredResponse result = BoxAI.extractMetadataStructured( api, Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), template, null, agentExtractStructured ); +JsonObject sourceJson = result.getSourceJson(); ``` [extract-metadata-structured]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-com.box.sdk.ai.BoxAIExtractMetadataTemplate-java.util.List-com.box.sdk.ai.BoxAIAgentExtractStructured- \ No newline at end of file diff --git a/src/intTest/java/com/box/sdk/BoxAIIT.java b/src/intTest/java/com/box/sdk/BoxAIIT.java index dd5afdfa2..6ddb80dd8 100644 --- a/src/intTest/java/com/box/sdk/BoxAIIT.java +++ b/src/intTest/java/com/box/sdk/BoxAIIT.java @@ -233,7 +233,7 @@ public void aiExtractStructuredWithFields() throws InterruptedException { // When a file has been just uploaded, AI service may not be ready to return text response // and 412 is returned retry(() -> { - JsonObject response = BoxAI.extractMetadataStructured(api, + BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured(api, Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), null, new ArrayList() {{ @@ -261,19 +261,20 @@ public void aiExtractStructuredWithFields() throws InterruptedException { "Person hobby", "Hobby", "hobby", - new ArrayList() {{ - add("guitar"); - add("books"); + new ArrayList() {{ + add(new BoxAIExtractFieldOption("guitar")); + add(new BoxAIExtractFieldOption("books")); }}, "What is your hobby?")); }}, agentExtractStructured); - assertThat(response.get("firstName").asString(), is(equalTo("John"))); - assertThat(response.get("lastName").asString(), is(equalTo("Doe"))); - assertThat(response.get("dateOfBirth").asString(), is(equalTo("1990-07-04"))); - assertThat(response.get("age").asInt(), is(equalTo(34))); - assertThat(response.get("hobby").asArray().get(0).asString(), is(equalTo("guitar"))); - assertThat(response.get("hobby").asArray().get(1).asString(), is(equalTo("books"))); + JsonObject sourceJson = response.getSourceJson(); + assertThat(sourceJson.get("firstName").asString(), is(equalTo("John"))); + assertThat(sourceJson.get("lastName").asString(), is(equalTo("Doe"))); + assertThat(sourceJson.get("dateOfBirth").asString(), is(equalTo("1990-07-04"))); + assertThat(sourceJson.get("age").asInt(), is(equalTo(34))); + assertThat(sourceJson.get("hobby").asArray().get(0).asString(), is(equalTo("guitar"))); + assertThat(sourceJson.get("hobby").asArray().get(1).asString(), is(equalTo("books"))); }, 2, 2000); } finally { deleteFile(uploadedFile); @@ -316,17 +317,18 @@ public void aiExtractStructuredWithMetadataTemplate() throws InterruptedExceptio // When a file has been just uploaded, AI service may not be ready to return text response // and 412 is returned retry(() -> { - JsonObject response = BoxAI.extractMetadataStructured(api, + BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured(api, Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), new BoxAIExtractMetadataTemplate(templateKey, "enterprise"), null, agentExtractStructured); - assertThat(response.get("firstName").asString(), is(equalTo("John"))); - assertThat(response.get("lastName").asString(), is(equalTo("Doe"))); - assertThat(response.get("dateOfBirth").asString(), is(equalTo("1990-07-04"))); - assertThat(response.get("age").asInt(), is(equalTo(34))); - assertThat(response.get("hobby").asArray().get(0).asString(), is(equalTo("guitar"))); - assertThat(response.get("hobby").asArray().get(1).asString(), is(equalTo("books"))); + JsonObject sourceJson = response.getSourceJson(); + assertThat(sourceJson.get("firstName").asString(), is(equalTo("John"))); + assertThat(sourceJson.get("lastName").asString(), is(equalTo("Doe"))); + assertThat(sourceJson.get("dateOfBirth").asString(), is(equalTo("1990-07-04"))); + assertThat(sourceJson.get("age").asInt(), is(equalTo(34))); + assertThat(sourceJson.get("hobby").asArray().get(0).asString(), is(equalTo("guitar"))); + assertThat(sourceJson.get("hobby").asArray().get(1).asString(), is(equalTo("books"))); }, 2, 2000); } finally { deleteFile(uploadedFile); diff --git a/src/main/java/com/box/sdk/BoxAI.java b/src/main/java/com/box/sdk/BoxAI.java index 32d1ac741..f63de014c 100644 --- a/src/main/java/com/box/sdk/BoxAI.java +++ b/src/main/java/com/box/sdk/BoxAI.java @@ -284,7 +284,7 @@ public static BoxAIResponse extractMetadataFreeform(BoxAPIConnection api, } } - public static JsonObject extractMetadataStructured(BoxAPIConnection api, List items, + public static BoxAIExtractStructuredResponse extractMetadataStructured(BoxAPIConnection api, List items, BoxAIExtractMetadataTemplate template, List fields, BoxAIAgentExtractStructured agent) { @@ -317,7 +317,7 @@ public static JsonObject extractMetadataStructured(BoxAPIConnection api, List options; + private List options; /** * The prompt of the field. */ @@ -45,7 +45,7 @@ public class BoxAIExtractField extends BoxJSONObject { public BoxAIExtractField(String type, String description, String displayName, - String key, List options, + String key, List options, String prompt) { this.type = type; this.description = description; @@ -63,8 +63,8 @@ public JsonObject getJSONObject() { JsonUtils.addIfNotNull(jsonObject, "key", this.key); if (this.options != null) { JsonArray options = new JsonArray(); - for (String option : this.options) { - options.add(new JsonObject().add("key", option)); + for (BoxAIExtractFieldOption option : this.options) { + options.add(option.getJSONObject()); } jsonObject.add("options", options); } diff --git a/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java b/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java new file mode 100644 index 000000000..1597f663e --- /dev/null +++ b/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java @@ -0,0 +1,27 @@ +package com.box.sdk; + + +import com.box.sdk.internal.utils.JsonUtils; +import com.eclipsesource.json.JsonObject; + +public class BoxAIExtractFieldOption extends BoxJSONObject { + /** + * A unique identifier for the option. + */ + final String key; + + /** + * Constructs a BoxAIExtractFieldOption object with a given key. + * + * @param key the key of the field option. + */ + public BoxAIExtractFieldOption(String key) { + this.key = key; + } + + public JsonObject getJSONObject() { + JsonObject jsonObject = new JsonObject(); + JsonUtils.addIfNotNull(jsonObject, "key", this.key); + return jsonObject; + } +} diff --git a/src/main/java/com/box/sdk/BoxAIExtractStructuredResponse.java b/src/main/java/com/box/sdk/BoxAIExtractStructuredResponse.java new file mode 100644 index 000000000..662244fa5 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxAIExtractStructuredResponse.java @@ -0,0 +1,38 @@ +package com.box.sdk; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; + +/** + * AI response to a user request. + */ +public class BoxAIExtractStructuredResponse extends BoxJSONObject { + private final JsonObject sourceJson; + + /** + * Constructs a BoxAIResponse object. + */ + public BoxAIExtractStructuredResponse(String json) { + super(json); + this.sourceJson = Json.parse(json).asObject(); + } + + /** + * Constructs an BoxAIResponse object using an already parsed JSON object. + * + * @param jsonObject the parsed JSON object. + */ + BoxAIExtractStructuredResponse(JsonObject jsonObject) { + super(jsonObject); + this.sourceJson = jsonObject; + } + + /** + * Gets the source JSON of the AI response. + * + * @return the source JSON of the AI response. + */ + public JsonObject getSourceJson() { + return sourceJson; + } +} diff --git a/src/test/java/com/box/sdk/BoxAITest.java b/src/test/java/com/box/sdk/BoxAITest.java index 5302119f1..7607f9088 100644 --- a/src/test/java/com/box/sdk/BoxAITest.java +++ b/src/test/java/com/box/sdk/BoxAITest.java @@ -327,9 +327,9 @@ public void testExtractMetadataStructuredSuccess() { "The name of the file", "Name", "name", - new ArrayList() {{ - add("option 1"); - add("option 2"); + new ArrayList() {{ + add(new BoxAIExtractFieldOption("option 1")); + add(new BoxAIExtractFieldOption("option 2")); }}, "What is the name of the file?"); @@ -352,7 +352,7 @@ public void testExtractMetadataStructuredSuccess() { .withHeader("Content-Type", APPLICATION_JSON) .withBody(result))); - JsonObject response = BoxAI.extractMetadataStructured( + BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured( api, items, template, @@ -361,6 +361,6 @@ public void testExtractMetadataStructuredSuccess() { ); assertThat(response, equalTo(Json.parse(result).asObject())); - assertThat(response.get("firstName").asString(), equalTo("John")); + assertThat(response.getSourceJson().get("firstName").asString(), equalTo("John")); } } From 134947ad07fff6287636a8411ce8eb14cb496a6f Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 01:15:07 +0200 Subject: [PATCH 3/8] Fix code --- src/main/java/com/box/sdk/BoxAIExtractFieldOption.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java b/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java index 1597f663e..a1cae5ceb 100644 --- a/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java +++ b/src/main/java/com/box/sdk/BoxAIExtractFieldOption.java @@ -8,7 +8,7 @@ public class BoxAIExtractFieldOption extends BoxJSONObject { /** * A unique identifier for the option. */ - final String key; + private final String key; /** * Constructs a BoxAIExtractFieldOption object with a given key. From b38e67851ab22cb43b8cab323c7191422534ecc9 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 11:00:38 +0200 Subject: [PATCH 4/8] Update code --- src/test/java/com/box/sdk/BoxAITest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/box/sdk/BoxAITest.java b/src/test/java/com/box/sdk/BoxAITest.java index 7607f9088..0217c83ea 100644 --- a/src/test/java/com/box/sdk/BoxAITest.java +++ b/src/test/java/com/box/sdk/BoxAITest.java @@ -360,7 +360,7 @@ public void testExtractMetadataStructuredSuccess() { agent ); - assertThat(response, equalTo(Json.parse(result).asObject())); + assertThat(response.getSourceJson(), equalTo(Json.parse(result).asObject())); assertThat(response.getSourceJson().get("firstName").asString(), equalTo("John")); } } From 5e31c9605a23238180fb9d05c4910107abe7919a Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 12:01:01 +0200 Subject: [PATCH 5/8] Update docs --- doc/ai.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/doc/ai.md b/doc/ai.md index d457bf291..e675196d3 100644 --- a/doc/ai.md +++ b/doc/ai.md @@ -95,19 +95,16 @@ Extract metadata freeform -------------------------- To send an AI request to supported Large Language Models (LLMs) and extract metadata in form of key-value pairs, call static -[` extractMetadataFreeform(BoxAPIConnection api, String prompt, List items, BoxAIAgentExtract agent)`][extract-metadata-freeform] method. +[`extractMetadataFreeform(BoxAPIConnection api, String prompt, List items, BoxAIAgentExtract agent)`][extract-metadata-freeform] method. In the request you have to provide a prompt, a list of items that your prompt refers to and an optional agent configuration. ```java -BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT, "en-US", null); -BoxAIAgentExtract agentExtract = (BoxAIAgentExtract) agent; - BoxAIResponse response = BoxAI.extractMetadataFreeform( api, - "What is the content of the file?", + "firstName, lastName, location, yearOfBirth, company", Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), - agentExtract + agent ); ``` @@ -122,18 +119,15 @@ To send an AI request to extract metadata from files, call static ```java -BoxAIAgent agent = BoxAI.getAiAgentDefaultConfig(api, BoxAIAgent.Mode.EXTRACT_STRUCTURED, "en-US", null); -BoxAIAgentExtractStructured agentExtractStructured = (BoxAIAgentExtractStructured) agent; BoxAIExtractMetadataTemplate template = new BoxAIExtractMetadataTemplate("templateKey", "enterprise"); - BoxAIExtractStructuredResponse result = BoxAI.extractMetadataStructured( api, Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), template, null, - agentExtractStructured + agent ); JsonObject sourceJson = result.getSourceJson(); ``` -[extract-metadata-structured]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-com.box.sdk.ai.BoxAIExtractMetadataTemplate-java.util.List-com.box.sdk.ai.BoxAIAgentExtractStructured- \ No newline at end of file +[extract-metadata-structured]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-com.box.sdk.ai.BoxAIExtractMetadataTemplate-java.util.List-com.box.sdk.ai.BoxAIAgentExtractStructured- From d65fde1f234e66b429435e29e3b2e8aadfd45601 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 13:47:01 +0200 Subject: [PATCH 6/8] Update code --- doc/ai.md | 35 +++++-- src/intTest/java/com/box/sdk/BoxAIIT.java | 12 +-- src/main/java/com/box/sdk/BoxAI.java | 91 +++++++++++++++++++ .../java/com/box/sdk/BoxAIExtractField.java | 7 ++ src/test/java/com/box/sdk/BoxAITest.java | 56 +++++++++--- 5 files changed, 169 insertions(+), 32 deletions(-) diff --git a/doc/ai.md b/doc/ai.md index e675196d3..61e7e9dcd 100644 --- a/doc/ai.md +++ b/doc/ai.md @@ -95,7 +95,7 @@ Extract metadata freeform -------------------------- To send an AI request to supported Large Language Models (LLMs) and extract metadata in form of key-value pairs, call static -[`extractMetadataFreeform(BoxAPIConnection api, String prompt, List items, BoxAIAgentExtract agent)`][extract-metadata-freeform] method. +[`extractMetadataFreeform(BoxAPIConnection api, String prompt, List items)`][extract-metadata-freeform] method. In the request you have to provide a prompt, a list of items that your prompt refers to and an optional agent configuration. @@ -103,19 +103,19 @@ In the request you have to provide a prompt, a list of items that your prompt re BoxAIResponse response = BoxAI.extractMetadataFreeform( api, "firstName, lastName, location, yearOfBirth, company", - Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), - agent + Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)) ); ``` -[extract-metadata-freeform]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataFreeform-com.box.sdk.BoxAPIConnection-java.lang.String-java.util.List-com.box.sdk.ai.BoxAIAgentExtract- +[extract-metadata-freeform]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataFreeform-com.box.sdk.BoxAPIConnection-java.lang.String-java.util.List- Extract metadata structured -------------------------- Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of key-value pairs. For this request, you need to use an already defined metadata template or define a schema yourself. -To send an AI request to extract metadata from files, call static -[`extractMetadataStructured extractMetadataStructured(BoxAPIConnection api, List items, BoxAIExtractMetadataTemplate template, List fields, BoxAIAgentExtractStructured agent)`][extract-metadata-structured] method. + +To send an AI request to extract metadata from files with a predefined metadata template, call static +[`extractMetadataStructured extractMetadataStructured(BoxAPIConnection api, List items, BoxAIExtractMetadataTemplate template)`][extract-metadata-structured-metadata-template] method. ```java @@ -123,11 +123,26 @@ BoxAIExtractMetadataTemplate template = new BoxAIExtractMetadataTemplate("templa BoxAIExtractStructuredResponse result = BoxAI.extractMetadataStructured( api, Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), - template, - null, - agent + template +); +JsonObject sourceJson = result.getSourceJson(); +``` + +To send an AI request to extract metadata from files with a custom fields, call static +[`extractMetadataStructured extractMetadataStructured(BoxAPIConnection api, List items, List fields)`][extract-metadata-structured-fields] method. + + +```java +List fields = new ArrayList<>(); +fields.add(new BoxAIExtractField("firstName")); + +BoxAIExtractStructuredResponse result = BoxAI.extractMetadataStructured( + api, + Collections.singletonList(new BoxAIItem("123456", BoxAIItem.Type.FILE)), + fields ); JsonObject sourceJson = result.getSourceJson(); ``` -[extract-metadata-structured]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-com.box.sdk.ai.BoxAIExtractMetadataTemplate-java.util.List-com.box.sdk.ai.BoxAIAgentExtractStructured- +[extract-metadata-structured-metadata-template]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-com.box.sdk.ai.metadata.BoxAIExtractMetadataTemplate- +[extract-metadata-structured-fields]: https://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxAI.html#extractMetadataStructured-com.box.sdk.BoxAPIConnection-java.util.List-java.util.List- \ No newline at end of file diff --git a/src/intTest/java/com/box/sdk/BoxAIIT.java b/src/intTest/java/com/box/sdk/BoxAIIT.java index 6ddb80dd8..9f60f563d 100644 --- a/src/intTest/java/com/box/sdk/BoxAIIT.java +++ b/src/intTest/java/com/box/sdk/BoxAIIT.java @@ -235,16 +235,9 @@ public void aiExtractStructuredWithFields() throws InterruptedException { retry(() -> { BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured(api, Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), - null, new ArrayList() {{ - add(new BoxAIExtractField("string", - "Person first name", - "First name", - "firstName", - null, - "What is the your first name?")); - add(new BoxAIExtractField("string", - "Person last name", "Last name", "lastName", null, "What is the your last name?")); + add(new BoxAIExtractField("firstName")); + add(new BoxAIExtractField("lastName")); add(new BoxAIExtractField("date", "Person date of birth", "Birth date", @@ -320,7 +313,6 @@ public void aiExtractStructuredWithMetadataTemplate() throws InterruptedExceptio BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured(api, Collections.singletonList(new BoxAIItem(uploadedFile.getID(), BoxAIItem.Type.FILE)), new BoxAIExtractMetadataTemplate(templateKey, "enterprise"), - null, agentExtractStructured); JsonObject sourceJson = response.getSourceJson(); assertThat(sourceJson.get("firstName").asString(), is(equalTo("John"))); diff --git a/src/main/java/com/box/sdk/BoxAI.java b/src/main/java/com/box/sdk/BoxAI.java index f63de014c..a38eb6316 100644 --- a/src/main/java/com/box/sdk/BoxAI.java +++ b/src/main/java/com/box/sdk/BoxAI.java @@ -248,6 +248,21 @@ public String toString() { } } + /** + * Sends an AI request to supported Large Language Models (LLMs) and extracts metadata in form of key-value pairs. + * Freeform metadata extraction does not require any metadata template setup before sending the request. + * + * @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 extractMetadataFreeform(BoxAPIConnection api, + String prompt, + List items) { + return extractMetadataFreeform(api, prompt, items, null); + } + /** * Sends an AI request to supported Large Language Models (LLMs) and extracts metadata in form of key-value pairs. * Freeform metadata extraction does not require any metadata template setup before sending the request. @@ -284,7 +299,83 @@ public static BoxAIResponse extractMetadataFreeform(BoxAPIConnection api, } } + /** + * Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of + * key-value pairs. For this request, you need to use an already defined metadata template or a define a + * schema yourself. + * + * @param api The API connection to be used by the created user. + * @param items The items to be processed by the LLM, currently only files are supported. + * @param template The metadata template to be used for the request. + * @return The response from the AI. + */ public static BoxAIExtractStructuredResponse extractMetadataStructured(BoxAPIConnection api, List items, + BoxAIExtractMetadataTemplate template) { + return extractMetadataStructured(api, items, template, null, null); + } + + /** + * Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of + * key-value pairs. For this request, you need to use an already defined metadata template or a define a + * schema yourself. + * + * @param api The API connection to be used by the created user. + * @param items The items to be processed by the LLM, currently only files are supported. + * @param template The metadata template to be used for the request. + * @param agent The AI agent configuration to be used for the request. + * @return The response from the AI. + */ + public static BoxAIExtractStructuredResponse extractMetadataStructured(BoxAPIConnection api, List items, + BoxAIExtractMetadataTemplate template, + BoxAIAgentExtractStructured agent) { + return extractMetadataStructured(api, items, template, null, agent); + } + + /** + * Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of + * key-value pairs. For this request, you need to use an already defined metadata template or a define a + * schema yourself. + * + * @param api The API connection to be used by the created user. + * @param items The items to be processed by the LLM, currently only files are supported. + * @param fields The fields to be extracted from the items. + * @return The response from the AI. + */ + public static BoxAIExtractStructuredResponse extractMetadataStructured(BoxAPIConnection api, List items, + List fields) { + return extractMetadataStructured(api, items, null, fields, null); + } + + /** + * Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of + * key-value pairs. For this request, you need to use an already defined metadata template or a define a + * schema yourself. + * + * @param api The API connection to be used by the created user. + * @param items The items to be processed by the LLM, currently only files are supported. + * @param fields The fields to be extracted from the items. + * @param agent The AI agent configuration to be used for the request. + * @return The response from the AI. + */ + public static BoxAIExtractStructuredResponse extractMetadataStructured(BoxAPIConnection api, List items, + List fields, + BoxAIAgentExtractStructured agent) { + return extractMetadataStructured(api, items, null, fields, agent); + } + + /** + * Sends an AI request to supported Large Language Models (LLMs) and returns extracted metadata as a set of + * key-value pairs. For this request, you need to use an already defined metadata template or a define a + * schema yourself. + * + * @param api The API connection to be used by the created user. + * @param items The items to be processed by the LLM, currently only files are supported. + * @param template The metadata template to be used for the request. + * @param fields The fields to be extracted from the items. + * @param agent The AI agent configuration to be used for the request. + * @return The response from the AI. + */ + private static BoxAIExtractStructuredResponse extractMetadataStructured(BoxAPIConnection api, List items, BoxAIExtractMetadataTemplate template, List fields, BoxAIAgentExtractStructured agent) { diff --git a/src/main/java/com/box/sdk/BoxAIExtractField.java b/src/main/java/com/box/sdk/BoxAIExtractField.java index cd45d9e7e..4aba85906 100644 --- a/src/main/java/com/box/sdk/BoxAIExtractField.java +++ b/src/main/java/com/box/sdk/BoxAIExtractField.java @@ -32,6 +32,13 @@ public class BoxAIExtractField extends BoxJSONObject { */ private String prompt; + /** + * Constructs a BoxAIExtractField object with a given key. + */ + public BoxAIExtractField(String key) { + this.key = key; + } + /** * Constructs a BoxAIExtractField object with a given type, description, display name, key, options, and prompt. * diff --git a/src/test/java/com/box/sdk/BoxAITest.java b/src/test/java/com/box/sdk/BoxAITest.java index 0217c83ea..5aabdfe31 100644 --- a/src/test/java/com/box/sdk/BoxAITest.java +++ b/src/test/java/com/box/sdk/BoxAITest.java @@ -315,23 +315,13 @@ public void testExtractMetadataFreeformSuccess() { } @Test - public void testExtractMetadataStructuredSuccess() { + public void testExtractMetadataStructuredWithTemplateSuccess() { String agentString = TestUtils.getFixture("BoxAI/GetAIAgentDefaultConfigExtractStructured200"); final String fileId = "12345"; final BoxAIExtractMetadataTemplate template = new BoxAIExtractMetadataTemplate("templateId", "enterprise"); final List items = Collections.singletonList(new BoxAIItem(fileId, BoxAIItem.Type.FILE)); final String result = TestUtils.getFixture("BoxAI/ExtractMetadataStructured200"); final BoxAIAgentExtractStructured agent = new BoxAIAgentExtractStructured(Json.parse(agentString).asObject()); - final BoxAIExtractField field = new BoxAIExtractField( - "text", - "The name of the file", - "Name", - "name", - new ArrayList() {{ - add(new BoxAIExtractFieldOption("option 1")); - add(new BoxAIExtractFieldOption("option 2")); - }}, - "What is the name of the file?"); final JsonObject expectedRequestBody = new JsonObject() .add("items", new JsonArray().add(new JsonObject() @@ -341,6 +331,49 @@ public void testExtractMetadataStructuredSuccess() { .add("type", "metadata_template") .add("template_key", template.getTemplateKey()) .add("scope", template.getScope())) + .add("ai_agent", agent.getJSONObject()); + + wireMockRule.stubFor(WireMock.post(WireMock.urlPathEqualTo("/2.0/ai/extract_structured")) + .withRequestBody(WireMock.equalToJson( + expectedRequestBody.toString() + )) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(result))); + + BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured( + api, + items, + template, + agent + ); + + assertThat(response.getSourceJson(), equalTo(Json.parse(result).asObject())); + assertThat(response.getSourceJson().get("firstName").asString(), equalTo("John")); + } + + @Test + public void testExtractMetadataStructuredWithFieldSuccess() { + String agentString = TestUtils.getFixture("BoxAI/GetAIAgentDefaultConfigExtractStructured200"); + final String fileId = "12345"; + final List items = Collections.singletonList(new BoxAIItem(fileId, BoxAIItem.Type.FILE)); + final String result = TestUtils.getFixture("BoxAI/ExtractMetadataStructured200"); + final BoxAIAgentExtractStructured agent = new BoxAIAgentExtractStructured(Json.parse(agentString).asObject()); + final BoxAIExtractField field = new BoxAIExtractField( + "text", + "The name of the file", + "Name", + "name", + new ArrayList() {{ + add(new BoxAIExtractFieldOption("option 1")); + add(new BoxAIExtractFieldOption("option 2")); + }}, + "What is the name of the file?"); + + final JsonObject expectedRequestBody = new JsonObject() + .add("items", new JsonArray().add(new JsonObject() + .add("type", "file") + .add("id", fileId))) .add("ai_agent", agent.getJSONObject()) .add("fields", new JsonArray().add(field.getJSONObject())); @@ -355,7 +388,6 @@ public void testExtractMetadataStructuredSuccess() { BoxAIExtractStructuredResponse response = BoxAI.extractMetadataStructured( api, items, - template, Collections.singletonList(field), agent ); From 188d4715f907de31523a8e12c9132845f0b654b4 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 13:49:29 +0200 Subject: [PATCH 7/8] Update code --- src/test/java/com/box/sdk/BoxAITest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/box/sdk/BoxAITest.java b/src/test/java/com/box/sdk/BoxAITest.java index 5aabdfe31..a54fbcb5e 100644 --- a/src/test/java/com/box/sdk/BoxAITest.java +++ b/src/test/java/com/box/sdk/BoxAITest.java @@ -365,9 +365,9 @@ public void testExtractMetadataStructuredWithFieldSuccess() { "Name", "name", new ArrayList() {{ - add(new BoxAIExtractFieldOption("option 1")); - add(new BoxAIExtractFieldOption("option 2")); - }}, + add(new BoxAIExtractFieldOption("option 1")); + add(new BoxAIExtractFieldOption("option 2")); + }}, "What is the name of the file?"); final JsonObject expectedRequestBody = new JsonObject() From a7f1d21f13882c2e14f76b915a83c7e703c6b1d8 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Cong Date: Fri, 11 Oct 2024 14:05:00 +0200 Subject: [PATCH 8/8] Update code --- doc/ai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ai.md b/doc/ai.md index 61e7e9dcd..fcc3a683e 100644 --- a/doc/ai.md +++ b/doc/ai.md @@ -128,7 +128,7 @@ BoxAIExtractStructuredResponse result = BoxAI.extractMetadataStructured( JsonObject sourceJson = result.getSourceJson(); ``` -To send an AI request to extract metadata from files with a custom fields, call static +To send an AI request to extract metadata from files with custom fields, call static [`extractMetadataStructured extractMetadataStructured(BoxAPIConnection api, List items, List fields)`][extract-metadata-structured-fields] method.