diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..fc0a8d1f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +Thanks for submitting a pull request! Please check CONTRIBUTING.md for style guidelines. + +### Changes +Describe your changes here + +### New API Checklist +See CONTRIBUTING.md for more info. + +1. [ ] Documentation for every variable +2. [ ] Class-level documentation +3. [ ] POJO JSON parsing tests +4. [ ] Service integration tests \ No newline at end of file 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/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..77d94afb --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,21 @@ +name: Compile + +on: + pull_request: + branches: [ main ] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + + - name: Compile + run: ./gradlew compileJava compileTestJava diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbf1af23..830e797f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,6 @@ name: Test on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: test: diff --git a/.gitignore b/.gitignore index 71295530..4b473169 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ build # Ignore any files in /bin and /obj Folders **/bin/* **/obj/* + +# Ignore the macOS folder attribute file +**/.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3a930669 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# How to Contribute + +## How to Add a New API + +### Add POJOs to API library +I usually have ChatGPT write them for me by copying and pasting from teh OpenAI API reference ([example chat](https://chat.openai.com/share/af48ef11-0354-40b2-a8e2-3bf8e93a94a3)), but double check everything because Chat always makes mistakes, especially around adding `@JsonProperty` annotations. + +- Make all java variables camel case, and use `@JsonProperty` for fields that OpenAI returns as snake case +- Include comments for each variable, I take these directly from the OpenAI website +- Include `@Data` on every response class, and `@Builder @NoArgsConstructor @AllArgsConstructor @Data` on every request +- Include basic class-level documentation and a link to the OpenAI reference page, [example](api/src/main/java/com/theokanning/openai/threads/Thread.java) +- Add a JSON test for every new java object, this ensures that your definition and variable name overrides are correct. + - Copy the sample response from OpenAI into an api test [fixture](api/src/test/resources/fixtures) + - Add any missing fields to the JSON file (OpenAI doesn't always include everything) + - Add the class name to the test cases here [JSON test](api/src/test/java/com/theokanning/openai/JsonTest.java) + +### Add to [OpenAiApi](client/src/main/java/com/theokanning/openai/client/OpenAiApi.java) +This is usually straightforward, use [OpenAiResponse](api/src/main/java/com/theokanning/openai/OpenAiResponse.java) for endpoints that return lists. + +### Add to [OpenAiService](service/src/main/java/com/theokanning/openai/OpenAiService.java) + +### Add an Integration Test +Since 99% of the work of this library is done on OpenAI's servers, the objective of these tests is to call each endpoint at least once. +Specify every available parameter to make sure that OpenAI accepts everything, but don't create extra test cases unless a parameter drastically affects the results. +For example, [CompletionTest](service/src/test/java/com/theokanning/openai/service/CompletionTest.java) has one test for normal completions, and one for streaming. + +If your test relies on creating and retrieving external resources, [FineTuningTest](service/src/test/java/com/theokanning/openai/service/FineTuningTest.java) is a good example of how to share resources between tests and clean up afterwards. \ No newline at end of file diff --git a/README.md b/README.md index 5d420a65..3698db58 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ ![Maven Central](https://img.shields.io/maven-central/v/com.theokanning.openai-gpt3-java/client?color=blue) -> ⚠️ Please switch to using the new 'service' library if you need to use OpenAiService. The old 'client' OpenAiService is deprecated as of 0.10.0. +> ⚠️ Notice: This project is no longer maintained and has been archived as of June 6th, 2024. +Thank you to everyone who has contributed and supported this project. While the repository will remain available in its current state, no further updates or support will be provided. Please feel free to fork and modify the code as needed. + > ⚠️OpenAI has deprecated all Engine-based APIs. See [Deprecated Endpoints](https://github.com/TheoKanning/openai-java#deprecated-endpoints) below for more info. # OpenAI-Java @@ -19,13 +21,16 @@ as well as an example project using the service. - [Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) - [Edits](https://platform.openai.com/docs/api-reference/edits) - [Embeddings](https://platform.openai.com/docs/api-reference/embeddings) +- [Audio](https://platform.openai.com/docs/api-reference/audio) - [Files](https://platform.openai.com/docs/api-reference/files) -- [Fine-tunes](https://platform.openai.com/docs/api-reference/fine-tunes) +- [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) - [Images](https://platform.openai.com/docs/api-reference/images) - [Moderations](https://platform.openai.com/docs/api-reference/moderations) +- [Assistants](https://platform.openai.com/docs/api-reference/assistants) #### Deprecated by OpenAI - [Engines](https://platform.openai.com/docs/api-reference/engines) +- [Legacy Fine-Tunes](https://platform.openai.com/docs/guides/legacy-fine-tuning) ## Importing @@ -59,7 +64,7 @@ If you're looking for the fastest solution, import the `service` module and use OpenAiService service = new OpenAiService("your_token"); CompletionRequest completionRequest = CompletionRequest.builder() .prompt("Somebody once told me the world is gonna roll me") - .model("ada") + .model("babbage-002"") .echo(true) .build(); service.createCompletion(completionRequest).getChoices().forEach(System.out::println); @@ -95,15 +100,90 @@ OpenAiApi api = retrofit.create(OpenAiApi.class); OpenAiService service = new OpenAiService(api); ``` +### Functions +You can create your functions and define their executors easily using the ChatFunction class, along with any of your custom classes that will serve to define their available parameters. You can also process the functions with ease, with the help of an executor called FunctionExecutor. + +First we declare our function parameters: +```java +public class Weather { + @JsonPropertyDescription("City and state, for example: León, Guanajuato") + public String location; + @JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'") + @JsonProperty(required = true) + public WeatherUnit unit; +} +public enum WeatherUnit { + CELSIUS, FAHRENHEIT; +} +public static class WeatherResponse { + public String location; + public WeatherUnit unit; + public int temperature; + public String description; + + // constructor +} +``` + +Next, we declare the function itself and associate it with an executor, in this example we will fake a response from some API: +```java +ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny")) + .build() +``` + +Then, we employ the FunctionExecutor object from the 'service' module to assist with execution and transformation into an object that is ready for the conversation: +```java +List functionList = // list with functions +FunctionExecutor functionExecutor = new FunctionExecutor(functionList); + +List messages = new ArrayList<>(); +ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Tell me the weather in Barcelona."); +messages.add(userMessage); +ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .functionCall(new ChatCompletionRequestFunctionCall("auto")) + .maxTokens(256) + .build(); + +ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); +ChatFunctionCall functionCall = responseMessage.getFunctionCall(); // might be null, but in this case it is certainly a call to our 'get_weather' function. + +ChatMessage functionResponseMessage = functionExecutor.executeAndConvertToMessageHandlingExceptions(functionCall); +messages.add(response); +``` +> **Note:** The `FunctionExecutor` class is part of the 'service' module. + +You can also create your own function executor. The return object of `ChatFunctionCall.getArguments()` is a JsonNode for simplicity and should be able to help you with that. + +For a more in-depth look, refer to a conversational example that employs functions in: [OpenAiApiFunctionsExample.java](example/src/main/java/example/OpenAiApiFunctionsExample.java). +Or for an example using functions and stream: [OpenAiApiFunctionsWithStreamExample.java](example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java) + ### Streaming thread shutdown -If you want to shut down your process immediately after streaming responses, call `OpenAiService.shutdown()`. +If you want to shut down your process immediately after streaming responses, call `OpenAiService.shutdownExecutor()`. This is not necessary for non-streaming calls. ## Running the example project All the [example](example/src/main/java/example/OpenAiApiExample.java) project requires is your OpenAI api token ```bash export OPENAI_TOKEN="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" -./gradlew example:run +``` +You can try all the capabilities of this project using: +```bash +./gradlew runExampleOne +``` +And you can also try the new capability of using functions: +```bash +./gradlew runExampleTwo +``` +Or functions with 'stream' mode enabled: +```bash +./gradlew runExampleThree ``` ## FAQ @@ -111,6 +191,11 @@ export OPENAI_TOKEN="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" Yes! GPT-4 uses the ChatCompletion Api, and you can see the latest model options [here](https://platform.openai.com/docs/models/gpt-4). GPT-4 is currently in a limited beta (as of 4/1/23), so make sure you have access before trying to use it. +### Does this support functions? +Absolutely! It is very easy to use your own functions without worrying about doing the dirty work. +As mentioned above, you can refer to [OpenAiApiFunctionsExample.java](example/src/main/java/example/OpenAiApiFunctionsExample.java) or +[OpenAiApiFunctionsWithStreamExample.java](example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java) projects for an example. + ### Why am I getting connection timeouts? Make sure that OpenAI is available in your country. diff --git a/api/build.gradle b/api/build.gradle index 8c25de96..db97b833 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -2,12 +2,14 @@ apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" dependencies { - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.0' - compileOnly 'org.projectlombok:lombok:1.18.24' - annotationProcessor 'org.projectlombok:lombok:1.18.24' + api libs.jacksonAnnotations + api libs.jacksonDatabind + api libs.jtokkit + compileOnly libs.lombok + annotationProcessor libs.lombok - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' - testImplementation(platform('org.junit:junit-bom:5.8.2')) + testImplementation libs.jacksonDatabind + testImplementation(platform(libs.junitBom)) testImplementation('org.junit.jupiter:junit-jupiter') } diff --git a/api/src/main/java/com/theokanning/openai/ListSearchParameters.java b/api/src/main/java/com/theokanning/openai/ListSearchParameters.java new file mode 100644 index 00000000..53fd484f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/ListSearchParameters.java @@ -0,0 +1,51 @@ +package com.theokanning.openai; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Common options when getting a list of objects + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ListSearchParameters { + /** + * 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. + */ + Order 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; + + public enum Order { + @JsonProperty("asc") + ASCENDING, + + @JsonProperty("desc") + DESCENDING + } +} diff --git a/api/src/main/java/com/theokanning/openai/OpenAiResponse.java b/api/src/main/java/com/theokanning/openai/OpenAiResponse.java index 4b718aad..f062fc77 100644 --- a/api/src/main/java/com/theokanning/openai/OpenAiResponse.java +++ b/api/src/main/java/com/theokanning/openai/OpenAiResponse.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -18,4 +19,22 @@ public class OpenAiResponse { * The type of object returned, should be "list" */ public String object; + + /** + * The first id included + */ + @JsonProperty("first_id") + public String firstId; + + /** + * The last id included + */ + @JsonProperty("last_id") + public String lastId; + + /** + * True if there are objects after lastId + */ + @JsonProperty("has_more") + public boolean hasMore; } 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..49cf17e9 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java @@ -0,0 +1,67 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Assistant { + + /** + * 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; + + /** + * The name of the assistant. The maximum length is 256 + */ + String name; + + /** + * The description of the assistant. + */ + String description; + + /** + * ID of the model to use + */ + @NonNull + String model; + + /** + * 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 fileIds; + + /** + * 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/AssistantFunction.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFunction.java new file mode 100644 index 00000000..3abee5ae --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFunction.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-20 10:09 + **/ + + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantFunction { + + private String description; + + private String name; + + private Map parameters; +} 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..bf38ff0b --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java @@ -0,0 +1,52 @@ +package com.theokanning.openai.assistants; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantRequest { + + /** + * 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 fileIds; + + /** + * 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/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/ModifyAssistantRequest.java b/api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java new file mode 100644 index 00000000..0fcc4f85 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java @@ -0,0 +1,51 @@ +package com.theokanning.openai.assistants; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ModifyAssistantRequest { + + /** + * ID of the model to use + */ + 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 fileIds; + + /** + * 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/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java new file mode 100644 index 00000000..f35af0ef --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -0,0 +1,20 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Tool { + /** + * The type of tool being defined + */ + AssistantToolsEnum type; + + /** + * Function definition, only used if type is "function" + */ + AssistantFunction function; +} 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/audio/CreateTranscriptionRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateTranscriptionRequest.java new file mode 100644 index 00000000..5c50480f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateTranscriptionRequest.java @@ -0,0 +1,46 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * A request for OpenAi to create transcription based on an audio file + * All fields except model are optional + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateTranscriptionRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. + */ + String prompt; + + /** + * The format of the transcript output, in one of these options: json or verbose_json + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The sampling temperature, between 0 and 1. + * Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + * If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. + */ + Double temperature; + + /** + * The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency. + */ + String language; +} diff --git a/api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java new file mode 100644 index 00000000..ace5cc36 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java @@ -0,0 +1,41 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * A request for OpenAi to create English translation based on an audio file + * All fields except model are optional + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateTranslationRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * An optional text to guide the model's style or continue a previous audio segment. The prompt should be in English. + */ + String prompt; + + /** + * The format of the translated output, in one of these options: json or verbose_json + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The sampling temperature, between 0 and 1. + * Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + * If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. + */ + Double temperature; +} diff --git a/api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java b/api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java new file mode 100644 index 00000000..2794a346 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java @@ -0,0 +1,44 @@ +package com.theokanning.openai.audio; + +import lombok.Data; + +import java.util.List; + +/** + * An object with the text transcription + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Data +public class TranscriptionResult { + + /** + * The text transcription. + */ + String text; + + /** + * Task name + * @apiNote verbose_json response format only + */ + String task; + + /** + * Speech language + * @apiNote verbose_json response format only + */ + String language; + + /** + * Speech duration + * @apiNote verbose_json response format only + */ + Double duration; + + /** + * List of segments + * @apiNote verbose_json response format only + */ + List segments; + +} diff --git a/api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java b/api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java new file mode 100644 index 00000000..7f382073 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java @@ -0,0 +1,32 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * An object represents transcription segment + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Data +public class TranscriptionSegment { + + Integer id; + Integer seek; + Double start; + Double end; + String text; + List tokens; + Double temperature; + @JsonProperty("avg_logprob") + Double averageLogProb; + @JsonProperty("compression_ratio") + Double compressionRatio; + @JsonProperty("no_speech_prob") + Double noSpeechProb; + @JsonProperty("transient") + Boolean transientFlag; + +} diff --git a/api/src/main/java/com/theokanning/openai/audio/TranslationResult.java b/api/src/main/java/com/theokanning/openai/audio/TranslationResult.java new file mode 100644 index 00000000..f1f1c446 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/TranslationResult.java @@ -0,0 +1,44 @@ +package com.theokanning.openai.audio; + +import lombok.Data; + +import java.util.List; + +/** + * An object with the English transcription + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Data +public class TranslationResult { + + /** + * Translated text. + */ + String text; + + /** + * Task name + * @apiNote verbose_json response format only + */ + String task; + + /** + * Translated language + * @apiNote verbose_json response format only + */ + String language; + + /** + * Speech duration + * @apiNote verbose_json response format only + */ + Double duration; + + /** + * List of segments + * @apiNote verbose_json response format only + */ + List segments; + +} diff --git a/api/src/main/java/com/theokanning/openai/billing/BillingUsage.java b/api/src/main/java/com/theokanning/openai/billing/BillingUsage.java new file mode 100644 index 00000000..3c6db957 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/BillingUsage.java @@ -0,0 +1,29 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * Amount consumption information + * + */ +@Data +public class BillingUsage { + + @JsonProperty("object") + private String object; + /** + * Account expenditure details + */ + @JsonProperty("daily_costs") + private List dailyCosts; + /** + * Total usage amount: cents + */ + @JsonProperty("total_usage") + private BigDecimal totalUsage; + +} diff --git a/api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java b/api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java new file mode 100644 index 00000000..71f681d1 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java @@ -0,0 +1,35 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * Return value of balance inquiry interface + * + */ +@Data +public class CreditGrantsResponse implements Serializable { + private String object; + /** + * Total amount: US dollars + */ + @JsonProperty("total_granted") + private BigDecimal totalGranted; + /** + * Total usage amount: US dollars + */ + @JsonProperty("total_used") + private BigDecimal totalUsed; + /** + * Total remaining amount: US dollars + */ + @JsonProperty("total_available") + private BigDecimal totalAvailable; + /** + * Balance details + */ + private Grants grants; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/DailyCost.java b/api/src/main/java/com/theokanning/openai/billing/DailyCost.java new file mode 100644 index 00000000..6aede61c --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/DailyCost.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * List of amount consumption + * + */ +@Data +public class DailyCost { + /** + * + */ + @JsonProperty("timestamp") + private long timestamp; + /** + * Model consumption amount details + */ + @JsonProperty("line_items") + private List lineItems; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Datum.java b/api/src/main/java/com/theokanning/openai/billing/Datum.java new file mode 100644 index 00000000..2eee7b3f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Datum.java @@ -0,0 +1,36 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * + * + */ +@Data +public class Datum { + private String object; + private String id; + /** + * Gift amount: US dollars + */ + @JsonProperty("grant_amount") + private BigDecimal grantAmount; + /** + * Usage amount: US dollars + */ + @JsonProperty("used_amount") + private BigDecimal usedAmount; + /** + * Effective timestamp + */ + @JsonProperty("effective_at") + private Long effectiveAt; + /** + * Expiration timestamp + */ + @JsonProperty("expires_at") + private Long expiresAt; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Grants.java b/api/src/main/java/com/theokanning/openai/billing/Grants.java new file mode 100644 index 00000000..8826f04a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Grants.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * + * + */ +@Data +public class Grants { + private String object; + @JsonProperty("data") + private List data; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/LineItem.java b/api/src/main/java/com/theokanning/openai/billing/LineItem.java new file mode 100644 index 00000000..e12c4912 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/LineItem.java @@ -0,0 +1,21 @@ +package com.theokanning.openai.billing; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * List of amount consumption + * + */ +@Data +public class LineItem { + /** + * model name + */ + private String name; + /** + * Expenditure amount + */ + private BigDecimal cost; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Plan.java b/api/src/main/java/com/theokanning/openai/billing/Plan.java new file mode 100644 index 00000000..e3db3dd4 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Plan.java @@ -0,0 +1,13 @@ +package com.theokanning.openai.billing; + +import lombok.Data; + +/** + * + * + */ +@Data +public class Plan { + private String title; + private String id; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Subscription.java b/api/src/main/java/com/theokanning/openai/billing/Subscription.java new file mode 100644 index 00000000..541e5af6 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Subscription.java @@ -0,0 +1,51 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * Account information + * + * + */ +@Data +public class Subscription { + @JsonProperty("object") + private String object; + @JsonProperty("has_payment_method") + private boolean hasPaymentMethod; + @JsonProperty("canceled") + private boolean canceled; + @JsonProperty("canceled_at") + private Object canceledAt; + @JsonProperty("delinquent") + private Object delinquent; + @JsonProperty("access_until") + private long accessUntil; + @JsonProperty("soft_limit") + private long softLimit; + @JsonProperty("hard_limit") + private long hardLimit; + @JsonProperty("system_hard_limit") + private long systemHardLimit; + @JsonProperty("soft_limit_usd") + private double softLimitUsd; + @JsonProperty("hard_limit_usd") + private double hardLimitUsd; + @JsonProperty("system_hard_limit_usd") + private double systemHardLimitUsd; + @JsonProperty("plan") + private Plan plan; + @JsonProperty("account_name") + private String accountName; + @JsonProperty("po_number") + private Object poNumber; + @JsonProperty("billing_email") + private Object billingEmail; + @JsonProperty("tax_ids") + private Object taxIds; + @JsonProperty("billing_address") + private Object billingAddress; + @JsonProperty("business_address") + private Object businessAddress; +} diff --git a/api/src/main/java/com/theokanning/openai/common/LastError.java b/api/src/main/java/com/theokanning/openai/common/LastError.java new file mode 100644 index 00000000..6a9f99de --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/common/LastError.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:27 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LastError { + + private String code; + + private String message; +} diff --git a/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java b/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java index c0e3cab7..d63d5c73 100644 --- a/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java +++ b/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java @@ -2,7 +2,6 @@ import com.theokanning.openai.Usage; import lombok.Data; -import lombok.NoArgsConstructor; import java.util.List; 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 f109fbd0..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 @@ -94,4 +94,28 @@ public class ChatCompletionRequest { * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. */ String user; + + /** + * A list of the available functions. + */ + List functions; + + /** + * Controls how the model responds to function calls, as specified in the OpenAI documentation. + */ + @JsonProperty("function_call") + ChatCompletionRequestFunctionCall functionCall; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ChatCompletionRequestFunctionCall { + String name; + + public static ChatCompletionRequestFunctionCall of(String name) { + return new ChatCompletionRequestFunctionCall(name); + } + + } } 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 new file mode 100644 index 00000000..820f4bd6 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -0,0 +1,70 @@ +package com.theokanning.openai.completion.chat; + +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; + + @JsonIgnore + private Function executor; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String description; + private Class parameters; + private Function executor; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder executor(Class requestClass, Function executor) { + this.parameters = requestClass; + this.executor = (Function) executor; + return this; + } + + public ChatFunction build() { + ChatFunction chatFunction = new ChatFunction(); + chatFunction.setName(name); + chatFunction.setDescription(description); + chatFunction.setParametersClass(parameters); + chatFunction.setExecutor(executor); + return chatFunction; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java new file mode 100644 index 00000000..962fbe12 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatFunctionCall { + + /** + * The name of the function being called + */ + String name; + + /** + * The arguments of the call produced by the model, represented as a JsonNode for easy manipulation. + */ + JsonNode arguments; + +} 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/completion/chat/ChatMessage.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java index 7bc24faf..912a71f0 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java @@ -1,8 +1,8 @@ package com.theokanning.openai.completion.chat; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; /** *

