From f1e587d685419765e9ab81c763610847f8fa0317 Mon Sep 17 00:00:00 2001 From: vacuityv Date: Mon, 13 Nov 2023 05:17:36 +0800 Subject: [PATCH 01/11] feat(image): new feature for dalle api. (#393) You can set model, quality and style now. --- .../openai/image/CreateImageEditRequest.java | 5 +++++ .../openai/image/CreateImageRequest.java | 21 ++++++++++++++++--- .../image/CreateImageVariationRequest.java | 5 +++++ .../com/theokanning/openai/image/Image.java | 6 ++++++ .../openai/service/OpenAiService.java | 2 ++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java index 72046953..7d37f689 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java @@ -21,6 +21,11 @@ public class CreateImageEditRequest { @NonNull String prompt; + /** + * The model to use for image generation. Only dall-e-2 is supported at this time. Defaults to dall-e-2. + */ + String model; + /** * The number of images to generate. Must be between 1 and 10. Defaults to 1. */ diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java index b8a1d05d..13672c24 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java @@ -17,18 +17,28 @@ public class CreateImageRequest { /** - * A text description of the desired image(s). The maximum length in 1000 characters. + * A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3. */ @NonNull String prompt; /** - * The number of images to generate. Must be between 1 and 10. Defaults to 1. + * The model to use for image generation. Defaults to "dall-e-2". + */ + String model; + + /** + * The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported. Defaults to 1. */ Integer n; /** - * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". + * The quality of the image that will be generated. "hd" creates images with finer details and greater consistency across the image. This param is only supported for dall-e-3. Defaults to "standard". + */ + String quality; + + /** + * The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. Defaults to 1024x1024. */ String size; @@ -38,6 +48,11 @@ public class CreateImageRequest { @JsonProperty("response_format") String responseFormat; + /** + * The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3. Defaults to vivid. + */ + String style; + /** * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. */ diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java index 2bc0c5d1..f16f613d 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java @@ -20,6 +20,11 @@ public class CreateImageVariationRequest { */ Integer n; + /** + * The model to use for image generation. Only dall-e-2 is supported at this time. Defaults to dall-e-2. + */ + String model; + /** * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". */ diff --git a/api/src/main/java/com/theokanning/openai/image/Image.java b/api/src/main/java/com/theokanning/openai/image/Image.java index e3214844..6b8391ed 100644 --- a/api/src/main/java/com/theokanning/openai/image/Image.java +++ b/api/src/main/java/com/theokanning/openai/image/Image.java @@ -21,4 +21,10 @@ public class Image { */ @JsonProperty("b64_json") String b64Json; + + /** + * The prompt that was used to generate the image, if there was any revision to the prompt. + */ + @JsonProperty("revised_prompt") + String revisedPrompt; } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 0296c15c..cedbd805 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -250,6 +250,7 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File .setType(MediaType.get("multipart/form-data")) .addFormDataPart("prompt", request.getPrompt()) .addFormDataPart("size", request.getSize()) + .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -276,6 +277,7 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.get("multipart/form-data")) .addFormDataPart("size", request.getSize()) + .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); From 0ec5a9ee303f91f6d66da9e91ab726ca27f28534 Mon Sep 17 00:00:00 2001 From: Daniel Faria Date: Sun, 12 Nov 2023 18:22:08 -0300 Subject: [PATCH 02/11] add support to audio/createSpeech API (#392) --- .../openai/audio/CreateSpeechRequest.java | 45 +++++++++++++++++++ .../theokanning/openai/client/OpenAiApi.java | 4 ++ .../openai/service/OpenAiService.java | 5 +++ .../theokanning/openai/service/AudioTest.java | 19 ++++++++ 4 files changed, 73 insertions(+) create mode 100644 api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java diff --git a/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java new file mode 100644 index 00000000..6d2e69ac --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java @@ -0,0 +1,45 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateSpeechRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * The text to generate audio for. The maximum length is 4096 characters. + */ + @NonNull + String input; + + /** + * The voice to use when generating the audio. + */ + @NonNull + String voice; + + /** + * The format to audio in. Supported formats are mp3, opus, aac, and flac. Defaults to mp3. + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The speed of the generated audio. Select a value from 0.25 to 4.0. Defaults to 1.0. + */ + Double speed; +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 497dacd5..f2665ee2 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; import com.theokanning.openai.billing.BillingUsage; @@ -149,6 +150,9 @@ public interface OpenAiApi { @POST("/v1/audio/translations") Single createTranslation(@Body RequestBody requestBody); + @POST("/v1/audio/speech") + Single createSpeech(@Body CreateSpeechRequest requestBody); + @POST("/v1/moderations") Single createModeration(@Body ModerationRequest request); diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index cedbd805..ea59417e 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -8,6 +8,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; @@ -347,6 +348,10 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public ResponseBody createSpeech(CreateSpeechRequest request) { + return execute(api.createSpeech(request)); + } + /** * Calls the Open AI api, returns the response, and parses error messages if the request fails */ diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java index d5a54a23..9cb083de 100644 --- a/service/src/test/java/com/theokanning/openai/service/AudioTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -1,13 +1,18 @@ package com.theokanning.openai.service; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.time.Duration; +import okhttp3.MediaType; +import okhttp3.ResponseBody; + import static org.junit.jupiter.api.Assertions.*; @@ -69,4 +74,18 @@ void createTranslationVerbose() { assertTrue(result.getDuration() > 0); assertEquals(1, result.getSegments().size()); } + + @Test + void createSpeech() throws IOException { + CreateSpeechRequest createSpeechRequest = CreateSpeechRequest.builder() + .model("tts-1") + .input("Hello World.") + .voice("alloy") + .build(); + + final ResponseBody speech = service.createSpeech(createSpeechRequest); + assertNotNull(speech); + assertEquals(MediaType.get("audio/mpeg"), speech.contentType()); + assertTrue(speech.bytes().length > 0); + } } From fb62307a61f35075d198cd8298be8404ba2a0173 Mon Sep 17 00:00:00 2001 From: Remy Ohajinwa Date: Sun, 12 Nov 2023 21:32:56 +0000 Subject: [PATCH 03/11] Support Assistants (#395) * #390 Create Assistant #390 Create Assistant * #390 Retrieve Assistant * #390 Modify Assistant * RemyOhajinwa#390 Delete Assistant * RemyOhajinwa#390 List Assistants * RemyOhajinwa#390 Create Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * Remove DeleteAssistantResult import --------- Co-authored-by: Remy Ohajinwa Co-authored-by: Theo Kanning --- .../openai/assistants/Assistant.java | 24 ++ .../openai/assistants/AssistantBase.java | 55 +++++ .../openai/assistants/AssistantFile.java | 30 +++ .../assistants/AssistantFileRequest.java | 17 ++ .../openai/assistants/AssistantRequest.java | 6 + .../openai/assistants/AssistantSortOrder.java | 12 + .../openai/assistants/AssistantToolsEnum.java | 15 ++ .../openai/assistants/ListAssistant.java | 16 ++ .../assistants/ListAssistantQueryRequest.java | 39 ++++ .../theokanning/openai/assistants/Tool.java | 12 + .../openai/utils/TikTokensUtil.java | 7 +- .../theokanning/openai/client/OpenAiApi.java | 43 ++++ .../openai/service/OpenAiService.java | 46 ++++ .../openai/service/AssistantTest.java | 214 ++++++++++++++++++ .../test/resources/assistant-file-data.json | 1 + .../src/test/resources/assistants-data.html | 1 + 16 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/assistants/Assistant.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/Tool.java create mode 100644 service/src/test/java/com/theokanning/openai/service/AssistantTest.java create mode 100644 service/src/test/resources/assistant-file-data.json create mode 100644 service/src/test/resources/assistants-data.html diff --git a/api/src/main/java/com/theokanning/openai/assistants/Assistant.java b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java new file mode 100644 index 00000000..111ef169 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class Assistant extends AssistantBase { + + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type which is always 'assistant' + */ + String object; + + /** + * The Unix timestamp(in seconds) for when the assistant was created + */ + @JsonProperty("created_at") + Integer createdAt; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java new file mode 100644 index 00000000..d771bd30 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java @@ -0,0 +1,55 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantBase { + + /** + * ID of the model to use + */ + @NonNull + String model; + + /** + * The name of the assistant. The maximum length is 256 + */ + String name; + + /** + * The description of the assistant. + */ + String description; + + /** + * The system instructions that the assistant uses. + */ + String instructions; + + /** + * A list of tools enabled on the assistant. + */ + List tools; + + /** + * A list of file IDs attached to this assistant. + */ + @JsonProperty("file_ids") + List fields; + + /** + * Set of 16 key-value pairs that can be attached to an object. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java new file mode 100644 index 00000000..c5d551a9 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java @@ -0,0 +1,30 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class AssistantFile { + + /** + * The identifier of the Assistant File + */ + String id; + + /** + * The object type, which is always assistant.file. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the assistant file was created. + */ + @JsonProperty("created_at") + String createdAt; + + /** + * The assistant ID that the file is attached to + */ + @JsonProperty("assistant_id") + String assistantId; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java new file mode 100644 index 00000000..98ee009f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantFileRequest { + + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java new file mode 100644 index 00000000..dc0a66df --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java @@ -0,0 +1,6 @@ +package com.theokanning.openai.assistants; + + +public class AssistantRequest extends AssistantBase { + +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java new file mode 100644 index 00000000..9f784a66 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java @@ -0,0 +1,12 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AssistantSortOrder { + + @JsonProperty("asc") + ASC, + + @JsonProperty("desc") + DESC +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java new file mode 100644 index 00000000..f6b5021d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java @@ -0,0 +1,15 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AssistantToolsEnum { + + @JsonProperty("code_interpreter") + CODE_INTERPRETER, + + @JsonProperty("function") + FUNCTION, + + @JsonProperty("retrieval") + RETRIEVAL +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java new file mode 100644 index 00000000..8478a547 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java @@ -0,0 +1,16 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.OpenAiResponse; + +public class ListAssistant extends OpenAiResponse { + + @JsonProperty("first_id") + String firstId; + + @JsonProperty("last_id") + String lastId; + + @JsonProperty("has_more") + boolean hasMore; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java new file mode 100644 index 00000000..3e5f3c68 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java @@ -0,0 +1,39 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ListAssistantQueryRequest { + /** + * A limit on the number of objects to be returned. + * Limit can range between 1 and 100, and the default is 20 + */ + + Integer limit; + + /** + * Sort order by the 'created_at' timestamp of the objects. + * 'asc' for ascending order and 'desc' for descending order. + */ + AssistantSortOrder order; + + /** + * A cursor for use in pagination. after is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, + * your subsequent call can include after=obj_foo in order to fetch the next page of the list + */ + String after; + + /** + * A cursor for use in pagination. before is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, + * your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + */ + String before; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java new file mode 100644 index 00000000..00027d72 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -0,0 +1,12 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Tool { + AssistantToolsEnum type; +} diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java index c30871f7..0a50907e 100644 --- a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -32,6 +32,7 @@ public class TikTokensUtil { modelMap.put(ModelEnum.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4)); modelMap.put(ModelEnum.GPT_4_32K_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); modelMap.put(ModelEnum.GPT_4_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); + modelMap.put(ModelEnum.GPT_4_1106_preview.getName(), registry.getEncodingForModel(ModelType.GPT_4)); } /** @@ -261,7 +262,11 @@ public enum ModelEnum { * Temporary model, not recommended for use. */ GPT_4_32K_0314("gpt-4-32k-0314"), - ; + + /** + * Temporary model, not recommended for use. + */ + GPT_4_1106_preview("gpt-4-1106-preview"); private String name; } diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index f2665ee2..0bfc03aa 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,12 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; @@ -36,6 +42,7 @@ import retrofit2.http.*; import java.time.LocalDate; +import java.util.Map; public interface OpenAiApi { @@ -185,4 +192,40 @@ public interface OpenAiApi { @GET("v1/dashboard/billing/usage") Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants") + Single createAssistant(@Body AssistantBase request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}") + Single retrieveAssistant(@Path("assistant_id") String assistantId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants/{assistant_id}") + Single modifyAssistant(@Path("assistant_id") String assistantId, @Body AssistantBase request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/assistants/{assistant_id}") + Single deleteAssistant(@Path("assistant_id") String assistantId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants") + Single> listAssistants(@QueryMap Map filterRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants/{assistant_id}/files") + Single createAssistantFile(@Path("assistant_id") String assistantId, @Body AssistantFileRequest fileRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}/files/{file_id}") + Single retrieveAssistantFile(@Path("assistant_id") String assistantId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/assistants/{assistant_id}/files/{file_id}") + Single deleteAssistantFile(@Path("assistant_id") String assistantId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}/files") + Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index ea59417e..0a7d9459 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -1,6 +1,7 @@ package com.theokanning.openai.service; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; @@ -8,6 +9,12 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; @@ -53,6 +60,7 @@ import java.time.Duration; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -348,6 +356,44 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public Assistant createAssistant(AssistantBase request) { + return execute(api.createAssistant(request)); + } + + public Assistant retrieveAssistant(String assistantId) { + return execute(api.retrieveAssistant(assistantId)); + } + + public Assistant modifyAssistant(String assistantId, AssistantBase request) { + return execute(api.modifyAssistant(assistantId, request)); + } + + public DeleteResult deleteAssistant(String assistantId) { + return execute(api.deleteAssistant(assistantId)); + } + + public ListAssistant listAssistants(ListAssistantQueryRequest filterRequest) { + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + return execute(api.listAssistants(queryParameters)); + } + + public AssistantFile createAssistantFile(String assistantId, AssistantFileRequest fileRequest) { + return execute(api.createAssistantFile(assistantId, fileRequest)); + } + + public AssistantFile retrieveAssistantFile(String assistantId, String fileId) { + return execute(api.retrieveAssistantFile(assistantId, fileId)); + } + + public DeleteResult deleteAssistantFile(String assistantId, String fileId) { + return execute(api.deleteAssistantFile(assistantId, fileId)); + } + + public ListAssistant listAssistantFiles(String assistantId, ListAssistantQueryRequest filterRequest) { + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + return execute(api.listAssistantFiles(assistantId, queryParameters)); + } + public ResponseBody createSpeech(CreateSpeechRequest request) { return execute(api.createSpeech(request)); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java new file mode 100644 index 00000000..4f21b739 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -0,0 +1,214 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.assistants.AssistantSortOrder; +import com.theokanning.openai.assistants.AssistantToolsEnum; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.file.File; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class AssistantTest { + public static final String MATH_TUTOR = "Math Tutor"; + public static final String ASSISTANT_INSTRUCTION = "You are a personal Math Tutor."; + + static String token = System.getenv("OPENAI_TOKEN");; + + static OpenAiService service = new OpenAiService(token); + + + + @Test + void retrieveAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + Assistant retrieveAssistantResponse = service.retrieveAssistant(createAssistantResponse.getId()); + validateAssistantResponse(retrieveAssistantResponse); + } + + @Test + void modifyAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + String modifiedName = MATH_TUTOR + " Modified"; + createAssistantResponse.setName(modifiedName);//modify a field + + Assistant modifiedAssistantResponse = service.modifyAssistant(createAssistantResponse.getId(), createAssistantResponse); + assertNotNull(modifiedAssistantResponse); + assertEquals(modifiedName, modifiedAssistantResponse.getName()); + } + + @Test + void deleteAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + DeleteResult deletedAssistant = service.deleteAssistant(createAssistantResponse.getId()); + + assertNotNull(deletedAssistant); + assertEquals(createAssistantResponse.getId(), deletedAssistant.getId()); + assertTrue(deletedAssistant.isDeleted()); + } + + @Test + void listAssistants() { + ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); + + assertNotNull(assistants); + // this should be more than 2 depending on how many times createAndValidateAssistant method is called + assertTrue(assistants.getData().size() > 1); + } + + @Test + void listAssistants_returnsTwoAssistants() { + int expectedLimit = 2; + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + assertEquals(expectedLimit, data.size()); + } + + + + @Test + void listAssistants_returnsAscSortedAssistants() { + int expectedLimit = 3; + + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .order(AssistantSortOrder.ASC) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + + boolean firstTwoAscending = data.get(0).getCreatedAt() <= data.get(1).getCreatedAt(); + boolean lastTwoAscending = data.get(1).getCreatedAt() <= data.get(2).getCreatedAt(); + assertTrue(firstTwoAscending && lastTwoAscending); + } + + @Test + void listAssistants_returnsDescSortedAssistants() { + int expectedLimit = 3; + + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .order(AssistantSortOrder.DESC) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + + boolean firstTwoDescending = data.get(0).getCreatedAt() >= data.get(1).getCreatedAt(); + boolean lastTwoDescending = data.get(1).getCreatedAt() >= data.get(2).getCreatedAt(); + assertTrue(firstTwoDescending && lastTwoDescending); + } + + @Test + void createAssistantFile() { + File uploadedFile = uploadAssistantFile(); + + Assistant assistant = createAndValidateAssistant(); + + AssistantFile assistantFile = service.createAssistantFile(assistant.getId(), new AssistantFileRequest(uploadedFile.getId())); + + assertNotNull(assistantFile); + assertEquals(uploadedFile.getId(), assistantFile.getId()); + assertEquals(assistant.getId(), assistantFile.getAssistantId()); + } + + + + @Test + void retrieveAssistantFile() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @Test + void deleteAssistantFile() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @Test + void listAssistantFiles() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @AfterAll + static void clean() { + //Clean up all data created during this test + ListAssistantQueryRequest queryFilter = ListAssistantQueryRequest.builder() + .limit(100) + .build(); + ListAssistant assistantListAssistant = service.listAssistants(queryFilter); + assistantListAssistant.getData().forEach(assistant ->{ + service.deleteAssistant(assistant.getId()); + }); + } + + private static File uploadAssistantFile() { + String filePath = "src/test/resources/assistants-data.html"; + return service.uploadFile("assistants", filePath); + } + + private static Assistant createAndValidateAssistant() { + AssistantBase assistantRequest = assistantStub(); + Assistant createAssistantResponse = service.createAssistant(assistantRequest); + validateAssistantResponse(createAssistantResponse); + + return createAssistantResponse; + } + + + private static AssistantBase assistantStub() { + return AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name(MATH_TUTOR) + .instructions(ASSISTANT_INSTRUCTION) + .tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER))) + .build(); + } + + private static void validateAssistantResponse(Assistant assistantResponse) { + assertNotNull(assistantResponse); + assertNotNull(assistantResponse.getId()); + assertNotNull(assistantResponse.getCreatedAt()); + assertNotNull(assistantResponse.getObject()); + assertEquals(assistantResponse.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); + assertEquals(MATH_TUTOR, assistantResponse.getName()); + } + + private static List validateListAssistants(ListAssistant assistants) { + assertNotNull(assistants); + List data = assistants.getData(); + assertNotNull(data); + return data; + } +} diff --git a/service/src/test/resources/assistant-file-data.json b/service/src/test/resources/assistant-file-data.json new file mode 100644 index 00000000..8b42bc07 --- /dev/null +++ b/service/src/test/resources/assistant-file-data.json @@ -0,0 +1 @@ +{"prompt": "prompt", "completion": "text"} \ No newline at end of file diff --git a/service/src/test/resources/assistants-data.html b/service/src/test/resources/assistants-data.html new file mode 100644 index 00000000..6c70bcfe --- /dev/null +++ b/service/src/test/resources/assistants-data.html @@ -0,0 +1 @@ + \ No newline at end of file From fee68bad25374d4553654d323652289ad9724696 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:23:26 -0600 Subject: [PATCH 04/11] Fix tests --- .../openai/service/OpenAiService.java | 10 +++- .../openai/service/AssistantTest.java | 60 +------------------ 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 0a7d9459..04f2e459 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -259,7 +259,6 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File .setType(MediaType.get("multipart/form-data")) .addFormDataPart("prompt", request.getPrompt()) .addFormDataPart("size", request.getSize()) - .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -272,6 +271,10 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File builder.addFormDataPart("mask", "mask", maskBody); } + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + return execute(api.createImageEdit(builder.build())); } @@ -286,7 +289,6 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.get("multipart/form-data")) .addFormDataPart("size", request.getSize()) - .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -294,6 +296,10 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav builder.addFormDataPart("n", request.getN().toString()); } + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + return execute(api.createImageVariation(builder.build())); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java index 4f21b739..781161b2 100644 --- a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -19,9 +19,7 @@ import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class AssistantTest { @@ -70,59 +68,7 @@ void listAssistants() { ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); assertNotNull(assistants); - // this should be more than 2 depending on how many times createAndValidateAssistant method is called - assertTrue(assistants.getData().size() > 1); - } - - @Test - void listAssistants_returnsTwoAssistants() { - int expectedLimit = 2; - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - assertEquals(expectedLimit, data.size()); - } - - - - @Test - void listAssistants_returnsAscSortedAssistants() { - int expectedLimit = 3; - - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .order(AssistantSortOrder.ASC) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - - boolean firstTwoAscending = data.get(0).getCreatedAt() <= data.get(1).getCreatedAt(); - boolean lastTwoAscending = data.get(1).getCreatedAt() <= data.get(2).getCreatedAt(); - assertTrue(firstTwoAscending && lastTwoAscending); - } - - @Test - void listAssistants_returnsDescSortedAssistants() { - int expectedLimit = 3; - - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .order(AssistantSortOrder.DESC) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - - boolean firstTwoDescending = data.get(0).getCreatedAt() >= data.get(1).getCreatedAt(); - boolean lastTwoDescending = data.get(1).getCreatedAt() >= data.get(2).getCreatedAt(); - assertTrue(firstTwoDescending && lastTwoDescending); + assertFalse(assistants.getData().isEmpty()); } @Test @@ -138,8 +84,6 @@ void createAssistantFile() { assertEquals(assistant.getId(), assistantFile.getAssistantId()); } - - @Test void retrieveAssistantFile() { //TODO From a356a2e713ed7364aef601da29cb2a9434dbaa79 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:34:23 -0600 Subject: [PATCH 05/11] Add NoArgsConstructor to ChatFunction --- .../com/theokanning/openai/completion/chat/ChatFunction.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java index 7d72829a..67162edf 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -3,11 +3,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.NonNull; import java.util.function.Function; @Data +@NoArgsConstructor public class ChatFunction { @NonNull @@ -46,7 +48,8 @@ public Builder executor(Class requestClass, Function executor) } public ChatFunction build() { - ChatFunction chatFunction = new ChatFunction(name); + ChatFunction chatFunction = new ChatFunction(); + chatFunction.setName(name); chatFunction.setDescription(description); chatFunction.setParametersClass(parameters); chatFunction.setExecutor(executor); From 47fe4780238485c34f1401cdeb5047760b9b872f Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:41:25 -0600 Subject: [PATCH 06/11] Change compilation github workflow (#398) Only the released artifacts need to compile with 1.8 --- .github/workflows/pull_request.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 77d94afb..8ea36f7e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,5 +17,14 @@ jobs: distribution: temurin java-version: 8 - - name: Compile - run: ./gradlew compileJava compileTestJava + - name: Compile Artifacts + run: ./gradlew api:compileJava client:compileJava service:compileJava + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + + - name: Compile Tests + run: ./gradlew compileTestJava From 3a2d01080661a50d3a6e68fa71214b01df345d5a Mon Sep 17 00:00:00 2001 From: Bartosz Date: Sun, 12 Nov 2023 23:44:35 +0100 Subject: [PATCH 07/11] Implement creation of "function" parameters in runtime (#339) * Enable dynamic definition of "function" parameters instead of using Class instance * Add tests to new "function" capabilities * Add example of creating "function" parameters in runtime * Add documentation to ChatFunctions Co-authored-by: Theo Kanning --- .../chat/ChatCompletionRequest.java | 2 +- .../openai/completion/chat/ChatFunction.java | 11 +++ .../completion/chat/ChatFunctionDynamic.java | 62 +++++++++++++ .../chat/ChatFunctionParameters.java | 27 ++++++ .../completion/chat/ChatFunctionProperty.java | 25 ++++++ .../OpenAiApiDynamicFunctionExample.java | 90 +++++++++++++++++++ .../openai/service/ChatCompletionTest.java | 90 +++++++++++++++++++ 7 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java create mode 100644 example/src/main/java/example/OpenAiApiDynamicFunctionExample.java diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java index 1f055fbf..e4479ff3 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java @@ -98,7 +98,7 @@ public class ChatCompletionRequest { /** * A list of the available functions. */ - List functions; + List functions; /** * Controls how the model responds to function calls, as specified in the OpenAI documentation. diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java index 67162edf..820f4bd6 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -12,9 +12,20 @@ @NoArgsConstructor public class ChatFunction { + /** + * The name of the function being called. + */ @NonNull private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ private String description; + + /** + * The parameters the functions accepts. + */ @JsonProperty("parameters") private Class parametersClass; diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java new file mode 100644 index 00000000..9b4f2070 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java @@ -0,0 +1,62 @@ +package com.theokanning.openai.completion.chat; + +import lombok.Data; +import lombok.NonNull; + + +@Data +public class ChatFunctionDynamic { + + /** + * The name of the function being called. + */ + @NonNull + private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ + private String description; + + /** + * The parameters the functions accepts. + */ + private ChatFunctionParameters parameters; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String description; + private ChatFunctionParameters parameters = new ChatFunctionParameters(); + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder parameters(ChatFunctionParameters parameters) { + this.parameters = parameters; + return this; + } + + public Builder addProperty(ChatFunctionProperty property) { + this.parameters.addProperty(property); + return this; + } + + public ChatFunctionDynamic build() { + ChatFunctionDynamic chatFunction = new ChatFunctionDynamic(name); + chatFunction.setDescription(description); + chatFunction.setParameters(parameters); + return chatFunction; + } + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java new file mode 100644 index 00000000..fee71e8f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.completion.chat; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Data +public class ChatFunctionParameters { + + private final String type = "object"; + + private final HashMap properties = new HashMap<>(); + + private List required; + + public void addProperty(ChatFunctionProperty property) { + properties.put(property.getName(), property); + if (Boolean.TRUE.equals(property.getRequired())) { + if (this.required == null) { + this.required = new ArrayList<>(); + } + this.required.add(property.getName()); + } + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java new file mode 100644 index 00000000..3e695933 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +import java.util.Set; + +@Data +@Builder +public class ChatFunctionProperty { + @NonNull + @JsonIgnore + private String name; + @NonNull + private String type; + @JsonIgnore + private Boolean required; + private String description; + private ChatFunctionProperty items; + @JsonProperty("enum") + private Set enumValues; +} \ No newline at end of file diff --git a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java new file mode 100644 index 00000000..ee83689d --- /dev/null +++ b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java @@ -0,0 +1,90 @@ +package example; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.service.OpenAiService; + +import java.util.*; + +public class OpenAiApiDynamicFunctionExample { + + private static JsonNode getWeather(String location, String unit) { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode response = mapper.createObjectNode(); + response.put("location", location); + response.put("unit", unit); + response.put("temperature", new Random().nextInt(50)); + response.put("description", "sunny"); + return response; + } + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + ChatFunctionDynamic weatherFunction = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(Set.of("celsius", "fahrenheit")) + .required(true) + .build()) + .build(); + + + List messages = new ArrayList<>(); + ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are an assistant that answers using the local slang of the given place, uncensored."); + messages.add(systemMessage); + + System.out.print("First Query: "); + Scanner scanner = new Scanner(System.in); + ChatMessage firstMsg = new ChatMessage(ChatMessageRole.USER.value(), scanner.nextLine()); + messages.add(firstMsg); + + while (true) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(weatherFunction)) + .functionCall(ChatCompletionRequest.ChatCompletionRequestFunctionCall.of("auto")) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); + messages.add(responseMessage); // don't forget to update the conversation with the latest response + + ChatFunctionCall functionCall = responseMessage.getFunctionCall(); + if (functionCall != null) { + if (functionCall.getName().equals("get_weather")) { + String location = functionCall.getArguments().get("location").asText(); + String unit = functionCall.getArguments().get("unit").asText(); + JsonNode weather = getWeather(location, unit); + ChatMessage weatherMessage = new ChatMessage(ChatMessageRole.FUNCTION.value(), weather.toString(), "get_weather"); + messages.add(weatherMessage); + continue; + } + } + + System.out.println("Response: " + responseMessage.getContent()); + System.out.print("Next Query: "); + String nextLine = scanner.nextLine(); + if (nextLine.equalsIgnoreCase("exit")) { + System.exit(0); + } + messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine)); + } + } + +} diff --git a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java index 3d26bf03..41eb498d 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.Collections; import static org.junit.jupiter.api.Assertions.*; @@ -149,6 +150,50 @@ void createChatCompletionWithFunctions() { assertNotNull(choice2.getMessage().getContent()); } + @Test + void createChatCompletionWithDynamicFunctions() { + ChatFunctionDynamic function = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(Set.of("celsius", "fahrenheit")) + .required(true) + .build()) + .build(); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(function)) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatCompletionChoice choice = service.createChatCompletion(chatCompletionRequest).getChoices().get(0); + assertEquals("function_call", choice.getFinishReason()); + assertNotNull(choice.getMessage().getFunctionCall()); + assertEquals("get_weather", choice.getMessage().getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, choice.getMessage().getFunctionCall().getArguments()); + assertNotNull(choice.getMessage().getFunctionCall().getArguments().get("location")); + assertNotNull(choice.getMessage().getFunctionCall().getArguments().get("unit")); + } + @Test void streamChatCompletionWithFunctions() { final List functions = Collections.singletonList(ChatFunction.builder() @@ -214,4 +259,49 @@ void streamChatCompletionWithFunctions() { assertNotNull(accumulatedMessage2.getContent()); } + @Test + void streamChatCompletionWithDynamicFunctions() { + ChatFunctionDynamic function = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(Set.of("celsius", "fahrenheit")) + .required(true) + .build()) + .build(); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(function)) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatMessage accumulatedMessage = service.mapStreamToAccumulator(service.streamChatCompletion(chatCompletionRequest)) + .blockingLast() + .getAccumulatedMessage(); + assertNotNull(accumulatedMessage.getFunctionCall()); + assertEquals("get_weather", accumulatedMessage.getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, accumulatedMessage.getFunctionCall().getArguments()); + assertNotNull(accumulatedMessage.getFunctionCall().getArguments().get("location")); + assertNotNull(accumulatedMessage.getFunctionCall().getArguments().get("unit")); + } + } From 7fbd0a7b4c3138f4788ee9aaf021573d5c24a5a5 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:50:32 -0600 Subject: [PATCH 08/11] Update to version 0.17.0 --- .github/workflows/publish.yml | 10 ++++++++-- .github/workflows/test.yml | 4 ++-- gradle.properties | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6e978294..0da11d72 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,17 +12,23 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 1.8 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 8 + java-version: 17 - name: Test run: ./gradlew test env: OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} + - name: Set up JDK 1.8 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + - name: Publish run: ./gradlew build publish --no-parallel env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 830e797f..cf169464 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 1.8 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 8 + java-version: 17 - name: Test run: ./gradlew test --stacktrace diff --git a/gradle.properties b/gradle.properties index 85a94ec8..671b33b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.16.1 +VERSION_NAME=0.17.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 8768a4977ab3865f72a36d728b8eca55a0952157 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:57:43 -0600 Subject: [PATCH 09/11] Deprecate Model.permissions This field is no longer returned by OpenAI --- api/src/main/java/com/theokanning/openai/model/Model.java | 3 ++- .../test/java/com/theokanning/openai/service/ModelTest.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/model/Model.java b/api/src/main/java/com/theokanning/openai/model/Model.java index 5180f2ae..2add679d 100644 --- a/api/src/main/java/com/theokanning/openai/model/Model.java +++ b/api/src/main/java/com/theokanning/openai/model/Model.java @@ -29,8 +29,9 @@ public class Model { public String ownedBy; /** - * List of permissions for this model + * List of permissions for this model. No longer returned by OpenAI */ + @Deprecated public List permission; /** diff --git a/service/src/test/java/com/theokanning/openai/service/ModelTest.java b/service/src/test/java/com/theokanning/openai/service/ModelTest.java index 4461dacf..31d23da9 100644 --- a/service/src/test/java/com/theokanning/openai/service/ModelTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ModelTest.java @@ -27,6 +27,5 @@ void getModel() { assertEquals("ada", ada.id); assertEquals("openai", ada.ownedBy); - assertFalse(ada.permission.isEmpty()); } } From 987801bcc1c3f0b7aad006e0dad0577159710bbc Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 17:05:00 -0600 Subject: [PATCH 10/11] Remove example project from publishing build step --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0da11d72..d2e93051 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ jobs: java-version: 8 - name: Publish - run: ./gradlew build publish --no-parallel + run: ./gradlew build -x :example:build publish --no-parallel env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} From bc81a295e65b383dbd1134f8cf10e6c1294f4c6f Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 17:18:56 -0600 Subject: [PATCH 11/11] Require all steps to run in java 1.8 There's definitely a better way to manage version, but trying to run individual steps against later versions was causing my problems than it solved. I'll think of the best way to handle this. --- .github/workflows/gradle-wrapper-validation.yml | 14 -------------- .github/workflows/publish.yml | 12 +++--------- .github/workflows/pull_request.yml | 13 ++----------- .github/workflows/test.yml | 4 ++-- .../example/OpenAiApiDynamicFunctionExample.java | 2 +- .../openai/service/ChatCompletionTest.java | 10 +++------- 6 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/gradle-wrapper-validation.yml diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index 805c1fcf..00000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: "Validate Gradle Wrapper" -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d2e93051..6e978294 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,25 +12,19 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 17 + - name: Set up JDK 1.8 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 8 - name: Test run: ./gradlew test env: OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} - - name: Set up JDK 1.8 - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 8 - - name: Publish - run: ./gradlew build -x :example:build publish --no-parallel + run: ./gradlew build publish --no-parallel env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8ea36f7e..77d94afb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,14 +17,5 @@ jobs: distribution: temurin java-version: 8 - - name: Compile Artifacts - run: ./gradlew api:compileJava client:compileJava service:compileJava - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 17 - - - name: Compile Tests - run: ./gradlew compileTestJava + - name: Compile + run: ./gradlew compileJava compileTestJava diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf169464..830e797f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 17 + - name: Set up JDK 1.8 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 8 - name: Test run: ./gradlew test --stacktrace diff --git a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java index ee83689d..75f9b8e2 100644 --- a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java +++ b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java @@ -36,7 +36,7 @@ public static void main(String... args) { .name("unit") .type("string") .description("The temperature unit, can be 'celsius' or 'fahrenheit'") - .enumValues(Set.of("celsius", "fahrenheit")) + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) .required(true) .build()) .build(); diff --git a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java index 41eb498d..25f0defb 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -7,11 +7,7 @@ import com.theokanning.openai.completion.chat.*; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.Collections; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -164,7 +160,7 @@ void createChatCompletionWithDynamicFunctions() { .name("unit") .type("string") .description("The temperature unit, can be 'celsius' or 'fahrenheit'") - .enumValues(Set.of("celsius", "fahrenheit")) + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) .required(true) .build()) .build(); @@ -273,7 +269,7 @@ void streamChatCompletionWithDynamicFunctions() { .name("unit") .type("string") .description("The temperature unit, can be 'celsius' or 'fahrenheit'") - .enumValues(Set.of("celsius", "fahrenheit")) + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) .required(true) .build()) .build();