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/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/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/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 7d72829a..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 @@ -3,16 +3,29 @@ 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 { + /** + * 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; @@ -46,7 +59,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); 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/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/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/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 497dacd5..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,13 @@ 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; import com.theokanning.openai.billing.BillingUsage; @@ -35,6 +42,7 @@ import retrofit2.http.*; import java.time.LocalDate; +import java.util.Map; public interface OpenAiApi { @@ -149,6 +157,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); @@ -181,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/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java new file mode 100644 index 00000000..75f9b8e2 --- /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(new HashSet<>(Arrays.asList("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/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 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..04f2e459 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,13 @@ 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; import com.theokanning.openai.audio.TranscriptionResult; @@ -52,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; @@ -262,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())); } @@ -283,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())); } @@ -345,6 +362,48 @@ 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)); + } + /** * 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/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java new file mode 100644 index 00000000..781161b2 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -0,0 +1,158 @@ +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.*; + + +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); + assertFalse(assistants.getData().isEmpty()); + } + + @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/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); + } } 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..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,10 +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.Collections; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -149,6 +146,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(new HashSet<>(Arrays.asList("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 +255,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(new HashSet<>(Arrays.asList("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")); + } + } 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()); } } 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