Each object has a role (either "system", "user", or "assistant") and content (the content of the message). Conversations can be as short as 1 message or fill many pages.

@@ -15,14 +15,33 @@ * see OpenAi documentation */ @Data -@NoArgsConstructor +@NoArgsConstructor(force = true) +@RequiredArgsConstructor @AllArgsConstructor public class ChatMessage { /** - * Must be either 'system', 'user', or 'assistant'.
+ * Must be either 'system', 'user', 'assistant' or 'function'.
* You may use {@link ChatMessageRole} enum. */ + @NonNull String role; + @JsonInclude() // content should always exist in the call, even if it is null String content; + //name is optional, The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters. + String name; + @JsonProperty("function_call") + ChatFunctionCall functionCall; + + public ChatMessage(String role, String content) { + this.role = role; + this.content = content; + } + + public ChatMessage(String role, String content, String name) { + this.role = role; + this.content = content; + this.name = name; + } + } diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java index eac1f754..255641e0 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java @@ -6,7 +6,8 @@ public enum ChatMessageRole { SYSTEM("system"), USER("user"), - ASSISTANT("assistant"); + ASSISTANT("assistant"), + FUNCTION("function"); private final String value; diff --git a/api/src/main/java/com/theokanning/openai/file/File.java b/api/src/main/java/com/theokanning/openai/file/File.java index 36107e73..07708f8e 100644 --- a/api/src/main/java/com/theokanning/openai/file/File.java +++ b/api/src/main/java/com/theokanning/openai/file/File.java @@ -41,4 +41,16 @@ public class File { * Description of the file's purpose. */ String purpose; + + /** + * The current status of the file, which can be either uploaded, processed, pending, error, deleting or deleted. + */ + String status; + + /** + * Additional details about the status of the file. + * If the file is in the error state, this will include a message describing the error. + */ + @JsonProperty("status_details") + String statusDetails; } diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java new file mode 100644 index 00000000..c653c048 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java @@ -0,0 +1,43 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * An object representing an event in the lifecycle of a fine-tuning job + * + * https://platform.openai.com/docs/api-reference/fine-tuning/list-events + */ +@Data +public class FineTuningEvent { + /** + * The type of object returned, should be "fine-tuneing.job.event". + */ + String object; + + /** + * The ID of the fine-tuning event. + */ + String id; + + /** + * The creation time in epoch seconds. + */ + @JsonProperty("created_at") + Long createdAt; + + /** + * The log level of this message. + */ + String level; + + /** + * The event message. + */ + String message; + + /** + * The type of event, i.e. "message" + */ + String type; +} diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java new file mode 100644 index 00000000..685d751e --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java @@ -0,0 +1,90 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * Fine-tuning job + * https://platform.openai.com/docs/api-reference/fine-tuning/object + */ +@Data +public class FineTuningJob { + /** + * The object identifier, which can be referenced in the API endpoints. + */ + String id; + + /** + * The object type, which is always "fine_tuning.job". + */ + String object; + + /** + * The unix timestamp for when the fine-tuning job was created. + */ + @JsonProperty("created_at") + Long createdAt; + + /** + * The unix timestamp for when the fine-tuning job was finished. + */ + @JsonProperty("finished_at") + Long finishedAt; + + /** + * The base model that is being fine-tuned. + */ + String model; + + /** + * The name of the fine-tuned model that is being created. + * Can be null if no fine-tuned model is created yet. + */ + @JsonProperty("fine_tuned_model") + String fineTunedModel; + + /** + * The organization that owns the fine-tuning job. + */ + @JsonProperty("organization_id") + String organizationId; + + /** + * The current status of the fine-tuning job. + * Can be either created, pending, running, succeeded, failed, or cancelled. + */ + String status; + + /** + * The hyperparameters used for the fine-tuning job. + * See the fine-tuning guide for more details. + */ + Hyperparameters hyperparameters; + + /** + * The file ID used for training. + */ + @JsonProperty("training_file") + String trainingFile; + + /** + * The file ID used for validation. + * Can be null if validation is not used. + */ + @JsonProperty("validation_file") + String validationFile; + + /** + * The compiled results files for the fine-tuning job. + */ + @JsonProperty("result_files") + List resultFiles; + + /** + * The total number of billable tokens processed by this fine-tuning job. + */ + @JsonProperty("trained_tokens") + Integer trainedTokens; +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java new file mode 100644 index 00000000..44a501c3 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java @@ -0,0 +1,46 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + + +/** + * Request to create a fine tuning job + * https://platform.openai.com/docs/api-reference/fine-tuning/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class FineTuningJobRequest { + + /** + * The ID of an uploaded file that contains training data. + */ + @NonNull + @JsonProperty("training_file") + String trainingFile; + + /** + * The ID of an uploaded file that contains validation data. + * Optional. + */ + @JsonProperty("validation_file") + String validationFile; + + /** + * The name of the model to fine-tune. + */ + @NonNull + String model; + + /** + * The hyperparameters used for the fine-tuning job. + */ + Hyperparameters hyperparameters; + + /** + * A string of up to 40 characters that will be added to your fine-tuned model name. + */ + String suffix; +} diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java b/api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java new file mode 100644 index 00000000..3d59913d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Hyperparameters for a fine-tuning job + * https://platform.openai.com/docs/api-reference/fine-tuning/object#hyperparameters + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Hyperparameters { + + /** + * The number of epochs to train the model for. + * An epoch refers to one full cycle through the training dataset. + * "Auto" decides the optimal number of epochs based on the size of the dataset. + * If setting the number manually, we support any number between 1 and 50 epochs. + */ + @JsonProperty("n_epochs") + Integer nEpochs; +} diff --git a/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java b/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java index 855d774e..7b8f76c3 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java @@ -8,6 +8,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class FineTuneEvent { /** diff --git a/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java b/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java index fbf89db1..2d145aa0 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java @@ -11,6 +11,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes/create */ +@Deprecated @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java b/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java index 49c0170c..6cbcce41 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java @@ -11,6 +11,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class FineTuneResult { /** diff --git a/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java b/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java index e83af27c..d1d383ed 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java +++ b/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java @@ -8,6 +8,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class HyperParameters { 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/image/ImageResult.java b/api/src/main/java/com/theokanning/openai/image/ImageResult.java index 8db05108..9576dbfd 100644 --- a/api/src/main/java/com/theokanning/openai/image/ImageResult.java +++ b/api/src/main/java/com/theokanning/openai/image/ImageResult.java @@ -1,6 +1,5 @@ package com.theokanning.openai.image; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; diff --git a/api/src/main/java/com/theokanning/openai/messages/Message.java b/api/src/main/java/com/theokanning/openai/messages/Message.java new file mode 100644 index 00000000..44144780 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/Message.java @@ -0,0 +1,81 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + + +/** + * Represents a Message within a thread. + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Message { + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type, which is always thread.message. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the message was created. + */ + @JsonProperty("created_at") + int createdAt; + + /** + * The thread ID that this message belongs to. + */ + @JsonProperty("thread_id") + String threadId; + + /** + * The entity that produced the message. One of user or assistant. + */ + String role; + + /** + * The content of the message in an array of text and/or images. + */ + List content; + + /** + * If applicable, the ID of the assistant that authored this message. + */ + @JsonProperty("assistant_id") + String assistantId; + + /** + * If applicable, the ID of the run associated with the authoring of this message. + */ + @JsonProperty("run_id") + String runId; + + /** + * A list of file IDs that the assistant should use. + * Useful for tools like retrieval and code_interpreter that can access files. + * A maximum of 10 files can be attached to a message. + */ + @JsonProperty("file_ids") + List fileIds; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageContent.java b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java new file mode 100644 index 00000000..a9ff489e --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java @@ -0,0 +1,31 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.messages.content.ImageFile; +import com.theokanning.openai.messages.content.Text; +import lombok.Data; + + +/** + * Represents the content of a message + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +public class MessageContent { + /** + * The content type, either "text" or "image_file" + */ + String type; + + /** + * Text content of the message. Only present if type == text + */ + Text text; + + /** + * The image content of a message. Only present if type == image_file + */ + @JsonProperty("image_file") + ImageFile imageFile; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageFile.java b/api/src/main/java/com/theokanning/openai/messages/MessageFile.java new file mode 100644 index 00000000..7a0dd3bd --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageFile.java @@ -0,0 +1,38 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A list of files attached to a Message + *

+ * https://platform.openai.com/docs/api-reference/messages/file-object + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class MessageFile { + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type, which is always thread.message.file. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the message file was created. + */ + @JsonProperty("created_at") + int createdAt; + + /** + * The ID of the message that the File is attached to. + */ + @JsonProperty("message_id") + String messageId; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageRequest.java b/api/src/main/java/com/theokanning/openai/messages/MessageRequest.java new file mode 100644 index 00000000..6c49a110 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageRequest.java @@ -0,0 +1,48 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +/** + * Creates a Message + *

+ * https://platform.openai.com/docs/api-reference/messages/createMessage + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class MessageRequest { + /** + * The role of the entity that is creating the message. + * Currently only "user" is supported. + */ + @NonNull + @Builder.Default + String role = "user"; + + /** + * The content of the message. + */ + @NonNull + String content; + + /** + * A list of File IDs that the message should use. + * Defaults to an empty list. + * There can be a maximum of 10 files attached to a message. + * Useful for tools like retrieval and code_interpreter that can access and use files. + */ + @JsonProperty("file_ids") + List fileIds; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java b/api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java new file mode 100644 index 00000000..32344c96 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.messages; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Modifies a Message + *

+ * https://platform.openai.com/docs/api-reference/messages/modifyMessage + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ModifyMessageRequest { + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/Annotation.java b/api/src/main/java/com/theokanning/openai/messages/content/Annotation.java new file mode 100644 index 00000000..473a5dfc --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/Annotation.java @@ -0,0 +1,44 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * An annotation for a text Message + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Annotation { + /** + * The type of annotation, either file_citation or file_path + */ + String type; + + /** + * The text in the message content that needs to be replaced + */ + String text; + + /** + * File citation details, only present when type == file_citation + */ + @JsonProperty("file_citation") + FileCitation fileCitation; + + /** + * File path details, only present when type == file_path + */ + @JsonProperty("file_path") + FilePath filePath; + + @JsonProperty("start_index") + int startIndex; + + @JsonProperty("end_index") + int endIndex; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java b/api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java new file mode 100644 index 00000000..ff486d55 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java @@ -0,0 +1,29 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A citation within the message that points to a specific quote from a specific File associated with the + * assistant or the message. Generated when the assistant uses the "retrieval" tool to search files. + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FileCitation { + + /** + * The ID of the specific File the citation is from. + */ + @JsonProperty("file_id") + String fileId; + + /** + * The specific quote in the file. + */ + String quote; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/FilePath.java b/api/src/main/java/com/theokanning/openai/messages/content/FilePath.java new file mode 100644 index 00000000..b11cc1af --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/FilePath.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A URL for the file that's generated when the assistant used the code_interpreter tool to generate a file. + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FilePath { + + /** + * The ID of the file that was generated + */ + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java b/api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java new file mode 100644 index 00000000..2a43fa02 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * References an image File int eh content of a message. + *

+ * /https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ImageFile { + + /** + * The File ID of the image in the message content. + */ + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/Text.java b/api/src/main/java/com/theokanning/openai/messages/content/Text.java new file mode 100644 index 00000000..6efa28bf --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/Text.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.messages.content; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * The text content that is part of a message + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Text { + + /** + * The data that makes up the text. + */ + String value; + + /** + * Text annotations that show additional details + */ + List annotations; +} 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/moderation/ModerationCategories.java b/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java index 585109b1..238d0a05 100644 --- a/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java +++ b/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java @@ -1,7 +1,6 @@ package com.theokanning.openai.moderation; import com.fasterxml.jackson.annotation.JsonProperty; -import com.theokanning.openai.completion.CompletionChoice; import lombok.Data; import java.util.List; diff --git a/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java b/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java index 6e51e466..08513420 100644 --- a/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java +++ b/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java @@ -2,8 +2,6 @@ import lombok.*; -import java.util.List; - /** * A request for OpenAi to detect if text violates OpenAi's content policy. * diff --git a/api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java b/api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java new file mode 100644 index 00000000..b27de696 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java @@ -0,0 +1,39 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.threads.ThreadRequest; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 23:08 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateThreadAndRunRequest { + + @JsonProperty("assistant_id") + private String assistantId; + + private ThreadRequest thread; + + private String model; + + private String instructions; + + private List tools; + + private Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java b/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java new file mode 100644 index 00000000..535fd7e2 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MessageCreation { + + @JsonProperty("message_id") + String messageId; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RequiredAction.java b/api/src/main/java/com/theokanning/openai/runs/RequiredAction.java new file mode 100644 index 00000000..959b89dc --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RequiredAction.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:44 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RequiredAction { + + private String type; + + @JsonProperty("submit_tool_outputs") + private SubmitToolOutputs submitToolOutputs; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/Run.java b/api/src/main/java/com/theokanning/openai/runs/Run.java new file mode 100644 index 00000000..1da9ec95 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/Run.java @@ -0,0 +1,66 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.common.LastError; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Run { + + private String id; + + private String object; + + @JsonProperty("created_at") + private Integer createdAt; + + @JsonProperty("thread_id") + private String threadId; + + @JsonProperty("assistant_id") + private String assistantId; + + private String status; + + @JsonProperty("required_action") + private RequiredAction requiredAction; + + @JsonProperty("last_error") + private LastError lastError; + + @JsonProperty("expires_at") + private Integer expiresAt; + + @JsonProperty("started_at") + private Integer startedAt; + + @JsonProperty("cancelled_at") + private Integer cancelledAt; + + @JsonProperty("failed_at") + private Integer failedAt; + + @JsonProperty("completed_at") + private Integer completedAt; + + private String model; + + private String instructions; + + private List tools; + + @JsonProperty("file_ids") + private List fileIds; + + private Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java b/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java new file mode 100644 index 00000000..cba5f283 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.runs; + +import com.theokanning.openai.assistants.Tool; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class RunCreateRequest { + String assistantId; + + // Optional + String model; + + String instructions; + + List tools; + + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunImage.java b/api/src/main/java/com/theokanning/openai/runs/RunImage.java new file mode 100644 index 00000000..18135187 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunImage.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:33 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RunImage { + + @JsonProperty("file_id") + private String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunStep.java b/api/src/main/java/com/theokanning/openai/runs/RunStep.java new file mode 100644 index 00000000..7cd39342 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunStep.java @@ -0,0 +1,58 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.common.LastError; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class RunStep { + + private String id; + + private String object; + + @JsonProperty("created_at") + private Integer createdAt; + + @JsonProperty("assistant_id") + private String assistantId; + + @JsonProperty("thread_id") + private String threadId; + + @JsonProperty("run_id") + private String runId; + + private String type; + + private String status; + + @JsonProperty("step_details") + private StepDetails stepDetails; + + @JsonProperty("last_error") + private LastError lastError; + + @JsonProperty("expired_at") + private Integer expiredAt; + + @JsonProperty("cancelled_at") + private Integer cancelledAt; + + @JsonProperty("failed_at") + private Integer failedAt; + + @JsonProperty("completed_at") + private Integer completedAt; + + private Map metadata; + +} diff --git a/api/src/main/java/com/theokanning/openai/runs/StepDetails.java b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java new file mode 100644 index 00000000..08972623 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StepDetails { + + + private String type; + + @JsonProperty("message_creation") + private MessageCreation messageCreation; + + @JsonProperty("tool_calls") + private List toolCalls; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java new file mode 100644 index 00000000..ec2b346a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:45 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmitToolOutputRequestItem { + + @JsonProperty("tool_call_id") + private String toolCallId; + + private String output; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java new file mode 100644 index 00000000..e0aca757 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:45 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmitToolOutputs { + + @JsonProperty("tool_calls") + List toolCalls; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java new file mode 100644 index 00000000..f892f168 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:45 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmitToolOutputsRequest { + + @JsonProperty("tool_outputs") + private List toolOutputs; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCall.java b/api/src/main/java/com/theokanning/openai/runs/ToolCall.java new file mode 100644 index 00000000..8aa6c26c --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCall.java @@ -0,0 +1,34 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:32 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCall { + + private String id; + + private String type; + + @JsonProperty("code_interpreter") + private ToolCallCodeInterpreter codeInterpreter; + + private Map retrieval; + + private ToolCallFunction function; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java new file mode 100644 index 00000000..c2a3a446 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:34 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCallCodeInterpreter { + + private String input; + + private List outputs; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java new file mode 100644 index 00000000..179ef2f1 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:34 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCallCodeInterpreterOutput { + + private String type; + + private String logs; + + private RunImage image; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java new file mode 100644 index 00000000..34de58d1 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:38 + **/ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCallFunction { + + private String name; + + private String arguments; + + private String output; +} diff --git a/api/src/main/java/com/theokanning/openai/threads/Thread.java b/api/src/main/java/com/theokanning/openai/threads/Thread.java new file mode 100644 index 00000000..24adb73f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/threads/Thread.java @@ -0,0 +1,41 @@ +package com.theokanning.openai.threads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Represents a Thread with an assistant + *

+ * https://platform.openai.com/docs/api-reference/threads/object + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Thread { + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type, which is always thread. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the thread was created. + */ + @JsonProperty("created_at") + int createdAt; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java new file mode 100644 index 00000000..35fd888a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java @@ -0,0 +1,34 @@ +package com.theokanning.openai.threads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.messages.MessageRequest; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Creates a thread + *

+ * https://platform.openai.com/docs/api-reference/threads/createThread + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ThreadRequest { + /** + * A list of messages to start the thread with. Optional. + */ + List messages; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java new file mode 100644 index 00000000..0a50907e --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -0,0 +1,300 @@ +package com.theokanning.openai.utils; + +import com.knuddels.jtokkit.Encodings; +import com.knuddels.jtokkit.api.Encoding; +import com.knuddels.jtokkit.api.EncodingRegistry; +import com.knuddels.jtokkit.api.EncodingType; +import com.knuddels.jtokkit.api.ModelType; +import com.theokanning.openai.completion.chat.ChatMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.*; + +/** + * Token calculation tool class + */ +public class TikTokensUtil { + /** + * Model name corresponds to Encoding + */ + private static final Map modelMap = new HashMap<>(); + /** + * Registry instance + */ + private static final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); + + static { + for (ModelType modelType : ModelType.values()) { + modelMap.put(modelType.getName(), registry.getEncodingForModel(modelType)); + } + modelMap.put(ModelEnum.GPT_3_5_TURBO_0301.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO)); + 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)); + } + + /** + * Get encoding array through Encoding and text. + * + * @param enc Encoding type + * @param text Text information + * @return Encoding array + */ + public static List encode(Encoding enc, String text) { + return isBlank(text) ? new ArrayList<>() : enc.encode(text); + } + + /** + * Calculate tokens of text information through Encoding. + * + * @param enc Encoding type + * @param text Text information + * @return Number of tokens + */ + public static int tokens(Encoding enc, String text) { + return encode(enc, text).size(); + } + + + /** + * Reverse calculate text information through Encoding and encoded array + * + * @param enc Encoding + * @param encoded Encoding array + * @return Text information corresponding to the encoding array. + */ + public static String decode(Encoding enc, List encoded) { + return enc.decode(encoded); + } + + /** + * Get an Encoding object by Encoding type + * + * @param encodingType + * @return Encoding + */ + public static Encoding getEncoding(EncodingType encodingType) { + Encoding enc = registry.getEncoding(encodingType); + return enc; + } + + /** + * Obtain the encoding array by encoding; + * + * @param text + * @return Encoding array + */ + public static List encode(EncodingType encodingType, String text) { + if (isBlank(text)) { + return new ArrayList<>(); + } + Encoding enc = getEncoding(encodingType); + List encoded = enc.encode(text); + return encoded; + } + + /** + * Compute the tokens of the specified string through EncodingType. + * + * @param encodingType + * @param text + * @return Number of tokens + */ + public static int tokens(EncodingType encodingType, String text) { + return encode(encodingType, text).size(); + } + + + /** + * Reverse the encoded array to get the string text using EncodingType and the encoded array. + * + * @param encodingType + * @param encoded + * @return The string corresponding to the encoding array. + */ + public static String decode(EncodingType encodingType, List encoded) { + Encoding enc = getEncoding(encodingType); + return enc.decode(encoded); + } + + + /** + * Get an Encoding object by model name. + * + * @param modelName + * @return Encoding + */ + public static Encoding getEncoding(String modelName) { + return modelMap.get(modelName); + } + + /** + * Get the encoded array by model name using encode. + * + * @param text Text information + * @return Encoding array + */ + public static List encode(String modelName, String text) { + if (isBlank(text)) { + return new ArrayList<>(); + } + Encoding enc = getEncoding(modelName); + if (Objects.isNull(enc)) { + return new ArrayList<>(); + } + List encoded = enc.encode(text); + return encoded; + } + + /** + * Calculate the tokens of a specified string by model name. + * + * @param modelName + * @param text + * @return Number of tokens + */ + public static int tokens(String modelName, String text) { + return encode(modelName, text).size(); + } + + + /** + * Calculate the encoded array for messages by model name. + * Refer to the official processing logic: + * https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + * + * @param modelName + * @param messages + * @return Number of tokens + */ + public static int tokens(String modelName, List messages) { + Encoding encoding = getEncoding(modelName); + int tokensPerMessage = 0; + int tokensPerName = 0; + //3.5统一处理 + if (modelName.equals("gpt-3.5-turbo-0301") || modelName.equals("gpt-3.5-turbo")) { + tokensPerMessage = 4; + tokensPerName = -1; + } + //4.0统一处理 + if (modelName.equals("gpt-4") || modelName.equals("gpt-4-0314")) { + tokensPerMessage = 3; + tokensPerName = 1; + } + int sum = 0; + for (ChatMessage msg : messages) { + sum += tokensPerMessage; + sum += tokens(encoding, msg.getContent()); + sum += tokens(encoding, msg.getRole()); + sum += tokens(encoding, msg.getName()); + if (isNotBlank(msg.getName())) { + sum += tokensPerName; + } + } + sum += 3; + return sum; + } + + /** + * Reverse the string text through the model name and the encoded array. + * + * @param modelName + * @param encoded + * @return + */ + public static String decode(String modelName, List encoded) { + Encoding enc = getEncoding(modelName); + return enc.decode(encoded); + } + + + /** + * Obtain the modelType. + * + * @param name + * @return + */ + public static ModelType getModelTypeByName(String name) { + if (ModelEnum.GPT_3_5_TURBO_0301.getName().equals(name)) { + return ModelType.GPT_3_5_TURBO; + } + if (ModelEnum.GPT_4.getName().equals(name) + || ModelEnum.GPT_4_32K.getName().equals(name) + || ModelEnum.GPT_4_32K_0314.getName().equals(name) + || ModelEnum.GPT_4_0314.getName().equals(name)) { + return ModelType.GPT_4; + } + + for (ModelType modelType : ModelType.values()) { + if (modelType.getName().equals(name)) { + return modelType; + } + } + return null; + } + + @Getter + @AllArgsConstructor + public enum ModelEnum { + /** + * gpt-3.5-turbo + */ + GPT_3_5_TURBO("gpt-3.5-turbo"), + /** + * Temporary model, not recommended for use. + */ + GPT_3_5_TURBO_0301("gpt-3.5-turbo-0301"), + /** + * GPT4.0 + */ + GPT_4("gpt-4"), + /** + * Temporary model, not recommended for use. + */ + GPT_4_0314("gpt-4-0314"), + /** + * GPT4.0 超长上下文 + */ + GPT_4_32K("gpt-4-32k"), + /** + * 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; + } + + public static boolean isBlankChar(int c) { + return Character.isWhitespace(c) || Character.isSpaceChar(c) || c == 65279 || c == 8234 || c == 0 || c == 12644 || c == 10240 || c == 6158; + } + + public static boolean isBlankChar(char c) { + return isBlankChar((int) c); + } + + public static boolean isNotBlank(CharSequence str) { + return !isBlank(str); + } + + public static boolean isBlank(CharSequence str) { + int length; + if (str != null && (length = str.length()) != 0) { + for (int i = 0; i < length; ++i) { + if (!isBlankChar(str.charAt(i))) { + return false; + } + } + + return true; + } else { + return true; + } + } + +} diff --git a/api/src/test/java/com/theokanning/openai/JsonTest.java b/api/src/test/java/com/theokanning/openai/JsonTest.java index 1ca23a06..58b5c5f2 100644 --- a/api/src/test/java/com/theokanning/openai/JsonTest.java +++ b/api/src/test/java/com/theokanning/openai/JsonTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.*; +import com.theokanning.openai.audio.TranscriptionResult; +import com.theokanning.openai.audio.TranslationResult; import com.theokanning.openai.completion.chat.ChatCompletionRequest; import com.theokanning.openai.completion.chat.ChatCompletionResult; import com.theokanning.openai.edit.EditRequest; @@ -10,11 +12,13 @@ import com.theokanning.openai.embedding.EmbeddingResult; import com.theokanning.openai.engine.Engine; import com.theokanning.openai.file.File; +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneResult; -import com.theokanning.openai.image.CreateImageEditRequest; -import com.theokanning.openai.image.CreateImageRequest; import com.theokanning.openai.image.ImageResult; +import com.theokanning.openai.messages.Message; import com.theokanning.openai.model.Model; import com.theokanning.openai.moderation.ModerationRequest; import com.theokanning.openai.moderation.ModerationResult; @@ -41,7 +45,13 @@ public class JsonTest { File.class, FineTuneEvent.class, FineTuneResult.class, + FineTuningEvent.class, + FineTuningJob.class, + FineTuningJobRequest.class, ImageResult.class, + TranscriptionResult.class, + TranslationResult.class, + Message.class, Model.class, ModerationRequest.class, ModerationResult.class diff --git a/api/src/test/resources/fixtures/File.json b/api/src/test/resources/fixtures/File.json index 8ede985d..9fa0c564 100644 --- a/api/src/test/resources/fixtures/File.json +++ b/api/src/test/resources/fixtures/File.json @@ -4,5 +4,7 @@ "bytes": 175, "created_at": 1613677385, "filename": "train.jsonl", - "purpose": "search" -} \ No newline at end of file + "purpose": "search", + "status": "error", + "status_details": "File is too large." +} diff --git a/api/src/test/resources/fixtures/FineTuningEvent.json b/api/src/test/resources/fixtures/FineTuningEvent.json new file mode 100644 index 00000000..cd07faa5 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuningEvent.json @@ -0,0 +1,8 @@ +{ + "object": "fine_tuning.job.event", + "id": "ft-event-ddTJfwuMVpfLXseO0Am0Gqjm", + "created_at": 1692407401, + "level": "info", + "message": "Fine tuning job successfully completed", + "type": "message" +} diff --git a/api/src/test/resources/fixtures/FineTuningJob.json b/api/src/test/resources/fixtures/FineTuningJob.json new file mode 100644 index 00000000..51eb64c1 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuningJob.json @@ -0,0 +1,19 @@ +{ + "id": "ftjob-abc123", + "object": "fine_tuning.job", + "model": "davinci-002", + "status": "succeeded", + "hyperparameters": { + "n_epochs": 4 + }, + "created_at": 1692661014, + "finished_at": 1692661190, + "fine_tuned_model": "ft:davinci-002:my-org:custom_suffix:7q8mpxmy", + "organization_id": "org-123", + "training_file": "file-abc123", + "result_files": [ + "file-abc123" + ], + "validation_file": "validation-file", + "trained_tokens": 5768 +} diff --git a/api/src/test/resources/fixtures/FineTuningJobRequest.json b/api/src/test/resources/fixtures/FineTuningJobRequest.json new file mode 100644 index 00000000..45e79f73 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuningJobRequest.json @@ -0,0 +1,9 @@ +{ + "model": "davinci-002", + "validation_file": "file-abc123", + "training_file": "file-abc123", + "hyperparameters": { + "n_epochs": 4 + }, + "suffix": "test" +} diff --git a/api/src/test/resources/fixtures/Message.json b/api/src/test/resources/fixtures/Message.json new file mode 100644 index 00000000..878e7a47 --- /dev/null +++ b/api/src/test/resources/fixtures/Message.json @@ -0,0 +1,46 @@ +{ + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1698983503, + "thread_id": "thread_abc123", + "role": "assistant", + "content": [ + { + "type": "text", + "text": { + "value": "Hi! How can I help you today?", + "annotations": [ + { + "type": "file_citation", + "text": "file citation text", + "file_citation": { + "file_id": "file citation id", + "quote": "Enough, Reggie" + }, + "start_index": 0, + "end_index": 1 + }, + { + "type": "file_path", + "text": "file path text", + "file_path": { + "file_id": "file id" + }, + "start_index": 1, + "end_index": 2 + } + ] + } + }, + { + "type": "image_file", + "image_file": { + "file_id": "image file id" + } + } + ], + "file_ids": [], + "assistant_id": "asst_abc123", + "run_id": "run_abc123", + "metadata": {} +} diff --git a/api/src/test/resources/fixtures/TranscriptionResult.json b/api/src/test/resources/fixtures/TranscriptionResult.json new file mode 100644 index 00000000..769a139e --- /dev/null +++ b/api/src/test/resources/fixtures/TranscriptionResult.json @@ -0,0 +1,27 @@ +{ + "task": "transcribe", + "language": "english", + "duration": 1.1, + "segments": [ + { + "id": 0, + "seek": 0, + "start": 0.0, + "end": 0.96, + "text": " Hello World.", + "tokens": [ + 50364, + 2425, + 3937, + 13, + 50412 + ], + "temperature": 0.0, + "avg_logprob": -0.7308251063028971, + "compression_ratio": 0.6, + "no_speech_prob": 0.015335720032453537, + "transient": false + } + ], + "text": "Hello World." +} \ No newline at end of file diff --git a/api/src/test/resources/fixtures/TranslationResult.json b/api/src/test/resources/fixtures/TranslationResult.json new file mode 100644 index 00000000..3e211cab --- /dev/null +++ b/api/src/test/resources/fixtures/TranslationResult.json @@ -0,0 +1,37 @@ +{ + "task": "translate", + "language": "english", + "duration": 4.38, + "segments": [ + { + "id": 0, + "seek": 0, + "start": 0.0, + "end": 4.32, + "text": " Hello, my name is Yuna. I am Korean voice.", + "tokens": [ + 50364, + 2425, + 11, + 452, + 1315, + 307, + 398, + 5051, + 13, + 286, + 669, + 6933, + 3177, + 13, + 50580 + ], + "temperature": 0.0, + "avg_logprob": -0.6644304394721985, + "compression_ratio": 0.84, + "no_speech_prob": 0.006824055220931768, + "transient": false + } + ], + "text": "Hello, my name is Yuna. I am Korean voice." +} \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle index de86867d..6bc08d7d 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -3,11 +3,11 @@ apply plugin: "com.vanniktech.maven.publish" dependencies { api project(":api") - api 'com.squareup.retrofit2:retrofit:2.9.0' - api 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' - implementation 'com.squareup.retrofit2:converter-jackson:2.9.0' + api libs.retrofit + api libs.retrofitRxJava2 + implementation libs.retrofitJackson - testImplementation(platform('org.junit:junit-bom:5.8.2')) + testImplementation(platform(libs.junitBom)) testImplementation('org.junit.jupiter:junit-jupiter') } diff --git a/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java b/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java index 7e53c6a4..fbe9a5b4 100644 --- a/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java +++ b/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java @@ -1,28 +1,15 @@ package com.theokanning.openai; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -import java.io.IOException; - /** * OkHttp Interceptor that adds an authorization token header + * + * @deprecated Use {@link com.theokanning.openai.client.AuthenticationInterceptor} */ -public class AuthenticationInterceptor implements Interceptor { - - private final String token; +@Deprecated +public class AuthenticationInterceptor extends com.theokanning.openai.client.AuthenticationInterceptor { AuthenticationInterceptor(String token) { - this.token = token; + super(token); } - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request() - .newBuilder() - .header("Authorization", "Bearer " + token) - .build(); - return chain.proceed(request); - } } diff --git a/client/src/main/java/com/theokanning/openai/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/OpenAiApi.java index 77e00552..feb2f6fc 100644 --- a/client/src/main/java/com/theokanning/openai/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/OpenAiApi.java @@ -1,122 +1,9 @@ package com.theokanning.openai; -import com.theokanning.openai.completion.CompletionRequest; -import com.theokanning.openai.completion.CompletionResult; -import com.theokanning.openai.completion.chat.ChatCompletionRequest; -import com.theokanning.openai.completion.chat.ChatCompletionResult; -import com.theokanning.openai.edit.EditRequest; -import com.theokanning.openai.edit.EditResult; -import com.theokanning.openai.embedding.EmbeddingRequest; -import com.theokanning.openai.embedding.EmbeddingResult; -import com.theokanning.openai.engine.Engine; -import com.theokanning.openai.file.File; -import com.theokanning.openai.finetune.FineTuneEvent; -import com.theokanning.openai.finetune.FineTuneRequest; -import com.theokanning.openai.finetune.FineTuneResult; -import com.theokanning.openai.image.CreateImageEditRequest; -import com.theokanning.openai.image.CreateImageRequest; -import com.theokanning.openai.image.ImageResult; -import com.theokanning.openai.model.Model; -import com.theokanning.openai.moderation.ModerationRequest; -import com.theokanning.openai.moderation.ModerationResult; -import io.reactivex.Single; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.http.*; - -public interface OpenAiApi { - - @GET("v1/models") - Single> listModels(); - - @GET("/v1/models/{model_id}") - Single getModel(@Path("model_id") String modelId); - - @POST("/v1/completions") - Single createCompletion(@Body CompletionRequest request); - - @Streaming - @POST("/v1/completions") - Call createCompletionStream(@Body CompletionRequest request); - - @POST("/v1/chat/completions") - Single createChatCompletion(@Body ChatCompletionRequest request); - - @Streaming - @POST("/v1/chat/completions") - Call createChatCompletionStream(@Body ChatCompletionRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/completions") - Single createCompletion(@Path("engine_id") String engineId, @Body CompletionRequest request); - - @POST("/v1/edits") - Single createEdit(@Body EditRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/edits") - Single createEdit(@Path("engine_id") String engineId, @Body EditRequest request); - - @POST("/v1/embeddings") - Single createEmbeddings(@Body EmbeddingRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/embeddings") - Single createEmbeddings(@Path("engine_id") String engineId, @Body EmbeddingRequest request); - - @GET("/v1/files") - Single> listFiles(); - - @Multipart - @POST("/v1/files") - Single uploadFile(@Part("purpose") RequestBody purpose, @Part MultipartBody.Part file); - - @DELETE("/v1/files/{file_id}") - Single deleteFile(@Path("file_id") String fileId); - - @GET("/v1/files/{file_id}") - Single retrieveFile(@Path("file_id") String fileId); - - @POST("/v1/fine-tunes") - Single createFineTune(@Body FineTuneRequest request); - - @POST("/v1/completions") - Single createFineTuneCompletion(@Body CompletionRequest request); - - @GET("/v1/fine-tunes") - Single> listFineTunes(); - - @GET("/v1/fine-tunes/{fine_tune_id}") - Single retrieveFineTune(@Path("fine_tune_id") String fineTuneId); - - @POST("/v1/fine-tunes/{fine_tune_id}/cancel") - Single cancelFineTune(@Path("fine_tune_id") String fineTuneId); - - @GET("/v1/fine-tunes/{fine_tune_id}/events") - Single> listFineTuneEvents(@Path("fine_tune_id") String fineTuneId); - - @DELETE("/v1/models/{fine_tune_id}") - Single deleteFineTune(@Path("fine_tune_id") String fineTuneId); - - @POST("/v1/images/generations") - Single createImage(@Body CreateImageRequest request); - - @POST("/v1/images/edits") - Single createImageEdit(@Body RequestBody requestBody); - - @POST("/v1/images/variations") - Single createImageVariation(@Body RequestBody requestBody); - - @POST("/v1/moderations") - Single createModeration(@Body ModerationRequest request); - - @Deprecated - @GET("v1/engines") - Single> getEngines(); - - @Deprecated - @GET("/v1/engines/{engine_id}") - Single getEngine(@Path("engine_id") String engineId); +/** + * @deprecated Use {@link com.theokanning.openai.client.OpenAiApi} + */ +@Deprecated +public interface OpenAiApi extends com.theokanning.openai.client.OpenAiApi { + // For legacy compatibility only. } diff --git a/client/src/main/java/com/theokanning/openai/OpenAiService.java b/client/src/main/java/com/theokanning/openai/OpenAiService.java deleted file mode 100644 index 9a04647f..00000000 --- a/client/src/main/java/com/theokanning/openai/OpenAiService.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.theokanning.openai; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import com.theokanning.openai.completion.CompletionRequest; -import com.theokanning.openai.completion.CompletionResult; -import com.theokanning.openai.completion.chat.ChatCompletionRequest; -import com.theokanning.openai.completion.chat.ChatCompletionResult; -import com.theokanning.openai.edit.EditRequest; -import com.theokanning.openai.edit.EditResult; -import com.theokanning.openai.embedding.EmbeddingRequest; -import com.theokanning.openai.embedding.EmbeddingResult; -import com.theokanning.openai.engine.Engine; -import com.theokanning.openai.file.File; -import com.theokanning.openai.finetune.FineTuneEvent; -import com.theokanning.openai.finetune.FineTuneRequest; -import com.theokanning.openai.finetune.FineTuneResult; -import com.theokanning.openai.image.*; -import com.theokanning.openai.model.Model; -import com.theokanning.openai.moderation.ModerationRequest; -import com.theokanning.openai.moderation.ModerationResult; -import okhttp3.*; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.jackson.JacksonConverterFactory; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static java.time.Duration.ofSeconds; - -/** - * Use the OpenAiService from the new 'service' library. See README for more details. - */ -@Deprecated -public class OpenAiService { - - private static final String BASE_URL = "https://api.openai.com/"; - - final OpenAiApi api; - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * - * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - */ - public OpenAiService(final String token) { - this(token, BASE_URL, ofSeconds(10)); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * - * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - * @param timeout http read timeout in seconds, 0 means no timeout - * @deprecated use {@link OpenAiService(String, Duration)} - */ - @Deprecated - public OpenAiService(final String token, final int timeout) { - this(token, BASE_URL, ofSeconds(timeout)); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * - * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - * @param timeout http read timeout, Duration.ZERO means no timeout - */ - public OpenAiService(final String token, final Duration timeout) { - this(token, BASE_URL, timeout); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * - * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - * @param timeout http read timeout, Duration.ZERO means no timeout - */ - public OpenAiService(final String token, final String baseUrl, final Duration timeout) { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new AuthenticationInterceptor(token)) - .connectionPool(new ConnectionPool(5, 1, TimeUnit.SECONDS)) - .readTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS) - .build(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .client(client) - .addConverterFactory(JacksonConverterFactory.create(mapper)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build(); - - this.api = retrofit.create(OpenAiApi.class); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * - * @param api OpenAiApi instance to use for all methods - */ - public OpenAiService(final OpenAiApi api) { - this.api = api; - } - - public List listModels() { - return api.listModels().blockingGet().data; - } - - public Model getModel(String modelId) { - return api.getModel(modelId).blockingGet(); - } - - public CompletionResult createCompletion(CompletionRequest request) { - return api.createCompletion(request).blockingGet(); - } - - public ChatCompletionResult createChatCompletion(ChatCompletionRequest request) { - return api.createChatCompletion(request).blockingGet(); - } - - /** - * Use {@link OpenAiService#createCompletion(CompletionRequest)} and {@link CompletionRequest#model}instead - */ - @Deprecated - public CompletionResult createCompletion(String engineId, CompletionRequest request) { - return api.createCompletion(engineId, request).blockingGet(); - } - - public EditResult createEdit(EditRequest request) { - return api.createEdit(request).blockingGet(); - } - - /** - * Use {@link OpenAiService#createEdit(EditRequest)} and {@link EditRequest#model}instead - */ - @Deprecated - public EditResult createEdit(String engineId, EditRequest request) { - return api.createEdit(engineId, request).blockingGet(); - } - - public EmbeddingResult createEmbeddings(EmbeddingRequest request) { - return api.createEmbeddings(request).blockingGet(); - } - - /** - * Use {@link OpenAiService#createEmbeddings(EmbeddingRequest)} and {@link EmbeddingRequest#model}instead - */ - @Deprecated - public EmbeddingResult createEmbeddings(String engineId, EmbeddingRequest request) { - return api.createEmbeddings(engineId, request).blockingGet(); - } - - public List listFiles() { - return api.listFiles().blockingGet().data; - } - - public File uploadFile(String purpose, String filepath) { - java.io.File file = new java.io.File(filepath); - RequestBody purposeBody = RequestBody.create(okhttp3.MultipartBody.FORM, purpose); - RequestBody fileBody = RequestBody.create(MediaType.parse("text"), file); - MultipartBody.Part body = MultipartBody.Part.createFormData("file", filepath, fileBody); - - return api.uploadFile(purposeBody, body).blockingGet(); - } - - public DeleteResult deleteFile(String fileId) { - return api.deleteFile(fileId).blockingGet(); - } - - public File retrieveFile(String fileId) { - return api.retrieveFile(fileId).blockingGet(); - } - - public FineTuneResult createFineTune(FineTuneRequest request) { - return api.createFineTune(request).blockingGet(); - } - - public CompletionResult createFineTuneCompletion(CompletionRequest request) { - return api.createFineTuneCompletion(request).blockingGet(); - } - - public List listFineTunes() { - return api.listFineTunes().blockingGet().data; - } - - public FineTuneResult retrieveFineTune(String fineTuneId) { - return api.retrieveFineTune(fineTuneId).blockingGet(); - } - - public FineTuneResult cancelFineTune(String fineTuneId) { - return api.cancelFineTune(fineTuneId).blockingGet(); - } - - public List listFineTuneEvents(String fineTuneId) { - return api.listFineTuneEvents(fineTuneId).blockingGet().data; - } - - public DeleteResult deleteFineTune(String fineTuneId) { - return api.deleteFineTune(fineTuneId).blockingGet(); - } - - public ImageResult createImage(CreateImageRequest request) { - return api.createImage(request).blockingGet(); - } - - public ImageResult createImageEdit(CreateImageEditRequest request, String imagePath, String maskPath) { - java.io.File image = new java.io.File(imagePath); - java.io.File mask = null; - if (maskPath != null) { - mask = new java.io.File(maskPath); - } - return createImageEdit(request, image, mask); - } - - public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File image, java.io.File mask) { - RequestBody imageBody = RequestBody.create(MediaType.parse("image"), image); - - MultipartBody.Builder builder = new MultipartBody.Builder() - .setType(MediaType.get("multipart/form-data")) - .addFormDataPart("prompt", request.getPrompt()) - .addFormDataPart("size", request.getSize()) - .addFormDataPart("response_format", request.getResponseFormat()) - .addFormDataPart("image", "image", imageBody); - - if (request.getN() != null) { - builder.addFormDataPart("n", request.getN().toString()); - } - - if (mask != null) { - RequestBody maskBody = RequestBody.create(MediaType.parse("image"), mask); - builder.addFormDataPart("mask", "mask", maskBody); - } - - return api.createImageEdit(builder.build()).blockingGet(); - } - - public ImageResult createImageVariation(CreateImageVariationRequest request, String imagePath) { - java.io.File image = new java.io.File(imagePath); - return createImageVariation(request, image); - } - - public ImageResult createImageVariation(CreateImageVariationRequest request, java.io.File image) { - RequestBody imageBody = RequestBody.create(MediaType.parse("image"), image); - - MultipartBody.Builder builder = new MultipartBody.Builder() - .setType(MediaType.get("multipart/form-data")) - .addFormDataPart("size", request.getSize()) - .addFormDataPart("response_format", request.getResponseFormat()) - .addFormDataPart("image", "image", imageBody); - - if (request.getN() != null) { - builder.addFormDataPart("n", request.getN().toString()); - } - - return api.createImageVariation(builder.build()).blockingGet(); - } - - public ModerationResult createModeration(ModerationRequest request) { - return api.createModeration(request).blockingGet(); - } - - @Deprecated - public List getEngines() { - return api.getEngines().blockingGet().data; - } - - @Deprecated - public Engine getEngine(String engineId) { - return api.getEngine(engineId).blockingGet(); - } -} diff --git a/client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java b/client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java new file mode 100644 index 00000000..d89c36f1 --- /dev/null +++ b/client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.client; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * OkHttp Interceptor that adds an authorization token header + */ +public class AuthenticationInterceptor implements Interceptor { + + private final String token; + + protected AuthenticationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request() + .newBuilder() + .header("Authorization", "Bearer " + token) + .build(); + return chain.proceed(request); + } +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java new file mode 100644 index 00000000..7342e953 --- /dev/null +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -0,0 +1,324 @@ +package com.theokanning.openai.client; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.*; +import com.theokanning.openai.audio.CreateSpeechRequest; +import com.theokanning.openai.audio.TranscriptionResult; +import com.theokanning.openai.audio.TranslationResult; +import com.theokanning.openai.billing.BillingUsage; +import com.theokanning.openai.billing.Subscription; +import com.theokanning.openai.completion.CompletionRequest; +import com.theokanning.openai.completion.CompletionResult; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatCompletionResult; +import com.theokanning.openai.edit.EditRequest; +import com.theokanning.openai.edit.EditResult; +import com.theokanning.openai.embedding.EmbeddingRequest; +import com.theokanning.openai.embedding.EmbeddingResult; +import com.theokanning.openai.engine.Engine; +import com.theokanning.openai.file.File; +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; +import com.theokanning.openai.finetune.FineTuneEvent; +import com.theokanning.openai.finetune.FineTuneRequest; +import com.theokanning.openai.finetune.FineTuneResult; +import com.theokanning.openai.image.CreateImageRequest; +import com.theokanning.openai.image.ImageResult; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageFile; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.messages.ModifyMessageRequest; +import com.theokanning.openai.model.Model; +import com.theokanning.openai.moderation.ModerationRequest; +import com.theokanning.openai.moderation.ModerationResult; +import com.theokanning.openai.runs.CreateThreadAndRunRequest; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.runs.RunStep; +import com.theokanning.openai.runs.SubmitToolOutputsRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import io.reactivex.Single; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.*; + +import java.time.LocalDate; +import java.util.Map; + +public interface OpenAiApi { + + @GET("v1/models") + Single> listModels(); + + @GET("/v1/models/{model_id}") + Single getModel(@Path("model_id") String modelId); + + @POST("/v1/completions") + Single createCompletion(@Body CompletionRequest request); + + @Streaming + @POST("/v1/completions") + Call createCompletionStream(@Body CompletionRequest request); + + @POST("/v1/chat/completions") + Single createChatCompletion(@Body ChatCompletionRequest request); + + @Streaming + @POST("/v1/chat/completions") + Call createChatCompletionStream(@Body ChatCompletionRequest request); + + @Deprecated + @POST("/v1/engines/{engine_id}/completions") + Single createCompletion(@Path("engine_id") String engineId, @Body CompletionRequest request); + + @POST("/v1/edits") + Single createEdit(@Body EditRequest request); + + @Deprecated + @POST("/v1/engines/{engine_id}/edits") + Single createEdit(@Path("engine_id") String engineId, @Body EditRequest request); + + @POST("/v1/embeddings") + Single createEmbeddings(@Body EmbeddingRequest request); + + @Deprecated + @POST("/v1/engines/{engine_id}/embeddings") + Single createEmbeddings(@Path("engine_id") String engineId, @Body EmbeddingRequest request); + + @GET("/v1/files") + Single> listFiles(); + + @Multipart + @POST("/v1/files") + Single uploadFile(@Part("purpose") RequestBody purpose, @Part MultipartBody.Part file); + + @DELETE("/v1/files/{file_id}") + Single deleteFile(@Path("file_id") String fileId); + + @GET("/v1/files/{file_id}") + Single retrieveFile(@Path("file_id") String fileId); + + @Streaming + @GET("/v1/files/{file_id}/content") + Single retrieveFileContent(@Path("file_id") String fileId); + + @POST("/v1/fine_tuning/jobs") + Single createFineTuningJob(@Body FineTuningJobRequest request); + + @GET("/v1/fine_tuning/jobs") + Single> listFineTuningJobs(); + + @GET("/v1/fine_tuning/jobs/{fine_tuning_job_id}") + Single retrieveFineTuningJob(@Path("fine_tuning_job_id") String fineTuningJobId); + + @POST("/v1/fine_tuning/jobs/{fine_tuning_job_id}/cancel") + Single cancelFineTuningJob(@Path("fine_tuning_job_id") String fineTuningJobId); + + @GET("/v1/fine_tuning/jobs/{fine_tuning_job_id}/events") + Single> listFineTuningJobEvents(@Path("fine_tuning_job_id") String fineTuningJobId); + + @Deprecated + @POST("/v1/fine-tunes") + Single createFineTune(@Body FineTuneRequest request); + + @POST("/v1/completions") + Single createFineTuneCompletion(@Body CompletionRequest request); + + @Deprecated + @GET("/v1/fine-tunes") + Single> listFineTunes(); + + @Deprecated + @GET("/v1/fine-tunes/{fine_tune_id}") + Single retrieveFineTune(@Path("fine_tune_id") String fineTuneId); + + @Deprecated + @POST("/v1/fine-tunes/{fine_tune_id}/cancel") + Single cancelFineTune(@Path("fine_tune_id") String fineTuneId); + + @Deprecated + @GET("/v1/fine-tunes/{fine_tune_id}/events") + Single> listFineTuneEvents(@Path("fine_tune_id") String fineTuneId); + + @DELETE("/v1/models/{fine_tune_id}") + Single deleteFineTune(@Path("fine_tune_id") String fineTuneId); + + @POST("/v1/images/generations") + Single createImage(@Body CreateImageRequest request); + + @POST("/v1/images/edits") + Single createImageEdit(@Body RequestBody requestBody); + + @POST("/v1/images/variations") + Single createImageVariation(@Body RequestBody requestBody); + + @POST("/v1/audio/transcriptions") + Single createTranscription(@Body RequestBody requestBody); + + @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); + + @Deprecated + @GET("v1/engines") + Single> getEngines(); + + @Deprecated + @GET("/v1/engines/{engine_id}") + Single getEngine(@Path("engine_id") String engineId); + + /** + * Account information inquiry: It contains total amount (in US dollars) and other information. + * + * @return + */ + @Deprecated + @GET("v1/dashboard/billing/subscription") + Single subscription(); + + /** + * Account call interface consumption amount inquiry. + * totalUsage = Total amount used by the account (in US cents). + * + * @param starDate + * @param endDate + * @return Consumption amount information. + */ + @Deprecated + @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 AssistantRequest 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 ModifyAssistantRequest 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); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads") + Single createThread(@Body ThreadRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}") + Single retrieveThread(@Path("thread_id") String threadId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads/{thread_id}") + Single modifyThread(@Path("thread_id") String threadId, @Body ThreadRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/threads/{thread_id}") + Single deleteThread(@Path("thread_id") String threadId); + + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads/{thread_id}/messages") + Single createMessage(@Path("thread_id") String threadId, @Body MessageRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}") + Single retrieveMessage(@Path("thread_id") String threadId, @Path("message_id") String messageId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads/{thread_id}/messages/{message_id}") + Single modifyMessage(@Path("thread_id") String threadId, @Path("message_id") String messageId, @Body ModifyMessageRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages") + Single> listMessages(@Path("thread_id") String threadId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages") + Single> listMessages(@Path("thread_id") String threadId, @QueryMap Map filterRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}/files/{file_id}") + Single retrieveMessageFile(@Path("thread_id") String threadId, @Path("message_id") String messageId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}/files") + Single> listMessageFiles(@Path("thread_id") String threadId, @Path("message_id") String messageId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}/files") + Single> listMessageFiles(@Path("thread_id") String threadId, @Path("message_id") String messageId, @QueryMap Map filterRequest); + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs") + Single createRun(@Path("thread_id") String threadId, @Body RunCreateRequest runCreateRequest); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs/{run_id}") + Single retrieveRun(@Path("thread_id") String threadId, @Path("run_id") String runId); + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs/{run_id}") + Single modifyRun(@Path("thread_id") String threadId, @Path("run_id") String runId, @Body Map metadata); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs") + Single> listRuns(@Path("thread_id") String threadId, @QueryMap Map listSearchParameters); + + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs/{run_id}/submit_tool_outputs") + Single submitToolOutputs(@Path("thread_id") String threadId, @Path("run_id") String runId, @Body SubmitToolOutputsRequest submitToolOutputsRequest); + + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs/{run_id}/cancel") + Single cancelRun(@Path("thread_id") String threadId, @Path("run_id") String runId); + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/runs") + Single createThreadAndRun(@Body CreateThreadAndRunRequest createThreadAndRunRequest); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs/{run_id}/steps/{step_id}") + Single retrieveRunStep(@Path("thread_id") String threadId, @Path("run_id") String runId, @Path("step_id") String stepId); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs/{run_id}/steps") + Single> listRunSteps(@Path("thread_id") String threadId, @Path("run_id") String runId, @QueryMap Map listSearchParameters); +} diff --git a/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java b/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java deleted file mode 100644 index 546dc489..00000000 --- a/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.theokanning.openai; -import com.theokanning.openai.completion.CompletionChoice; -import com.theokanning.openai.completion.CompletionRequest; -import com.theokanning.openai.completion.chat.ChatCompletionChoice; -import com.theokanning.openai.completion.chat.ChatCompletionRequest; -import com.theokanning.openai.completion.chat.ChatMessage; -import com.theokanning.openai.completion.chat.ChatMessageRole; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ChatCompletionTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void createChatCompletion() { - final List messages = new ArrayList<>(); // java version agnostic - final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a dog and will speak as such."); - messages.add(systemMessage); - - ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest - .builder() - .model("gpt-3.5-turbo") - .messages(messages) - .n(5) - .maxTokens(50) - .logitBias(new HashMap<>()) - .build(); - - List choices = service.createChatCompletion(chatCompletionRequest).getChoices(); - assertEquals(5, choices.size()); - - } -} diff --git a/client/src/test/java/com/theokanning/openai/CompletionTest.java b/client/src/test/java/com/theokanning/openai/CompletionTest.java deleted file mode 100644 index df614ce0..00000000 --- a/client/src/test/java/com/theokanning/openai/CompletionTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.completion.CompletionChoice; -import com.theokanning.openai.completion.CompletionRequest; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - - -public class CompletionTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void createCompletion() { - CompletionRequest completionRequest = CompletionRequest.builder() - .model("ada") - .prompt("Somebody once told me the world is gonna roll me") - .echo(true) - .n(5) - .maxTokens(50) - .user("testing") - .logitBias(new HashMap<>()) - .build(); - - List choices = service.createCompletion(completionRequest).getChoices(); - assertEquals(5, choices.size()); - } - - @Test - void createCompletionDeprecated() { - CompletionRequest completionRequest = CompletionRequest.builder() - .prompt("Somebody once told me the world is gonna roll me") - .echo(true) - .user("testing") - .build(); - - List choices = service.createCompletion("ada", completionRequest).getChoices(); - assertFalse(choices.isEmpty()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/EditTest.java b/client/src/test/java/com/theokanning/openai/EditTest.java deleted file mode 100644 index ff260046..00000000 --- a/client/src/test/java/com/theokanning/openai/EditTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.edit.EditRequest; -import com.theokanning.openai.edit.EditResult; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class EditTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void edit() { - EditRequest request = EditRequest.builder() - .model("text-davinci-edit-001") - .input("What day of the wek is it?") - .instruction("Fix the spelling mistakes") - .build(); - - EditResult result = service.createEdit(request); - assertNotNull(result.getChoices().get(0).getText()); - } - - @Test - void editDeprecated() { - EditRequest request = EditRequest.builder() - .input("What day of the wek is it?") - .instruction("Fix the spelling mistakes") - .build(); - - EditResult result = service.createEdit("text-davinci-edit-001", request); - - assertNotNull(result.getChoices().get(0).getText()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java b/client/src/test/java/com/theokanning/openai/EmbeddingTest.java deleted file mode 100644 index 16628e0c..00000000 --- a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.embedding.Embedding; -import com.theokanning.openai.embedding.EmbeddingRequest; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertFalse; - - -public class EmbeddingTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void createEmbeddings() { - EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() - .model("text-similarity-babbage-001") - .input(Collections.singletonList("The food was delicious and the waiter...")) - .build(); - - List embeddings = service.createEmbeddings(embeddingRequest).getData(); - - assertFalse(embeddings.isEmpty()); - assertFalse(embeddings.get(0).getEmbedding().isEmpty()); - } - - @Test - void createEmbeddingsDeprecated() { - EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() - .input(Collections.singletonList("The food was delicious and the waiter...")) - .build(); - - List embeddings = service.createEmbeddings("text-similarity-babbage-001", embeddingRequest).getData(); - - assertFalse(embeddings.isEmpty()); - assertFalse(embeddings.get(0).getEmbedding().isEmpty()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/EngineTest.java b/client/src/test/java/com/theokanning/openai/EngineTest.java deleted file mode 100644 index 5e522731..00000000 --- a/client/src/test/java/com/theokanning/openai/EngineTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.engine.Engine; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - - -public class EngineTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void getEngines() { - List engines = service.getEngines(); - - assertFalse(engines.isEmpty()); - } - - @Test - void getEngine() { - Engine ada = service.getEngine("ada"); - - assertEquals("ada", ada.id); - } -} diff --git a/client/src/test/java/com/theokanning/openai/FileTest.java b/client/src/test/java/com/theokanning/openai/FileTest.java deleted file mode 100644 index 9bfe5aeb..00000000 --- a/client/src/test/java/com/theokanning/openai/FileTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.file.File; -import org.junit.jupiter.api.*; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class FileTest { - static String filePath = "src/test/resources/fine-tuning-data.jsonl"; - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - static String fileId; - - @Test - @Order(1) - void uploadFile() throws Exception { - File file = service.uploadFile("fine-tune", filePath); - fileId = file.getId(); - - assertEquals("fine-tune", file.getPurpose()); - assertEquals(filePath, file.getFilename()); - - // wait for file to be processed - TimeUnit.SECONDS.sleep(10); - } - - @Test - @Order(2) - void listFiles() { - List files = service.listFiles(); - - assertTrue(files.stream().anyMatch(file -> file.getId().equals(fileId))); - } - - @Test - @Order(3) - void retrieveFile() { - File file = service.retrieveFile(fileId); - - assertEquals(filePath, file.getFilename()); - } - - @Test - @Order(4) - void deleteFile() { - DeleteResult result = service.deleteFile(fileId); - assertTrue(result.isDeleted()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/FineTuneTest.java b/client/src/test/java/com/theokanning/openai/FineTuneTest.java deleted file mode 100644 index 4c93a6bd..00000000 --- a/client/src/test/java/com/theokanning/openai/FineTuneTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.finetune.FineTuneRequest; -import com.theokanning.openai.finetune.FineTuneEvent; -import com.theokanning.openai.finetune.FineTuneResult; -import org.junit.jupiter.api.*; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.jupiter.api.Assertions.*; - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class FineTuneTest { - static OpenAiService service; - static String fileId; - static String fineTuneId; - - - @BeforeAll - static void setup() throws Exception { - String token = System.getenv("OPENAI_TOKEN"); - service = new OpenAiService(token); - fileId = service.uploadFile("fine-tune", "src/test/resources/fine-tuning-data.jsonl").getId(); - - // wait for file to be processed - TimeUnit.SECONDS.sleep(10); - } - - @AfterAll - static void teardown() { - service.deleteFile(fileId); - } - - @Test - @Order(1) - void createFineTune() { - FineTuneRequest request = FineTuneRequest.builder() - .trainingFile(fileId) - .model("ada") - .nEpochs(4) - .build(); - - FineTuneResult fineTune = service.createFineTune(request); - fineTuneId = fineTune.getId(); - - assertEquals("pending", fineTune.getStatus()); - } - - @Test - @Order(2) - void listFineTunes() { - List fineTunes = service.listFineTunes(); - - assertTrue(fineTunes.stream().anyMatch(fineTune -> fineTune.getId().equals(fineTuneId))); - } - - @Test - @Order(3) - void listFineTuneEvents() { - List events = service.listFineTuneEvents(fineTuneId); - - assertFalse(events.isEmpty()); - } - - @Test - @Order(3) - void retrieveFineTune() { - FineTuneResult fineTune = service.retrieveFineTune(fineTuneId); - - assertEquals("ada", fineTune.getModel()); - } - - @Test - @Order(4) - void cancelFineTune() { - FineTuneResult fineTune = service.cancelFineTune(fineTuneId); - - assertEquals("cancelled", fineTune.getStatus()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/ImageTest.java b/client/src/test/java/com/theokanning/openai/ImageTest.java deleted file mode 100644 index be29073f..00000000 --- a/client/src/test/java/com/theokanning/openai/ImageTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.image.CreateImageEditRequest; -import com.theokanning.openai.image.CreateImageRequest; -import com.theokanning.openai.image.CreateImageVariationRequest; -import com.theokanning.openai.image.Image; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - - -public class ImageTest { - - static String filePath = "src/test/resources/penguin.png"; - static String fileWithAlphaPath = "src/test/resources/penguin_with_alpha.png"; - static String maskPath = "src/test/resources/mask.png"; - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token, 30); - - - @Test - void createImageUrl() { - CreateImageRequest createImageRequest = CreateImageRequest.builder() - .prompt("penguin") - .n(3) - .size("256x256") - .user("testing") - .build(); - - List images = service.createImage(createImageRequest).getData(); - assertEquals(3, images.size()); - assertNotNull(images.get(0).getUrl()); - } - - @Test - void createImageBase64() { - CreateImageRequest createImageRequest = CreateImageRequest.builder() - .prompt("penguin") - .responseFormat("b64_json") - .user("testing") - .build(); - - List images = service.createImage(createImageRequest).getData(); - assertEquals(1, images.size()); - assertNotNull(images.get(0).getB64Json()); - } - - @Test - void createImageEdit() { - CreateImageEditRequest createImageRequest = CreateImageEditRequest.builder() - .prompt("a penguin with a red background") - .responseFormat("url") - .size("256x256") - .user("testing") - .n(2) - .build(); - - List images = service.createImageEdit(createImageRequest, fileWithAlphaPath, null).getData(); - assertEquals(2, images.size()); - assertNotNull(images.get(0).getUrl()); - } - - @Test - void createImageEditWithMask() { - CreateImageEditRequest createImageRequest = CreateImageEditRequest.builder() - .prompt("a penguin with a red hat") - .responseFormat("url") - .size("256x256") - .user("testing") - .n(2) - .build(); - - List images = service.createImageEdit(createImageRequest, filePath, maskPath).getData(); - assertEquals(2, images.size()); - assertNotNull(images.get(0).getUrl()); - } - - @Test - void createImageVariation() { - CreateImageVariationRequest createImageVariationRequest = CreateImageVariationRequest.builder() - .responseFormat("url") - .size("256x256") - .user("testing") - .n(2) - .build(); - - List images = service.createImageVariation(createImageVariationRequest, filePath).getData(); - assertEquals(2, images.size()); - assertNotNull(images.get(0).getUrl()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/ModelTest.java b/client/src/test/java/com/theokanning/openai/ModelTest.java deleted file mode 100644 index be7f573e..00000000 --- a/client/src/test/java/com/theokanning/openai/ModelTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.model.Model; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - - -public class ModelTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void listModels() { - List models = service.listModels(); - - assertFalse(models.isEmpty()); - } - - @Test - void getModel() { - Model ada = service.getModel("ada"); - - assertEquals("ada", ada.id); - assertEquals("openai", ada.ownedBy); - assertFalse(ada.permission.isEmpty()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/ModerationTest.java b/client/src/test/java/com/theokanning/openai/ModerationTest.java deleted file mode 100644 index c6024c08..00000000 --- a/client/src/test/java/com/theokanning/openai/ModerationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.moderation.ModerationRequest; -import com.theokanning.openai.moderation.Moderation; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertTrue; - - -public class ModerationTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void createModeration() { - ModerationRequest moderationRequest = ModerationRequest.builder() - .input("I want to kill them") - .model("text-moderation-latest") - .build(); - - Moderation moderationScore = service.createModeration(moderationRequest).getResults().get(0); - - assertTrue(moderationScore.isFlagged()); - } -} diff --git a/client/src/test/resources/fine-tuning-data.jsonl b/client/src/test/resources/fine-tuning-data.jsonl deleted file mode 100644 index 2f0e4c79..00000000 --- a/client/src/test/resources/fine-tuning-data.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"prompt": "prompt", "completion": "text"} -{"prompt": "prompt", "completion": "text"} \ No newline at end of file diff --git a/client/src/test/resources/mask.png b/client/src/test/resources/mask.png deleted file mode 100644 index 84fcfb35..00000000 Binary files a/client/src/test/resources/mask.png and /dev/null differ diff --git a/client/src/test/resources/penguin.png b/client/src/test/resources/penguin.png deleted file mode 100644 index 91b023c3..00000000 Binary files a/client/src/test/resources/penguin.png and /dev/null differ diff --git a/client/src/test/resources/penguin_with_alpha.png b/client/src/test/resources/penguin_with_alpha.png deleted file mode 100644 index 4636c4f4..00000000 Binary files a/client/src/test/resources/penguin_with_alpha.png and /dev/null differ diff --git a/example/build.gradle b/example/build.gradle index d70ac091..f64639e9 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -7,4 +7,24 @@ application { dependencies { implementation project(":service") +} + +task runExampleOne(type: JavaExec) { + mainClass.set('example.OpenAiApiExample') + classpath = sourceSets.main.runtimeClasspath + args = [] +} + +task runExampleTwo(type: JavaExec) { + mainClass.set('example.OpenAiApiFunctionsExample') + classpath = sourceSets.main.runtimeClasspath + args = [] + standardInput = System.in +} + +task runExampleThree(type: JavaExec) { + mainClass.set('example.OpenAiApiFunctionsWIthStreamExample') + classpath = sourceSets.main.runtimeClasspath + args = [] + standardInput = System.in } \ 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..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/example/src/main/java/example/OpenAiApiExample.java b/example/src/main/java/example/OpenAiApiExample.java index e6e78b44..52ae1ccf 100644 --- a/example/src/main/java/example/OpenAiApiExample.java +++ b/example/src/main/java/example/OpenAiApiExample.java @@ -7,6 +7,7 @@ import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.image.CreateImageRequest; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -14,11 +15,11 @@ class OpenAiApiExample { public static void main(String... args) { String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); + OpenAiService service = new OpenAiService(token, Duration.ofSeconds(30)); System.out.println("\nCreating completion..."); CompletionRequest completionRequest = CompletionRequest.builder() - .model("ada") + .model("babbage-002") .prompt("Somebody once told me the world is gonna roll me") .echo(true) .user("testing") diff --git a/example/src/main/java/example/OpenAiApiFunctionsExample.java b/example/src/main/java/example/OpenAiApiFunctionsExample.java new file mode 100644 index 00000000..954b9104 --- /dev/null +++ b/example/src/main/java/example/OpenAiApiFunctionsExample.java @@ -0,0 +1,109 @@ +package example; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.completion.chat.ChatCompletionRequest.ChatCompletionRequestFunctionCall; +import com.theokanning.openai.service.FunctionExecutor; +import com.theokanning.openai.service.OpenAiService; + +import java.util.*; + +class OpenAiApiFunctionsExample { + + public static class Weather { + @JsonPropertyDescription("City and state, for example: León, Guanajuato") + public String location; + + @JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'") + @JsonProperty(required = true) + public WeatherUnit unit; + } + + public enum WeatherUnit { + CELSIUS, FAHRENHEIT; + } + + public static class WeatherResponse { + public String location; + public WeatherUnit unit; + public int temperature; + public String description; + + public WeatherResponse(String location, WeatherUnit unit, int temperature, String description) { + this.location = location; + this.unit = unit; + this.temperature = temperature; + this.description = description; + } + } + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + FunctionExecutor functionExecutor = new FunctionExecutor(Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny")) + .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(functionExecutor.getFunctions()) + .functionCall(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) { + System.out.println("Trying to execute " + functionCall.getName() + "..."); + Optional message = functionExecutor.executeAndConvertToMessageSafely(functionCall); + /* You can also try 'executeAndConvertToMessage' inside a try-catch block, and add the following line inside the catch: + "message = executor.handleException(exception);" + The content of the message will be the exception itself, so the flow of the conversation will not be interrupted, and you will still be able to log the issue. */ + + if (message.isPresent()) { + /* At this point: + 1. The function requested was found + 2. The request was converted to its specified object for execution (Weather.class in this case) + 3. It was executed + 4. The response was finally converted to a ChatMessage object. */ + + System.out.println("Executed " + functionCall.getName() + "."); + messages.add(message.get()); + continue; + } else { + System.out.println("Something went wrong with the execution of " + functionCall.getName() + "..."); + break; + } + } + + 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/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java b/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java new file mode 100644 index 00000000..e6de65b6 --- /dev/null +++ b/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java @@ -0,0 +1,86 @@ +package example; + +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.service.FunctionExecutor; +import com.theokanning.openai.service.OpenAiService; +import example.OpenAiApiFunctionsExample.Weather; +import example.OpenAiApiFunctionsExample.WeatherResponse; +import io.reactivex.Flowable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +public class OpenAiApiFunctionsWithStreamExample { + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + FunctionExecutor functionExecutor = new FunctionExecutor(Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny")) + .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(functionExecutor.getFunctions()) + .functionCall(ChatCompletionRequest.ChatCompletionRequestFunctionCall.of("auto")) + .n(1) + .maxTokens(256) + .logitBias(new HashMap<>()) + .build(); + Flowable flowable = service.streamChatCompletion(chatCompletionRequest); + + AtomicBoolean isFirst = new AtomicBoolean(true); + ChatMessage chatMessage = service.mapStreamToAccumulator(flowable) + .doOnNext(accumulator -> { + if (accumulator.isFunctionCall()) { + if (isFirst.getAndSet(false)) { + System.out.println("Executing function " + accumulator.getAccumulatedChatFunctionCall().getName() + "..."); + } + } else { + if (isFirst.getAndSet(false)) { + System.out.print("Response: "); + } + if (accumulator.getMessageChunk().getContent() != null) { + System.out.print(accumulator.getMessageChunk().getContent()); + } + } + }) + .doOnComplete(System.out::println) + .lastElement() + .blockingGet() + .getAccumulatedMessage(); + messages.add(chatMessage); // don't forget to update the conversation with the latest response + + if (chatMessage.getFunctionCall() != null) { + System.out.println("Trying to execute " + chatMessage.getFunctionCall().getName() + "..."); + ChatMessage functionResponse = functionExecutor.executeAndConvertToMessageHandlingExceptions(chatMessage.getFunctionCall()); + System.out.println("Executed " + chatMessage.getFunctionCall().getName() + "."); + messages.add(functionResponse); + continue; + } + + System.out.print("Next Query: "); + String nextLine = scanner.nextLine(); + if (nextLine.equalsIgnoreCase("exit")) { + System.exit(0); + } + messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine)); + } + } + +} \ No newline at end of file diff --git a/example/src/main/java/example/TikTokensExample.java b/example/src/main/java/example/TikTokensExample.java new file mode 100644 index 00000000..3da1a4d7 --- /dev/null +++ b/example/src/main/java/example/TikTokensExample.java @@ -0,0 +1,21 @@ +package example; + +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; +import com.theokanning.openai.utils.TikTokensUtil; + +import java.util.ArrayList; +import java.util.List; + +class TikTokensExample { + + public static void main(String... args) { + List messages = new ArrayList<>(); + messages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), "Hello OpenAI 1.")); + messages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), "Hello OpenAI 2. ")); + + int tokens_1 = TikTokensUtil.tokens(TikTokensUtil.ModelEnum.GPT_3_5_TURBO.getName(), messages); + int tokens_2 = TikTokensUtil.tokens(TikTokensUtil.ModelEnum.GPT_3_5_TURBO.getName(), "Hello OpenAI 1."); + } + +} diff --git a/gradle.properties b/gradle.properties index 50f68438..a4f840ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.12.0 +VERSION_NAME=0.18.2 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ecccc347 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +[versions] +jackson = "2.14.2" +retrofit = "2.9.0" + +[libraries] +jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jacksonAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } +jacksonJsonSchema = { module = "com.kjetland:mbknor-jackson-jsonschema_2.12", version = "1.0.34" } +lombok = { module = "org.projectlombok:lombok", version = "1.18.24" } +junitBom = { module = "org.junit:junit-bom", version = "5.8.2" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofitJackson = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } +retrofitRxJava2 = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" } +retrofitMock = { module = "com.squareup.retrofit2:retrofit-mock", version.ref = "retrofit" } +jtokkit = { module = "com.knuddels:jtokkit", version = "0.5.1" } diff --git a/service/build.gradle b/service/build.gradle index 6d8ee9dc..6c242724 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -3,13 +3,14 @@ apply plugin: "com.vanniktech.maven.publish" dependencies { api project(":client") - api 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' - implementation 'com.squareup.retrofit2:converter-jackson:2.9.0' + api libs.retrofit + implementation libs.retrofitRxJava2 + implementation libs.retrofitJackson + implementation libs.jacksonJsonSchema - testImplementation(platform('org.junit:junit-bom:5.8.2')) + testImplementation(platform(libs.junitBom)) testImplementation 'org.junit.jupiter:junit-jupiter' - testImplementation 'com.squareup.retrofit2:retrofit-mock:2.9.0' + testImplementation libs.retrofitMock } compileJava { diff --git a/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java new file mode 100644 index 00000000..e2f24ffb --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java @@ -0,0 +1,13 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; + +public abstract class ChatCompletionRequestMixIn { + + @JsonSerialize(using = ChatCompletionRequestSerializerAndDeserializer.Serializer.class) + @JsonDeserialize(using = ChatCompletionRequestSerializerAndDeserializer.Deserializer.class) + abstract ChatCompletionRequest.ChatCompletionRequestFunctionCall getFunctionCall(); + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java new file mode 100644 index 00000000..e9a8b023 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java @@ -0,0 +1,41 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; + +import java.io.IOException; + +public class ChatCompletionRequestSerializerAndDeserializer { + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(ChatCompletionRequest.ChatCompletionRequestFunctionCall value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null || value.getName() == null) { + gen.writeNull(); + } else if ("none".equals(value.getName()) || "auto".equals(value.getName())) { + gen.writeString(value.getName()); + } else { + gen.writeStartObject(); + gen.writeFieldName("name"); + gen.writeString(value.getName()); + gen.writeEndObject(); + } + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public ChatCompletionRequest.ChatCompletionRequestFunctionCall deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.getCurrentToken().isStructStart()) { + p.nextToken(); //key + p.nextToken(); //value + } + return new ChatCompletionRequest.ChatCompletionRequestFunctionCall(p.getValueAsString()); + } + } +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java new file mode 100644 index 00000000..9b7be0f9 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java @@ -0,0 +1,64 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.TextNode; + +import java.io.IOException; + +public class ChatFunctionCallArgumentsSerializerAndDeserializer { + + private final static ObjectMapper MAPPER = new ObjectMapper(); + + private ChatFunctionCallArgumentsSerializerAndDeserializer() { + } + + public static class Serializer extends JsonSerializer { + + private Serializer() { + } + + @Override + public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeString(value instanceof TextNode ? value.asText() : value.toPrettyString()); + } + } + } + + public static class Deserializer extends JsonDeserializer { + + private Deserializer() { + } + + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String json = p.getValueAsString(); + if (json == null || p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + try { + JsonNode node = null; + try { + node = MAPPER.readTree(json); + } catch (JsonParseException ignored) { + } + if (node == null || node.getNodeType() == JsonNodeType.MISSING) { + node = MAPPER.readTree(p); + } + return node; + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + } + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java new file mode 100644 index 00000000..7b32e051 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java @@ -0,0 +1,13 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +public abstract class ChatFunctionCallMixIn { + + @JsonSerialize(using = ChatFunctionCallArgumentsSerializerAndDeserializer.Serializer.class) + @JsonDeserialize(using = ChatFunctionCallArgumentsSerializerAndDeserializer.Deserializer.class) + abstract JsonNode getArguments(); + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java new file mode 100644 index 00000000..d94a9179 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java @@ -0,0 +1,10 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +public abstract class ChatFunctionMixIn { + + @JsonSerialize(using = ChatFunctionParametersSerializer.class) + abstract Class getParametersClass(); + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java new file mode 100644 index 00000000..c0273916 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java @@ -0,0 +1,37 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.kjetland.jackson.jsonSchema.JsonSchemaConfig; +import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator; + +import java.io.IOException; + +public class ChatFunctionParametersSerializer extends JsonSerializer> { + + private final ObjectMapper mapper = new ObjectMapper(); + private final JsonSchemaConfig config = JsonSchemaConfig.vanillaJsonSchemaDraft4(); + private final JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(mapper, config); + + @Override + public void serialize(Class value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + try { + JsonNode schema = jsonSchemaGenerator.generateJsonSchema(value); + gen.writeObject(schema); + } catch (Exception e) { + throw new RuntimeException("Failed to generate JSON Schema", e); + } + } + } +} + + + + + diff --git a/service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java b/service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java new file mode 100644 index 00000000..a55173cc --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java @@ -0,0 +1,84 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.completion.chat.ChatFunctionCall; +import com.theokanning.openai.completion.chat.ChatMessage; + +/** + * Class that accumulates chat messages and provides utility methods for + * handling message chunks and function calls within a chat stream. This + * class is immutable. + * + * @author [Your Name] + */ +public class ChatMessageAccumulator { + + private final ChatMessage messageChunk; + private final ChatMessage accumulatedMessage; + + /** + * Constructor that initializes the message chunk and accumulated message. + * + * @param messageChunk The message chunk. + * @param accumulatedMessage The accumulated message. + */ + public ChatMessageAccumulator(ChatMessage messageChunk, ChatMessage accumulatedMessage) { + this.messageChunk = messageChunk; + this.accumulatedMessage = accumulatedMessage; + } + + /** + * Checks if the accumulated message contains a function call. + * + * @return true if the accumulated message contains a function call, false otherwise. + */ + public boolean isFunctionCall() { + return getAccumulatedMessage().getFunctionCall() != null && getAccumulatedMessage().getFunctionCall().getName() != null; + } + + /** + * Checks if the accumulated message contains a chat message. + * + * @return true if the accumulated message contains a chat message, false otherwise. + */ + public boolean isChatMessage() { + return !isFunctionCall(); + } + + /** + * Retrieves the message chunk. + * + * @return the message chunk. + */ + public ChatMessage getMessageChunk() { + return messageChunk; + } + + /** + * Retrieves the accumulated message. + * + * @return the accumulated message. + */ + public ChatMessage getAccumulatedMessage() { + return accumulatedMessage; + } + + /** + * Retrieves the function call from the message chunk. + * This is equivalent to getMessageChunk().getFunctionCall(). + * + * @return the function call from the message chunk. + */ + public ChatFunctionCall getChatFunctionCallChunk() { + return getMessageChunk().getFunctionCall(); + } + + /** + * Retrieves the function call from the accumulated message. + * This is equivalent to getAccumulatedMessage().getFunctionCall(). + * + * @return the function call from the accumulated message. + */ + public ChatFunctionCall getAccumulatedChatFunctionCall() { + return getAccumulatedMessage().getFunctionCall(); + } +} diff --git a/service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java b/service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java new file mode 100644 index 00000000..5d143a95 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java @@ -0,0 +1,105 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.theokanning.openai.completion.chat.ChatFunction; +import com.theokanning.openai.completion.chat.ChatFunctionCall; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; + +import java.util.*; + +public class FunctionExecutor { + + private ObjectMapper MAPPER = new ObjectMapper(); + private final Map FUNCTIONS = new HashMap<>(); + + public FunctionExecutor(List functions) { + setFunctions(functions); + } + + public FunctionExecutor(List functions, ObjectMapper objectMapper) { + setFunctions(functions); + setObjectMapper(objectMapper); + } + + public Optional executeAndConvertToMessageSafely(ChatFunctionCall call) { + try { + return Optional.ofNullable(executeAndConvertToMessage(call)); + } catch (Exception ignored) { + return Optional.empty(); + } + } + + public ChatMessage executeAndConvertToMessageHandlingExceptions(ChatFunctionCall call) { + try { + return executeAndConvertToMessage(call); + } catch (Exception exception) { + exception.printStackTrace(); + return convertExceptionToMessage(exception); + } + } + + public ChatMessage convertExceptionToMessage(Exception exception) { + String error = exception.getMessage() == null ? exception.toString() : exception.getMessage(); + return new ChatMessage(ChatMessageRole.FUNCTION.value(), "{\"error\": \"" + error + "\"}", "error"); + } + + public ChatMessage executeAndConvertToMessage(ChatFunctionCall call) { + return new ChatMessage(ChatMessageRole.FUNCTION.value(), executeAndConvertToJson(call).toPrettyString(), call.getName()); + } + + public JsonNode executeAndConvertToJson(ChatFunctionCall call) { + try { + Object execution = execute(call); + if (execution instanceof TextNode) { + JsonNode objectNode = MAPPER.readTree(((TextNode) execution).asText()); + if (objectNode.isMissingNode()) + return (JsonNode) execution; + return objectNode; + } + if (execution instanceof ObjectNode) { + return (JsonNode) execution; + } + if (execution instanceof String) { + JsonNode objectNode = MAPPER.readTree((String) execution); + if (objectNode.isMissingNode()) + throw new RuntimeException("Parsing exception"); + return objectNode; + } + return MAPPER.readValue(MAPPER.writeValueAsString(execution), JsonNode.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public T execute(ChatFunctionCall call) { + ChatFunction function = FUNCTIONS.get(call.getName()); + Object obj; + try { + JsonNode arguments = call.getArguments(); + obj = MAPPER.readValue(arguments instanceof TextNode ? arguments.asText() : arguments.toPrettyString(), function.getParametersClass()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return (T) function.getExecutor().apply(obj); + } + + public List getFunctions() { + return new ArrayList<>(FUNCTIONS.values()); + } + + public void setFunctions(List functions) { + this.FUNCTIONS.clear(); + functions.forEach(f -> this.FUNCTIONS.put(f.getName(), f)); + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.MAPPER = objectMapper; + } + +} 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 8b3b1cb7..52ab6b0f 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -1,24 +1,29 @@ 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; -import com.theokanning.openai.DeleteResult; -import com.theokanning.openai.OpenAiApi; -import com.theokanning.openai.OpenAiError; -import com.theokanning.openai.OpenAiHttpException; +import com.fasterxml.jackson.databind.node.TextNode; +import com.theokanning.openai.*; +import com.theokanning.openai.assistants.*; +import com.theokanning.openai.audio.*; +import com.theokanning.openai.billing.BillingUsage; +import com.theokanning.openai.billing.Subscription; +import com.theokanning.openai.client.OpenAiApi; import com.theokanning.openai.completion.CompletionChunk; import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.completion.CompletionResult; -import com.theokanning.openai.completion.chat.ChatCompletionChunk; -import com.theokanning.openai.completion.chat.ChatCompletionRequest; -import com.theokanning.openai.completion.chat.ChatCompletionResult; +import com.theokanning.openai.completion.chat.*; import com.theokanning.openai.edit.EditRequest; import com.theokanning.openai.edit.EditResult; import com.theokanning.openai.embedding.EmbeddingRequest; import com.theokanning.openai.embedding.EmbeddingResult; import com.theokanning.openai.file.File; +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneRequest; import com.theokanning.openai.finetune.FineTuneResult; @@ -26,23 +31,37 @@ import com.theokanning.openai.image.CreateImageRequest; import com.theokanning.openai.image.CreateImageVariationRequest; import com.theokanning.openai.image.ImageResult; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageFile; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.messages.ModifyMessageRequest; import com.theokanning.openai.model.Model; import com.theokanning.openai.moderation.ModerationRequest; import com.theokanning.openai.moderation.ModerationResult; - +import com.theokanning.openai.runs.CreateThreadAndRunRequest; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.runs.RunStep; +import com.theokanning.openai.runs.SubmitToolOutputsRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; import io.reactivex.Single; import okhttp3.*; +import retrofit2.Call; import retrofit2.HttpException; import retrofit2.Retrofit; -import retrofit2.Call; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.jackson.JacksonConverterFactory; +import javax.validation.constraints.NotNull; import java.io.IOException; import java.time.Duration; +import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -149,7 +168,7 @@ public List listFiles() { public File uploadFile(String purpose, String filepath) { java.io.File file = new java.io.File(filepath); - RequestBody purposeBody = RequestBody.create(okhttp3.MultipartBody.FORM, purpose); + RequestBody purposeBody = RequestBody.create(MultipartBody.FORM, purpose); RequestBody fileBody = RequestBody.create(MediaType.parse("text"), file); MultipartBody.Part body = MultipartBody.Part.createFormData("file", filepath, fileBody); @@ -164,6 +183,31 @@ public File retrieveFile(String fileId) { return execute(api.retrieveFile(fileId)); } + public ResponseBody retrieveFileContent(String fileId) { + return execute(api.retrieveFileContent(fileId)); + } + + public FineTuningJob createFineTuningJob(FineTuningJobRequest request) { + return execute(api.createFineTuningJob(request)); + } + + public List listFineTuningJobs() { + return execute(api.listFineTuningJobs()).data; + } + + public FineTuningJob retrieveFineTuningJob(String fineTuningJobId) { + return execute(api.retrieveFineTuningJob(fineTuningJobId)); + } + + public FineTuningJob cancelFineTuningJob(String fineTuningJobId) { + return execute(api.cancelFineTuningJob(fineTuningJobId)); + } + + public List listFineTuningJobEvents(String fineTuningJobId) { + return execute(api.listFineTuningJobEvents(fineTuningJobId)).data; + } + + @Deprecated public FineTuneResult createFineTune(FineTuneRequest request) { return execute(api.createFineTune(request)); } @@ -172,18 +216,22 @@ public CompletionResult createFineTuneCompletion(CompletionRequest request) { return execute(api.createFineTuneCompletion(request)); } + @Deprecated public List listFineTunes() { return execute(api.listFineTunes()).data; } + @Deprecated public FineTuneResult retrieveFineTune(String fineTuneId) { return execute(api.retrieveFineTune(fineTuneId)); } + @Deprecated public FineTuneResult cancelFineTune(String fineTuneId) { return execute(api.cancelFineTune(fineTuneId)); } + @Deprecated public List listFineTuneEvents(String fineTuneId) { return execute(api.listFineTuneEvents(fineTuneId)).data; } @@ -224,6 +272,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())); } @@ -245,13 +297,214 @@ 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())); } + public TranscriptionResult createTranscription(CreateTranscriptionRequest request, String audioPath) { + java.io.File audio = new java.io.File(audioPath); + return createTranscription(request, audio); + } + + public TranscriptionResult createTranscription(CreateTranscriptionRequest request, java.io.File audio) { + RequestBody audioBody = RequestBody.create(MediaType.parse("audio"), audio); + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart("model", request.getModel()) + .addFormDataPart("file", audio.getName(), audioBody); + + if (request.getPrompt() != null) { + builder.addFormDataPart("prompt", request.getPrompt()); + } + if (request.getResponseFormat() != null) { + builder.addFormDataPart("response_format", request.getResponseFormat()); + } + if (request.getTemperature() != null) { + builder.addFormDataPart("temperature", request.getTemperature().toString()); + } + if (request.getLanguage() != null) { + builder.addFormDataPart("language", request.getLanguage()); + } + + return execute(api.createTranscription(builder.build())); + } + + public TranslationResult createTranslation(CreateTranslationRequest request, String audioPath) { + java.io.File audio = new java.io.File(audioPath); + return createTranslation(request, audio); + } + + public TranslationResult createTranslation(CreateTranslationRequest request, java.io.File audio) { + RequestBody audioBody = RequestBody.create(MediaType.parse("audio"), audio); + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart("model", request.getModel()) + .addFormDataPart("file", audio.getName(), audioBody); + + if (request.getPrompt() != null) { + builder.addFormDataPart("prompt", request.getPrompt()); + } + if (request.getResponseFormat() != null) { + builder.addFormDataPart("response_format", request.getResponseFormat()); + } + if (request.getTemperature() != null) { + builder.addFormDataPart("temperature", request.getTemperature().toString()); + } + + return execute(api.createTranslation(builder.build())); + } + public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public ResponseBody createSpeech(CreateSpeechRequest request) { + return execute(api.createSpeech(request)); + } + + public Assistant createAssistant(AssistantRequest request) { + return execute(api.createAssistant(request)); + } + + public Assistant retrieveAssistant(String assistantId) { + return execute(api.retrieveAssistant(assistantId)); + } + + public Assistant modifyAssistant(String assistantId, ModifyAssistantRequest request) { + return execute(api.modifyAssistant(assistantId, request)); + } + + public DeleteResult deleteAssistant(String assistantId) { + return execute(api.deleteAssistant(assistantId)); + } + + public OpenAiResponse listAssistants(ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, 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 OpenAiResponse listAssistantFiles(String assistantId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { + }); + return execute(api.listAssistantFiles(assistantId, queryParameters)); + } + + public Thread createThread(ThreadRequest request) { + return execute(api.createThread(request)); + } + + public Thread retrieveThread(String threadId) { + return execute(api.retrieveThread(threadId)); + } + + public Thread modifyThread(String threadId, ThreadRequest request) { + return execute(api.modifyThread(threadId, request)); + } + + public DeleteResult deleteThread(String threadId) { + return execute(api.deleteThread(threadId)); + } + + public Message createMessage(String threadId, MessageRequest request) { + return execute(api.createMessage(threadId, request)); + } + + public Message retrieveMessage(String threadId, String messageId) { + return execute(api.retrieveMessage(threadId, messageId)); + } + + public Message modifyMessage(String threadId, String messageId, ModifyMessageRequest request) { + return execute(api.modifyMessage(threadId, messageId, request)); + } + + public OpenAiResponse listMessages(String threadId) { + return execute(api.listMessages(threadId)); + } + + public OpenAiResponse listMessages(String threadId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { + }); + return execute(api.listMessages(threadId, queryParameters)); + } + + public MessageFile retrieveMessageFile(String threadId, String messageId, String fileId) { + return execute(api.retrieveMessageFile(threadId, messageId, fileId)); + } + + public OpenAiResponse listMessageFiles(String threadId, String messageId) { + return execute(api.listMessageFiles(threadId, messageId)); + } + + public OpenAiResponse listMessageFiles(String threadId, String messageId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { + }); + return execute(api.listMessageFiles(threadId, messageId, queryParameters)); + } + + public Run createRun(String threadId, RunCreateRequest runCreateRequest) { + return execute(api.createRun(threadId, runCreateRequest)); + } + + public Run retrieveRun(String threadId, String runId) { + return execute(api.retrieveRun(threadId, runId)); + } + + public Run modifyRun(String threadId, String runId, Map metadata) { + return execute(api.modifyRun(threadId, runId, metadata)); + } + + public OpenAiResponse listRuns(String threadId, ListSearchParameters listSearchParameters) { + Map search = new HashMap<>(); + if (listSearchParameters != null) { + ObjectMapper mapper = defaultObjectMapper(); + search = mapper.convertValue(listSearchParameters, Map.class); + } + return execute(api.listRuns(threadId, search)); + } + + public Run submitToolOutputs(String threadId, String runId, SubmitToolOutputsRequest submitToolOutputsRequest) { + return execute(api.submitToolOutputs(threadId, runId, submitToolOutputsRequest)); + } + + public Run cancelRun(String threadId, String runId) { + return execute(api.cancelRun(threadId, runId)); + } + + public Run createThreadAndRun(CreateThreadAndRunRequest createThreadAndRunRequest) { + return execute(api.createThreadAndRun(createThreadAndRunRequest)); + } + + public RunStep retrieveRunStep(String threadId, String runId, String stepId) { + return execute(api.retrieveRunStep(threadId, runId, stepId)); + } + + public OpenAiResponse listRunSteps(String threadId, String runId, ListSearchParameters listSearchParameters) { + Map search = new HashMap<>(); + if (listSearchParameters != null) { + ObjectMapper mapper = defaultObjectMapper(); + search = mapper.convertValue(listSearchParameters, Map.class); + } + return execute(api.listRunSteps(threadId, runId, search)); + } + /** * Calls the Open AI api, returns the response, and parses error messages if the request fails */ @@ -329,6 +582,9 @@ public static ObjectMapper defaultObjectMapper() { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class); + mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); + mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class); return mapper; } @@ -348,4 +604,59 @@ public static Retrofit defaultRetrofit(OkHttpClient client, ObjectMapper mapper) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); } + + public Flowable mapStreamToAccumulator(Flowable flowable) { + ChatFunctionCall functionCall = new ChatFunctionCall(null, null); + ChatMessage accumulatedMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), null); + + return flowable.map(chunk -> { + ChatMessage messageChunk = chunk.getChoices().get(0).getMessage(); + if (messageChunk.getFunctionCall() != null) { + if (messageChunk.getFunctionCall().getName() != null) { + String namePart = messageChunk.getFunctionCall().getName(); + functionCall.setName((functionCall.getName() == null ? "" : functionCall.getName()) + namePart); + } + if (messageChunk.getFunctionCall().getArguments() != null) { + String argumentsPart = messageChunk.getFunctionCall().getArguments() == null ? "" : messageChunk.getFunctionCall().getArguments().asText(); + functionCall.setArguments(new TextNode((functionCall.getArguments() == null ? "" : functionCall.getArguments().asText()) + argumentsPart)); + } + accumulatedMessage.setFunctionCall(functionCall); + } else { + accumulatedMessage.setContent((accumulatedMessage.getContent() == null ? "" : accumulatedMessage.getContent()) + (messageChunk.getContent() == null ? "" : messageChunk.getContent())); + } + + if (chunk.getChoices().get(0).getFinishReason() != null) { // last + if (functionCall.getArguments() != null) { + functionCall.setArguments(mapper.readTree(functionCall.getArguments().asText())); + accumulatedMessage.setFunctionCall(functionCall); + } + } + + return new ChatMessageAccumulator(messageChunk, accumulatedMessage); + }); + } + + /** + * Account information inquiry: including total amount and other information. + * + * @return Account information. + */ + public Subscription subscription() { + Single subscription = api.subscription(); + return subscription.blockingGet(); + } + + /** + * Account API consumption amount information inquiry. + * Up to 100 days of inquiry. + * + * @param starDate + * @param endDate + * @return Consumption amount information. + */ + public BillingUsage billingUsage(@NotNull LocalDate starDate, @NotNull LocalDate endDate) { + Single billingUsage = api.billingUsage(starDate, endDate); + return billingUsage.blockingGet(); + } + } diff --git a/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java b/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java index 3f0dbf1c..c5404e0f 100644 --- a/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java +++ b/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import com.theokanning.openai.OpenAiError; @@ -54,11 +55,11 @@ public void onResponse(Call call, Response response) } InputStream in = response.body().byteStream(); - reader = new BufferedReader(new InputStreamReader(in)); + reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); String line; SSE sse = null; - while ((line = reader.readLine()) != null) { + while (!emitter.isCancelled() && (line = reader.readLine()) != null) { if (line.startsWith("data:")) { String data = line.substring(5).trim(); sse = new SSE(data); @@ -86,7 +87,7 @@ public void onResponse(Call call, Response response) try { reader.close(); } catch (IOException e) { - // do nothing + // do nothing } } } @@ -96,4 +97,4 @@ public void onResponse(Call call, Response response) public void onFailure(Call call, Throwable t) { emitter.onError(t); } -} \ No newline at end of file +} diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java new file mode 100644 index 00000000..9ad819a7 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java @@ -0,0 +1,148 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +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; +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantFunction; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.assistants.AssistantToolsEnum; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatFunction; +import com.theokanning.openai.completion.chat.ChatFunctionCall; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.runs.RequiredAction; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.runs.RunStep; +import com.theokanning.openai.runs.SubmitToolOutputRequestItem; +import com.theokanning.openai.runs.SubmitToolOutputs; +import com.theokanning.openai.runs.SubmitToolOutputsRequest; +import com.theokanning.openai.runs.ToolCall; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class AssistantFunctionTest { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token, Duration.ofMinutes(1)); + + @Test + void createRetrieveRun() throws JsonProcessingException { + + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class); + mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); + mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class); + + String funcDef = "{\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"location\": {\n" + + " \"type\": \"string\",\n" + + " \"description\": \"The city and state, e.g. San Francisco, CA\"\n" + + " },\n" + + " \"unit\": {\n" + + " \"type\": \"string\",\n" + + " \"enum\": [\"celsius\", \"fahrenheit\"]\n" + + " }\n" + + " },\n" + + " \"required\": [\"location\"]\n" + + "}"; + Map funcParameters = mapper.readValue(funcDef, new TypeReference>() {}); + AssistantFunction function = AssistantFunction.builder() + .name("weather_reporter") + .description("Get the current weather of a location") + .parameters(funcParameters) + .build(); + + List toolList = new ArrayList<>(); + Tool funcTool = new Tool(AssistantToolsEnum.FUNCTION, function); + toolList.add(funcTool); + + + AssistantRequest assistantRequest = AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name("MATH_TUTOR") + .instructions("You are a personal Math Tutor.") + .tools(toolList) + .build(); + Assistant assistant = service.createAssistant(assistantRequest); + + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + Thread thread = service.createThread(threadRequest); + + MessageRequest messageRequest = MessageRequest.builder() + .content("What's the weather of Xiamen?") + .build(); + + Message message = service.createMessage(thread.getId(), messageRequest); + + RunCreateRequest runCreateRequest = RunCreateRequest.builder() + .assistantId(assistant.getId()) + .build(); + + Run run = service.createRun(thread.getId(), runCreateRequest); + assertNotNull(run); + + Run retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + while (!(retrievedRun.getStatus().equals("completed")) + && !(retrievedRun.getStatus().equals("failed")) + && !(retrievedRun.getStatus().equals("requires_action"))){ + retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + } + if (retrievedRun.getStatus().equals("requires_action")) { + RequiredAction requiredAction = retrievedRun.getRequiredAction(); + System.out.println("requiredAction"); + System.out.println(mapper.writeValueAsString(requiredAction)); + List toolCalls = requiredAction.getSubmitToolOutputs().getToolCalls(); + ToolCall toolCall = toolCalls.get(0); + String toolCallId = toolCall.getId(); + + SubmitToolOutputRequestItem toolOutputRequestItem = SubmitToolOutputRequestItem.builder() + .toolCallId(toolCallId) + .output("sunny") + .build(); + List toolOutputRequestItems = new ArrayList<>(); + toolOutputRequestItems.add(toolOutputRequestItem); + SubmitToolOutputsRequest submitToolOutputsRequest = SubmitToolOutputsRequest.builder() + .toolOutputs(toolOutputRequestItems) + .build(); + retrievedRun = service.submitToolOutputs(retrievedRun.getThreadId(), retrievedRun.getId(), submitToolOutputsRequest); + + while (!(retrievedRun.getStatus().equals("completed")) + && !(retrievedRun.getStatus().equals("failed")) + && !(retrievedRun.getStatus().equals("requires_action"))){ + retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + } + + OpenAiResponse response = service.listMessages(thread.getId()); + + List messages = response.getData(); + + System.out.println(mapper.writeValueAsString(messages)); + + } + } +} 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..8b687e34 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -0,0 +1,128 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.*; +import com.theokanning.openai.file.File; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.*; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AssistantTest { + + static OpenAiService service = new OpenAiService(System.getenv("OPENAI_TOKEN")); + static String assistantId; + static String fileId; + + + @AfterAll + static void teardown() { + try { + service.deleteAssistantFile(assistantId, fileId); + } catch (Exception e) { + // do nothing + } + try { + service.deleteAssistant(assistantId); + } catch (Exception e) { + // do nothing + } + } + + @Test + @Order(1) + void createAssistant() { + AssistantRequest assistantRequest = AssistantRequest.builder().model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()).name("Math Tutor").instructions("You are a personal Math Tutor.").tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER, null))).build(); + Assistant assistant = service.createAssistant(assistantRequest); + + assistantId = assistant.getId(); + + assertEquals(assistant.getName(), "Math Tutor"); + assertEquals(assistant.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); + } + + @Test + @Order(2) + void retrieveAssistant() { + Assistant assistant = service.retrieveAssistant(assistantId); + + assertEquals(assistant.getName(), "Math Tutor"); + } + + @Test + @Order(3) + void modifyAssistant() { + String modifiedName = "Science Tutor"; + ModifyAssistantRequest modifyRequest = ModifyAssistantRequest.builder().name(modifiedName).build(); + + Assistant modifiedAssistant = service.modifyAssistant(assistantId, modifyRequest); + assertEquals(modifiedName, modifiedAssistant.getName()); + } + + @Test + @Order(4) + void listAssistants() { + OpenAiResponse assistants = service.listAssistants(ListSearchParameters.builder().build()); + + assertNotNull(assistants); + assertFalse(assistants.getData().isEmpty()); + } + + @Test + @Order(5) + void createAssistantFile() { + String filePath = "src/test/resources/assistants-data.html"; + File uploadedFile = service.uploadFile("assistants", filePath); + + AssistantFile assistantFile = service.createAssistantFile(assistantId, new AssistantFileRequest(uploadedFile.getId())); + + fileId = assistantFile.getId(); + assertNotNull(assistantFile); + assertEquals(uploadedFile.getId(), assistantFile.getId()); + assertEquals(assistantId, assistantFile.getAssistantId()); + } + + @Test + @Order(6) + void retrieveAssistantFile() { + AssistantFile file = service.retrieveAssistantFile(assistantId, fileId); + + assertEquals(file.getId(), fileId); + } + + + @Test + @Order(7) + void listAssistantFiles() { + List files = service.listAssistantFiles(assistantId, new ListSearchParameters()).data; + + assertFalse(files.isEmpty()); + assertEquals(files.get(0).getId(), fileId); + assertEquals(files.get(0).getObject(), "assistant.file"); + } + + @Test + @Order(8) + void deleteAssistantFile() { + DeleteResult deletedFile = service.deleteAssistantFile(assistantId, fileId); + + assertEquals(deletedFile.getId(), fileId); + assertTrue(deletedFile.isDeleted()); + } + + @Test + @Order(9) + void deleteAssistant() { + DeleteResult deletedAssistant = service.deleteAssistant(assistantId); + + assertEquals(assistantId, deletedAssistant.getId()); + assertTrue(deletedAssistant.isDeleted()); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java new file mode 100644 index 00000000..9cb083de --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -0,0 +1,91 @@ +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.*; + + +public class AudioTest { + + static String englishAudioFilePath = "src/test/resources/hello-world.mp3"; + + static String koreanAudioFilePath = "src/test/resources/korean-hello.mp3"; + + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token, Duration.ofSeconds(30)); + + @Test + void createTranscription() { + CreateTranscriptionRequest createTranscriptionRequest = CreateTranscriptionRequest.builder() + .model("whisper-1") + .build(); + + String text = service.createTranscription(createTranscriptionRequest, englishAudioFilePath).getText(); + assertEquals("Hello World.", text); + } + + @Test + void createTranscriptionVerbose() { + CreateTranscriptionRequest createTranscriptionRequest = CreateTranscriptionRequest.builder() + .model("whisper-1") + .responseFormat("verbose_json") + .build(); + + TranscriptionResult result = service.createTranscription(createTranscriptionRequest, englishAudioFilePath); + assertEquals("Hello World.", result.getText()); + assertEquals("transcribe", result.getTask()); + assertEquals("english", result.getLanguage()); + assertTrue(result.getDuration() > 0); + assertEquals(1, result.getSegments().size()); + } + + @Test + void createTranslation() { + CreateTranslationRequest createTranslationRequest = CreateTranslationRequest.builder() + .model("whisper-1") + .build(); + + String text = service.createTranslation(createTranslationRequest, koreanAudioFilePath).getText(); + assertEquals("Hello, my name is Yoona. I am a Korean native speaker.", text); + } + + @Test + void createTranslationVerbose() { + CreateTranslationRequest createTranslationRequest = CreateTranslationRequest.builder() + .model("whisper-1") + .responseFormat("verbose_json") + .build(); + + TranslationResult result = service.createTranslation(createTranslationRequest, koreanAudioFilePath); + assertEquals("Hello, my name is Yoona. I am a Korean native speaker.", result.getText()); + assertEquals("translate", result.getTask()); + assertEquals("english", result.getLanguage()); + 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 f1f79ed1..25f0defb 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -1,15 +1,45 @@ package com.theokanning.openai.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; 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.*; import static org.junit.jupiter.api.Assertions.*; class ChatCompletionTest { + static class Weather { + @JsonPropertyDescription("City and state, for example: León, Guanajuato") + public String location; + + @JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'") + @JsonProperty(required = true) + public WeatherUnit unit; + } + + enum WeatherUnit { + CELSIUS, FAHRENHEIT; + } + + static class WeatherResponse { + public String location; + public WeatherUnit unit; + public int temperature; + public String description; + + public WeatherResponse(String location, WeatherUnit unit, int temperature, String description) { + this.location = location; + this.unit = unit; + this.temperature = temperature; + this.description = description; + } + } + String token = System.getenv("OPENAI_TOKEN"); OpenAiService service = new OpenAiService(token); @@ -54,4 +84,220 @@ void streamChatCompletion() { assertNotNull(chunks.get(0).getChoices().get(0)); } + @Test + void createChatCompletionWithFunctions() { + final List functions = Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather in a given location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, 25, "sunny")) + .build()); + final FunctionExecutor functionExecutor = new FunctionExecutor(functions); + + 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(functionExecutor.getFunctions()) + .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()); + + ChatMessage callResponse = functionExecutor.executeAndConvertToMessageHandlingExceptions(choice.getMessage().getFunctionCall()); + assertNotEquals("error", callResponse.getName()); + + // this performs an unchecked cast + WeatherResponse functionExecutionResponse = functionExecutor.execute(choice.getMessage().getFunctionCall()); + assertInstanceOf(WeatherResponse.class, functionExecutionResponse); + assertEquals(25, functionExecutionResponse.temperature); + + JsonNode jsonFunctionExecutionResponse = functionExecutor.executeAndConvertToJson(choice.getMessage().getFunctionCall()); + assertInstanceOf(ObjectNode.class, jsonFunctionExecutionResponse); + assertEquals("25", jsonFunctionExecutionResponse.get("temperature").asText()); + + messages.add(choice.getMessage()); + messages.add(callResponse); + + ChatCompletionRequest chatCompletionRequest2 = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatCompletionChoice choice2 = service.createChatCompletion(chatCompletionRequest2).getChoices().get(0); + assertNotEquals("function_call", choice2.getFinishReason()); // could be stop or length, but should not be function_call + assertNull(choice2.getMessage().getFunctionCall()); + 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() + .name("get_weather") + .description("Get the current weather in a given location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, 25, "sunny")) + .build()); + final FunctionExecutor functionExecutor = new FunctionExecutor(functions); + + 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(functionExecutor.getFunctions()) + .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()); + + ChatMessage callResponse = functionExecutor.executeAndConvertToMessageHandlingExceptions(accumulatedMessage.getFunctionCall()); + assertNotEquals("error", callResponse.getName()); + + // this performs an unchecked cast + WeatherResponse functionExecutionResponse = functionExecutor.execute(accumulatedMessage.getFunctionCall()); + assertInstanceOf(WeatherResponse.class, functionExecutionResponse); + assertEquals(25, functionExecutionResponse.temperature); + + JsonNode jsonFunctionExecutionResponse = functionExecutor.executeAndConvertToJson(accumulatedMessage.getFunctionCall()); + assertInstanceOf(ObjectNode.class, jsonFunctionExecutionResponse); + assertEquals("25", jsonFunctionExecutionResponse.get("temperature").asText()); + + + messages.add(accumulatedMessage); + messages.add(callResponse); + + ChatCompletionRequest chatCompletionRequest2 = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatMessage accumulatedMessage2 = service.mapStreamToAccumulator(service.streamChatCompletion(chatCompletionRequest2)) + .blockingLast() + .getAccumulatedMessage(); + assertNull(accumulatedMessage2.getFunctionCall()); + 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/CompletionTest.java b/service/src/test/java/com/theokanning/openai/service/CompletionTest.java index c2ad90a1..69284c3b 100644 --- a/service/src/test/java/com/theokanning/openai/service/CompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/CompletionTest.java @@ -21,7 +21,7 @@ public class CompletionTest { @Test void createCompletion() { CompletionRequest completionRequest = CompletionRequest.builder() - .model("ada") + .model("babbage-002") .prompt("Somebody once told me the world is gonna roll me") .echo(true) .n(5) @@ -39,7 +39,7 @@ void createCompletion() { @Test void streamCompletion() { CompletionRequest completionRequest = CompletionRequest.builder() - .model("ada") + .model("babbage-002") .prompt("Somebody once told me the world is gonna roll me") .echo(true) .n(1) diff --git a/service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java b/service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java index 89bb01af..3320dcea 100644 --- a/service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java +++ b/service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java @@ -18,7 +18,7 @@ public class EmbeddingTest { @Test void createEmbeddings() { EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() - .model("text-similarity-babbage-001") + .model("text-embedding-ada-002") .input(Collections.singletonList("The food was delicious and the waiter...")) .build(); diff --git a/service/src/test/java/com/theokanning/openai/service/FileTest.java b/service/src/test/java/com/theokanning/openai/service/FileTest.java index 2b51aee3..51dd9407 100644 --- a/service/src/test/java/com/theokanning/openai/service/FileTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FileTest.java @@ -7,6 +7,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; import java.util.concurrent.TimeUnit; @@ -18,7 +22,7 @@ public class FileTest { static String filePath = "src/test/resources/fine-tuning-data.jsonl"; String token = System.getenv("OPENAI_TOKEN"); - com.theokanning.openai.service.OpenAiService service = new OpenAiService(token); + OpenAiService service = new OpenAiService(token); static String fileId; @Test @@ -52,6 +56,14 @@ void retrieveFile() { @Test @Order(4) + void retrieveFileContent() throws IOException { + String fileBytesToString = service.retrieveFileContent(fileId).string(); + String contents = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); + assertEquals(contents, fileBytesToString); + } + + @Test + @Order(5) void deleteFile() { DeleteResult result = service.deleteFile(fileId); assertTrue(result.isDeleted()); diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java index 0886b547..e47460d0 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.*; +@Deprecated @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class FineTuneTest { static com.theokanning.openai.service.OpenAiService service; diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java new file mode 100644 index 00000000..c0b98f6f --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -0,0 +1,92 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; +import com.theokanning.openai.fine_tuning.Hyperparameters; +import org.junit.jupiter.api.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class FineTuningTest { + static OpenAiService service; + static String fileId; + static String fineTuningJobId; + + + @BeforeAll + static void setup() throws Exception { + String token = System.getenv("OPENAI_TOKEN"); + service = new OpenAiService(token); + fileId = service.uploadFile("fine-tune", "src/test/resources/chat-fine-tuning-data.jsonl").getId(); + + // wait for file to be processed + TimeUnit.SECONDS.sleep(10); + } + + @AfterAll + static void teardown() { + try { + service.deleteFile(fileId); + } catch (Exception e) { + // ignore + } + } + + @Test + @Order(1) + void createFineTuningJob() { + Hyperparameters hyperparameters = Hyperparameters.builder() + .nEpochs(4) + .build(); + FineTuningJobRequest request = FineTuningJobRequest.builder() + .trainingFile(fileId) + .model("gpt-3.5-turbo") + .hyperparameters(hyperparameters) + .build(); + + FineTuningJob fineTuningJob = service.createFineTuningJob(request); + fineTuningJobId = fineTuningJob.getId(); + + assertNotNull(fineTuningJob); + } + + @Test + @Order(2) + void listFineTuningJobs() { + List fineTuningJobs = service.listFineTuningJobs(); + + assertTrue(fineTuningJobs.stream().anyMatch(fineTuningJob -> fineTuningJob.getId().equals(fineTuningJobId))); + } + + @Test + @Order(2) + void listFineTuningEvents() { + List events = service.listFineTuningJobEvents(fineTuningJobId); + + assertFalse(events.isEmpty()); + } + + @Test + @Order(2) + void retrieveFineTuningJob() { + FineTuningJob fineTune = service.retrieveFineTuningJob(fineTuningJobId); + + assertTrue(fineTune.getModel().startsWith("gpt-3.5-turbo")); + } + + @Test + @Order(3) + void cancelFineTuningJob() throws Exception { + FineTuningJob fineTuningJob = service.cancelFineTuningJob(fineTuningJobId); + + assertEquals("cancelled", fineTuningJob.getStatus()); + + // wait before cleaning up to prevent job failure emails + TimeUnit.SECONDS.sleep(3); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/MessageTest.java b/service/src/test/java/com/theokanning/openai/service/MessageTest.java new file mode 100644 index 00000000..06a903c0 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/MessageTest.java @@ -0,0 +1,137 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.file.File; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageFile; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.messages.ModifyMessageRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +public class MessageTest { + + static OpenAiService service; + + static String threadId; + + @BeforeAll + static void setup() { + String token = System.getenv("OPENAI_TOKEN"); + service = new OpenAiService(token); + + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + threadId = service.createThread(threadRequest).getId(); + } + + @AfterAll + static void teardown() { + try { + service.deleteThread(threadId); + } catch (Exception e) { + // ignore + } + } + + @Test + void createMessage() { + File file = service.uploadFile("assistants", "src/test/resources/penguin.png"); + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .fileIds(Collections.singletonList(file.getId())) + .metadata(metadata) + .build(); + + Message message = service.createMessage(threadId, messageRequest); + + assertNotNull(message.getId()); + assertEquals("thread.message", message.getObject()); + assertEquals(1, message.getFileIds().size()); + } + + @Test + void retrieveMessage() { + String messageId = createTestMessage().getId(); + + Message message = service.retrieveMessage(threadId, messageId); + + assertEquals(messageId, message.getId()); + } + + @Test + void modifyMessage() { + String messageId = createTestMessage().getId(); + + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + ModifyMessageRequest request = ModifyMessageRequest.builder() + .metadata(metadata) + .build(); + Message message = service.modifyMessage(threadId, messageId, request); + + assertEquals(messageId, message.getId()); + assertEquals("value", message.getMetadata().get("key")); + } + + @Test + void listMessages() { + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + String separateThreadId = service.createThread(threadRequest).getId(); + createTestMessage(separateThreadId); + createTestMessage(separateThreadId); + createTestMessage(separateThreadId); + + List messages = service.listMessages(separateThreadId).getData(); + + assertEquals(3, messages.size()); + } + + @Test + void retrieveAndListMessageFile() { + File file = service.uploadFile("assistants", "src/test/resources/penguin.png"); + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .fileIds(Collections.singletonList(file.getId())) + .build(); + + Message message = service.createMessage(threadId, messageRequest); + + MessageFile messageFile = service.retrieveMessageFile(threadId, message.getId(), file.getId()); + + assertEquals(file.getId(), messageFile.getId()); + assertEquals(message.getId(), messageFile.getMessageId()); + + List messageFiles = service.listMessageFiles(threadId, message.getId(), new ListSearchParameters()).getData(); + assertEquals(1, messageFiles.size()); + } + + Message createTestMessage() { + return createTestMessage(threadId); + } + + Message createTestMessage(String threadId) { + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + return service.createMessage(threadId, messageRequest); + } +} \ No newline at end of file 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..637ed5e6 100644 --- a/service/src/test/java/com/theokanning/openai/service/ModelTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ModelTest.java @@ -23,10 +23,9 @@ void listModels() { @Test void getModel() { - Model ada = service.getModel("ada"); + Model model = service.getModel("babbage-002"); - assertEquals("ada", ada.id); - assertEquals("openai", ada.ownedBy); - assertFalse(ada.permission.isEmpty()); + assertEquals("babbage-002", model.id); + assertEquals("system", model.ownedBy); } } diff --git a/service/src/test/java/com/theokanning/openai/service/ModerationTest.java b/service/src/test/java/com/theokanning/openai/service/ModerationTest.java index 9df3b447..f28bc083 100644 --- a/service/src/test/java/com/theokanning/openai/service/ModerationTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ModerationTest.java @@ -15,7 +15,7 @@ public class ModerationTest { @Test void createModeration() { ModerationRequest moderationRequest = ModerationRequest.builder() - .input("I want to kill them") + .input("I want to kill him") .model("text-moderation-latest") .build(); diff --git a/service/src/test/java/com/theokanning/openai/service/RunTest.java b/service/src/test/java/com/theokanning/openai/service/RunTest.java new file mode 100644 index 00000000..2bd0c166 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/RunTest.java @@ -0,0 +1,69 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class RunTest { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + @Test + @Timeout(10) + void createRetrieveRun() { + AssistantRequest assistantRequest = AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name("MATH_TUTOR") + .instructions("You are a personal Math Tutor.") + .build(); + Assistant assistant = service.createAssistant(assistantRequest); + + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + Thread thread = service.createThread(threadRequest); + + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + Message message = service.createMessage(thread.getId(), messageRequest); + + RunCreateRequest runCreateRequest = RunCreateRequest.builder() + .assistantId(assistant.getId()) + .build(); + + Run run = service.createRun(thread.getId(), runCreateRequest); + assertNotNull(run); + + Run retrievedRun; + do { + retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + assertEquals(run.getId(), retrievedRun.getId()); + } + while (!(retrievedRun.getStatus().equals("completed")) && !(retrievedRun.getStatus().equals("failed"))); + + + assertNotNull(retrievedRun); + + OpenAiResponse response = service.listMessages(thread.getId()); + + List messages = response.getData(); + assertEquals(2, messages.size()); + assertEquals("user", messages.get(1).getRole()); + assertEquals("assistant", messages.get(0).getRole()); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/ThreadTest.java b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java new file mode 100644 index 00000000..212653af --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java @@ -0,0 +1,67 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ThreadTest { + + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + static String threadId; + + @Test + @Order(1) + void createThread() { + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + ThreadRequest threadRequest = ThreadRequest.builder() + .messages(Collections.singletonList(messageRequest)) + .build(); + + Thread thread = service.createThread(threadRequest); + threadId = thread.getId(); + assertEquals("thread", thread.getObject()); + } + + @Test + @Order(2) + void retrieveThread() { + Thread thread = service.retrieveThread(threadId); + System.out.println(thread.getMetadata()); + assertEquals("thread", thread.getObject()); + } + + @Test + @Order(3) + void modifyThread() { + Map metadata = new HashMap<>(); + metadata.put("action", "modify"); + ThreadRequest threadRequest = ThreadRequest.builder() + .metadata(metadata) + .build(); + Thread thread = service.modifyThread(threadId, threadRequest); + assertEquals("thread", thread.getObject()); + assertEquals("modify", thread.getMetadata().get("action")); + } + + @Test + @Order(4) + void deleteThread() { + DeleteResult deleteResult = service.deleteThread(threadId); + assertEquals("thread.deleted", deleteResult.getObject()); + } +} \ No newline at end of file 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 diff --git a/service/src/test/resources/chat-fine-tuning-data.jsonl b/service/src/test/resources/chat-fine-tuning-data.jsonl new file mode 100644 index 00000000..31604bf2 --- /dev/null +++ b/service/src/test/resources/chat-fine-tuning-data.jsonl @@ -0,0 +1,10 @@ +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} \ No newline at end of file diff --git a/service/src/test/resources/hello-world.mp3 b/service/src/test/resources/hello-world.mp3 new file mode 100644 index 00000000..b5bd8594 Binary files /dev/null and b/service/src/test/resources/hello-world.mp3 differ diff --git a/service/src/test/resources/korean-hello.mp3 b/service/src/test/resources/korean-hello.mp3 new file mode 100644 index 00000000..d1e65b60 Binary files /dev/null and b/service/src/test/resources/korean-hello.mp3 differ