From cb0747a55302c517e6facea22029e430e7c43125 Mon Sep 17 00:00:00 2001 From: Drakeet Date: Mon, 19 Jun 2023 23:50:10 +0800 Subject: [PATCH 01/59] If Flowable is disposed, the SSE readLine loop should be exited (#258) --- .../theokanning/openai/service/ResponseBodyCallback.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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..9a4c7939 100644 --- a/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java +++ b/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java @@ -58,7 +58,7 @@ public void onResponse(Call call, Response response) 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 +86,7 @@ public void onResponse(Call call, Response response) try { reader.close(); } catch (IOException e) { - // do nothing + // do nothing } } } @@ -96,4 +96,4 @@ public void onResponse(Call call, Response response) public void onFailure(Call call, Throwable t) { emitter.onError(t); } -} \ No newline at end of file +} From 3f699951aa903addd11b1ea21fa58f27b3156f68 Mon Sep 17 00:00:00 2001 From: ronilbhattarai <114216019+ronilbhattarai@users.noreply.github.com> Date: Mon, 19 Jun 2023 21:20:45 +0530 Subject: [PATCH 02/59] Removed unused imports (#262) * removed unused import * removed some more unused imports * removed more unused imports --- .../com/theokanning/openai/completion/CompletionResult.java | 1 - api/src/main/java/com/theokanning/openai/image/ImageResult.java | 1 - .../com/theokanning/openai/moderation/ModerationCategories.java | 1 - .../com/theokanning/openai/moderation/ModerationRequest.java | 2 -- client/src/main/java/com/theokanning/openai/OpenAiApi.java | 1 - 5 files changed, 6 deletions(-) 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/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/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/client/src/main/java/com/theokanning/openai/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/OpenAiApi.java index 77e00552..dcaee149 100644 --- a/client/src/main/java/com/theokanning/openai/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/OpenAiApi.java @@ -13,7 +13,6 @@ 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; From 465b16396991c06f4587c836bb3e2828b97cced5 Mon Sep 17 00:00:00 2001 From: Bernhard Haumacher Date: Mon, 19 Jun 2023 17:57:03 +0200 Subject: [PATCH 03/59] Moved OpenAiApi to its own package. (#243) When consuming this library from a Java 11 modular application, no classes in the same package must be exported from multiple JARs. Co-authored-by: Theo Kanning --- .../openai/AuthenticationInterceptor.java | 23 +- .../com/theokanning/openai/OpenAiApi.java | 124 +------- .../com/theokanning/openai/OpenAiService.java | 228 +------------- .../client/AuthenticationInterceptor.java | 28 ++ .../theokanning/openai/client/OpenAiApi.java | 124 ++++++++ .../openai/client/OpenAiService.java | 288 ++++++++++++++++++ .../openai/ChatCompletionTest.java | 1 + .../theokanning/openai/CompletionTest.java | 1 + .../java/com/theokanning/openai/EditTest.java | 1 + .../com/theokanning/openai/EmbeddingTest.java | 1 + .../com/theokanning/openai/EngineTest.java | 1 + .../java/com/theokanning/openai/FileTest.java | 1 + .../com/theokanning/openai/FineTuneTest.java | 1 + .../com/theokanning/openai/ImageTest.java | 1 + .../com/theokanning/openai/ModelTest.java | 1 + .../theokanning/openai/ModerationTest.java | 1 + .../openai/service/OpenAiService.java | 2 +- 17 files changed, 468 insertions(+), 359 deletions(-) create mode 100644 client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java create mode 100644 client/src/main/java/com/theokanning/openai/client/OpenAiApi.java create mode 100644 client/src/main/java/com/theokanning/openai/client/OpenAiService.java 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 dcaee149..feb2f6fc 100644 --- a/client/src/main/java/com/theokanning/openai/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/OpenAiApi.java @@ -1,121 +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.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 index 9a04647f..21d6de81 100644 --- a/client/src/main/java/com/theokanning/openai/OpenAiService.java +++ b/client/src/main/java/com/theokanning/openai/OpenAiService.java @@ -1,47 +1,16 @@ 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 static java.time.Duration.*; 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 Has moved to {@link com.theokanning.openai.client.OpenAiService}. */ @Deprecated -public class OpenAiService { - - private static final String BASE_URL = "https://api.openai.com/"; - - final OpenAiApi api; - +public class OpenAiService extends com.theokanning.openai.client.OpenAiService { /** * Creates a new OpenAiService that wraps OpenAiApi * @@ -80,25 +49,7 @@ public OpenAiService(final String token, final Duration timeout) { * @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); + super(token, baseUrl, timeout); } /** @@ -107,173 +58,6 @@ public OpenAiService(final String token, final String baseUrl, final Duration ti * @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(); + super(api); } } 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..34cb7dc1 --- /dev/null +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -0,0 +1,124 @@ +package com.theokanning.openai.client; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.OpenAiResponse; +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); +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiService.java b/client/src/main/java/com/theokanning/openai/client/OpenAiService.java new file mode 100644 index 00000000..85a69d14 --- /dev/null +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiService.java @@ -0,0 +1,288 @@ +package com.theokanning.openai.client; + +import static java.time.Duration.*; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +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.DeleteResult; +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.CreateImageVariationRequest; +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 okhttp3.ConnectionPool; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; + +/** + * Use the OpenAiService from the new 'service' library. See README for more details. + */ +@Deprecated +public class OpenAiService { + + protected 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/test/java/com/theokanning/openai/ChatCompletionTest.java b/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java index 546dc489..41e3c76f 100644 --- a/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java +++ b/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java @@ -1,4 +1,5 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.completion.CompletionChoice; import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.completion.chat.ChatCompletionChoice; diff --git a/client/src/test/java/com/theokanning/openai/CompletionTest.java b/client/src/test/java/com/theokanning/openai/CompletionTest.java index df614ce0..2b3024b6 100644 --- a/client/src/test/java/com/theokanning/openai/CompletionTest.java +++ b/client/src/test/java/com/theokanning/openai/CompletionTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.completion.CompletionChoice; import com.theokanning.openai.completion.CompletionRequest; import org.junit.jupiter.api.Test; diff --git a/client/src/test/java/com/theokanning/openai/EditTest.java b/client/src/test/java/com/theokanning/openai/EditTest.java index ff260046..11d14439 100644 --- a/client/src/test/java/com/theokanning/openai/EditTest.java +++ b/client/src/test/java/com/theokanning/openai/EditTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.edit.EditRequest; import com.theokanning.openai.edit.EditResult; import org.junit.jupiter.api.Test; diff --git a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java b/client/src/test/java/com/theokanning/openai/EmbeddingTest.java index 16628e0c..e2ddbda8 100644 --- a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java +++ b/client/src/test/java/com/theokanning/openai/EmbeddingTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.embedding.Embedding; import com.theokanning.openai.embedding.EmbeddingRequest; import org.junit.jupiter.api.Test; diff --git a/client/src/test/java/com/theokanning/openai/EngineTest.java b/client/src/test/java/com/theokanning/openai/EngineTest.java index 5e522731..2e047ab2 100644 --- a/client/src/test/java/com/theokanning/openai/EngineTest.java +++ b/client/src/test/java/com/theokanning/openai/EngineTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.engine.Engine; import org.junit.jupiter.api.Test; diff --git a/client/src/test/java/com/theokanning/openai/FileTest.java b/client/src/test/java/com/theokanning/openai/FileTest.java index 9bfe5aeb..998b27a2 100644 --- a/client/src/test/java/com/theokanning/openai/FileTest.java +++ b/client/src/test/java/com/theokanning/openai/FileTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.file.File; import org.junit.jupiter.api.*; diff --git a/client/src/test/java/com/theokanning/openai/FineTuneTest.java b/client/src/test/java/com/theokanning/openai/FineTuneTest.java index 4c93a6bd..54d805ce 100644 --- a/client/src/test/java/com/theokanning/openai/FineTuneTest.java +++ b/client/src/test/java/com/theokanning/openai/FineTuneTest.java @@ -1,6 +1,7 @@ package com.theokanning.openai; import com.theokanning.openai.finetune.FineTuneRequest; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneResult; import org.junit.jupiter.api.*; diff --git a/client/src/test/java/com/theokanning/openai/ImageTest.java b/client/src/test/java/com/theokanning/openai/ImageTest.java index be29073f..304ea0bf 100644 --- a/client/src/test/java/com/theokanning/openai/ImageTest.java +++ b/client/src/test/java/com/theokanning/openai/ImageTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.image.CreateImageEditRequest; import com.theokanning.openai.image.CreateImageRequest; import com.theokanning.openai.image.CreateImageVariationRequest; diff --git a/client/src/test/java/com/theokanning/openai/ModelTest.java b/client/src/test/java/com/theokanning/openai/ModelTest.java index be7f573e..095433ba 100644 --- a/client/src/test/java/com/theokanning/openai/ModelTest.java +++ b/client/src/test/java/com/theokanning/openai/ModelTest.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.model.Model; import org.junit.jupiter.api.Test; diff --git a/client/src/test/java/com/theokanning/openai/ModerationTest.java b/client/src/test/java/com/theokanning/openai/ModerationTest.java index c6024c08..f5c21738 100644 --- a/client/src/test/java/com/theokanning/openai/ModerationTest.java +++ b/client/src/test/java/com/theokanning/openai/ModerationTest.java @@ -1,6 +1,7 @@ package com.theokanning.openai; import com.theokanning.openai.moderation.ModerationRequest; +import com.theokanning.openai.client.OpenAiService; import com.theokanning.openai.moderation.Moderation; import org.junit.jupiter.api.Test; 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..321c1bad 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -5,9 +5,9 @@ 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.theokanning.openai.client.OpenAiApi; import com.theokanning.openai.completion.CompletionChunk; import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.completion.CompletionResult; From 21ef7f017b2cbc9befe06f0761d6752e2572d420 Mon Sep 17 00:00:00 2001 From: Teerapap Changwichukarn Date: Mon, 19 Jun 2023 23:06:03 +0700 Subject: [PATCH 04/59] Add Audio APIs (#292) * Add Audio Transcription API (#272) * Add Audio Translation API (#272) --------- Co-authored-by: Theo Kanning --- .../audio/CreateTranscriptionRequest.java | 46 +++++++++++ .../audio/CreateTranslationRequest.java | 41 ++++++++++ .../openai/audio/TranscriptionResult.java | 44 +++++++++++ .../openai/audio/TranscriptionSegment.java | 32 ++++++++ .../openai/audio/TranslationResult.java | 44 +++++++++++ .../java/com/theokanning/openai/JsonTest.java | 4 + .../fixtures/TranscriptionResult.json | 27 +++++++ .../resources/fixtures/TranslationResult.json | 37 +++++++++ .../openai/service/OpenAiService.java | 59 ++++++++++++++ .../theokanning/openai/service/AudioTest.java | 72 ++++++++++++++++++ service/src/test/resources/hello-world.mp3 | Bin 0 -> 12781 bytes service/src/test/resources/korean-hello.mp3 | Bin 0 -> 42445 bytes 12 files changed, 406 insertions(+) create mode 100644 api/src/main/java/com/theokanning/openai/audio/CreateTranscriptionRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java create mode 100644 api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java create mode 100644 api/src/main/java/com/theokanning/openai/audio/TranslationResult.java create mode 100644 api/src/test/resources/fixtures/TranscriptionResult.json create mode 100644 api/src/test/resources/fixtures/TranslationResult.json create mode 100644 service/src/test/java/com/theokanning/openai/service/AudioTest.java create mode 100644 service/src/test/resources/hello-world.mp3 create mode 100644 service/src/test/resources/korean-hello.mp3 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/test/java/com/theokanning/openai/JsonTest.java b/api/src/test/java/com/theokanning/openai/JsonTest.java index 1ca23a06..09d89f5b 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; @@ -42,6 +44,8 @@ public class JsonTest { FineTuneEvent.class, FineTuneResult.class, ImageResult.class, + TranscriptionResult.class, + TranslationResult.class, Model.class, ModerationRequest.class, ModerationResult.class 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/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 321c1bad..0c6639aa 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -7,6 +7,10 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +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 com.theokanning.openai.client.OpenAiApi; import com.theokanning.openai.completion.CompletionChunk; import com.theokanning.openai.completion.CompletionRequest; @@ -248,6 +252,61 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav 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)); } 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..d4f7b61b --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -0,0 +1,72 @@ +package com.theokanning.openai.service; + +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.time.Duration; + +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 Yuna. I am Korean voice.", 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 Yuna. I am Korean voice.", result.getText()); + assertEquals("translate", result.getTask()); + assertEquals("english", result.getLanguage()); + assertTrue(result.getDuration() > 0); + assertEquals(1, result.getSegments().size()); + } +} diff --git a/service/src/test/resources/hello-world.mp3 b/service/src/test/resources/hello-world.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b5bd8594f12a9bae302a73dde9c85270f99447e7 GIT binary patch literal 12781 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 literal 0 HcmV?d00001 diff --git a/service/src/test/resources/korean-hello.mp3 b/service/src/test/resources/korean-hello.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d1e65b6097ffe696ae69ad27a3840d1d9200e541 GIT binary patch literal 42445 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 P5*33t8UXko{>c9W&{3&K literal 0 HcmV?d00001 From 4ac341241344590a422f13e8a298c854649ebb7f Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Mon, 19 Jun 2023 11:10:27 -0500 Subject: [PATCH 05/59] Add audio apis to moved OpenAiApi A previous commit caused a merge conflict with the Audio PR, and this was the simplest way to fix it. --- .../com/theokanning/openai/client/OpenAiApi.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 34cb7dc1..72f5c9e2 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,8 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.audio.TranscriptionResult; +import com.theokanning.openai.audio.TranslationResult; import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.completion.CompletionResult; import com.theokanning.openai.completion.chat.ChatCompletionRequest; @@ -15,7 +17,6 @@ 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; @@ -42,13 +43,13 @@ public interface OpenAiApi { @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); + Call createChatCompletionStream(@Body ChatCompletionRequest request); @Deprecated @POST("/v1/engines/{engine_id}/completions") @@ -111,6 +112,12 @@ public interface OpenAiApi { @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/moderations") Single createModeration(@Body ModerationRequest request); From 8a422cec5c0053de79e5c985df08d02811ddb811 Mon Sep 17 00:00:00 2001 From: Mrblw <32608255+Mrblw@users.noreply.github.com> Date: Tue, 20 Jun 2023 00:10:38 +0800 Subject: [PATCH 06/59] add chinese support (#265) * add chinese support * Update ResponseBodyCallback.java fix --------- Co-authored-by: bu.liwen --- .../com/theokanning/openai/service/ResponseBodyCallback.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9a4c7939..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,7 +55,7 @@ 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; From 33ba847a659fd2893f04b65565508505ca0ee856 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Tue, 20 Jun 2023 00:13:09 +0800 Subject: [PATCH 07/59] fix typo in `Streaming thread shutdown` of Readme (#276) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d420a65..79c2911c 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ OpenAiService service = new OpenAiService(api); ``` ### 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 From 62bae371c0248f06298ebd5f657042e166ffe1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20M=C3=BCller?= Date: Mon, 19 Jun 2023 18:14:26 +0200 Subject: [PATCH 08/59] Upgrade jackson-annotations (#232) * Upgrade jackson-annotations to 2.14.2 * Move dependencies to version catalog * Extract versions of dependencies versioned together --- api/build.gradle | 10 +++++----- client/build.gradle | 8 ++++---- gradle/libs.versions.toml | 13 +++++++++++++ service/build.gradle | 10 +++++----- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/api/build.gradle b/api/build.gradle index 8c25de96..8f5fefc5 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -2,12 +2,12 @@ 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' + implementation libs.jacksoAnnotations + 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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..f02d7c22 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,13 @@ +[versions] +jackson = "2.14.2" +retrofit = "2.9.0" + +[libraries] +jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jacksoAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } +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" } diff --git a/service/build.gradle b/service/build.gradle index 6d8ee9dc..23e6de39 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -3,13 +3,13 @@ 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 - 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 { From a292898cfa61d6eb4abd9095fe1d6bbfda341ccf Mon Sep 17 00:00:00 2001 From: Grogdunn Date: Mon, 19 Jun 2023 18:54:36 +0200 Subject: [PATCH 09/59] :bug: missing name field (#299) --- .../openai/completion/chat/ChatMessage.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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..a8fc64f7 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,6 @@ package com.theokanning.openai.completion.chat; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +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,7 +13,8 @@ * see OpenAi documentation */ @Data -@NoArgsConstructor +@NoArgsConstructor(force = true) +@RequiredArgsConstructor @AllArgsConstructor public class ChatMessage { @@ -23,6 +22,10 @@ public class ChatMessage { * Must be either 'system', 'user', or 'assistant'.
* You may use {@link ChatMessageRole} enum. */ + @NonNull String role; + @NonNull 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; } From e5c97121eafae0454c34a648d7e2e6368cd9ee32 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Mon, 19 Jun 2023 11:58:55 -0500 Subject: [PATCH 10/59] Delete deprecated client module OpenAiService This was deprecated in February, and stopped receiving updates then. Migrate to the OpenAiService in the service module to get new features. --- .../com/theokanning/openai/OpenAiService.java | 63 ---- .../openai/client/OpenAiService.java | 288 ------------------ .../openai/ChatCompletionTest.java | 41 --- .../theokanning/openai/CompletionTest.java | 46 --- .../java/com/theokanning/openai/EditTest.java | 38 --- .../com/theokanning/openai/EmbeddingTest.java | 43 --- .../com/theokanning/openai/EngineTest.java | 31 -- .../java/com/theokanning/openai/FileTest.java | 56 ---- .../com/theokanning/openai/FineTuneTest.java | 82 ----- .../com/theokanning/openai/ImageTest.java | 96 ------ .../com/theokanning/openai/ModelTest.java | 32 -- .../theokanning/openai/ModerationTest.java | 27 -- .../src/test/resources/fine-tuning-data.jsonl | 2 - client/src/test/resources/mask.png | Bin 12563 -> 0 bytes client/src/test/resources/penguin.png | Bin 67413 -> 0 bytes .../src/test/resources/penguin_with_alpha.png | Bin 59320 -> 0 bytes 16 files changed, 845 deletions(-) delete mode 100644 client/src/main/java/com/theokanning/openai/OpenAiService.java delete mode 100644 client/src/main/java/com/theokanning/openai/client/OpenAiService.java delete mode 100644 client/src/test/java/com/theokanning/openai/ChatCompletionTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/CompletionTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/EditTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/EmbeddingTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/EngineTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/FileTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/FineTuneTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/ImageTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/ModelTest.java delete mode 100644 client/src/test/java/com/theokanning/openai/ModerationTest.java delete mode 100644 client/src/test/resources/fine-tuning-data.jsonl delete mode 100644 client/src/test/resources/mask.png delete mode 100644 client/src/test/resources/penguin.png delete mode 100644 client/src/test/resources/penguin_with_alpha.png 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 21d6de81..00000000 --- a/client/src/main/java/com/theokanning/openai/OpenAiService.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.theokanning.openai; - -import static java.time.Duration.*; - -import java.time.Duration; - -/** - * Use the OpenAiService from the new 'service' library. See README for more details. - * - * @deprecated Has moved to {@link com.theokanning.openai.client.OpenAiService}. - */ -@Deprecated -public class OpenAiService extends com.theokanning.openai.client.OpenAiService { - /** - * 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) { - super(token, baseUrl, timeout); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * - * @param api OpenAiApi instance to use for all methods - */ - public OpenAiService(final OpenAiApi api) { - super(api); - } -} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiService.java b/client/src/main/java/com/theokanning/openai/client/OpenAiService.java deleted file mode 100644 index 85a69d14..00000000 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiService.java +++ /dev/null @@ -1,288 +0,0 @@ -package com.theokanning.openai.client; - -import static java.time.Duration.*; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.TimeUnit; - -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.DeleteResult; -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.CreateImageVariationRequest; -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 okhttp3.ConnectionPool; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttpClient; -import okhttp3.RequestBody; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.jackson.JacksonConverterFactory; - -/** - * Use the OpenAiService from the new 'service' library. See README for more details. - */ -@Deprecated -public class OpenAiService { - - protected 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/test/java/com/theokanning/openai/ChatCompletionTest.java b/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java deleted file mode 100644 index 41e3c76f..00000000 --- a/client/src/test/java/com/theokanning/openai/ChatCompletionTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.theokanning.openai; -import com.theokanning.openai.client.OpenAiService; -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 2b3024b6..00000000 --- a/client/src/test/java/com/theokanning/openai/CompletionTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 11d14439..00000000 --- a/client/src/test/java/com/theokanning/openai/EditTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 e2ddbda8..00000000 --- a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 2e047ab2..00000000 --- a/client/src/test/java/com/theokanning/openai/EngineTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 998b27a2..00000000 --- a/client/src/test/java/com/theokanning/openai/FileTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 54d805ce..00000000 --- a/client/src/test/java/com/theokanning/openai/FineTuneTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.finetune.FineTuneRequest; -import com.theokanning.openai.client.OpenAiService; -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 304ea0bf..00000000 --- a/client/src/test/java/com/theokanning/openai/ImageTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 095433ba..00000000 --- a/client/src/test/java/com/theokanning/openai/ModelTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.client.OpenAiService; -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 f5c21738..00000000 --- a/client/src/test/java/com/theokanning/openai/ModerationTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.moderation.ModerationRequest; -import com.theokanning.openai.client.OpenAiService; -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 84fcfb35c5a9261f9443f73e0336a6f4cd8b8021..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12563 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 Fe*vRRT(|%L diff --git a/client/src/test/resources/penguin.png b/client/src/test/resources/penguin.png deleted file mode 100644 index 91b023c33ad956c5ccb3d725d806862ee97b550a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67413 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℘TSw;Zx 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�;TL{X3c#f+3%vT)J1niP((CnzqA_V2I}AV-J^;BMpm>xkY&j_;AN%+VeCBhX zbBQ_SEMFPBYHBr6v}bh@OZ6vSL~F6rOp|&9n5nke@4oS&M$DnyXXkTdh4+sTjuTW7 z15p7Y^MWL)um#K3lPXo1>c^=v1}(2)*zfVLf93z=`nAhQVQOR*vGew;wY_EsxV6Ke z+tWdARFZyIIUb)AQO``mk9RlS<4dY&6oqcH;on@4r6%5uEQLe9`Pztl#cVl$h zeqeGFvb=?IbqQP=qLcDLlcUel(SJ!WG8;duR}d2|+K{nrjI0R0Mt>DqyNy*n0fxfe zH(v4P4FF$z>N!07{7X1<_AJ)cR?zQvje~4m*;XKo{a7gOlDOFR?cI(4;upSv`OTYS z!bWi96xo$P(JCj)v<6NATuLrYj`zQjm}xku*Y)H?ax_QbMN{}XpyYDr-v*3G4orYa zMClLanS{~dcY#OJi^YZbt#5n-zxU-Y8u3yQ%xFjr;`tV0TP}J({Af&oGn_DwlKO_^ z&V_rIIDT#y{FEZ(wlx?V8AYoY!JfUlFg-brJv+DK&RgGt4?g}74qbHs2QS%!nTav< z`dzGbJLnBc4EhzyejnxfDh5kuP^~XxYGwxWyZ3?T_aK|v22_1is|!ZF)eTWo0*PHb z;2{U!R}B;idO!yN03ZNKL_t(XjbZIU;=}H%H_e24u#TcVg_jpf9Jpi$E)m>-H%$w0 z?BpVjoj8lNwRQCSJ=9fY%73TLt|63ZG*aUV5TR(b@Uc&P9G6~psYTXJ->sRV1r4S+ zQ~TMtQejV@f{!3m3b$KnO=Y(J$oBfANv!LW{~%5EYLQ_@AZ>v@c84gKb~?0Q`LbPb z$bI=pU^J43Ck}bHAAyTl!mdD1NOZR*sGd0Sx>d(vNfHQ?SHxpPIZNT_-x)yW)qq(Z z1i7;z0)f-qS3dh$sdXY`1ZdJAO~zH&D)*LUiGIJ2_4N)G7M5`I^;6iBm~z~9%T2iL zmYeX?0G@g7MSSu1zlQ(k5B>zt9zBKCUI%qGK!4ChueXlw@;OXw--FG&_F;VDb}Y?p z#p>2?VCk8!04rzAXOhq_W6i?|28|sRrDYu@#EJB4ij{{3t4mlu`dw5b)A%R9_WQW^ zhAZBD6~OZ^pTf7Fejdk9zK-SPC3L%8l-0llf~fp=+jyb$RF+M5$J=hhfBN~)qG*jo zYAlis$4E;5s4SH&0g{<;TKp4)E1p6#1)?%X*nu5>UM zRH*Di(O_j2-K7OgO--P^eLn`{n=v{$g~9R}RNYluB~r9RKD0tiN>fTp{PQJ$`EBe~ z)liiKQ~vz#WDCeUSEtf{D$j$`O1>+x-ATC02$`7=~=3 z947LjedZvrbT3vBw&9$NB4;ilvAj#(3Q>Zg%+v@zq$uDR2zKi3=bN0Gh`_28^^Phh zU)57k|BYZUirOqWVVB@w2)+h91UryX`S+Tf8nI?DX-^BU>e`Bg^s&Cy#fjsm{#@7E zuI*d#@%KKA``-2zeDRN-!moVk_wlDM97VUghC#oJve!X>eHC-Nc47P0y;yGF1vR+| zOW*zy`ma6>HK?q}sTKfW($yrVn&a{(x-&Oe!gjRkfMz08{T@0eUjk2V!ax0uui(xb zufV?f**8xG@Oyvs9lZR?YdCxE4Awhq==TS{_t$kD2LZ9orwABrweZ=`eg^M;{84id zAf0i@U!*B`GbD8T+#O991Zu?sD`3GL2#Hb{X9$zfXnogR5F+5XRb<(x%YT!Ao!@N` zRe@0^^v|M1 z-gG~+Ge%KH+{5@b6ewB+#>PgmY4av*+cN*>y5^=QCvekMm*bwd-iS@p6F7PLG?tc^ zG3a-}=+go7tpcgzxL>VCCdLG}f!V`W>|F6KqJnhkh`dw*9jL2S`8s8Tdg_w|v=m`n!A;b|6BTz#1 zb^2EB!WWZtSq@9JS`te(qCAccv$YvktwsWH;LvrKyg35EcfR)`{_MGzP5-~L zV%L95tN&No6<{4@fA>ZmJa{Gk*5Ce{n46!oz?Txj6k^WZ*q##?C=Wa66a*n;3jqoo z1UU@!3nPKh0Y`^zRDJQDKT#~~8D5MEL>>}uK=S(t(H;PajH(1>8BzZqj|qKXQ3`Xj zv-tKmzlpN!`@ld+Fbjeh&5gH!bCA#`X!07|`{_O#_T{dVLyIdN^S*?PNLDeKltPy0 zD2f8@u`x`~OykfE|6wY-GgITZ<+_8o?WXI&b%mFYzJ^}EgsvIgwI%4@I@(jS7@66M z*8ENbLar`=>N33dBvFe5B_JeQnlyCE;x4lw3Gd!_Z2~}^p~N`0?AY|?2moLC<7e=~ zkymi$>?y3TuNeoII!pnaiG~LNZC-D;+xXeP_IccV^C9!*ikW}JNuz{BGvq)9N+!ez zjKyM(uovgh*|N;;jK-pUV#*UtCMYhGA^7~9f_38XD^bFy<3Y!{4-#SF=T@HPDDDf2 zbO?xm+1WXigC1UZ_E|qS@G+BPz2N2?E9_T9oOeRB9xPpp{G`LF^I8$qxOxfI4v8S3 zlrmeFN+B-_jJC!wH93XdySHOzdh$Q`dfYTKiTiFpgq>SA;oQ;^PMupqRWZu`I;!p} zM%xo8rZywrvI|-f2Io#f)q@07rHJD%N7Zs>$Pj8zlJDR+56UniiIia`vPNBN^Fz`Z^qVE++HJRhfEE)bz zQ7RuM$Uh{i4wBDf9(>2nop|Qyr?IlUJoIfM63W6XW~&}B4S_{YU535Y&bZb0{8`XI zB)7mwp&|$h$+iVhnV|~j1xAVjBT$zJx}iIfzJg*eyehjE-S+Y!uHt{ioonj*!mgz5x%RPTD<)XfJYj-^(nm z4otQKWx)V7@z%O+fP;4j8|Z?Zjxw_SfFu0601=ayG+ z^we2Y{T`~$3hF@@BcqcT*}NOsw%sVZ9rV|hq3YT$hbcrtvvN%1hob>ci7}s`Xq;tv zP=AI3b8Vf8%k&-Ic(;?M&Kle1exUsNl3z=4hQKGEnVrGU{_N+ld-wK;btU9kBQ!jw z{idKxtf*sQGC>eBA0l&dpqL__t7o!_Ow35uhNIuZ1+GK}C#YoAn4%1rB5#q9s0GrL zcpRTOojkTQ7Iof&0z6dq``>!|9k~6jyNzyt1Mq9gj!W>1VkyChR1`^-3$snR%tWcq zfUPW;<0T{wwk}I%G&rdGSnqUk;^f)C@ay#UYp=lH|J+CM!TayR*ytGQTBEmo8tbQ? zL#ymycH8CH_O^GU*mJd6QI@VyZrqyEe<|ij3{?%XWu~bHfJ_l`r9jmH{pB+oKLDIK zbr!4E(6Op2)OPl-bsgDHyIfg}wD8HF`~>d0>um@Yz7kai8Z)|KBew`x7ND4zoRjEd zNl7n07g)p+hY~dhDRIb}j=m|fA6}Dk?Q>M>rU8SjD@NFL5MYrAJPuq#R4phEsKtRt zyl$nC=Q$pJ>@n=zySFjeqVuZb3_0hiR=EVrQl}%U!(f<>6l1QSc<;higigcH8GrWr zeVkiZ!b`6l{|moXchAq_AAI&B`0F2g0=qU%gS1AqwuH5F$53`wkdJT1uG=0)`|`J- z%EtgyAg%*;!XjC4%AOqJ@hlkB%BCiz7+IbnS3uoa-Pi%(`6EYh`rIN`*E?qQ*KPfI z%V{l`C+6#W-f=fR^pOuhuT=~#S2(>2#v6CUdSht*GuM<}RRCSI1 zpof*!HJmtg?l0}S{nX)m@IQayKf!&s-Hg$(2@F;j(K+z~1}kS#jm=>8wns5}-P=)* zOc~c{73Tht76m1^K>7wYq=d~il`DmjOhM^tV+VkfXO^(Mx`y@jHI!v(fIl77e#rLx zj`_EL|9_c9dnd zpIIj+z7oV!0=V;=fJC4yjK5;?k0kLVEly+_Do?$@3xsbWB`eObtpZt4?$BFq#$ET^ z>%}=ED>gAbhyfNJ#*Oohl`Dl=8X7Agh?EMr#ryMdv?S!0!PpqB)|ye(75e=W>uYN` zb?VGt>a~05%6<4>|K?BOQ}1~f=4U36Ree-vUj-IU07_%#`nxgn*2Ac$cUzfrmV=&% zd`0JJr6e`Y@lTZfl_^4|cw+~E6Q|B$d36oFe&2|Gazgv{$$-&njo{-S{}>J&xZJ*P zTG(0faDmI12;Atch$6hdF^^5bHHj-S!q}0LQWitAt1$eOsJ8{)B4uEI2rVZ-dItDW zUK}g}f}9*^=O$$5%E~MMBAT@$$;$3AvMk4g?|uxI960D{%5tnpiAYJ{7ckPPsK;Aa zP?-gBi-n4C#P*P%QzXD+S^#a0Vyn7FSq{+c^>F&k!Vl#d{?9)A2!8%!@5Q#+c_>Aw zmQF*RK7m}*Vj>( zLG{;3e`#UML?Ar=_+xnJ;ddd+6k=U=H0RMy282m=at?7sTCMfz)pO>0|t{)(pLwfpI83%3Vi|RtVvR`3nExigip-O z&Ev@r{}?7_W`l1)RMIz;6#RrFVg`5+lH)_*v%3;ZVM$Y#bY-ClBc?J&eYr+y4MZ0f zm+;E5Q$LjJ_(S*IhQIrnCvn}SyHSj^(Ce&WaONno<#)@5lDL-jCMKt58#(;3KhW z5)!;L%%nJ?Rhy3`L!RXuHvl|)&i%f*?!*%03EpU3ViZYA4Pk7 zH0~Fp!`j57n&J|izOCZm$MjZez+Ca1J*^qMg|yhfi@`g4e-$6DDD!%fi{R=GpbW>a|Iz~oaPI>TfU>;FI!q=a&7ef!21EXQDk0WVjnRzW zlxQv3VBvs3t<9uiwYY@*2)AEdS81=X>6AE&jWo|0%rt?wc{uZbA2Z=q{c_y}pQI zdNX$1{vM28@)qcei>9hfx+G6Yy*g|oAgU#ztu|^1wL}FRQKIERF%`kubo(}i z0>ceT*^IB^71%_z$BpjNgRcZ0Z~0tcKHuPONLYO+js^0cBZYz0<+DvkO1eYkL2d zS^T%3`3OFB_&&@}PXJ{Ny}F2U&mXCu=~P@yX%G9>vYK-s~NEnSzjJ6e zYa<|0gcAakvn#XzWu(wC&bIcLVKd zXfcwF0bThyo*y3|Y~TQJZeay0Yim~W%i3o}n*WST_wT_+e*D8285=>C=R-=yaPYlO zD8vj|rVFGc17dd;`)B?E+I8Npi>X;5xGe7*UI!A|pdb#Mf{OV45ojo~KU3VN$2Q;z zXeclTG3!HPH6>agqNJ)pgO=>mV=@_3__he6e^5nO^L90#KRyo{w?AMuvE$qjy@vAg;_Vn z^Y@5`3%LMdi9Ki%fx_XBi9$>*)<9dwdfbOmVs|)C^tz+WS>#*sjd%!chP*cmk#~iN?v!tju+azq%3Seoi zgXNVqtaVHPU|ks?$LjtQH12=!emwHTqZk<-L8d4g14UV$12p3F|AlI}oY{rQ>K25e zzl)LoP+FvkDmdRrRiZE!4_{N#)Dl!QDL_gDinjS2X($ATEaJf<5QdTofPWl0qEytJ zK2VsLL<>TQSBTvN!C9G)jka<410TZdw(VYfF!d^<211+I2UDQJvSV}-8lDeP$(wj^ zSXaML0kEket6F1FmRMi!Vs&->4ZYT{-M16J^mCuUPab|3=Ee)?{wmb^68fDkM)zHd zt+zjjZ2P6Cvr#Ywg{si<=(r9@bXqoK064d}f|b>E^aert%Xq$JS8`$jUEr6cyJTX)de&b?Enlx;nCFV zGb%Y0%uVI0uUnlJ)q*05Gy}qjZ^(j@DDf##4VDBbe;*49?%A^!hu{AJP?m)VN)(q( ziB=#14bFj(JcufR_`T;wWyYQi6jugXn+me3N_0A1tgNhG5ZCq+ z{Vvd5L$$t&k-6>IdD{ah_Fe;>kNVcp|6&TuaA@*gHe>);Tw2EJ`kL8j(w5SxwUJ-u z8i(KWZd`uFWyrHEmdP4&V{fn35T1%S>#?z@`#a^U02w5hqcoES-Z1)Wc=1UTCswja z_W)=Jl5VGX)+L8xJWXbP4>Bq-&J)Z(kWc}Nw+tg7&^=xvQLacsFBon@3OE8vky?7I10jO@D}oVSB&lX%~pHzvu-ls9MqSYBSm zdZ&Y`F3k^Sx!j61F2DQ|JofmbDDvEL>4guV_`g&LmT#0>9`ZPW_{E4xBy2r{81aHh zLvp;q9&p(#Y$*~URj>t>e@ei*CG6!&wLoST(ut=m#BKHs8$wH)xf4}?nL{tn^CtWgDsuMHaPfW ziUhUc0>IG|XR*50L8sF-#2;J!6B`4~!%sYh9XoaeD{pI~on~d`A#|Pez>7^nPDEKD zi6-*kC;xYMq@mwTh}C^W35}RV!h1jTVYJ5D4eJ0B(T@?oH_^|680B{sX+(%5;A9}+lszW| zW3^_~wMJEzSY2Jc;LgBp2QR^Y_1PcCgKxV5V_6N|TSwhlgCdQY12f2CHZidbb zIH=iJ$vJ-v7HrlH7XVf}T`aGxqSxyiw;#LxM*yz4;xat&;JZv;D_efcE7$%|lQ`$( zXTd4lB>P1;*H_0QkW~P2G89N099dFN5qG7MNE4^^!0Au808uJVlz&5pR*_a=irPmj z&7jyvATr{JpI?^samGec5r}>~0xl6*l_{0s#zQybfkz&Nq_IUF2#JJPygK2604Ld; zJXb>!VuB00*fkJ*0d1w@Yh7bdR#@wFF359m$@We7rN8z`{N$teV!oXNgAVG>8c5|B z-+MLYu6-NS+%D*R1WW`CLvpI^?t`0rY`6fhw%*0+Y6tytU?ZCG`O7lG6Newe<}F)7 zwq?WzAHf7OLiLfL{|*}vOGW(THH684me(bO8a^_0-(4IjFXn`<0BNx&1dIPP&nK#9d2Hw{3Wn15X6 zM#~%&IG+ijN6vy-a)qSNp`r$$=2w(cbp%Q7;RJC{^(QE{_%IU=6}#Lh6;X;6DU6Lz z;sYP~C}y{8^_10k*P=PgO(Da6&(!X6U%pqOv;KuKU zL>5{-&Dx9u%MzV#Z$q4oJFdL~fA_N=#oMmG5@SUMy|x53SVP{P#^mLl?f zV)S%l8a|$Mk4=fVAD?HoFDn5MLFE}9eDvMewQqk^896V+WEcWDf279wPAK0gEhv@K z66!(h`2ZCYtH;h_b!{DG**BX1tT8t?jkn)@r#hh{{6K z0vKi$yv210B3_)bwISvTNbW=A`{%6MtXBX0<}G;NlOMupdoujoLh_H3X#kKoHm9;; zaxHc!Z96zHvF!mJwY;us4Elo&l_d9W*@XZ4XFi5cz3&lh+B64LeN^jbkQW8Uc3p+m zu4|A_%!0EGHU{kWjBN9uEUol+joWU!6_;MJ&tzENcrI01~= zuLuCzGO4r|k6@=d!~tZCrN}s<7zbc5EEq92*Iq-wpeS3Vo9|==FR7sgYe767Qk@I* zf4I!K$hJwD?W-2)xyL{xGeH!I)I`(DSk2N;PRO?k!O&7Eh3jrQga;mZ45W%+j*679 zIb>CskanlbpWyUgI)L8nQ>rMR@jKQ&pi1E{>z{G5U$w01*18kUSCF$<(S-k z0PVfkAe-K~u>!zqr-x3ji>fvsNYTphj(gq?$}$1;32?_B!k`K7!97jkFl$(Scu~ST zMwpcub9@7^E#*buuLW2T0Kuf(XO6f5T718(CcxHNSUSKq*6{)e=z4Ze#$cRUhT(kj zXT!^o#-6=zEBHLcOB``X===>^NtKUA4TJztd4YF6^f0cz@kYPol9II?L->001BWNkl_2&JB9Wg3BvOXq2MWK|%)R79AVoWi5;c|WG-=RNdC!^6&bos^m$ z1V|{v990uQXbo)|u*+r}_jcEf*Whn|?&G-Wik+CA$kFe1P*DpL+xBj-0I)tle^45) zJp~*(bQ8wM+n!wG$vW=f$ivJ6^546~)WKtbbeIvdU94aP0TTBdF?mRE z1QHR-xISo(3>&~;JmiK@b`xc9;N?fy147SBB#%)H{PDl{6OfBg)T|^VCLkSv#mY6M zNL_*>xIZ~1qf0Np0uMa;I8Kwi&r#yo=(igWT#CQ< z`H$nVw_c5H6FI7WAH9vX4AdJK%WpuVRTQ}K#_NI#kQR(TE$9B?L$swn``_(B_nL6F z*VlCb@$6W^-N&^7PJ6&{$J2434+>f+1#vu%^c4l-3L6oviL0-_$zm6J zsOU;OA&KqPWZRMMdjxC)V#1@v7#BkqyEfu&-?lCIdq4MSyyu>qv2QAes!HS=EdZ2* z0f-2@cJ08y1D64?^LcToyyTM({U>XVY@t|=h*&pEcCigeQ6>>Hf94o(5K6N$%dqBX zgovSsIarE$V-v)gJgN!2$p$xr0teQf1hrB#uHZGvQ9_P15o1+pvBpFpvswcVh)OKH zcCyizWZ@DXPrJV`Erio&W>Q;YA9ByWcdt@#7JUX`i7Hh((|A^-*B(pY7ot|9D4@$BU zIS(W2oD-dfVGiAmz~Ua+U+kI_Ybj22^pf7$M+O<_tigxyN6;L950Q9WzotjNVshii z%dWTr@Bgun;-CHFU&5f@Y2p)T3Ol>^nmtxRVtk>%#w$0DhH^_(C=I>YZlC(#OP4O}?+MwQCKub%0^h*VhcLWm#`bAso`Lbr}1xYYBAi z+XPegBlaQmK%v17v@8An2$7?tloEl`EX^c$4&+FXlWy6L(TJihoiPlf8>SL#iMlKh zd8S4iB1>tGEVe}HOdM+`aKf{1P8yr#HIp#@ARiSami(uK-*CsY&ohOUms*~ z06V%PvPiCTfe~3sa`{C$PI7_B_2EOby*L{0#X9t?ld&|C)*$f`h}&daEl8DHaVUH^ zU`jGz$o^6SJ1FH+0z#%oJuQplN{1U3DdJcSGgskw|31#uaY$ny8AS8rI|M?J=XmI` z!#MK%^LX{8=X{Wqz=HgU$WrHCU(hmybznX*PtFE*O^d^Feu;Sa@y7*29LpkzwGOaa=-6m65Ny_If<{_GM!m=>V6#vcSSl3#-KqctbAT^3w?x#?w2;rR|4bDU{8k*8*<>c`q>`6bIHQI{TB zg^5E+0BO-RNF?2#MD~v_!N6gL4=f66jU-u%R)wSQ;6xlF0Th{s92iQ>P~)wn31C`{U+cT(VG>JQHHzUc^_VW@dZ5f^*=%&@rss-5>!8BL{dUt8nV$y1Y}B~ zmFE`&z#Crz7@#W46t-^N5=c9K&hG%d+Wecv00P}ti$gAHyMn5VB%^USu$j074dYt z^`Xenaf>2*d~S|7{lPrXRBhO9}<$LUZlg7@N7}|+WfM@A?Dv}uOM7oWM6xP z-}wuQ@3Z&Q0ux~0Di}}s=-(V|IGQ4yzt@lT={Z&%)?w3-5OdVs{xXS?CMOOhEWHjm z9z_m4CEopjx_~2`Ce|c3`twd z9GBqCR;M(+zorB>=O)e{RS@DeMLr0!>cRPz9pv}1s6HqVsFJA$6US^l@mQuj&v59L zTk+Bh&*A_5(r?;sMA(TlhU`70f~bg`wxyw|Qb1l5D2j_1z#IQUl|q&g#wXes9UB#r zTw;^YW?yd<>gV=4ruGU8Fn@{bMBAT`An;C5REHwd&c5YJE=ykikg;6=4JgW592!^; zqEl1~%W~i5N}Z5}IA#_^y*CRmA-NwXtRUiH3`AMRrzEBjB>V!Bw&WYJ93?Y+ga2TI zRA3Nm{COBStud;ddt@wsR5`YW#p-Fpu6y(r+iu%nRj1%h59c}JxpFEBDPa1z5 zYv{TXW5n8slDKw*E8k)`a#8fV*(;02jymBWu+SQSogPF>El9;j<+73SMxbbfmu63n zMr^5kndam$N6aNk;R~^;n2a$c{Z^gFqv~FQkv%l#>Pf5(kdbi>X0m=LeJ^-O)^g9It6xInUp zK4jr0IvA9c-^tuNQpef4knuoLz=k5Kuv9|!>w-}OdOos-g z-90y0lZnc>AP|KRO9W7s;hpb#07qVU9^d(sKQ^tJog2mz0p}MG%cKVzb7d^$6d})Y zw8z>Q9UZwC04{{v*jNkMin51R1${oV1mMy6TGyr(V2AXJ9e!p3uEmNxIs&KU6({}t zVnGcOZnTkKy|eSCknNMYAjoBVKE6m)T29lFgTbM(ZP1BDAdvk(MHLPW#SCQIK!~{n zay%*zW~h)7!UEaQ!b5TVcyf>iJK38%P=SVxCj-tvL}-ss;PCsN#PMTCapuH{=zhv; zl~Je&DAg+t&zvcRqA1XAx3PWO^u+*hA>5`W#*v*_LR~ZR%=N8e{O5c3%9sAy1A4aSM>3|3snprS$JXAjJVtl0m?t`A(^aDFhHDxB-;RrF|hF#tIy5s|ogf#3kU! zz%+31W6+J5Vo;IeDuIU~{Tu_4luQO>8Sh`6TghgQGyt0xos40F1K);7l2FPc;P|-- z!0tVJ@#qup#V`NU{{{ViN4kp%S0txuEUPxb{!;{{3XHZ!&~CqZv;W2m08?!clv7O~Jrq^M;`N3RTkGKHJpdK+GP;RXEum%kKMB+P7sfLR1q9s>vlC6pQTKg$$G zN5?QRK6WtxTqw7h=?Uan23^(QB9GQ_10hF{cr5y_#SVbqAApqh8h-G7=}y!L1mspn zNr5z3S1mH88{}46$y)U(Vd|lTexL2Vtt>S;>aRznIKsyWV!rP(u8zU72n%4*9;84( za@aMefkf~KS!QHIgElq^I__B_&T_nHW0;fn=OB64W)PwCO$1VI#(6mYBO{}D@R4`p z$V)Hc=qpDAjM8*5ivG?LjFeKyvjStIW0;y8zZd{6l-vH@+cDLC5mi|Nqpd{q&u;PA zv7B)}DWlO|*bodOxM5WQr8$^XY=Rlta@!JD{5e=&!P;`MxWF_xvvyq$5f?sx(h;aB z;6ZUFmEsjFs6P#342}T8SffzLc_mIofx@e!z{9fFh{gpHJk>Fuf`v^O4}k>4`<-eJ zfEbkeF}i6BH#Zp=z@{x*@z{Ibhkx>q{wMVN-H3@zQ5z@Ve`NqbMH%u|3*+Mxn4X-x z7yvG$+m=l;sLLuQkZ3j{A6g#%%Fg~PQyKUwfIafBB`^piy)gn(OsT!L3V$XIA_Yxs z&|~{&QX0w|VaDbh%BDefmf1@5>*SPyg0B^;Fw<@tl0Zou2#{to=! zZ+|gpDN*bc=mRxbAyTEFREDA`FgY=Ssi})Cz=ah6=BKeNMwH)t8A58S-C?^1g*3uk&q^^Qczl($57@zCRmWS-qs+XHeW}^ znJ)7ZMXIzU$AL_7xgU-EVoacc`pZp70nkaE0S=;wp~6TigLGUO@JKu1a9fRHQou=H z?)&qU`NwDqh6vHrhPekJ3NS@c;Qogm#)~gJkE5@?EZ;<1c_}X>;Fy1z%8=(dMn~J2 znVH7E-CHgOfD7q%^?^P3<}*h_A7|ZmVaxkka|i%+g{rPhTTnS%Ag%;_5YRF5?J|WD z3KPnE)0wQHTy`hr4>u`iag~9U29yFwL$QKZ3IsM1Q>4I9LBK-CQ%ClyyHaz9#Nv#= z1S58ztQ15KPBCrQ^B*Wi|1lIql+Qs8(cq*DFt%naA17=z!c&_(uwhoCDIo>ftgJN0 z>?~f9LhYAP0ybc(2xV=&&FV9hU5=rqudPl-Z?PO%S|v0EPTkTCYaTlOO9@4hI+3-{ zrf7pW%=F#*@-T7Ln3JDW1l$}2rfbjM4Z%ydu~^r(pOyQWVYLKrek-1T?s#ggi6l|HOD3v$Ind1HgrNYmc?;2hZN=r*^JBR^>-m8eEy)-|PU?D610M zVFFr57yzfVT7dzO5*%@V;9b8M4!@ZnXP%a%CCsypWQI7W7`u3q19^P;YS|D(N|x^@z;-e`f#uzFrRCtGN>%W$jB%r zC&w{=F#uecw@Y`=;gwSzpD~qPKQsJ*3RHH%y*{|E00ye6!l0~BS9YPNHdR1vm41U9 zxO0gWqLmILQZ7TAem=8@-(~+S%RofPY{pkg`5=&GnW+khq1gO8!vqY{4$)=<7@{^> z`J^lcfM`mP49=pGJleh2DKl3mWx3cEaJxRPy^sQ_sglqT9zm@96IvR--It1{M*9lR zm@^QFqDhSCF~U~0Kq@%!_Z^PlDMl7NQ6OyIyczf1{{VjZfB)ykTqdIan!ru=vl<;8 z!_?Fy=I5s`27n9pMjDC~Knhxsp#q;diItaM#M*1K}8wL zy0T2cFgBEv04?U@Br1R?<12+)8NIlSOc2o8WN)oCGNnLShD<5{*jZ*O16Jn3Rm@;k zP>N%2fM*hFv6{+}=_N<<*J<~!A#D=Ed2>=YA}}M(G1No&{&Jj{!VTt;LqEWx zq2~zLXrlXmm0B`hwY(-gX8w3yqjDQ;wZaQ=b-+Jm#@Mq6_*9Udj!|wuh zQMAw=pTO45o3MB1{KWupq2BgwnZT>3`%q*u{`zy@L;ty_!3(Fs>#NY64ti^ASXy7l z+Mq;hdJ}eBcL-zKcA;eL*MWmbr}n$#5fz?Ot@ebNvknV zrOKq;d83yPYrfJQh=QzXbBTaB+J=mz1Sxj;iM>c-gA|WB$Yl%;)?m@ZBN`u?_o6k@ z!hP?007s6zgtFf^m0O-eW%lsr1x7|kFf%iQT|2hmN8z@iwT?&u1v3`E|1|pF`YJlh zXVK|(u-@-uz28TFFhE(?(1QUMkG+Bur%qw#)i+?8>@EFQp6Sq8;0?E)E-3uv}j1N(Nv zu>+YX91cy z;YXYemZ0)X!gQ55Odx0|r#hZe5-z{>e37-T2)ve;J_AzNtJ%-fCfVw2jU4^Vl*! zdoci9=(jzaM)3U8M{(+@Z(#MrYv}fS=vOuRRfTR@q2Di2S2Y+4nt-aaf|tJaHLQ2m zu=C1mP^lbM{nTaEUS@a8R{%YmYYADgP%jI8d{tC=}bd& zW&l7|;LP9vS*8KbOqGBPPzt0Cfrzv+SGyu}=_`ex6_C1baI8=U3^6cqi#a-+XV!LF zAWD7d3D8gs;-PE7G3h8gR+!RoO7n>*!yGN^vUC>{PDR?^*Hs-s`I`dhDO!oNg$W=k z;qJTNf$x3qdpLXMoWXN4Gy89~Mln7LeqEzqSLjzIY5-YQAfpofK?ld4{Vr+-wqAZEO3kS161uFdv4;-2{jS7RRw2e! z0C|Qy%WVr#A=5eXEJvPM5fYW7W;ShrZ7sCc9tTj00c2nXGJ(=7#UNH7hYM&jrM_md zdq9w?*O8{b6auBRycfUecmlvA%X*^y=_bgwX>i379+j|`hBm0wR{)Y&c#)4fQ z5kTr8Bo&LI3WSn<2;bGDfT8)$Lx>^Ur~u3%+-9pC(o-^3UH<-fq~?|u{yJo)4ge4OQ# zC9HHh==Qr9)HSM_QR)h{W|Ux5TBEZ1c1%E)<Q2Q6SGN=vINc&Qa6_m^E^pLuVSf*2wc*kdn;dX3fZRhoozO zbDNPhl&ubkm9GX^3zCXKBg4UWfr0CIOV};+=%oCPB*kqNt`Sprb;2H$*g(YXX8|H| zA|4X=BVO_(H1Wx*Q>>I6Q=a@BF*?R!v`EdMCjyCoeuh^ga+8*kkrCW?-@EW{|MkB@ zk+(29Hiq$b8+&%{z)ja*dNBa}W!@HFI)Xp@;_qPn&j}*DbM*Q0HjKT7S}cM+^`JuRuSBXwMLc& z|G-S=$P=qDu)zRAPt9PJSwa&d6GEYW_dqFgjCA6JOB8JhGFe_hY}x6d!AK`ihyFh1 z_cu8O=K~W>Y|!!AuGi~hN%|EzJx2ATc1CmJ+rbv}KmJy!zBrSUGtDE2qz(v$lp_uZux7Kvk82 zrB`cES>oBReFgu`^UvbJ4}A#l|MkDUa^5quhcI>ulyoMX#R%uCPVHH@L$w93(^12S51VWb}FYND{!Fg<~;qI3s z7>NRa$`~0dkjj9Jp=`!gtSmUF11$%Tq)do z+pYMWFMS!)6O-7#dp9oKzw2TE_{+SVed!3!yz(lR78bCyw1BcK(JlK{yu^g)6Qy}0dNgf)%e5z z`~Sl6lPB<#Kl3@<^p-;afPDw91Q#vz%i5IJaFkt+Hd4$YnuaD*wz{3~-k=2cdssYm z5~^r{3e(S5c4vU=+VAZvl|f~q%d8M71_gO3il$wuJvNKpT(S_A0L4a~uLQ z77UsK+KvRJ{gnmE$c+XAzdpDEIfzUT1q->kPzcZN`vIGS0GVJ$cEVtPex0ZT#cRV9 z&+wyV0BzbGIS;fOfWbA$#==x@KoFwj-;vp@vg_R@pcj;3Vxo;(Z$5;@(`Rw*)tBN& z?Uw!GFaF{${xF~L^lPu-)$crwg|lbS>2*<7W>ivXS3T0VBgQ1E7 z+AbCu{Q&JH#@yzh2jzsX3MCtZ5Thy6cZMQZ@o>i*N@v$Bck@NboXqScrOz9DXrX)@ zg&>YL`21KfHSrM0UW5;@6z5E~{Jg4A%m*0e#7!a?%AbKC{R>VliHGvF zLCB$|`;I)wVx^fQ!xL8#AhJD|ZaxtwAR7HqtOI~}Nr*(2C;5EIjsc`?nDhnUxRRjc z`-i+a$)8z_6_ZV4#{ppSMlK2pDkBssM{A^oiLo(Ej*nq(dK}jr+=m~<+Yh}3n3|h~ z8XbjZi_mLERclnWM$Oh;*QOw4MK!s$z@7!S6l-*s7V*FR<9~z~UwRQwyzjl(vHw!M z{LG(1)kd&VD?)A$ab{O)l|}T$WDHCSt)f8P@1yMZuzcn;#%AVF=%*1a0019jNkl6HgTFvCWVT$7d)yk9^9@Asqt z1UeK5nv8}&*{wX+L@H|nS(O8W~8RbIK+_c$Vn`@WGX{O87f_3u)c<> zssJKP&TmH6Dxg&6(e)q>VzdE_s7GikG#r?zp-|iV*U&Yg4yaaW6RH3RI;G<*GnUk> z6trR(1p$U4ZM6c-D3h2(TOvoO7{V$a$w2lM>5|xEJ*yg zj+;vWLYqL29L0#x2LT%bq*NFOMQ7Kt<(a~0tH9Xk2*z3krYA=6qkels0>JKTufqJE z-B?;!#CpGHN_Ow|t3_WaN?kZnO}n436*3~^ilDVde{J3B@wxKcD)Uect<3D*REC6% z&6USgTjbj8z~>pE)&|iYbk?zW>ICwlg=}sFc~RI=94snt7lrJWprh=%i_cw~+U^(B zn&6HP?*|K6=F4U;S)OIc+=imgY(M}M#I_`9Icp4+LWIj(~>=#)K zHIBYudD2qYOu0E2ES5DSxVH0^M_03{4aTyNw8zxzDEj7*tT?2#hJ z*hqoVR)LAJ7PfDmz8C=Bh$o%jwgU(6yaT6?zK(9Ui*iu!9BBV!%pE>Wk%9VYf!DxUth(!6UUGhBN)qO zF)}u0WTSjmF)!CE6Ipd2>pKODK^aFPO5B5LdtfU9@^FT0B2XQ&wemCf49fCq^<`SKbD{T@nd z=;@}^tO!a2nyuuvljUMUMrL_NXPGqyHTk8iG|HMmTtjDsLMdbliPpcVGBU+rMTUl} zOsim?3n-LAO$5{$YfFnz>NRZ6a*)c=9xtG>!VdrqLC7iv!%YN8O4a$&x@Pt!9K_l? z@}h=)K2s%B?vTT73$k$^RF+1}WN-{k+H4QnswCiTF-IT*(nJ>y;)cf`g|Rz$sQE{V z-otV(DPrmQy#lLd-nlbo3Q+6NM~d04T!&T4^z=CyWgW*HBT|4f^(I1}S4yY!Ri88V%D3Gtm2 zU4+bK9n!CM0y%a;NojkKYxA8InZuq%-Li@ibn|0+D=|`J=)#g1B&~D=m?}XtNDFh9 z)#bOxJ%JDgiDkGA4k7ggbi~?a3WtDYfJXs~53275N&T8IEC0Zzm4Gw-#^ z%oHe7)}gs(l&gzi>Hye(@#J3!0btAS-FW-q$FRDxir&+Gl#3Or+Gw}etZf!G<(|qE zRHczAhwjVKk;4XRWU98i0O4ZQ>;ySYhRKnkC=`mk1!M|+tx;-5ZACa3K&~=qE9J$^ zICt_m+EX)7MGIM;W4t|%%mRavYOuEp7rugo*!Uw6&oIG2`?<9*gZ%j49n{(rTv$21 z!eqork^mW%0MT?H%;sa75V;;)IVU z6@xkL2qbecUFqdybQoAbk;y4is`F*@yd}SxZ=tKnBKJcVgPuf0{{SQ*|`%F)6-Zubrvhj%c!aWYR!pa4B5y} zzM7@bi3bo)DP&gXEmH~^WhgR**|AYvzkeSdzU%F{@{&t2l4U?uL4nay3Zo+}D5X%= zH7c!5006- z(=i)$AWy+kzR8eB3`RnR{{d#yWr?!W#p~aH22XwYw{Z5A7m%@r0&2L6)v<*ds zqR650TzCY8%{}iKpn`B{uAHc-`#T#H?EOJm6cC7lGf`+On$>mO-^c92)D1Wq;|VlA zHVFY5+d)I0G0EQ?|L$1;!iY2qMw%0|QY~YKU`yJqN+;*8Yq0&ASR0N90L?Y(a)9pg zB3}6B*YW+Y{UQ2`XHi(8Rb^R&3un&Y%!w1Y{r>we27n9Y#cbKN6Wey~!qS;@SXo{| zSyuMqpcu#!2R{56zSLG$=rL0n+C_#N_U*@A*Ikd<$tf(JJBNjH=dilAiuG;>-F_c~ zszPmb?=qD^sT|$1G`k207UT-RU@$;?Vgl{iS!6|FRD$vhd6wC1Z&rqtJ?KgW#o%1o zswB(J>&sl*VDFd7DuRRrsWWHMNHi48d#`m^%F>mY7=amFS4PlPYt&_hy4T0*nUi?x_kRa3 zKl5!+ziV0oH=*&AVBpm8<5)O-1~=V#$Hf3}q1@)TZNHSJ!vf-_4dN1ICc+fH2<&b0Eqq+b>Q5E|lB0{rm95Cw>A`n>OKFfA~i@bNo0uYpba13WVCQ zK(!^wWQLDV+AY2q32nuM$yN(Hrspu;YN0ACEUYZ!%+eB;*VoZ2D^$!V8K^;Ci+}-S zM3^2QL#L`x^%%J_7iXy$N^5jiSI}8Fhso(VSaS}5`y zd8Vu#m3f_Eb5Yxj=`6`80Rkyf0`|_xPU>65Zb6=w%ZO~a{cfB`^do&8M8+DC{;t>s zpe6$_NOM!-O~}bql0u~r-B$z*Eh-S9tElVB8bQ}a;tc?C4W(-=pFV{n-+LP8UO$FR zm*&FGGC0;CDYFPfVK8H^m$Jm~{rdljZrR1Bzwiqe1Hc7S32fhn4}AKQ*tusfe)rdZ z1IJ!If|cb(lx1mk;%hSl*I@>)L^+#*FgZ4g_DBm%8P<9OoLXAK!g>eYvb355FnR+V zmPy=@zC{YN?KV1P4NZiM2)be@z}UzL zrluyad2S9fQ{!lljUmq!scP*IGNupfz`` zooTBfh1FJ&g=-TOz(yjbzrKbOFTI3QuN}dly8`7khNSKhA-9xerg(g6Z~(($%owQ3 z8ejR%-@@h{JMj3AfAnGixR7qw-gYzcBFC40<^MpI6*zJF1WN81wqFH{jMGb-l*ToI@=rT5~g*RlV~YoW3N zd8W`F9l^dmyKv3HD{#ZLm;L!$o~?7(wsj6TuonRE@@pq?_Ur;qoj!-9$eN;6Y zi82S6QKI4lo7z}xIm$EZ(y*l0I#wPvXZjqs=u+t~Li1RFeI)dJB&Hw%L%7`tE0Age zfN^DF>(yB6%4kID+Kl_^+Kl_S#-Ou`lSf~}+1HL>u(o1;Y}Uc;mji3i1Wn~_>sazdp~D(`B>NS0%KbPT;g4~!PFEJL3N znXWKcU%~Wv3)8c6xNQGkTzSPMxMc6n|LC7}|DJ8wzbEk3zyJKJID2*pXBQT+y4EpP z=Qcx{v6;}uryv6Ej{$LT;$=odSToY<ip8Mro_HC^`thM(!W0xwzD%kG&N+Vg;Sf1IJ-&w!k?|b1=S@31@0Nr5% z>a__&RZF7_=e%6FV}9X!`~sb8tK%@bqG2lpMVhfZJHx`o^UP15C2K5OCTQvVD7rKN zUDO5bPIAT=3Jp?P(U=gTj7F?Z4a(^i=BB6l>EHhYf3x^d7B+oN8ZXV`u{`T+KwR7iJ_WPf^u)y5>G8bp&SzKD7 z*=&*KnTt8MeY2^&0ZRi-v!)%;=6}!u3NG^ySY!Tqsk2Y%GGs2F`j?jHW#qVVeSEpM z*F}MLvkr_w7p^F|)t)d0ljmgZ7E80UoICj$%X1ef@)U_J3RISCvg^}dcZss>Ft_{) zq#&p$!j>s$30ocER)HonHlcaXzDM}$pZvt10IoLNykk3k>(_Dgx33Y#F&`iQh`HGr zOlHu+K5)V~ryiv!tS^UE`w3}}|GJy&)>)AWte~jlH(}d=P)elgah?E%ps6TK!OmT~ zzS5T;VDs<*n}-KryKNUvoSbHEZjqV!MOIfDw3EyopUZ#}=eE?|$4u^h?!6=u9 zeXdCwGF=D#ZmmF>b5PQ_3vF4HZhBw7@TTjFu0dx_3i3vSP%6@Pi`nV(tS&CFyu8TL z(gH=EbT$UnETf|?w3z~y8|n^kc5Y+!)Cet+vMWfHa%kL1UX}&|p-A(LHxD1?S3mt{ z_W$5deF3=Y)`VNPF!Jn^h(J~R^x3&tbn4uI1qh*#L5ME9JfV;-c1u@0bkPyc2-cJX z)9v_cAg)J*QJucNIwF9u9`pE9Pk-&#VsdPR$+1qmaN^T*EH1CIxV*y3N|RQakmWgf zuB#xEqOgNgU425Aip8!ZK4I2)^Hnp0Dy+Ag+@rf_yt)z?2yLa=!WaS}K?w+~O0j5!(C`qOHV*UZuU_ESZ@$6O(h~K0%nkj$m@Gl)qFQxImyKAZ51TSEx%6EqY=M$N zRlrMUV2v|najMaZ(O@)6i0WBT#tpc@@kKVIiXb4>oEfxHWGx2KYsGN+y0nGPpmjd_SGB5O1Tl@ib$ zxuqlsRAs-_(-TvVV|sgg=Vn!nSsLH zw(6!;jhpB?ySblN7&~MXPFAhmk6uXZ&R9ExcG;aykDN zgHQYk?RFbef+#1=em%2*NNT3}aN0VKC%H;mkKhXM9fA5V5Nd7RHcg z8GU{I3=C}Go;~;QooBv(&0M3gtzT?_aq8>^k|d$oN=ed`EGx*f+{U!!g`LE^LQpal zjR7mNbhKtEsnu%q)a%siF>xHRdDHKwv^#R>4PN}&&zZeAO}pJh=NVcX8jU9XeSJhx zWZjVSth3R$%u?M={IrviYfYe}+c;GJsG3^{t?dauu|LjvpB{9D=e0=;k?IcA7R-}Uq){NpF)~(YNXqxS&b0F4kQe1u3({ZEh zc+Ru-~eaOonockaKlXN3bop3aO2Pr zx?z)+WhOd)gQ9DMX*w;7B^B8Xg7s=uj*#T^1lZ|+0Z253QAp<_SO%eJ_XsFg{FfIO zc;lci09QR8dHQ?QYc=$9|G}w~C(%Vg7}k&~#HbJz2J}Y(gaWf5$Q!Fz5`q12ke7sc z*guPkD<{w_&dt!<)62G7C-As(9De>69C`b9th820^VEuZOo0ZHJOd)dWumg%3h2T*Eh#B0r)E1#uy@*;v>LQp%Y_ims=QjG8pE---r>y0#~Hol z8@>Qs&Di_h#|YQ;aqz$|IQ!Wtq>KoI2pNO~QA7|ZgbXM&7!=J`gQCbO`R$@hBj4#Y z?chrSjpaqh%JaBV9Q^m6^U802!{Wj`X_{KLpDEl_-)=CHG@&o96Q~d=Bn3sasx60( z@)Gx>k`byY$P0}SR;OVHgwB^ySQqG`98^k08EM$fCBRv(%}rnAv*RE80&p$h-fut1 z=-4)1dEp=*zI}|eoq$^b8YC)=>8Y;=6k!q z5bqs&g_jN-U}pNX^<*dt7X_y)5s)qlR#sQ&tM?$B$xB{^0aP`+6NsuE8?=f@H)MH1 zp6A4I4JDOr1B|9Hb_!_R&`@+a3Ta`5WE~llMCS!3PkhSt*aQUE;{&#N{3f3Ii<@}; zg+sjc@@p*4%vY&GG6)E25j9=62BASflC;UPl)T8$d5#g<9Va`pfYO6N7yblrCAjeE zNe&)(p3^6elc#O8&O70xEtg60v9RPDgHv*t;W*Kte*i~bEknv9$@P&;~aVYO+I|@ zee!mOPIFWs38E0y3JAi0D5}wFS%)A|gLcwJ=eZjII=Qj5e*bZ=Pm{S4y!PU+IdSA& z($=b_Gk5tC+7ECi0XBvtOG&dFsT4w4&&P5|C)^R;n9@_x#<5us0k^$y-9bBTqm-l` z*Fj3VCN!EtOPlUik{h&guF}%l&6ODrLeOrtd;z!?v1x3SO=F`>?YxsujveRRiO*S@ zog-=ogkeD1u8}0RO%QEGQRKuzxSrH>5`F<0McjPbE&eV3k~n(sRgN5fnboC5Qmx4h z)MG>09dQJ*M zuCZlBNUJOr&e&DR4#^0p#dTi*u4Rl)jx#!G#T{?{*I|yodxXW=d0MM27wshp!jLp= zQ)D?>+kG@xExJH3wfmdw`qp0mCVxr1{=$n)pE*Sr5qjgu6Yu1uvqK?_5gr8HVigpvd@z?g!f9Qta3a6a==h-wC)oQ;@@ zYi!@W(-(m21-rg|KNC~m;M7MaIQiixoc;V1D@&^ct$mRo+u_z3d*#o?tl_H zML`!%4CLIMWVhwG(WF^Mnq&m^0I5_Z9y5*xu8pM71}RM?I1*vVHy?a}eNQ~@3&8b+ zfuSJ=hK9K7fjyl0=u^&qewy>A&oFawhQ{g&TIUEUxM6UBTkp7?`yPG7zr|O8ne*pZ znxChcBqT)v00jU0juC>QiiE!;#HjnFO%rRou9VvWh$<1iqyr;_OJvq4Wm|v;hDQr-z6i68ot?y^=AMNKmKX~@Ky)S`(G-34C2}V7+=l>M# zG(ibLVG2@hdv47o(Oy!5QW%{(3ol5M1Y?Zb1jx#mv^%y?3W3qu2F8YwEeK9PRB}0# zQm7zgaBz^`-agu^4e~6-iEDTx zaeqIxE=&`2rmAW(u(26BlJg(Cx{6gnqx4$6f)JxHhU>nJ&mO#lu8=Y=3# zvXpgdR6#&bZ!g=%Cm9_dCr|+^3v(o`wjKP|>uekvV(jJ#w%@UX$sJSwFYb#k03Kfn zw(r`>ExYe#c5$9|W(hi47^}(`9U_h}2B`uo2Wva))ULLij2X&2JczoW25z23vE zQ``N^?+bv(*MadJQ#|o!KP3Cdf8v9qM_FmMP(fgWR-=gif%S}R-Nx4O2{v!t!r+Dt zY#1Emox`v2((}&~)?#EMKsYxc+d$aCo>a=2o*0`ADJ4SLPk$9e^!4^Lb;lj-x^Iua z5xxL;d|kNxzI)j)GR!9*e89?TgWh%PiTiqyQN-YlgG@|bZp3-x;0+vm>rJN5o*{}N z>l*BI0|Lt)45PpXhnS9zLkdX{1jO|^n>KIZTaP}%$hIy1Cinv2@pWQkVvLcAvETdq zH{CSKo`)aeKmYaLh~pm8ETb@)%?SWN959v*7)CV)hK6`>|HJ+w`U2o_ ztzyH-jXd+0Kj!wGySQ;^n0jA7VQe=8VGxq$DREq9-3Pswl~zArm1(K|!I($x5n0K|z0fg@ytkeC)otlv#Z2;4H-y#h{?- z;!s|V;Xi(ZOl8#+p`g5Jpr8UnprD>V4h0@ULAkR*K^+@GLGh^9uE=n5EQw5%xA6J5slN8hVzIqDr zN;Y3|-$Z(wW=2(b@;n_L4Tuq@7$oN zZQVSLxn=CD$5Op{Q@FWpu9P!bFBTji9ux?Noe`P+nbMSO^L3MSk`FS>=NUmopMWcc zsba^0g~e*n;R)`1IBj@n>4w<%Hh1oy_05*pKZI3!KHD|6bGG}fc9KK3|gr2!cG}k4O=W(Hns+%mj1NW{ftr* zTzbTCjK3x5jwb?&9zG~JB*>((WVyjK5z$d(lK4VkkW}Wu#cpc+zVnu-@hs}z)PnV! zPNU)T(3tO8Yr{7kZQ}+J5O;nRor_o0ZPX_7olDTcOA8oM#avShf3hC--tEI>3oriI zU5{WarX9@1XQaZPDyXX>j>#Y=q&BXk=rlYN-0{@4=GDJ3+zPooKl3@}GFu}!86K!+ zaa@fHc+&HI%a~i&-Ynvah@2Z>)rrJ|VZ1);0IZfXslKP9oZW;@rhoq@0*CRq#`1Q;9nS|9Jk_E@rAW(@Ek<-_{W8KS2>Sg!_ptxHw};eLkJ zptk#^wY1gAKEb1OLWNk(v`*Y4bYM&6#g`lfPE-2;h|E@l4GmBoz4_&aVBQ9DaEJ0D zGR|>zQVsHiTiiy`{-+MBzJ+`~#Vp?=3v@9$KtFFi133C0Ds6tBLP9^w-0T|mkG6l= z>G?s;>WL;L-J_dL~F0S8HjdwaGO9}{hMJf1;LJ_OwcNNDmk5EE1+@h>!4_NV}w zoXU%)CT8#$HM-X-czTujTVaA#cJQ>6l_R^kfsy+*ChAkc{X!6oL>Vu7k^m+FyEYvu z3VtN{fST6NoH?9z=TkOEpF@_-r^5}B%}zAjw?8ZQ@K0E4uYXG*O(I^|%7R{*cR*|y zY&?)Ot*U7f+@13=CFQ*I@w^R|YLzr_0q`Rpqyi^T9x6_^XE3eHQ6-lc!mA{k1k9l_ zvli!%mV4Gu6bml`(~`N#{ez}az+44YBG|?+{u#`^xDKpC_ZzwDU9V&}Vx$j2L>xQ* zA*QPkNUoAtK7$Z`VDWu-QoZBA9^)#kJUf! z30$FZ&-wsd%csp@AMh2z+j0A+jjjv-FLp2DuMwvr{>QfWJi<4qQ66wy4dMFXaqL_T zNo59zd$K%$qbq)$uub@5!%!%p>F`0IMCL$|&OUzr70Rp++CQz*+W`7XpD)0unM|gb zBOV?rR((rTuLOq{#Z*)*9b|5)?>)-9D4okkok^I1TtG%&T%(_5qS`9j;`C*5gSS>O zg+88Fxh;YN;HRg&<*o;RRJ#|7SNJtY|81R@?V6muQ=7OTfC}Y5k}e2~#r-LDv<)9$ z9*i#|=4lCHP2gXOMBsa zYv8{P>uCCZ>XEXw(73fA9Jo{gxk_SP20!m8yh6YMKQ*Ma@hF1h`(>2bS}a94P=5|O zR(Lft6I4zlKwgdW;)%ayDA`IR{+Sk5z^O!hXDV^R7ne`t-)e&{);PaN8K9Bi#&@ju z{YT_%i<4B$VuBfnTQ)>Tj^;*Gm;}r!$>rTuLwU%|bu{ZmK4QR-MFdf>_sO8sz)~J* z|EcR3`ll=u9=~S$zR-Necq!35w}gMz{WzlTyFGX^AWm*3ix~o<7Pvp(n}=W&7POhp z0)nJKV~Tn8G|CBM2N|Ln@9i6cit0Ptn5K4*zg=oS47}Rhvkw~aN z8aHB|ul%M(=Z$0sh&d4zhvkSUQ`zfWEAG=^ue--T8V@&qj3Pu?N@zSmF0s7T*j zV_4T@#0I(~hA~wq?6V4X4^tHdHeY$f|Aev>Evve;8_a%|B!MogwB|W{stI0IsgCjar(jeVt*8lD?}Z9l@q zfOaQwXua0fHYdvw$xO_KNi?ObY>_lXai!rHB;}CAgEX|s+3-0`BO8((x`(fyY4ZGb zbc7#nkL+nLDVyyPFC}UXpDwljEOHgeCZh^TD1k^dCYNIgX*|!bQM)*pM+jWHrtP7j zG#AA-VYlWY*T4CRyptbXfW6K1ZUeKinzz`_JrM+s9d>uO^ar|^6{HG1N7XfuBcg@nl zc0k?wZ(Do@KS;p{S+p}Q_!yfN8D8&A*Nz%XCulT4edVZ}?N!pBX<@ui0D)j!fREi) z%vSbu%nae3ZVZkmxF7by7tvKF&LkLyg$=;#+b3N7N2#pYBThURPWPbQ5(@#8bRlf-eBag{CFBlD;;%irrBrOx55^MdN_m;4O3-!+II=l~L3 z9Tw%hLB0t;`vyURytUM4Aj6TT6J96hsO0p3ik&jM&_t$hUnu}2J!#g8ztL9tBW8gT zAl#T$Iv;eB{p#>f#_x4Fn9^1#9DoFd%kn^|lP(MCBy*%rgS-QSuA#9dt>&DbxSr-n z(ExOx{o=9P&tf)Chh3Pl{ESo_@c=xw1|JL#6=5@OjR#0#jaFjN`p(#b|1uGYc6{H$ z9Vu@*Y+%6vz)p;mfdQ&{`9_NVIA}0){40!$%vM|APnG^+=pW)D;{sUpaPs^r>~kfL zWT-=;ztPmhv>n;_1Y|Jht)rEl?p}4;vTW&!F(sFo+(}{ITrG8^u`yPxn!oQ4D%9NX zK@^PFuzE183||%*@Bi-bN7k81o21+nBLrz+)5HJXG_vy!qgcP!l-zilN^z9L`ki(q zb_9z$v}$stm8YNo6E84?t_%Ftjl z7Uu1vj=(%ZpY7Q)x^aw60~;?R+GfzHlCX=+?7TA*uAB|~A1qp?At=9DDG0X5BRix@ z&b!+}%bV`?J-<#W|9U<0u=@+3L_~LGV~;dVmbt2$90SzPiqRp{i|LV>ZHpJdQB#X) zOUQriXyFZ147B8K-++W{m zy>N7&|Igq~_bQ_9|IQ*n#@WU>rIFdHHLH`w>0~-GGS(C^!>V63tsm*lIy$QjTE#2+ z3{tSaNp%ie$+R?q$wSHpfO=|ZTK%2|rQj2lilvc88U<__?%yE)MISm8?W4E?1{svS_^;HRE_txP8S=0md|18vg_7W5RTqOH)xlg_9D&1gJVSBM9L2I_cR^h}T+bSq2x7VY!83##4r#5e`nT=&-Vldgi07aX z7!l;6FIL-^6nO0>4kwF;jgTNfxENS<35+Jd!}B7X@gt*T(v%A)$JL*lY<1fSk!JgA zrcDc2kH;?j?A=MOwXbDxHU???uM)#97oVUsKKq3cVKYt-g|-GEs~Ag81I-`mlFDXO zX>06J4^fk-i{ySP;nZ`wGq+^mY}fKrMwup3iCH2IwQRstdpL6PyNU?l=fe_j{7fvX z4FgbW;F7CUZ;0|=D`HA(=dT^bn-eZ-FKVU9?TGctu@#J8gc6WUY-kFLfYK<`#WwaX z05DUSZt|i9$w)T?RHk;3dc1ITnR)T%VdF#&G>XzcbS!8~%i$dmf<`%c(w6@J;QdyQ z#(2&g{da8L8jEtw%vLw9e{0Gt5-&f~{qwnRa9v{A&6nXgWIsfUc+pSvPy5bGMR+fl zM9wWYIy(^hteaF)=ZZYUi=uuq+*hB`O9qgCFp4hab|?3sL67ml1$0<6OSv7eXjAWG(<|3wQjwoFS)S1LRbCM*9 zaWCX>EJjVVFab6&{seFkphv~eeG%dG{&`3SiVwo4jNt-L&QurZsNXiUdkBcj<&Ui+ z3N?f9ameaNgG)RZIIjGtD z21H545^kwR4dcb|tHp3M3T)OtN3q?AOx+Op-K*O8Si#|23ju&g07Xk}FlISL2(sT} z-UB*+26~Y#TB=$CJhV7{_F?J?;n=$c)sh{Fd=) zNTfwZW74#?KdmX$Mv&X(aG!&{G~>0uUk^5%X6Nn(CCNk-<%5YkR?{HvwjU_}QhP9I z*zmrC(cNbvA_e!;EuEq-q7B(-S_VzPjR)I}pX%I`9l|2lEwyg@D~XIIsxG7Vcs{|X z9vt2S-m1Adkj0J|qc3#$*tW094>g|B{{Yxn;bzF}hq@X2W)1*tkxmF1(Bh3{F?re5Rg*tUQ47(n zgo=;)u1JWHm*jsU&NoHP^WV=p&B)rj5B5=!cXIY$AOm;13ZOlbuQkIy3ZEYD&=B(* zx!L`=OEGUmwdNcmoI(2CEw;E}G`&G})*{DmZ?{NGdQX~O+LU(Y618EGkv4k${IZD4 zAY8JVK$Nh|3?Wp;}Ih^-zL`gli6m{o(E$stxP*qPTL8Z za=d8eXpTfqWBY{tzi2xSbM(jFkt|7r>HdgG)XSW%D2A4?KPnmukV<7+qCBtp>M!qn z;{M{HwHB<-2VZ=qb9FcFm48nM8^pxtQ;keR|J)>uu);ky#pJ3rj!3{ZMHMeStPDM~ zFW`pN@JlvwBT@fkoPHZAz#L9Je>}oQMRuEjokjr%JyQSByZ72fTvrg%Se!Mw&jmQ4 zgdS_P<$0X_4BoAI-xZLnKV9g0tx;I>fC3lhbg?-G{EKvDVycO^8T-Oq)a9J>c5Kdn zhL*SuRX;ChWzKHFD~a*EY!*2Bjl~cZTm+LH$uY%J+1Jim_PB5!BtKP4#|e+TUlWDR z2~4X3^-k)hk16j0uX+*WTq7qY5&Dgz6-^@$(~UoET;#t9jbMX;$y)R9;z%KQz~M?~ z&hdhO2Les-2VmR<_cRJ^k-G4Xm+903P>L27Jo6InOz=e3<0xs^WqZ?rERq^|Fs2Qf zndwKQrRHU@W+Grw8j_2as0eT4RfC@ofed9Ih2xA#3@DLG^JVJP%*}TmDF_6ya+brS z`-B(KN3OZ#cXy+Qa%SA;81VRyWcl;U|Bk)W*EL0pC0nLBb!2tS3_@dV1Bzttq_MWe z!D$bTn+bHnHezsEf06g9I-J8|QO4(6HDEh;Vcg<)Pg5FJUa11K)9pi6ko$7OVOTm? zhWxe0pP`}qTnS08=z3`+fNnEq?hk|o2sUOg0(G$L{6~-Inu%!bZiu9P&veG7xL<-N zQlgE!$7vxSfO;e7|iW7inCwQOo|0aW({l1&`*rCP7n|3k*@he$mx%Av1 zU&`_A{LFuSq5LwFoP5zokw%L%StncQH%Kvrx*D<1p<^0iWcJg_CH8q2wLG+Osh0|N zr%bGh9p)&0&mi2mWpL6DF=LZYMx(;GPSSV-I(Z8`hn0T)Ri6Xfe0!P$Wvat1Xt?`% zyuy#MCS=RF0~a9EgPWRGuTSiWm4`qquTyiQC6*R_kR z4i!Rsiw^*8u!NC*UQq4c&CM{Kq$p#8L=bthJUqPiL6Bk&O}asNW2^XddOmk&l#>fl z1*IF$uceCwPNI+)tl2-x-A!Tv@8l&G2P1Cs_C-O+xH@X^ypr3uKcma?soHMK^NL<4 zWrW9$6qe3}hS42lphc)YCU`&?VH~CM49R7oBQizyqQe|rxp&L6wUs-Q& zVarwX!yA+1`{;w~;iW$fjg90ls$_ilXs(i{(D&UM=i34Hx9<0o?ybI+GzhOq~gQBs%##w?=BR+5TZ`teg=@i~haav*Dwg25gU%dwC zOvir3{AI(nN_lQN$;sG6myL4ADJ=}!qY_kStO^J{X9TuS-Cb&-DIr}>*(Jz}$9bh%76D~I^T zCD1u}B{eFZ=f*ogo5BkBD~P zxS{ep204{6Wx*y;3LZ-glf3vAClFN=xskglhYR5<(8&aWkZVLu!lWq*_$6$VgXZ8~ zvp8_;=fN`oLiQl-JFO%G|oZ(;$O-EPlUo(13YIh;MZvn+z;X5W2%)d z=Lx#6zKww)oI0LKREmVRZB#jEr?nU29(Yb`qiavotgn_G2Ywgk2d6+|xL;7za6dap z@f)$sG=J1}+~hbOoEQdV?yvGMR~lWl?(}FPZZTN&1^coXa%-mkYHQLyd4{WW`_gA) zTTMJ+9yKb`iX49x1qMdV6Pa?&n>~EY5U-d9E%E(NCQzIwQ-KQiWUt!x6J}qZ zbjwuD`wVSsTZaS2#VP{ad&dX4e8ctGAG~SIJVEu*CwTVETo%I!l9F3y|2%B?!up2( z>2+F}{qqJ$<%ddJC~Fk4-fjxFr>R13b*nX**RI%PuJFXrU+iU<^wK2U;qZ-ukx;2F zwfqPK7a^f&Ls7L9-47?Kc#tS41>~0udgW9$W}4*{5`{A5(E5Y<<9VJlTzPXe;UDb^ zUwhQ(s9bWu886}N#lz8^Ph*y%cMf;3Lz+X2M8yKtD5p+$B)|r42CzCsLy}%{AklNX z2^+2WrGfAv%GJJ;t^Cri2YI2FhewBs#m4I@rNZ~N!O*M8F>WdJzjPgRU}2d#;*3m6 zE}H8@U)8So5TclWU%ll$5&Az%T$p=!C)3r^K?jWpT-%KDd(4CJ-jR2(*MI?B6SlDs2 zak1S~hZD^yja00nniQbeJx4=$EB9<~mkM&OH+&Gjr3<*Zc(uPDSHzZ8nZ?q(9yDZ8 z(UXMUS}c-sIv!w;{Up$Lhf?$Cl5;Rd5Y?jl{-7Xi`TEt#`-OD>2i~}tF&2EEf$H{E zzhnaw^_WTY;`qeb4#9iWdm>NgiBNR3GS(w0_ydWpIjYz{Bo=z>#KaP${$8eF|2%_k zWmnT+kA~5|*Cd}CT)(xt1vRC>Cz=Hkfg{m~wJYQhQWyU!h9IYBZjRbun>G{`F>CEp z@4iL|Cb}GGm^7Jt6%J~3U+q-*Wp(V^e}DzS2?hcxg5QoooZYevExVefEb6~Gfyd(R zyB1zIE0G{vcz7*P)L^O}-m+^!CX6{#jPAf0t@7pIlz$kL0+MZ0fjNyMr0S>3{`eCK zbLpc^R1LZY!60jB=<#Koc&>tFQREmnhCN%*T_E5g;4#2u7&h*YhlwHSxLepFLXLTn zu4$84DLwyekwf~2V18pVv<0~AO9yjmRynH+NPgGc^+u8 z{1SQ!K5`s{q~+xQ`*xyiC3{q z|B?icoMR4`L4gbdQw;bJyphY7D5Qs^fmuLH6Pv`DI#rMH@ym(Qyb`d1M2w=uau0>9 zRMP#dyT_@>-w(P`6=PmBGlGo35Gu)#r$)#VJFXtmqpP{d{WShze>`*?ZJu#%AvXS( zsN*)lqiSx6yhltq6ma9GtHV?8r&H|0x}NSnQCbJise2*#7_Y5l=`iqgS{y%wd>&M4 zmI&`(l(Y^17O0lt{=3Y6%O*E<9jL+HLCHhjweH8)*>u4-PBljjceo1Po}T&ldi{Ps z#p}G3qn%v3ty}8C)Pua&_!a_E!7fA<%V(nzEmbOHQ~3!3M1&`T0N;3waMfyLqT^!N zR)OSlpL2cgvrzeo;cyzH&P41Tr#s5iS3M)Hj&#~iY-QCvJzSUu=L(D8bu_UHsV2T( zt?y~QXnHCB*Iu!t+Fn>Ekyl;`J^gIyI3rzmAEr;fMF9s+62$iZaq%VF8)Sy1zP8}~ z3!Rsmu|oPqQK_!AOO{xOE`|?w>WqZcl&R_)LgKj30;q54SW7v@^@az6I5~| zJKx0jw~vf;x}FrM6|RVJze&}6yO>)az$Ipq>2YBXH0s&tMN%1 zjn0NV6;m5@`Oa-RyMaHz5O(>0o~?xCp=pTX6)Hxzu(uryVUZ?sM=T*6L8V$vEXv$w zqmIRip84=fo7kuT)T)a7F7enYgA738kEFM016_-*tY!^EFL*DfVuCdZ=oi4a`hicc zflP{o=!iwuKe4e1>E4FkuP!c|J-fz2>1ZtB1j=2K9tfAS4NeUdzJt{EVW?)Y>3f%D zVd>#JyXN39`}iySSy$I$H^SIb#k!|ZrhGRh5H1rfG-Y=d5waM}Jl>%zM}du*vc?JG zyvA6RQLkV$nybGOfSq}^?kbX3*#CKD+&~&CeBb5w2*1e<+&g0!wWQjEFY6w8+xUQp#*9F5zDKkudX`}rDV3mE{CCYCn zCGOuEB*=;NSKjZ7*r8nYnJlD9P4hNfLqo%LLoSaa?B2m-V>~qtPOb@bzfC)Yf63uG z`vjjq16&*W;2K~yF^VyRJKPv|; zSu*cS<9!Ru4VdX4#sxZhjty;Hz5Dw#%7S;e$^j>OZa#iP($dSFuBj_W^qaT)JkBrW zN#herpM^CkQSlI>B(X!p#Be6F44da6E!vY=E#~I=AVfPz+RfX>jxyR zakq*0Fq5VLclAsxR@rB*}yDM&~hXPmm*!kU9Zn>O}@3>*wg>|C9*4RN^vk@Z7 zXl1Kd6276n(KQ`iZO~a`JcH;M6uqhBdTD!?B=c`}o|d#dPMWB2hcJ-R7FCTKoIG|} zyh&3%sV)ef)ZEmZR3z&XDI=zsF? z9yr*9W~MP*7C)ghu6sd~#FNGwaKs_2bG*djcXoo;N?C|yLd&l=U1%baEWlS6wH zZoU3y&4&N-%z@$pZk$8w{PSY|E8Rs)_syB$?X(2?(F1uvrgb#y1K~depZ!j#rwRoT z?D5VDDY>Jf-YA!1cL@oNKxEWkNKteJ>^ma@4Gpf@yzaCF27JS>3$~Im2+~sd>asaF zAMRvzo+THCXbRdIjWWj`~0vGk#Wc-Af4 zp@juQYsVzB2?&kBK7gaJe@GIy1*coIkNq7&c;Dyok6I8D=j}|o(tnA^X|Teh2-8P! zPV0Tf&U>6MO;69>N{}I=P{aO;vGKfU{+3q=8T76hvHWMd4b;=bu_I%BRO*&*h#%#Y zJH_v|-K?8Gao@bZ_slP1+%uQXS656lvntGitzE^SoG~C;r@WTOLx%zl{<`@P-f(Z; zND8NeLXoMdrCX0h3k@j8IPO33_RXw0SU&A5y0wG?Y^}&eW!;$MicL(-gPyx2#?P`% z{mhYU)qj9Q52HmxLc~NdLnOsSaohHbIOPn_^!B-|KUBH)<%KMi0lA&_gRRN-bBp(& zP9Um|#o^tH+2Z9%_e*#q0z0kJ#^gZcn)PCd^$FJO0491Cr{vGgx~zq$shb7buD`ty zA41Y{9QfG669hLH&Bw<4^??vfe{Mr5=rX%i#A|%r8j@X4f757UvD9K&XnFV##P1Cb z|MPJEQm0vfW+o?|X;nY|RUmo>6+vH9Q%n2XH!3;oK01@48KZ}Dp)X2`zcjXXd!37R ze$7aVg=o}I(_fwt(id0kp@e%%s*Wr_(jj7y?yXM>RgN2qqe)|m^Tmjhv&pj^GH9m3 z-d~7}oZg~NOmKy;(P0%h&1hF?mTFeugpxC;gf^@;Urhba%OB&ZZbuT%P+#-sLIr6DIn=ti#cxE2r|w*u@tn#r#{x6?W-V zuIbXI5T|%(e5ylD%bs5gD%M@pnuh6BRd!FoxAcF|(!rza4lIFefT(iOhm8j)b%7GO zhG<^rWP>VZ>%`i&`hk@+rJL&vf>!S{h7AM6B9x1NR2Cib(B@_Iw*qwn}*__OGzpHpN9P z+tb_)xkcbFk;6ab10RVX6=E#Q{1edE!^L2~hGF)R2Lj8l*S{e>tdKm&L^eB@(1gI| z8&D0PW*VtDWKq$dG?lj>)Y1`?_pJQR0M@p5EzDz!KJ99xI)iASC~iN`2Ar{CDnFhHy}r<< zZtuVVZVT8SaaB9XBZxbP0@MVkV2fY6-dMtJqH~3`JB3w zb>Uc3T)>agyjOlluO)#$kaHivSAb}$l@>=zy_HyVm!hhCq=phECqROTvhZ}hH7QCg z#%)^cH&lq5&7l0RB=T~#M!l@)h2!zlbZI-QdQNG0@}@HBPR&A1&!R#$REHV0_2!F( z|8k?Y*GRN`Y1k@eLT&STnYb0Q>zvj=EK#Cv(E1&TkH6ROY5JvgSpkTS6MK=fSN?Rs zw!}gy6~lBk7e$`F5s5}=2CQpaF>yD~>2&>U=U_K|=Jt(J^(%I1862;uviu%Arx4|I zYMIf@vn@$}T5)VRR4N-l|1r;kM8r9<^W_1D7AaV4+=;hn>F6?a%0H9U_ep^>99DY& zUTw`M`f^JyF_I7SXYVm`77u}Jn20K|A0FzqUf+ki$~i zUoGVUeEG)Y54vU@&6%E^`JwnF?jK`wpHpb;5=Ow#<7Q}3F_<;9iU}cSq^IZ4+5FNF z?bIw~pUpRjBSFC8y`E|Id;iZ#3AV113m5t#NQF%4e{U(g6;T38-nI!n2a|VtWU&gqd38Mw zPJNF5IYUXS+8+1We~D5uDe1ONV)5X>nwb^r;RW~$I6t(Uok7)1k2vIUJ-(DD1>@+w z)~;fFn4Z8bK`*o8tR4lg`v>_)@((tZ=-5~`VPP>pH`F>7kRs?@Y|9c&J~SS>9GMUu z!H$zBBWI;|WQ0!KmST$4ic`8wid+?lUG^Uz|F3Z*WkRO9n_fs4xqQppBE9gGXb4hP>sgP-(}u26o*(JyvR;Q0&bO#w^)B&F?YY+ak9M90N7SWX!*{n&Pj zSO^;&l*$d*)}xZgJ`?)6?3u0aDG~uBrQTcz8R(aGH})9$;hBogAJfsNr{|czzX>%~ z8<<~M1+xY`DV(OSjF$UccnU%(D|5dE%NDB)Bn+tV#VgZ~#*WItl&gF|N?3F(XPFchR~~}h+Jj?2bwhJ~z0E&fY0GL<>e}-i zNvw9ysZNWe`oeJZNY8-7?#6!3e~yrt_vi#~z#_9m6<=9Zqa zpn@Rxkdt-LYOUh>w?g4`iddYY7&-1Vq>DduDFf0(KwF(}-#~Mj-WtK*t}8N6J`Cd^ z-)h3L5$5dMPqgQ4Xjfr>cRu8+yMOT(U z?D))zU4sdBTfgwtj-ZDhd@v|G^OrEs#QG`S16iK&>pYq86>XgB?f^s(vWv6sF|5DL z1_HofqDPDe2T$S*pDV6mx z?~SmZdyjfgz^RJ-XEfZKu#2LGdZ-JN%Bw#^d7e0a1tHjA%s4AN|S$i?%`)K9)2V)R4{n#abLP z%q$f(S;@CQ-xE*>CM7TnN>6iNrU#%va+)dlA=DxwRV3f6_2 z`V8|mS|%3GCHb0y$-Wpx@DTiQ*%qX|=s?jB1%_CKXfdSH?Y*EzldC6!kkeP|g0HTe zE1f9E#M-S#`*V}Hlc0&@N^m2QF@F`zp#cdAi<=9jZAcdWkRe1b7W`lCns@vW&Q2rc z?(bo)2a{LtPqgohC&VYWZTiu5RB~UiF-AFJ?yu+0?>U9Ru5j^e06^cz$46sx*dYT(Mfl@;H}`FQXVbwr340ns_?bEjVfB<~{3*mn0mv*If%j~%pNzk`dtV+eCzDHQZLOu zw8H%3WDu-zo}N+z6T%Nt&m4_J@;F;LA@U9NC>FP6vxXUkDkK?GVU(hQ1$^{i1uG7p z*@Y+BlthuGPu!0`hyh~r#UWI+K}L(A&yJk`oz@%k4Uc1eeY1E))2Pujtz6hgP*csV zYHr(7GLNatLSX&i3L=pE*r-q`h++`#q=Th9KWZib=O4~%%X{hjB$4l#Yc@f30KJy|9x$c$jx6F^@w&6+q`0!NK~WxabYPUlMW? z-OKtx-NTqBF!71*+K8oS;D&hrI$*+)$$JP2=nC%6j_{7n2@smGku4zg$8#z|$lIU0iJz#k`u;cw7uOCIBs(V<16^*H*)0w{ z*aJEbaG{F&6W9ogBr4VNadqicpeXgSpZ3yL>>Q@gxCm+LmXfA!X4JZD7&X3fm+ogbXBX3)E*fTytEwh_7p+;7 z{vDVyErs|IWE29!?FvJ`A1f<_Z@nUq!}qy9=R2D|7uzEV-OD3e55m{Z?K8@JjRDH^ z=k&$OqwqoTr<FUcmgfg>zJN^Q0~aH`x=OhsCNWl8VdgU_@+r*>1-}yi>lhW{q$U&%kr9> z-)pq>H_duqR8FM0MYCcCfD2K|a`bmiStvGlBf?>jQmNr9ha!(}B9dpArK0)Cm63po z<4IRYNlBNLS~cQtl8{i18xcoV2~VgEV>{v|ipfAz>fv-dfVA)U?GA!w00$@bo2!Uj z69EJpjXW(T${ihn22OIM+{?Os7UIY; zikAA$cSIqGmz(0?I;i`%s)A#egD03SOnIMy8zyL!N!%)Aa-=?lmux!q=Ut4ujcSZ^ zVQ=fr!S~mz;gu#^g7?Qy5S*7D*azn+AnVQ)pH!UQC78<#E2=j9Ls4{D3jO^T^+f)uYjRcV)r_dh@6KlIx%zQ0U zuSN;68OtjIRn4ucnP;u$9Y=M1PwL$yzN}Ybh0Y-0{jm|#sBqnh^?6)P>kb@kToO@% z`_*TWJ40t0kIaati%_JRW5(+wS<)LQme{#!d&p%U3j26l+THWizj8ulS{ zq(1`dq+<*pr+xl-K!~RNPvj=ME;-c&4uDzN;_%SUB%e<)vyw%C6DdfxS5P_vhJLiB6^C2}|NcwK^y8Z=`jCaV#}>prZ1 zy4nyIO1$)wt_OMa&??8lGw@!i2RTynryC~r(uon5b>{TZL-#vMr+p|ql;Qh6;mcb0 z6Jk2k6$OyRk;(N{&9C5E_Saf2>(;l+dSpP`#WjhCt?3JXxC$I%br#cwLYg>*T0}T( zTPOE~MN-T&9iy%7w=TDoDgS%tx0?42aD$`wX^h}yPbZ0h$1S1@0+=6C4;d%u{wsm8 zae&{gLYHyzf|nTU+%FP)p7-wp($YOp^Y#MX8Z-3C3?-tD| zgW1fyy)y+a+R{RawV-*RN1?-!QOQ+T(aM2C>^TNmbS_Xk$HyGt$9pQ8)d)^yY0Z6n!Mc#{iibD9ohWDHXEo8S@0`XDiy&t@Hd*teS^K{Va zamNpF2p6UI_@bnSLWxjUva*C)x%K=?5yewYPDS?%`79s_V|081B|23?{PIBNF)Yue zuVx0J7|wVr!AZG$0#b*Yxb4*Q`pQK^Gt~P2OD1E5ufKdv+4)%yOmS!zzi0zwHJd%V z#pD11Y`(R5=Kw#}Dq4SuZD6c)Eu8xhdFEjyUXH0}7fwjJ-o-3D&OL~r=$^zU5YeUse2fLmKaceD(EvlDe(lF|(k;g3yVaHT4#k#a~7gtTlAbFf9 z#d!koj%-=Yv|c;QI>aZv=N3~)L!Aon_!T}n=EgN6-D05`oS_6&#=gA zc(>gr?@Q?O_IHE((-nxmV>}oVEV6SsYDyybCrwnA)_?otP48{Q0h0XjFT1LtLX3^E zevD1UOhVlWbX!JSCKlF)uC7rv+Yxz7*#PUHhSEGlTR?J6(u<1!$!v0J2QZY>tZUj+p!lcq?-C*rP&rr(Jq#R0P{Uc*xRB<0+d^v`ht-4L&%ej~0( z3K*qEzk5(oLw!-6*7q$M2De{vY}_yDGSgF+IAkPVEs+XIXrUNS+OD#uuH((YiNJCByUjF9JZG$gM39r2Y%D$oP`%lYLhs(U5UE9A8E5E|U1svnJtr35r zlVdJZtw7RTqROEvCZG)o2HJ9-F)n?S7)jl+oomH{N&Xj_;pLVKkSEyllWM0lP?C;X zg+Upf0S{fT8J_W98+;S`wC)^`aC@6acqoG)w?7@%$rZI7!t(O)DSyFc)}g7EVz3Bl z#YwiMVRZJGV?!)Y$&ncGQH&WXoru`j?VK(l6Ap*{&!yszMVLOT=|{fvN6fcHfqIUU zzKavRtM}{3y})R_Y(U@M_m*0))L!?XEiP|}Ctcr+K0J&mu5~K*Fl^ff`h-P{cjx5u zDp4A%!A{|)7a-#O#lfeysrThvUgsYPLxAq5_U>UH|KrXtO3_%}ldbB7?ya(|Z)f@T(q>S+L2ypV4Ej;9kyvq?0(kG|uomn#ACnX|NbP*MfOZ54qo{nh8$pX&6 zdTdHOX)75uwZvNdk5c*Iw7cxgV!(TnQ%FvBFE-^M$r4DxroBAed%B$xe9MVW)iN+X>uqjG$!TqhlF=S=U% zT5o6v243*iXj=r`S9d-+#)|uU?LuB*9Z5=QMV~5V7H>GrbX=O+#8p}3*{E-i5n}$R z6Tm@34D!;H@+>}lxTu>O5+V0Q$`(>n3y;);JP6IfWXFinw1tBSjO0#r6GCVazT~`& zZ9aSkdv@E4*tyrC*l|po?Yye+L0Y{PJU+#RvlFeV{5CN0ugRA{HH30bV<0zV!RU*#r zHlf^c@v%H*exoP)$Iv&D>Hw60=z?#rUi>DN)N=?q)@oI)F7~+}^SZ(J{{SRG+rIe2 zA7Oocg8+fSi)Ols8*aRn1c5?G|K6Z>(}VCnt)R+ z4#S9gy}`)X7*lh*IC|SDre|iIQgGh)Us?G0$gGqC@!t#KNr4JuElO)QNhna%>J9F^ z_W=$bJj~_G7qHf)I?13oC2A$7r7a7V)KTBAS;GNf)$68vNpSH`f*%z~79*?^gM3 zS%&?$;scb?L`CbaZ>=S7yC$K`u#rddGGr;di*$H z7K@W=K*^v(@I4pZW~H|A!yu`t^CI#w)x$mIWg$fe_S! zkSGj_*GMu$Vhq{}G8tfGfbo=Ig+-VIdQJ4O>ryhW15oDH!YTMl<@EPFW6>B#1r{QY z1S}{;nx?$+%6UW}dHdVn#r*Xf965TF@rfz+A2`Uqg9p7+%g|{`rZv)|^|dG}7kQ<$ zn}dB@EUl!WIX+i=lSj5 z`fW}>_X7RF&`Wez&xMJgwREU#!jm{gRGx0D6*38VsP-TVNhLjq+8f{c2j!5w$s%g!%;p4F8l z$92$#LBC6{(`MJMy>!|w)|*XimPY{{5}n<8H*MSZXj^&#-uKu^@rR%PmKDMhTw7-0 z<_5{+9@6nSYO}`}d+*&0A6cYz=F4>~{9EfQQ?D#5byv&h-hZX>{~}EK>0D_A$#`k=dOG1Ua*Z&P@i97e*GeY_5$hH9K-S5)Tigz^^RX;=l$=ZdG<-#=e|Zd z|1z{!vFX6+Qj>@4epk(T0F~&zC=F5~?OPHB?+i+san3K#aROW1ptlwft4>9V{QW5= z%?3fhPrUyFMIKw4EuswpUFhm|e@mQdKL6FH_*cL3Z&_Jga((mG^-M@4LI#9k zozbyzrgqFy9~}b~lF5K!X6Sdjbo+ghG-1oqPeEo^9IQcR^ zuw*#s)9?2fo0wqtzJ1Kk-*gUI8$uyyY-woAHI``;|~!XIE9(mMM{WCQ>;!9g@z7P6Y9L?UpRvzzjls6 zR4{=m8A$k>Di41}o@)3-eGA-FMj6G=?zd9F)Q%bc^S}LfIB?{Un>h1rCCL277uZU4 zV67XXnPr-l^-ccyzy7zJKYyMy8Tzo@QHX;eVq|2DnVH=jzwH#e_Z}b|86(qf4!}lp zgHESS5)Uz%J7132{sFmkS3*8l9^HX^home#DnCTiJuw zxbq+oWJc3!Ho1D`GRv#0q!wynjgeYJ5QcuVxsN8e6>kwmRbp#tx4b4r8IWp9!Iohs z`LSi%jU2DnYb@NHXYuB|_xa9Agi%CPYcM%EiPoAd(ai1M$2;Em2yc4$_U#bh2e9v| z2=tamPKxsv=IvKcUt*+DXLh~G{PGIxt8Iq84Vs-P+T(M~Oz&a$iO1M^+e5ThuhYKr z0=YTIsQJc-dZ)?;?}STG8tk=CJ2I%tk**7 zA)zY^kR++sTX1YVqXIX_Pe`{W z>4oRXvIJu?Qk}Yq%acw|;KYfOoH=uziIGwE?U{MC*U@X%{!~e@leal8{inYAf_>_x zi%gEzSz6oV=Gr>lVT-|_%Z<%-qOl#!PR=qlbC~I!hZx@VHd;3?(Y^Qry{j*fEM0{D zIx-s~Mc!6iF3KWZFS^VrJnD6aTsh7 z?r(`E@^WQfX7syknqsQm&$G0A zlU~1rwuaP{dVY>d;pd#ouNrxQ*EMcI~SxP(@xP zzy!*t1G0#u5Dq60LORSsyQpD#{wkmS*cz8FUF7X=dpi$3{0MvZ?4?#8K?+f(MRuz{ z&r7z(r)>_qaf-%eRAgdt zG#VpJOiXd?_%U|R&Twe|u2*{xnbv7GQOiWHQIyueG+a6%{$Xz7upP;vJiSCt`=v_NQx_S-U+d%4pW4%$` z(YMBu)m_`|;yE|)Rxy%WL`sU1Aw`wK-JnhgW8Z=O1VQM^?D$>E0>xxLw=?E|c0tN9 zF8q(D2tkL z9eC|3P`}#tJ*xuWet3@nc;Wmt`_v2P*)i5&ZDW(0OUrDu*XY(J=*L~wH7ps`nD|tUC7sNl^9gP1q!mx zR*r$#0z8(-pYQCoTAkUsosf%tLeb%uv;A^f65wgM)`9ZIP2Ek?6l}=uWB0M=v7cwy zm_#%edBg1o(VL5?Y{Z%8sci8udC8Q!|87=oo!fCtQp%qe$6uUPY0LSqB7KuM#ADMIrAD%$tam zbkT}bA;)h!;f}@QBQ6R^$THU^KGGQB&O1-M+H2@FZQu7?fO}6K5&%y>f8IWKVV>F9 zSyq--xN&oycCtxZjnhl(bUPdLmTnMEOgU#TK1I|RqjvO8vctEN#y`PeW0}GAbENYx zGhDui-dw~Ey9k|u&^}Gbk#HC07g#{y45~Ckwk-P6Jh4U!YV|s``Uu7dbY>l~u2M}9 z1+O9B;e0vJxXwN8#W#2EKEk6veH+Vm2cz9Z_V1sded85G++tIGB+8~>TD091e+-9Zw?EDyW1 zvo7Pc2Gcu7&`ClZ$0TV|bRbp!TPBhp+Y;dQIC8JQ(EB`%VggC%$#gzWB;@4({2-#cP+jwm45jNMDYUq>ACn zCjHfjV04Pw*fi1T7`4VIQKLb9&q1bkAHv-CcH-_T{ndGTH!lz`Ucj#1AZ@Nf7Gq6} zwAyC_PHfVdby8FLjazL`h%v5{FiW);tq=uY!T4xEcx)hNq~YuIMrQ~t zGc!9FwROUtU`kA2#gH&;(C#kMZFU^xxK>A7kUGPd3?T(tivo?8e)M;s4OaIt$&gG1 zm@vYq2r28JLccppay6hTvnemBmL@P^4HJZP^^id_BtkG&Yfx*9GAcdg**GPkB#ueb z1f9#e3xN?5q1+^2Ftl48bef^l7#)TlL@dMqt$h+Dc$&`!M7wiu@=As7jFr_DS02@F_CcB^#BstfPMBX|tvNt%lUVFT3|qdqxJR39ZA9YNIUG$v*lpP0iQ zxsR;hXV_Yyvv7|7!Z`*@7s=YIhh_Zl=o}f3foA$ucNm7RW0U}KZ zgeH*I?Q~%XcqgPY=Mb!GL{c`x#2H!)(XxkCb!=F}1a+4tmi>fU4CHs^(g3Gwqm&N}+=gi6s<#xuqBFghj6Qij7JgMOdQ zwH9Ca>No5=-}3M_4)6om4{e~@+a7cg!80#hve)L1aQWJGuFlVMZDp0!{*Z<>46`Bq z;U>dQK+>!cM0JAN2w{DMU}T(VWQwphf(#V(xxGX?_p5uyo{ zBvJv1Oc}7hp)dsqM*&m#un42c ztYN4@Xp77&Mg*8Jgqn0|hjKuu(i!L>GEf{je1tHFa4pvsAq~P%p5I%lHc$a%{nK2i zufT(#MwUfEQ%P@dy-r9W2m-}{!$(j-h{-Z6*46*Bj5rz6>vd^$I-Gmu8dq*C+am{d zZ99SQ(|+g^6(70xm;g9?dBI*^SmEmY5?607@yfLuEN(RE4rBU*K5@6hz*tfh5{ETp z)F5b#64XXfQG=*4hN_LAq8cd4$n0)LXZK)_KZH()q@4}orK`ltm+7z1Gi z++=ceoMdE@rR!Je4LS^ZeH!BvjPKe*ZDN+%ffLM0%dox4pt(l8wm`gko#E0XmDxRD=v7tP0%tt%@HggCAQ|Ph4oP5Gp|DN=H?$ zKEi`@0trNDY9_;K!|bj(ZomBw!l>rB0G^*;h`1E|jq2hr&t)$ACJ-2DQNE*5no4xw zOQKaqr;}ro?A*17#>g1`)@D(fH98|nVuphOolb|fjZIF!c;VGvORq`$3md6=`_a7u zU}2?cZ!D~H`Nkp_Z_IP?`aCz6SLpTn^x~NIU_iguCFu#$rbLAiB5V*e#!-z)RBZxT z8zHEVxXD6Mji@n2ZFHL2?!yTCHW>CW?PZ3|1%{h9>8~!5ZmwbnZPajpi3bEyFwz)h zYH9~#^*Y^7m(kI2kl8Jqf$Bo?%g5q&sr;WY7^|6`9Oc+VkM2T~@j%l{JB&w)te3EU z64DX`5w#;y89=(2(!O{gpZ=Dh2RB=U6Lj)1{ zuoxhef=D>Q)HoZGnE;{Oa|~rPor9!hX*CbSL3l!c&UCD@`jmC=5gLolQiRdGmAtr92|5@3n6ArlZ=OKc5FDy$Tad{fM2 zp!iV5(#&PL0K8(akP;=JCO{hF<{D-hcbz=N8yDwmP50GU9B3z}z_-M^FMcE>9sMR7rMAS*97^@4;LYgMT!ASW1UE2K~@vzOHx9PaxK}ZnQQPC)} zHbxpW5MhW60z?oYyrT%~BgiNuoZgK#lHov;hC3LJ4|z7*$+h*6w~UXow%%s{fs|TU zvIcYPpx8T$zJr{pr+rn<6f$XZBZ@~)?c?#MS7~QMqGX5?5Y{D;5ex?#=)M7=7z&Ay zVNpJ_LZG5LBC4UHh{oG%7z zT1{>)F7xC!&f4w1fbZRYqz<5Jx1TyvT%PgQrY{F2|4t|QKgd4jbGx9=XOfA1+Cf3nR+dkvAsgh)oEqSmOB zXic1C9z;xwc3EpJX%b^}R*X<12(T(53+n_2juXx9Mh{}bkuj!s?Io;_dT|uPSffF^ zxk=J(v3m79NxxN;)ud1bL(lk99l{Z9@>+WoNFpf^Y2v2xW+^g@nI5n6^FRGT4({Jy zv=wVlTQ(w>Y4-#qS#lP)b_8wh6!d@Om6ogH&~qHXqWvt0oOYeRK&@6Q1XfHXT1eB3 zVH{J72Xs0iBh40P&s}=8Zt2&gy=o4iYIojVI)k&XT(j41u5$kJ4X(^DvADTOrx(-e z4`}s!v^p(%oi>B^8ngu}s3B_&WNidd8$pE)lnUMAiu*{NqK6%9&*1?(Yexxe#`;>5 zPP@-&!_CvH2CbsfL`1&hbL(tAms4A-2t~$S2X?Y~=SjY@u}H7o!3|9oNCPzlp%7T< zD6&>6v=vVNODb#-kr~igO4{!s^d_On9QjBpOl_24b{D2GhDMMM``9c&rzx9jt3+f- z6;d0SbP{9v(Rx09F0r75APPcier$V|CS=1tSrTKdBPmVQ0)G0vKgr#9-i-=F5YlxR z3g4?y0Egv{o^!8@hN^;_;Lla1#9HqBmBF#z`5B|hd`F`nI_=7izJ1u>QyZhbT2M-y z3>gk1LKV?&wpm(U;VVzQVBh-2d$*&2@4KXItIa4(&`Vy|3Z;NsPr+*n>` zrP*b*)1f^W((d38@bM*D0|)udGG=kC|h-Nx!Z=++*na|K#IzTdSB%V2#)Mvp6{{ z<*2SoNQ6=lL?8n~sUTFCXu`n^T7yZ^ndb>C-K5VF*vwI&Y(OQU*mFYnK z_ze`pO_dNdqJWWl&5vAblO;pfQ;%k>7V-Y~{1xuM?|!%FaHzkw7GL)(Mwu6#ih1at zcTSc~816W11sS-dxvM&K#Z@x%c4kwC`E6mN%NT`ja#VyXX#vG&?Oen(GV)U2La` z3~Gd96JQNG@e4^LOpn&dk^$Xbi!*0l;aD&eawGJx`toS`cmOVB!Y z-%L$$|LrG9`z_8r`y`v|tHg2anuts;sV>VMP$}2ZC<72~0H#%e|Jl8h!nzhED+&jN zbV$CC5CoxfC{nm_W=fV5jeG~5@i1nVro_V`OY7?l`d!c&%4$Lkb5oPN|GhuS?RVUX z2m|o#znO2mu{nHTWeNU?O1@8?>f3_gvn3+{>@5x;|D&R%%P(pN0=h+>q$#~#uk2Z{ zWdw$GYC&-vI|rZ?tyYW0#TCBt<#^vja zT$x{Fer1EDW}DXBF1r05&1Q>sw?oqJxcYKB1Q8G^$@Ex*xU)tY4|(aO7kTH~-^SiO zdrJL5Ooc*xOMiR0RryCC4}S$9O~&4-I(OZ5H-QW}|H9KO&0i%R4nb!nu_!Os<-2Cy zh~#_pg>=0Du3N^X2~?1KJb)(5c3xv=W#-Ax1K<*gIHDBqZ@`qdCh2SNY{3)Z9TPvE9 zj8|N;t|!45LmUrjw_8PnQ9caCQJGVWu>{id0#raW=+WG0aqg9?ul6nbny}Z51E|{V zw;e3j<&|qU?ahTX&R@OBD_7^ays*T^VJOW~l0*^6n89E`r`u-! z#v)&T=8S#ABX@2`0e_0UCLKW4jvU%o@(eCqx7ThgasJ8;t}HIIu-Rgz-DkDcW4+zw z;Lce>DT(8REQ_(m@Y0zxy#1|jEoK4cz?G;(0Y#!qWw)#R%1z?U!+XQ1G8~*9VY4;H zzT5617~8@8^(!=27BRyPGK*aoV3xYK^A152@v!z{Ayk-r6f|Lc?}GC~Znw_em6i&^ zIx=b?g2>5%r70#EI&~(K655RE$q9b)z3=7jyY3;Z)e$Q2;vg1n4c3&x9xn9vwRs_n zyxxi&50TZT;z~@P!vQ{4DvByw@d4x0g)PD>UV3eDagkxaTaueR8OZ3&$)m}PEEU9F z1xdv*!(pFRyUFR7&cE8X=a0R;<{d!QPE;a;GiNW`#nnx&F03%Wyv9avz>d*LY#fuM zDVcU7&@WuL!0Os6d**h#P~PT1SIMBum4b^VWLwE&Ed`=2e3zRrn(2mQ=UBwGWsM%0 zW^@+}MMQsd1>I|6bmHi_feUA)$#Vm*5@a+voi|6$b4stxyX<`0U}b<%j`y#E2rUD& z1+7y^Vu%OGbbvA$q17DRzmNC-)K9Vh;9-;s5z=E3d2^Aqg^+7m%FmmRj1S@6XZHz5 zvAPQMTvb+~bJfv3LlvVG&jcWg%i-^E^64xnmx-hNa7T%TXIH zxycyEAbaTyd)~F%cc@Wl4ggzj_QGzV4i?l_-{oecL}wXCcJ1KB>&vXJ4bTFj@kulm zp+eGr8R2u zu*H{R@o(Av4O&}z{T>%zIq&rgKnR1C8e=p%(_}J5$pGyu42et$R6^`K1>5ZwXU=R# z0e_mkZXG~w2lu-Uv2VU`&TgzU84L#uhq24O5g4O+;q+W0>=?O3nwJ+0+ zoVjOxo2(VCQh*HHkWFx`M4P$#xK1%?49O7E7?TW8Izi}!6Q@q`zW4tWlao6@`i1ZN zdP_Qw%_)6;s!^`wL+*l1Cs0}JSW!AKlTTdASeTz@{`%D- zDzbppmNZKV)6@}zlyy#32(rwHhbCFdFdowH517BX#0xK7wD;b1{6`iAT)jGP`~3mK z;m|7>WxjvHfxLkqi$2n*Gd@1b{(ZY&wFu>P?*MXp=-yM}U;f)aw*7vOByj>5`AzuB zxwBlqah+pFj}>VE70^jmHTeoPQ@|>mJeNcYOM%epOawndjQq9pTE4KQb1>!TvBIn;8(}IV!#qe>iJD9v6$l`g zBeS`}uqnk^J^G)d3FluvL)`Bb=foX?KxYZVVIONXQLOCZ8Yhh&+qNV40K6CvFF5r!f4MuXq@tv|9e zJEoc4Im@0syEw4#N7vT;m&O5{dF6)vjWOP7SZtPIvjoxsV%T$~Pm>{R#;KF1_`uJ6h_T5jKNQ4| z@5av#%qtwSlv>LXylMzv-Tm`?-uSwousJ@E*8wR~2}|kB3T(hrsZ9m>cP$o%Ol#KG z)_CcK=dn8R!)E*ecx^yO8cNBGrp74B4>Hl#gP0bQ#8m~Z-nhxt8_V|a4@ocJ+O-9{ zva-tJ@-mkMrR_ z`KWIy-a`xlR0{H1XKmo8nS)9cai_eqn)5l(Ux+8Dm|>^FJ;``*X+VULmR;XR1|=?19%nACo;L~GN=lP^0P8=HuIBy5iKS}mcKDOzXfEFsM_+E}y!p#tiS28~98M!im>QO8=#+WIEXy>Rx=dH-CuOUo-PEv@qD z&wPPSyXE>*;vqUqT_cmx7_HHH+p^KFYGJh3lyu-_?q+jGEM-7*bAuqN5r%c@jRt4V zU*O;U+8^4Jw;kavZ+h@A%n|&haR67ZFVX3C=@0s(S%T5NImh?-U%YUh^XJd;(8G^{ zADB^yUf2@w%14#iVw$nUjY^gXX^=*i^Olst1|izOeRti-3oky;H_lvu`UD~x_mgsU zd61&)NhktuV_h4vwO}>E(`2PFU^QeZBm)=n>ja@QgmKe__U_)z2Y=>6j7{!vG-=@m zgUGzHAj`1dy{RD|ftE7~^K?Vltq#LhLVdP;UydU;#m;XEjG&ALJXP6x-hW>DOEXrM zmU-?QUnd#%+|0uql+8&;{@iPAu{L2C2Z*pvlDKKc^(bOuY>dW8o$;{|ERq+`oaM~9 z%l6JwN5ALAbn((PyS%*0!oo72{oI#mw>u1aJ%+;pNt%#lsn;mXFxCT;Ubr-m1kx-a z9`xxC`t%0_Ha0dG^!p@9>{0}65K>U9)fpX|U~+PX9kaWb+BwJg^iIxPyw0!uhd;6p z-g}BiANb*O8var^fEUkSwSV&QuP_|O#7Rt=IUv*7J+Sy@UFMMjf)pAvDRKe)CQQa24RHsVjI=g zSeNozz#y&h3*Su*7CJ-d1e?W}B!+B=%yiMJYp_gDP4K=C{0x&jc7jla&YsBs33seH zfvNZ$W$~k^M2N!KyF<6rp+6juCNW8pkY*`5&B$~{nx@59ou(vdLbu;#(Cg7^H)*$9bh;gq zIHsClRR>}UEd4>3&E^_6=C7cFi1DdiOz%3#)b9N}f9VEKzI2uU{Wm{pf99{efrGnt z{Lmc1UkV2>e{+T2VBn<4{4U|xX4*HNxEt{sPk)`i{tN#J(=)Sv25ChbK;>b%$;ADz3j-EuiwI9K+$GM|sm5AK{aq{xnHv08s;?1~#lYB92s!4(!9|JPpCR zA()Oum_j`w-QwI z0Kk@jzp-0?XKj%w6*Yh@Rl$7mo1*=>11Brq>vm~2+ccX^zVwB! z(&_f-^}6)?J%({i91ls;#3TAH+|RNMon>S?WjGwrX}4&%+jKf@y4^0rcu;9=b`_&? zLQ-B`D$n z((SqFvZ)WftXK81#uPdA(!v5SoqnFTzT=$;p(=>?t@7KsFZYc()_oPeY$m1k^8`U! zMG&}CZH(p3Z+3pJ6<7i#|!*4xgn(SfAyx zA;2m1#!?6+w{cv1RwaBpYe-dd4MVBD7XQkr4uOo z2+G*n|I0saMdL4D)J5b&j`9~zDP+8gYJts#LTp*b-_p%T;lPr3$TLqr$=TD-qjlz9 zFdt_QSgT6+>v7pEO)yysQh|!mHo%HpyRjS{lVu70L7%ma4aORy)M^n*2(;DcBqx0a>ydcG*7)4t zAw+JDs{WLd_g z3+H*}iLa2v!!kwj7=_8pp4eh;fU$-&i5)JYGtj#DJla|kYv}<>ScG+@!$Gf0tJWY4 z0_Wh3ag_wW%jd|wG5O+mxqUnw((Sfrw_9|(ZH7a4AGwUAR5y~vz}0+CLhcm3dQ?s( ztDIq5+474X6N(~J=`^fCch-w??>TGvUw$j&<4>NkKlP>`92|VzJAnD6HCpYK&;MgG zt=*35%VgLB_r_XFtJ&mhU;QdakKb10oBq~w(SQJ~2l#A$Q@RT*0js1R`YC*Mz=MSr zgmT}Hj*al1pL`!T=NGtk{W2!Y+|F9ross37Vhg?+V2W~5A(;bAPEGNlpZ$4`pFB~9 z_EMBLq~HCEo!nA$MpMsx}7%dc8lR)fVSF$nJyB_hnp19l_&*Qt*25KySdD} zw@d|0t$?lhmQkf$*sVkGuo}~E5-(n$W({YPlz;GRf6PapdD(vOkspjR@Va&YSLWC3 zM?U!kt#*rH>`FIUds?RP3cprV!Yy~=pZ@yS`MJOT3yhA9`<6$2<3h{fOK5l*ph6*B?Ln@_jO6&^mR#RVqXfIftt?2#qlUsWY_6 z7#dd{aovS(x^Un{Q=~^~tubVN@aUl5r`PS$?{$geA(<~1=L2J$W0J*rR?44W%KNBD zg(?oP=rpC`FpE!E=KfnjfJy_E6~%pL1|8ysS5QXsoPq!8S5rRo!X^8jd%u5R@OA9~ zZZ58~vDu>E@A($stf>3vA$0E1ow=_UE?(ryyXHkbab5?gFek?H1nr*qd2hUFD-6`3OVZcfg(#-bQH9ehTmwJv@A~4w8NQ_w%zq z_j4TBf4~ph1gVsF0K)J3RrKG_K=L9Y`GPNI`xOvC1*P&<3i#X-!sf>%s%NnzFCi>e z+bw^A$4y%87N7t0CpiD&X|M)aj2^Fotv>qkr_g#u=3Jyy)W?uBd)>9nT>pauOf@!5 z9lKI%tjVxCLWdD5aQq8LCd!D1Lxx_K-m&@83JB?=B%6o(4x3Sal5v$zT}+hPa`ZV4 zVk#%nQUsG!ks=Q*i>S8m!zkL>%toWSea z0o<5hqS@M@Kj@RBF*@(O%Qx=o)?c}7Ns@Br%*)(!-ve$j=g+Oy%VimYS74mVlX7&& zg^2lEI2Bz-6|Pa^gCF<+QV2f!(T~s{blqa{EwRE6`~ZiXyPXGuFyg@n9^@VGdKWXZ zv*b1LBL9@~+YtX8&jXV6LGd|CZ9s{z zhT)*kmmmKeFMQ(}j802Qc2S*oD5#bVbHLEau=#VWLWt@J6@nc{8tozhn>pIB)|f0K zOEP*%>{*2lPqErn4@_QBF~w%@fW{n$05>(sQ+J({-Kr#AXe9WZi)TXq*{^@<`(7ijTL*CI+LHbFXP%(lZZjOl zWT~fXTJ7h}`^0ohQVKc3_2TK%{Nn$?`rZJxP+#5U^pxGQcMo@wrE?C(Ei@zDRNq`@ zz#|DLWx*;MjS)Wh!4I-;|6V@!v5#`;;svr)d(f?%QlR`|-nV~0Z-3{zx%;lWoam|a zxu>u97g~V%j_(1rq6}48XgTn-wGc=d1&GQbr`){C8g@yBveo|z5XP7Lwj@{%%!-Et zzV!Le^7L1~j80;VlB=(hZ$Kl3*j(B5eYN2!&HO3KUWV-q~xC zH1(aADblzONtq5^-$a(Wv2sRNsyY^xmxH}5c^>Xl^iud^&Z|n~5l~s0-6EIfo?M*c zqUoy|859Dj6%XJ6#PV#qi(`75*v0e6pvG69dX~TUZ$E7R>EHU$_Z$VhZXLk<;yUY_ z4*YYp%hYwunUaN8^aOfaW ztx=59cCRZc0HTjJWVy2!$5hSjsW`F%j;6dZ^Jf+g!~c@}<}J=xd83NbV^l}B`|S;3 z4TC|K&wct+eC25t8iK?o|q2!#+DVWe}k zDnNvz1fzj(1NRY@wFse6sZ)&>!!8WFkPeVpinO|5b-VmOcK~^n!Y4rmF5^Q8qf0Q< z-r^=Jn@s6d^Pr(T2j#hkT$rWmN=5Z-w3}p$=jjJEe)qFa@~=PnRr_DR?=9bx6L{S^ zfa^Dw*l4v&2cYxTOr000x9&~1gtHrKYg~Ng73OyBDdYcY;#S?ji14XFIr582U4QH1 z&qL7fyz5iBA#ZU4w)(e~n{i92sti%9pjX~>H5d;0#K%6uQ(yj4Sw{1Ys>l#-!4mS) zVVVCrjKr5sTuXGn<)W;xh7D_uSY-pyDOxCy5>$++k6={z0V9!v$SWlIEP%g;ph+Spqb_P51Mg|n#m*DcSK zR--ppNNd+kv|vB_$*<6EIr2~9^SqpzY5YYi zcB@-164vSRU%!5x$AFwHFHHXI??#=?cYa&$yu#(YNRV3iiN8)T1kWLGuNabZ^R-fL zWLH%4>52m_ooI$1Q&z_BMLDXleAgxqE&YR{3O;Ya*{i8N`TkPf^~JY+*vjsg?}WKT zT9Fu)X+gKw;gcWvFkk=bSA08f87TC8``HAR@_E?U(Q_}JfX?Lr8Kejr7$ zyE!zdxM8cgze@mozVCPI0z5*x{8dPDm0efrldg5xw-GBWLIjSmqZBHSTC^$q%dKmf zt%k=%ds41ffbUWix7;v7eA~}kT7YjU`%yez*5Tbod@JBp!BVO?fmaLI+*s$sfB1X6 z_^ofcawk;MDk|^ezxu%}?lV#17#^XsR+U@{0ZJ;uFu+*pfZl!zYJl-`BMBonlToLT zh1gi3Re%iZ*sz8S8z}GqpL^T{5u-CFi2{XPzd?5M9NESVRMJK2#8HmPdm60oYRrmz zs+b~Gd``RNGqUxpd&f{>1O7ymbaaD{`tp=$>p2%#Nc#*IE)mtnIDKw}fAi5V|Cwv$ zb?E@+Z!Xhnm*Kz8T#vS`CT8v~b?3P_la<^O!R*eRMY*g9e~U2J6hU16{34ID5+O{v zLfmRcax)Q9blk(lLq``56sQQH0&EasGuK$9!x+K_L^a4VM3fSa zj&khq5$aQWY4z@BCi-E;6wYVLeygEn^QBC;{YM?U{G{^+R}?9crvW71xC4&cJoCHvuz ze~DJN!*J*tIxfGv9lC&&sQe*>LYs9B;9Ve5jdM0w_1AJSE+t z>vt<|kUJE+#Tgd6k*Ge=?OfNcT;?}^{XeoiKkt$Ct(Xyj#t+7;~p4g>x!G zV39~)RSBXRwVEGao@)*W2!y0AYffF*7~jO>+ox542tp^D9z>usC#0H2$gqb^>sTFg z{oD&oABdUVbC6x9?q+k}el`xDqI2y?4npsM%d=+6d+`>oL~;LFjc%@zE?s7$G0w04(dRk8v~ExCp81YW;C1H!7MIu9 z*w}PTt|TVYxmrs}rNwv-|1EhKmHX|kyYA-LvExP0d9ib{6#x?kPxE98;wwbnGa%gV zFO27cdkjEGUu)L5euJ{x4dL?K+|vtV9ZAM|2+SIk5FQJ#<-+mrpC3-)1}sIsdb0iPZA-^exEWhs z7D1I9RJk4&*P*91fhXtW(~N~5kFJCul%hD7SjY27(v&0~fX>`|8v{B)rwW@6uyK!# zxI<_4I%_lg*s=F8d-oq<`tVT(`}VWB@&Fr`o~L{9S>na>(AhxvK8-5s&z3op`Mr@} zvRja(DiZ23mHhKpW$L$>&0U218wnVlkgQxM9G~X&`6>SWN5Ar2*URh90o+_%quFWG z9}Gy6ym`ipX68dD%M7jf9;(GL%Oj6I;>J=J5P@H4)r(dEs`7|9*AZ}~#a!Vh|D`DU z!maT6os@{evkRn=egwB8o>=Me1uKwwxzTvAC`X)yZ?GvuChWH#7x`}Ofw~HC=r*QX zssj1r)iPBz)$!r&t$$@LT)c3R-};T;U~P2~WpmK9Oo_f7`L2SnqViZ-637Wj6`%kM zzG)}*Bq;5UP)dYxdI7FnstV>_5C+u4I#Iojkb=}0`bo@2vqLu?qNPG;g;g;oi%A9@ zvepX2m8-OFTwrR~KBnjPvU6?^lRNj&I({FkuRKTh;y1}|p2Kw35uT`&Pk72d&#fpx zx#?6Y1Gh-A!Pci&o>IB}wx)stut|@>jkAQ~d->Rxp5>!YzhFQ3=(nc=uPX;|b$-?U z;m5vAyVv9Y-`;!2S#})fpb_5SFt?&>>B zNt6T(W--0L*_nIqxhHm2SAF$WRkz=DCST==Kccd1%%vr6`WtSpg2IaB%W%y#*Mx)* z!ca*-F(Ud;+(kSJIAZ|Xfq)@&hiLw5vZ$2rC!pLU4&)r|OkJzVM<6UTT&7=T30nES zg5=MWLW$0KcwY+Zr9QsPQ;BQ4M4@?phR`F$&jUW;E~6$@4z9PYW2`93iLa3u*CpHYa03Ja&%6;gyzci7+bdm8@KMn`0DkTT(=e7 zecwQ9--DpmNw|`35c4iR0(VU!;t?(sQ6Yw?ig3}mZUD)idB1NJ?; z8vpf=zdlc9&L{F4iYu1elqHiwi1e{XAH}oJ zJO$2R;8iN_UI|fmzpN1SIH5T-#(>d91?wwFe%eZ$I@69(?R+JoMDFcxiGP?e-jW zze3*ap*K5=?!*y{tl5B-JI=t7(BKRtR zn&L}rawU-kq~B!{8VF@&(LTHnwXwDM&x+9{_xAm^_?*a;Ml|rPM(@U zyW7T~-}eP0Yy|$uhE+S>uV_Fi09Re{M%;7P9nji%6I@inRTQiM6->2=3~*!3K-Q2= zHT-G@g~-vj%n&5MYN?nIqBNkYF!9}PT_%Br@_YifTg?wVDb*tdJ%-Z~rm$b9*+UNJ z4qr@@1xz(SfPwoX|*|ITw@B=@9=Gdq+`+fDxc_bGf-HSmN9~tm1zP;TJV&INL zdvHR+Lg5q&y97SYPh3a9g$Y3-$QHmNZ}uw|1nD0RwfAB$QToB)bBfbY!JVE2Ly44_ zSj=2x#AUn46nJx7S5c4t(WbiY2tz7xCyCfFAU!Vtjlp_tqx&<;$B$JAH@KvQIdhG3oV8sN2NrFyGZ$6sm zXPvzld99AJ-$f8*32Y~zl||&e$H|>UDkRzs;ZOtxF4sg)@?uTqKUKcRf>T}&lqH`O zR#qhjRe^pnK&#uv#KcKF^7u3Kh6~P~7ehJS*ya_%OUEba4?cGvKJjONj{S#^q1W!C zDtegg&R}lh1Xk`m2kUmd4)yUhn0|UYW}f&OFms%j1%$W;0a4Ub#3V&eq5_UdM@rly z=}K}Wi%_;_&_Dhn9{$c4e&thN1aO8}0N;7?0R7Qt??bCShkmbzq9{>O<%@p~vH!$$ z({T0Y?z;O9y!UG4;7!VvNa#n>$ zw7H0fb~pwr3zto3BWIq=kgfQl+X&SNQqyv!W8>Pdait@SQw%e;hr(4M7tFqKHB2D z5c^~*Yst0oswxzN0ebyDW@cyc(xIc@^ZeVq-X+|;aOgOF`hR^DfAIM)FE{v_+1^xQlP+PSHQ{Vm)$|L(+@5A`Wg3HDKOHY8`37FH+svpC&QJor3 zJ2zxORe|!6G7Yd}T6GKms^&mn3$S*0iX;_;3s0F>vr z>6Y8@)MJmL)0z=3hHxYpuV4czVqZex#0L~DDS>mU=iyG@eDvuTkI=7w^3(W(FW!gQZV!DT^bb6PvAH>nZ`zG) z#|6koSK!pQzl6>UkHGZX2-Z)q42K6r*G$~A#7bb{7juc=cpk>{M9S_g?DQcVKfDHC zc<}LrcD`uG(c>pw;m=+-ly?p-nFIQdU;*&j(Ht4Uhd%s+*t~g@pK!vg=bP(+JBiNE z2X9y?5W;#0=0EQjLW#s`J z7OK$-vq>qu?T$Ne>n%4WC8$yqJbJ9ecR!i@O3E#yxgS6VE1LeFpP&XkKdNYZ>OCUJISc19K2rTTWzgI0Q~s)^0~4%=;pX57m;&KV(KmhZ5GZmI-3Gy6ElcJ zC*%Y=i2p|riU~>H(D%0MvX!fG>z#MuxBkn2M%n8{6I`*cE~hrzvZZij`+yhJ5u}%s%u*^bS9btmp%(LeTU9C=y|s5QE}}M5*hb`0i2! zqLnXdN(GA6G^P)o0T#fq7u>rmwxI4c+V}@ zqSBINZNKK#Q!gln&TQ}8cYqo$y$ zbQy-sY*q{y#s#G9!2b|jfuKl&?<`C^9Uh|)awM*X{0+p<;yG#VD=M0S%vTu7cg0ZJ z2scBAQrEF+aLiFij-m8v6puJ>t&wFJZolg;Y~H>dQMiMx5awpQ>h#aJl1b}CDv8t` zi$bm<0ZM?3K_Le`(5jVgVWu>;ZU!iQ<>JN z8x3vC#jOC2O-y5Eu7!5Hg`y}>m6d3k39mUL^Ka+Qo%lO{_s6ho*;ts=sSxvx;=2|* z(nRGdO|4*`ri)kt=7|aGO*Rr}d(aXL>V!kE+3nj>3Le6{3d#ddmt1-ouD#*LP@FSq#io?FUl_9clo}SIP zMAX5)kua0E;-lfRVtV;Cz1=_6^jvt?RS?BJh=k8R$@e4nue&p>p zWA%!0n6i&@@&It^D4;D?yzY9excnYe%eVS6=P}X|BTYceOpSLbosN-5$#X<21)~*= zrp2uQjvhaWnYk8v{k{wR(Gcx7ECz&Ht&R_U=!4k1_bh+ktY~K?P-7V>2x4&>Yf!|+ zBS|CumNdJQ=cCwd6^daPRiC%IiiV#oj2so<_kwG{LKs7hQ892RFI5))grh~k0u5Fy z2>yMHjz+~26b!2dzYk+F+;-<(*tPduC^6kwNP}fWL#~vPN!tkf_L`Dm*PvtxvEHF% z9!U31LJ4jT#AtveX8{z&0Nq{>Cniq4B6IlX-hYSsxetCHHmqC?trg1Y6VNA)BC{2i z?Y$TquDS<$^$t|J7G14y0SPFO%)vNgCI2O&ghngmMlWs!aAM*V+U+)~B3Aw7(qA?> ze9pMP`|i8&*4uA}F~Pn~Zdh^){Uqnrgwng7hPN0k4--(5*yJ(diW{r~07(c9NLF$T z`R{U@VatKRzhp9sNl!o|;b6X?2Z_a+sS3QiB5|L(neapSGGLYXWPb$9E8E0id%_>)G*<&PMF z>P?XqD781s3YT^x<_EDW;97%P=-lgYEaw zojQi9J%xPvT5P)N?P%<}6xQSr45>f~RET&vmC; z1*~-?p+en%vLI9#X*TfDpZEwiZr*@A&*P+pprFRP0x6VY(Z{?fL26`%hmoiVmF!MT zUL^jvdl`>MUQLs;c0)KZgbB6wD?SUSVD^Q5$I#ud;OlTE{4WZ82>WR%fD8_%(Fp0j zMEO-iZijDa=Dm|XpLqP>2BljKsRn+ik?<@YCt?E+9}e`< zfTAeS?sRZ+@|AG|ckeFY#bu#G zFR55aGf|abYjP+Tn+Gcgf&FnF4sXoinfXI_W?|75Xt-FZ;XyySI;S9BZ1@=j9K(2n z5IzWHz~?GgZ2|2|fKiF^(9tB{7#5Ggq#YG1t^V?k!85|-S5HJij_zdEDNSw z*@o-{1s!TBAYMj_8$iW`C?F7L)DxkG}hM ztXnaTvM5lrXHd=@K}Ep2H{OmF7hVsHEQb{#*%Z)~&+)XgFuY#03gG0aSmRVQPuG;9MO0Z~;t!&N0K^1HOnm_|A_|B!(-Y88U=C3RV~A6_CGoSkX7hgH z?2E^6bJQWvd(Sx+x7~dYbe4%mnNsP9eTa3OnU3Id_-<2MMczm7dzDI`3#s(EFnH{{ zFqNfC4mve8{qj%b?=pVyZP%%P{NeAzxjQx@wf#$O9-gG>**SE(U6f^s()Z8f@Q$?=GDUdrd)|eOTQ{LztGOC+ zBG+G*Ru1MMqI(#?qG56c4;gC`I#msA5(5Cju$3}HN~}=4Vu>YLga$+SCkTu1)q`uJ zLK923`VRw%erH3>LJB7pMOp_dU|8Hv14>G_Lb^3V2muA94Q{;Y7QF6_SGX$3^Jb_D z;HgwZK~X^5J9#~R^0U&59d=T@ho31a#}Ktt#d+g#fU+#n?+q|JGl%^Lj=my5;OWLa z*Ic6h)lYvvZoT?-Xk>uxwt%@)=(gGz+kFw%Tzms)#THbm=I=22#s)Y1yMDezYv^p z{tkdxMOW$odBbCzG2Z#`a1^56_V3Bh__@PsHeTd(np`9d$9NIkkY?K@@ z2%+$ygPIV6r-uYFn9=euN;&WlYzFIO_2v{oC9H)IMUeObQqWQyAei`J^%qJdT&Irj zX|1ts+jiXZ&UZl>6E&z(so5l~r+EG8y@8hJG^FUmy&vvA%vPXO7VbUk6NJjLK&R8i z?CioS03JW{{@c~heCS@RTh>I??*iQx%Jv-Ut2ScumA4|_egSOO2(DxJA>~t`r811X zmqn=nrlw~w*KVO?=AX~)22}u6xaaM6;;ggxAT!2$IcomI^&_MT51C5Bqw+dHQq1^0 z9jpMUBqDwgmCAfaLM8R!hBShINBG+UYy;x&VY6LngmmBpPm?Qu02y^qFmXYZ7KAuL zP#yvZDgCoUjJp|{Wrf!{rrdDjkC6HfU5dHX9mUWET}$? zEtlPh`i|FuvJvn4rT2|{V>-)91+BLz6~N5Q9NL`@%Bl#Jf32;c$>OZDcHyqO??9eq zo=Y$K0E+)*i77_#>Q_JUC?ZhZfk<&yEN@qArN0ICw%! zec{b)Ei3>p=&8xBlTaS7{8|Et5CWK37h+*nsRB~sTzP^eWFW-zbmTDbOGC-*dJ3YP>W6h96T~XbFB_K zovtJPc=KP8YoK}0y?0^LrcG{gFu?LB1)|QzyEnkLA)2Kr~GzmU&ly}luih~|FF~|O0Pi45q6zx)q=ME&p`H)8cj1`Il= zIxQ%jVRZZXSaso>pjT~y&FUbfVBttkmCt?P$j72n0IhZxb8{VE{F8lu3u6@Sz2`2h zUAHc-wuDshf!KUQBKSax{sRf0AbHP(tzW*LwL2(_GUog%i*4IBUZXH4Oj`ddIinValGsOAHd3W>jPyqgl7}kR>|?wYaRml zgqsC%RRIYPqz~R3_l|TpKv9+`1_QJ^gI9Q#e{bVMcf48s{D6>9J~g`oToICIh|2@C1kht}U@g$qD5?U5?u3vw_AaND)eFT~{Q#2rYowRvVp;EC1~} z`eV64CAHujF> zz5$iBD2f7|ZtoSHA}hFr{F7p! ziOH2vz7rK9FOK&kT8;%iK#5LoaVP))xcKZ{>SI6o1GxK!E3j-dhwjb7^jgU4V;J4> zdW>v8A7*qJ$Y@wvWF^5~JTyUbtu~6HAIg5OTD2V4U4M-idKHA&QbhMj{tX}~0gu*` zcMW)1u|l|0v(UK!j#&k7!b?TM0xepSAvIZp<2pQs<|J_manR6bUPwt!{8ZQyQ(c(o zL78xINg+h3w;?FPtO$k{O%g6j6g=b2|ej(5Kojgj$q z-TXCyzohU-bO4f_dW%dLSmL92eiZ5hxCzA6@~W&b=)bx#fZ?%Y{aW=ef9eO-kG$h; zShHpoQ1(%_Cy?binp@68ZOaA7##VyNA`1g{d#-HrpeTIl?+RC5c?EXw+TmM5vyUHz z-3H&^D+c~y#5ZiquiY1I;v!hjq_UxS1aZ}&5O7GN29a+dq7h+pb_L|J7|mR^T7bA( zNiP?0xEg#PLP|ez-;#1B2IB}R!3OEZOi%)bKt4)@K2$>Dca%zs)FjbL2_7nTgD5Bk zRyx{Kg)h4J65RT>yP$OLqI3aMkSBbaRIOp*mkL4L6KUx%Kx{)`m!F*r2bM+ovd`_8 zV|?^Icc@?Z(f8u)t?SUp6sqt2j(zW`?W=EbD|<~luedR>&2dmwqu;LSH& z2W5-^`rNju5XPGD0o=1H3bTc;4=+r3ct)5nGsb)akT>Op-){w25COpCB5Ji@QnvcmX0B{G@=mePVjXu} z`$qMTf9$kRTfg@9YcRX>1!VwIN`07U@sbOIrPKv`rY zD6%lbT2C?TCW}8vZ$2i(q@l3++CYi=^L)PIq=r_|^eX6dJ9M8=qJvI%8Au`?{!gtG z#>SW7j<>%P%U7=s(BCkwL8sY8x@Stag9JI#>x+6r2_V-Onpwo-x{EJVzxY!h!o_EA z#`3WY{cZ=Ps$p!y_C=-u+5_|l1;|<`4P0``#TXqO3FI0^a6<@43Ns0iFK_|8KX2K3 z@WRU7Vyf6=9`~-Ul zJ5nC$KRxhwx?>NYNjT4WiA+c_0iyV}NV!%j-PbUVz^}NH(eAU(#;teU4V~rby-03{ zns+K;HOG)LQ{p)!rxKryTm30nI2QZ3_?$iJpa1lSaM$JMW5Za6vfoFK7FjY-Z&0AB zN&ptMJjWZ}a8axRWCi0-2z2rgy{S*(w-)WfX1RKp-hRVn*s&~wE{nyd0E)o?N-1pJvI*zz-2=eI z=R+588Fy!b4@H3wf@diLeOyi~^of(d#mXO)LM$OgK>=SQ0f#8nALF{PWPLcqFY>7S`+n2_N3Wst2yV$KP>>(Wj?#Y)PI0gxV zC80bbz~CTJa0Nz2M)9_L-j3xf*E#?(=@~!?;3#fr@;hQO!>-NniAmH`D)uvH{F9IT zp!&f(-i)(1j4v()Fc=glstQ^uTyXw57#kY{kqN6bKVJ^lF|V5{_y_fr~_;Xcfe!B@jx0QGYq9{aeY98SUwuWE;W% z<*kkQ>3rirx(Mu^1myz=G&O!h7EO_6bx<&3hp{dnC6)uD|G_v=!^**9B}3$+J!hYd zcYgl|kk>~b)r7F<450nkNUB9-nzIHfNTF% z!YGAv&p9gr|9C~Wl?r}+W!W{bz5|RCa;ltCh-STO3PFh#AXa(bahTl%XF&i-AHo0> zvQqJ)5kiQREK0*fpy5@TTZ<0^c+z+d|0{}NvbeW6qTN0eE8uNO)FzBPffT2R-y{;> z6$*i4=|LYPKw=uQQ;BfOb9H2*aCKfuXr*w;2 z1xuChAY283=hEVqE5!2qTrWC2Z=#98~rmAECN*Mm-tq?!|+~M*oPEa3hD9Un>r9twiWU$ z!&~pV2RnA1l`s(H@kt(+tqm1#Q+yU4m{p7+@O@U0ZOKAkgEMq2nk%a;x$3`Uv32Vf ztX;b{2oC$*?CYbVAdFn;m4<|xQ;FFXNPK#!3_``B=>J=x5F)4;K|Sk1Mu=X8l>8lq zJpKR)W6fPzYQC}+q2&6U*bv|e&+i3dP}nBTJxHK|V4?Uu#S;z3Pt1IHLwepS4n!i; z@EKCL&XiT)1rp5J5a-~pLTimxt5)NlcfSWCW6Pv|2pGjY!lUfh7L*u7PJ^&igzKcj zHngMwUJZkV4c21Ywk^nO^=QIVjODZGFNK!{o%;tmA+MZuP&r{|3&DnM;%^eGd#oQX zfWOoJ#7ID39g65f_#7-GHn9juuDB!xLHOG=Pj=Hk7!<|uM*KQm&0~m+-Ip{#Ns-H( zggcXO1>yDQOQ$6B6}Lx>UL-vfl?xD_cj4=B+ud)6$!hV$JysA4n8b@dZL^jk zP?X;b&WF!W(8eIk%mU8*mv>|feFA^gQF$|4LAjD&anTH>cBJhJ&d_j=d?DVl^kuQ{%$R-N=C$ZC1;Np4tZ^c}C8kTkW zozxN-Hv5TFC6OEik~WKZM@;XJ1Sa|TLpzK2T6`-ilJuN{AZx%VgX`aN6JB`kSv>rW zuOgCo5zBxjJY>vgL?*6A`e)!ND!^!sTDEA!l~9j`85`Dkir?YWh>11gDozvfa=HBOv$#)q7l*$O#J?mRcj&z4U5Gr+ zd|%>p5jX{)IWmg3-E%JvA3lH+hYmU=5`m5s3=}ko;W(!XpT`;seE$^yqYdh{8qUK3_P*eO`qM!j%;vJ^xeLq3`+aBtS^PRj>g- ztcI|HG>U-l>g%&`M$Ul1r!0lfxTFT8dF=~dMpq*B*CQ(tLGh&Z3Wn~lG|3a`m8SmC zNA3~$=ODwHfY49ToaTEZtPd4Tk@Q*-!-_-4fF}tVZE(pO zufYCi_u+qi?oYfoqGECs#T8yY6@FgPDnL*Qq39_Tv<9*~N1iWX01G}ctznG9=-3Dv z%|=+^c(vpPAz>vBvH4*nUaZ0bHRD8~fP4pmj!T6jr#PQi1wWkvnjJysxSq`m+Xc{o zdAW>4gB@6R9tpEI4HXi%6|*TmAkFOrtm6YvTwxOig0K?!6o=kQ_m^7uaeox=fpc4g zV1tqsK5?1&c?eI*(Q*Is_N^8ZW*0qNsaMt^^Wah)S=68^$E>@^x`G zq}Ir@9QAtrWt-OvJr<1%kmVUNqcJ`<24k|Q06YOFnB5-5w-p7`oC_gVWreDAe{)Si z_Zj&}fZzmF> z4}z~i$p`T>1sEu7+`J9yeeB;DgJz?Nk&)&? zPyd%~EE)x{dHqV2X9goI1f>-XeohdY&;5^W7hI9jEX&bsHc+qS3qAc`wy|hj005}v2DQBIP2Y&> z2h+c*kTCeFGQkI8>^cHlBuqx)l}@Fg7lA}nNjAj@^By5`9SUYfyEF3I6URT=yD_#H zWks6y-c=Yz{i7hMaGt5f9y1B!d;VI59d1E@RmtjZh5es06;J2;)6n_yxvBIkDk=vG zw%GuH)JR0^QktijA`FSI(==W|H_0iCaGfat zKGe2z=M;-~rW1)tE6PuDS@lMRm54oK5+EZawiwSPk-@mMari66@`M%Pz92XWQ84FY z0r5BtN($@`ytNceqF^7ST1x?;{5izQg(0kkVPz+Ma|RVo!{%|y*Sjl9VPtd+_q^lX zICA(9CXOCWw$q_jneuk8pbVbI@i1B=&vT57j9}Bo6=!BL02Y}7ST;5aGck>-B4mdB zRuTRSzIz!MeJM}h&NE{vsnL&An855ZBvGaCJ`|Lu2q;M<;L&`p)I%yN5G}X?YA7xp z3;_{V2t0%sDoBuc64rwR?>I~-^@j?Tk%e*?h7O03{vp#m3ViZ0zw6nGDq;kp2H%WG6zZ0JuL3Bmps>af46YM{&pmmN$ zt&WkAGk5g|7MlWCHUb6hN;Ju8KkV}h;Et)jRq*{?jGp~&2H0Y4zIdGBaG6T~3uY%2 zpehBWEkfHY#T9jR3@Ig+f%4I+xEjq{1l?JH1wAf~GS(;vMi>_Kcs(q5TxT$iY`EW+ zSV*MFPe?bFpLq)=IhT0eviM0u2tK7Or~hO)|c)lxuc$?YrMYN-qmKll5G z^}^&+dV>q1JQm?MbF$7#K>%haH!7v zED2{>iT(R`{U)sY(i4kHWIlfUoZ_$eSTs=p0NAx{lUg>?Kv@*-=_fV+d={Ty^~=hlEDQHj zmF}l3Q57Z1vOrN3C`$KUQ4}bOf&X2g^gl&WhWE>&Kv5PVVo(N_VO0$&8;%l$gd=`U zE^L@&656Q9M7@Eu04cvlv|`}hBwyLLOrX zAP8;(;&*K9Ld&#i=eDKSB^#|ooI&oIQsL+G4xEel!x4Zr>(=A0x4#38rW4@EFNpJL zIME2&0f1T=WVISbN5`;ye0-s&{mVKQl?4C*>(;D5Rg_|)dj(KMSHQJB@}a!4be2J| z0yrW7@p=ddbALvpIQaJLGwS$SStzAp6*=_SzgF5phenuba}Mp7LGs}y?%>5fE=e?y z0Maq09aiTcFtJ_ne0hu(LkbEaFe0OHDW5OFI}$5639BPS@w{0sLx@9?o462&=kH-e z%m+v1dHXTM4hEw2pwe#%pu=JR7hd#wyy?0(J=ZbD?ORDPM?GYiMnd=Q+m5 z#;|PJk_+&v8tYasN4qElWJcZ?h@rr+DwkD==Z6)UD257$*nyFI&!7-Flij_7(y>}? zXl2DhtrV=bfkdLL0%;9xt=oro+c;&xOfz5NvT}bXjWgj_QKgcqv`U5pDGGOJd&V0M zVFLUsp$AJ4Q65)X6<*RD206sBLX6l z5RHgv1qJt7T>E?wQ658}@LJT|@a> z005}ha>?Za^V>p5(7OVqt)d$cT!Jdp7Kl}S5h=0+aEq(3DoYnY3Oujs>^p{)-}~%V z!x~>v#>e8d(x{lc0*b^UrMwH_0YQ!s48_3tuh^?JwE%eaORWSXt37c90pv@VMFo)j z7RQjn403fAx#d?P>zJT&&(rA06V>PhQ>2U`+sWjG5%>|7z#HzfF4f|p&6 zXZAgVZ`}WtuoqG;RRF?0(^?@j&io%69l^?#%NBa7zwBdCDu9t@%|CcIbu0lDclGfU zU0YgE>3n}j<*iVZ1uSC%fwn6_sNtBBUn8{OhzEgZe=!QbiyxanOV(OD!jje?rC|(t ze_lgt4~c8-BLEtes)7}v04S@YG5~@i2nD_p*gU#12!azYWggluor562DMK(Sn4vJA zf8SbhqY`&wo-7ApmZTm@Xi{QV0Aj>IaQKI@f=ISxD*)bNkl#?K)oQr?ZFk_%fde@5 z(qWMDLBJ51anIFg18ofI^#;brN3nWI0lYfLo^7ku3&+~TE2j48XO16GLKR(5uMet9 zfPk_rF(^t@rBCRooE1>{O1~TybfzUGt7!m|SA4B?Py)sn zXB8-dCigkPDGcSRhPbey)=;QiJ94Ex27nkhljb^t$1Pm^DAdXh31LEc1_Un73vmdE z24Iikd*KKPvPa2r`7L&7LUy09MkEj@2}RBe;CnBuJKEM`-=NWGV%f5BtX{o*p{M%GJ{F|{0O0&;r2(a3 zwQ^M8iDQ_3;W@NU96@X56k5F=P-~#MW<5qXZ9}OHimLQX!5B7_kpL^=<4RZn$%?Nv zD(&jU8Re9KwQlvc*1~8FWekkgVc*6$%Yd|-xS9w=8d_7z4G2s^kH`y7L6Ezo{PnQ+ z?_C-ZOlKE?z*%w%Bs4^1MmjOHl!$!ThOL2NxWVl?`U4csRs5}juK;oCL12|&I1U4U ziqbOSL2(J#l&tki0Exojt)UbyzT^@-^zegt^6|%lQim6QR@8Z3!^r3u)~{WI9b49% z(EuQ@h!w!j_2cTr<2};Kt@!PIPoTf=QP8Oqp!OVWr-NRrh3R%1twDj>@-^6W(IseZ z*n)y=NC%EZI&H`$Pp|;xTUM(m{Jc__EJJ0S1yEHQ+BgMZjfSxVGCE8wO9W#yjBjK@ z#z4dQ_)0rK5!S*g?F3#X9g&#w3NBSRgs@{SLO{|zhaf;_!)SlN1(3ea+lq)t5@i#t zNftj@tkfnN1@Ui1vUFhJ4Y|J=k8tOZR18a~&=FUNX}t0M$N&&-k%)pQ0JVAzH{EWw;9tXP39o7OG#6n{m=qE!HZi{j)&n0o3_^dI^fIx~~# z^g3ww`)K$3=nn=cstR^6z|`RvarF3cY(D?>*s%9p6wr<)Tvf4QNU#KKnBa^Svwv@l zfi?<8XV7Jd%ou2+!)Ml5*vi0W1WhiiL}a0~@pu418v{*n?t*enE?~)H4HVoBW(Shu zm^Hk65Ct!+_CZP5MD|idpT(g9td8IJ??%BFLTFj?kcLb7wTfZCbVOG}5haWN&}K2R z!Phm9C8*tB7DzHCAtMk72P+TWyoqZG#VpVyLvU& ztzNm%)B6<}i&_C}U)xaozVj>{fB2i2J9-e^eh>Y!Lcc80ElTwJ1*)lqIJOS1zH5$ob0v*u>-zpEl0y*T%q@3dR^zCWFb`YbW>_tgc|K z+s+ybOU40!q(R2v0LEB=jI$D80Ii{{BM>R80a!pQH}$m!KtU0}pPL($Rw#qCwBrZl z<8VQq$@*vkZ%M)D53l(z7Sc3fBK)HCpltXDu0{&o0g^l*jQg;yN(9`875uhrb;Qcw zfuxYQ67pQa@TlSS*S{H0Jn;l3Cr&y%$GF&kt=7Qk=s4D|S%uZB&TIxCk43Ej0N~{F z`!I3j5ZbeID2o#PvP7>e(XT4>s}lXPKn1{<97YxB4>~yf^kb+9Sbx?zC@i6@3fQ6o zclwFNGvG{2?JLBv1&|qJCi5;p31c&4CPQXC%~xlrh@2Zhk^0LxfYt=SfCw;%D81GM zMH-560jr#;Z#gG{f}blJbpnXid$iBp=Lu-%e`S>!dU$X*RH?2P7{eR$9pa2d%{B1h)^gbNezaO)m4w}oCW7qlT8*a{T^N^4|U4qa9lbh>!y*{4zE8CGoC zj-seg40@=_!Z-e8Ve(`k9ZN)Mzgn0qN0w#Cvka!HU~`KqGstZQn-L6Ikg*^#K@l)* z+4#F|45RRlG(=<^g&5k!ur&flfZY-DOJDSx-L|q8;e3W45R-^h5PaX$J5YGp|4u6e zBPauBhD%RdSOrYzfm9Sjws=Sy;**OUfOwWl*fuH%yY%omF^R7+Vjyyl2<}P>7`ks> za>=E5=;4QP;@DB>EJMBCM6=PvhPA7(ZS#g#UJw>&ESLhAeD+y7_^pTV#Gn2trjDP$ z)Z|H=nw!OJyNz#s=1=fX-}7F&>drfG+k4*g7b=38*=fvnI_UPh7*rL?icr`Rl_eA) zl-8mo1j2|h8EjRc>dfKDv(G^1P1MI%peQW{{XVL~DFGKEa;voRf@n;J%ot>Ojw~x- zYdNYaLtf<|vdC-(Yb-Krk!6`6B{|_HOUN=t(k(#l_LH^H-a1gEgEc@_kW?HR3E58D z5tgbnOV}#{@-g~geg`IB4EwPx2I{UXjEW&_AmR4M5L^i7Nh~3r2(QV0TVFvr*^a@- z_$zte?_7#;3@t|Lbb8M6C|qcTLdXzGpG41N9NNc@VrKFrrlzJa*J`2H@1iJrD2f6{p4*3y|J&#AzklTK z(A$6TgShaGm!0;Z^ve>{y$-tlp3k&%Fj!>?C4kaeRB%E@%JK7+F(``?)SAVy=bypG zv(JSkjk2gPCP`YqvaaAG99Lq4P<+1y)wJ;{u1T;27HqBf zQxixVI2@2*M21|*J;#lH!FpBJMGh)!kyTafNg(PHWV!!s36uq2!vAvH zWdJzc7XY*&7}8MM0EVFbimOR`aL}e2^PMHxu?J%zoA4l zAT5&?*KHAcP)Mu+fGzwOHINT30{s*$4_@r82xUropxmDXIh3O092qCI%moNoNF-9h z;{=)3xZ=ty@P*HP5zEKMv2*KI?B2O$Aw*)q#w)G>CZ5|*kNl5MVfxf5Oi!OeQ55JF zeV+6WW`EH@UhA>1zmQbqyua37BJ0?SJdP@f_tz)up%vxqQQ#0wa~8(v^d4xy-08? z?(R?u#ogWAibHWN6o*2v$v<;77qiw}W#!^T_St9e_kH#wv$a?g;?~#=Z3E+Mi63F!w8A+xcs4 zn;Uh;t8DMb5(SlCISh&<1^!HJEn?96rj0((WWrbugzOPFRT-4MHt>E6FSA-Z-c$Rf zy0b7?Aw>|kbmr^U+GdsMn!Fek8*X-hP~5ue>&07rlml%ZQ&Wf$t~)a zEnJrmh>%ca%97go{JQy!lZm==zLrHFVKiN70(0TjM7b^#uwO-)0_W0qtWt2-z z^06B<-8&c)7)~1UPLz$pXEvOYEGBhjn}c=w-848ji0>Clnw+@!NJ*KHtUhd+J02O*fo;=yRn;!RA9kE-p4>=G)X0xz}M z6C3c{B;wc~3zM*1a7}@>bYM*|IFZXZNvp%6kVh))J;Q;=*?;neL~;nga#j)3t}~YI zd!5%px(vBT)f-SR7M0kSQf8R;63c^c*GcBX**BRBu$i(?zJPrdDlm%FtC`1 zpj#M-&LEmt1&oK_cLaXqx!!4f7s)AYuM}vO7^<^pZ2jKT%vuR6&=5wxq0y939lVKf zYugK=*92fz*~}>&D4-{c$=DCiot7e4=|%~~sDRkT7hT!djr>|JMMY;i&xa}Jt0;i$ zRI~$>;dU~BALEPN;)Kawu0Dk5CffdFRj+sj?87bLxAKTCSy!AsH8k78NM~1Pw2$tZ zwPJu;uy+Zph2DJDzC97wjV|Mdm&l2It9(I@P(j}wzy8h*Bz`Lv4cj3N5fXbl|2Qrs znnLC5=3wOtz(IK6;w?ke0VPs*zc=fjCx5lbOCOxyx{pp1>#0oTE zN7fJ4Ven9?GWGE%b@!c;m{^Wb`|YiV&400)yHsrWU29O2bMR)oR4<_i#Nmgg*=|~j zyy|@X>T=us$N)_=Q9({+F{>=&>mD#(Oljc?eX0!LyuW>*_r}8}5{u!a5zN&|9xBDt zW`{sEA&@3CI%+)iX6mOA^WAE)pWb9cHIQ6!zNnMjm}zh+ti?#6#*(tyl^j--|@>rAwfca)dkN+9yhAB%au$CuB5 zqi?k18dQl5UAezyv_#f@P|DU+ZK85I%M>}&+zo9Dx*;JVBV*VvnSzTIjjE(xfuOe{ zk1A-u#9AJ22;i-?=HDrN!T|M(0fsnyQ?`m)LU1tjzn|!VHofBi5{b7Q)cMDLki`X8 z%@OM{oJpz5PEMkKJtiNPVj}K!WgKJuu++{Riy<@nX00sr8yi%MZA>3K5evOGZQ5ei ziJ!)m-Mu5Ty*;z6W0QD=U%nqGfX)r?0k}p4I^DTJ@TuCwbF??TDQjRc6U$%nm zBD}3Rk>w4QgM)j`m16~Lntu`2=2UEu0PIAz7~NLd+cD_pd4Hq84>X?!W0*#*VWoeD zE>=UgHdH)7&^r!ktbJ4`zPh83o1|uNj;(puv>n;jchp7}DvUoGIu!#R3Q2Fxn6ZL{mH zeF;TYqV^!W#t0)ghM=GA2UZ+v>NjN#cEGANOpT!^X>pfFl?oz;l)p9PJ*JG4`d&SR z&8G=Y!*BY)KJ|{w?Pb9w9LT@InPQ$0mdXN4_53xPmKqg9HOC?vHnO`vQW6gv>MwV# z?@IGdq?0HYRe@TGn@X>X*(&C#{UVxaEGD0)eP*onB_e~F$a@L!7^h5C;hX>+ zz8<9GwyZ*a&_LBUH{a-`#5`NgdgYw3bsEuoEK?SwZ_-HU~UA5 z_K`5{;MHnBUF*+Ez$AHV zPdsh(Pl8^_v6(lK0#cN-If^lMPW5rnZG}O!DxmhQ1NO$mVZ+zEBJD0Gma2 zhwmrjf9D&}v$d4EyZC&DqyQJt=r`W#ET;X)w!wX)t( zg`vPZbT1q}_MiV}bH<{FQag@fG|f)i3{RBZzlEI}QM=+j^oLSnQt5BKJE7WwZ)_To z1$D6WX>Y;XO;MZNfa#=3%x?*E(|%uWr$}SF872iT%CI0&9rdWr|3Y2}hfCeY`!zRH z&#mR7%1M3bj zH2>*=SBO>^67a@G+%(E?84Q}^eSHX)k7iZRr!9E zjsV}_XQkZHaag}w*j->V6~{i(g_t9K8*4xV7K$z8 zcq`98ILLFb5VtSDPm@+k9E9^hsph{iMuW(%M!a7{E-rZQy4`C&n&K*HsYpyxo?>hW z#ffOQ?sUIBinf)f>t1{s8SH+IoG61g{cVm4Rr2rH2pxoor5r~8>_j9U1ST{+xv*os z3}qr=TrJx3{scs$jBO@pQ*iMd^mlO$oAOcsoOV$gE5E;_BxmUJk zB523FWJ73}?KZ&EZQrs5nT^K%nm$EiVj3%)Oz5Deum4)EziGfwP8wZ4200uzd%Iuw z-^08fm?f74h$PxUolnnCG_>AN9?MPE3)b4WZEWPIVR4yD`crL>NI*W9cSYQ1D|p#a z>;k^+76U8<4WrcJmhs&wy9D=UDYWmC^?la2uW2h+n#lxppA^cp8qgGD=&+N(kxojl zG^h)-h^T1-hn;A1Z1Yf_b!!SVgDP+^zZR;M2?rku=FLOowaAO-oTSXd&d&dg?73M` z=htz+B(gA-S)BQTf8VUiScSj6imnW_Y$^w54CPakg0c_NG^s`Bw zeca_0ZUl9Za3JW7&*&x&R1 zpKjBzcv_;VN}ONq!03tbJPj=Zo5PiF8UQVF3>(pXMA?!D#h)k=ewzPgj|6sD&{IhM zy)u*X5|k2#jRdif>*{4{{JlE~ctzcR>aJZKJs*V1n;}I+UZc0Ekvaf2O9_ zP1~=Al5vZLw$u+r-PAd89dDifm%Q21_&W^r{2=K$H{8uGpqE7Hb9F~~5F`17D~2q` z=J9gEYquMmJ7Y}|?doTvwbi^r%GN=oFwld`M^m;h+P$h>w><%PgRIfpvttBiqP8yK zXn`a7Rp63v`0L!7AmHowbuFm}F0f)Hf{~ePk}5D;32Mz`o@m~p<{!|=)v{-*T!_8f zBb_*nMsa5FOj-U>1b+hgFMv=`PG{OVBt70Qn2y9AUdp@!&irCXeejH$ z9o^aTAl5Oea|AL$^Y(_!2$5(*PvJ}B>TfEO$h|p{p4CPJ0lvdmBXlr&c6Q$r1r>;k z`?p|YTyn@_fvq?O-dkas2&_8nf4SY5d^|2yrO-P$FL4C!EH+serHeTwCYNgkhMwHB zu|(5K7YT%xu;syp6Q(`mBOMV4Q{nS|+&q@xb8W(m&L*{@%zmW|Ww zN{Mn%oZ?&=PBb)w#GZ?377^318pWYH9ap^nrsnfS1tTVYCP4H%@U*fqH{A$Xy&>Tl zp%y7_+N0KXOw<}?V{u`KwZKLLKn1BB+R!IQOnSZYFB3HpOb2y3v7lDP1<7H`*l!)= zu8fHjdzbof?`nk{dLDDx);^N~|<3c8>`<(un zcg`J$_hF1gU@x~=%u?EGa(Q6v(Ghk0MSUtuk7IDy08slBS@QcyR5RYW;P}?t1Fz9}iSl^s>(K zr$GY&&*NVCSzd-?)e^cO0~;||3t?!^1liXTB2e#Hr`L*OZD5hT6$@b{=Kt73myUg= zf^$!DZ!K{YH`IcRP8OB8pX+2=A1ZWW2jG++eiFKQ_$B1H;X%;y1@iIfr{!NJxe*=( z=fYU3-An0IuLIpkQh+JeLi=B$hapMioksin2is_kLQIB1fyVXx7mv$=haV-*Tip-4 zB7}Gx(d2o~OsMgpN1lV28{_D~%Xi-239ypnTlu0AoH^_6(a9y&{XCB1-Ur0SewVf| zqf$6>O?>PS4H;HxISjZNdp(ww*|j(#;3I&_Ix(kGpcW|8X{By$ubk=xsOAv~i2DZ{ZNBKB!FF(86~1o@XD z`eZ$38#`w(R}`OfH9_;=TgJ;AYWWlOXBeCQ$E5d#FX~7|@guFgwD{l+`{p~rARN0pwGY;xS$fBEyKG9Mmh{%| z?#DivCV;a!f?O`F3wf4{3p7ZSP5`=W#XV_jp9b}NCY-r~-Uy@UfHTF$gAJTb8>_YF zArTWu_K*2dG&3SDlk`GL2?<3G52zft$+7&Eqk^)bB&c&)MpuP$-+~7icV^NTcE`f? zJ0K~=!A%b5L(s)brpbnOf;kV~PsHLRlOPFEOx(H(zkt$bi!As`I4yXUluf6FInE%9ELzi5G&+NpDDP+#`<25YwX+A zm(oiMc^pF|o<=(_(l>EXWJmzOYht}L+xz$(a=^UPuezG&*w;|ftjOG8_!_uzGjD^V zK9W0wl{TNZ3Q3`LX3)}Z)(ZwtgTc&&xrRKdm6&D*rq%~0)TUYnh&um9R1O1I`%Zj{ zYkp?qSH;C=lqcQv@TXeTGl++B*E?`QVfDfG&LK2SNF}1rJ_yriUQaA{-ky;2k z@w{q2U6WZ($(odi-y7aM_@U6o$?nwyJzd?Q7*lJ<1{$R>z$#{8Vud zS~X$6kWYATy2UoiB)5!;c*v^J0M>bRGN#@)uK>1RRFn3m7=$JKF*omuUJRZ-|EcUS zy7Xim%vS{f9Oi@fe->baer@Y}13U`+&2yGQ6B5XB0}PTf9KsQWFO1=Fy%BFMH4C%u z?BzG##e`obYUqm4W?TN?cPh^t|5k}BH6(%sR%^PASJrdeq<@hi_VMdV0(WwsmY6br?w;|N`%4Iz2%&i z`idO=CBUxU)ReeOT6%)1(m)!+^ovyklgy7g9V0%CALUx3JRLR!NAs1v1NBGeQIp<4 zea{2wv9Yn-B^!hOTWIlt>=Ij$2;C@8Y>IcQp#3P6{>3ZI!rI)fW}T{F?vbrp1z8H9 z7NoF87bb_oKc){iB7AQZ-B02aEhbTVr~x*Alr4gL+b-SLQk2^8*XJuQ=qOqds{Jg? zVbDe&X|ie(kxkx(DWE5XyJDv0NW@%F`--}>%2g1ik7KD<=tXb{HvL@_b-bIi_niAL zQoL1gQnZK?^rFzMD8j8M#;r91Wfc3AE7iHK*a3YX8;TxdJBYjd!F z4UkvK<4{`oQC^0&j+Prs_YT9N*QAI*!Fu+{HIhHW;th=jztv)!}Wpr)2sG%2+(``=$BU$j26yKK%aaHa1*wnxfWqSyn;{}4H-p7IftKS`SH zy+?J0L{H@ioZ!`{BEzd6z)9w;EjE}wbVXnq9mN?jY;Jl=0L45M*rS}L6bsh6kk_ba zzl;jq1LY?jnw3Ty%_`KUuta%~(23MPpNR`hJ*pQ^JuH!_U$#Z5y@?wBif|su^m_&} zj=j#O3113aZR|F67Lg&nOLiuD{2De#0j9FhSB#~}(*(e|*rWhvD4=~q<3#6R@T!LJ zQEWdWQ8T_cit zZvmBkUi^w?xR&A5;=QPP);}`_K^KW4SR5d4(tS=8nngXQ? z)JqGp1r6%Lc8X9X4MUpJ&PySDR>#S`Zt_*tK!?D&Pc*&*mSC|GG1ujVtsXDf6&OK3C38S}!2 z7oz@@*&c3r_DiF@C>wMDAf@w8vux zR{rMJ1Q+Fq^reTH#)CkBKxz8G{LZPArh~z+9Tx(~Uj1jn_q3ux($k7scW5t6WJzPo z*~aSkVDq&87e8kU2XBBunct^~=RDt74B;`8n+BuYcPwTN>m1o7v_4}v6hiwqWf=Hl zrpY5RfYc3T4hGMc_}(je$&Lg6v)s0gI-b(sK4=_lR*L%_hJD)LcS7-g{s_$4lELv< zMyF2y!O77R)qpz545r3t?}zAI4;ve_YXDCy+lx<>hyR{`Rc=i^nZt=Zs~QMwXPWWi zFSqesmN^qk)fp#w_t9WEIiJt|$u{cif66OUT3Bi>(N#^Y#1iA3aj~7Z*)1Mg#_c>> z*bs%Wch!?ejObfZ&ZVZM>K^F)tBK@O1)eoM{IH&jqXbPGuOo0iN+Ms4=mABchP+W_ z$-ORoCLSRuH#)mU)RG!ng5K@}YTytD9Q=x^XYZSaeEGw2DM$rEoGULrFMfp`G$80g zrDzWn$@1zb z)gQ$>!`Iv{RkDUU#Hod1Ubp-X?r`NE6)Hd>MmEdUYQV0%{p_OsY&>_&=5RG;Y9)`HL^4wZWv(`%*MzWj9=GLm^kBz>E9wT{3)GqY;Hj!; zQML4*21%KKL~`w>P7UD{Ly=+NSeYnU`gv#fa=w)?s+cpMj!FhKO_$Fgtz^{m)15D~ zdsLf3;Q)SHH_D+lu;4x$XM&NtX2Es!*Gp@FsuJLC^P7xJwBvgJ-!$L42f?p|qA6Qf zZ)@%-6!r?af`ggMT+c1KcJtYxnO_fS91gEFxa9#)>Dw9lw@lG=65mb?{ASQF|GAJP zW~TC+puE$Nk1t%xiHvLDNYOk_vQqO4VeB_9mRk=Pr8t2`er}EEru@8F{>3QMk4iOo z$3&Ytn*ji;B0s@$*72l}lKcPsE;6hKy@5je4u#*y?8fbq4EwI^%NwKXWi`zP8!8k* znrDVq*4PB29*J|IO*pVK^oKChcZu13KUAV=stD!q+dcSi`iV8C@A#M&DSw#Ih#>!X z2OWcJJE)%PG1b_cX_8dLWbK>Y1B9UQaWEPEk z2DZ3`VD&QXuy)Gq7{TA33CKfz}waKH;NecotzEc2Y7Ic&sH`; zjkRHv6%Y3mQB4t(Ro;RO6{BjWV8rWgC<6F0$StGNwR6G(fsC zlxY@JHKi@e07oAohFis!h*4SijU*36O8=mHtK5N^5nOgGSAp)3e5Z{=gz-xM%aRF| zf`VeT?Y^DOQZrfFFig;u^wDk-*g4KJU!fEF*UN2l6`#8+8-G&cmh#yq&wgc8BAL8v z#$A2@1y<+#TpK@UvJ`#H(+~=XR@!XQ-8^{%7Aul@9k4bWMB78Rr9~`ZUa)Pc_xw0m%dGDBtFL zE?P+!^7g?Og9di*t>B`a7{2zq5eEf|TVZ8T+{*ryg5l)!}#i87OMu6^Vr% z><(GEnrNNGdx_x|Lpf0uCqmH#u(RlgaBQ_U3SERm5bS70OAAY(ggP}~_Ah8LJVQr>WovCz_TCGkXGnH#9qq@D ze95|gh1C`V4dSA+mmkGthn&UEvlMx}tw{f>EJ`6c}Zgf;`o-1}ly$mHX{xkcoZpFDc9Ie3Ec(K`w(k-W! zGDXjWS|ZprcI{PtzcOyBuC)TX##@O`Q4N4bu}ll(sS_}{{6{^Wrf zO=@LhOOB}juQzQ!2A)A^41vyeeMRX20D##=Uem?I(8W~5*vS<20^o)5@^Qj=IAOe> xd3i Date: Mon, 19 Jun 2023 11:59:22 -0500 Subject: [PATCH 11/59] Update version to 0.13.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 50f68438..2e83eef7 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.13.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From c24e78b53d5ba462cd35c4a542ddae146c026fd8 Mon Sep 17 00:00:00 2001 From: "Eduardo R. T" Date: Thu, 22 Jun 2023 16:44:17 -0600 Subject: [PATCH 12/59] Added "function" capabilities (#298) * Initial commit * Updated Readme * Added the ability to publish to the local repository. * Stylized the README a bit more. * Fixed typo * Fixed visibility of variables in example * Improved the usage of functions with an executor. * Reverted changes. * Added information about functions to the README.md * Change reversed. * Added some helper method and cleaned the example a little bit. * :bug: custom serialization of function_call parameter should be an object or a string * Fixed example and added static instantiation method * Added null check * Set project version to SNAPSHOT and fixed vulnerable dependency * Allowing objectmapper to be overriden in order to supply a custom object mapper which is required when using the library in Kotlin * Updating the constructor to use the setter to be consistant * Added usage of streams with functions * Removed .DS_Store files and updated .gitignore * Small fixes and improvements and added tests * Simplified example a little and updated README --------- Co-authored-by: Lorenzo Caenazzo Co-authored-by: David Billings --- .gitignore | 3 + README.md | 82 ++++++++- api/build.gradle | 3 +- .../chat/ChatCompletionRequest.java | 24 +++ .../openai/completion/chat/ChatFunction.java | 56 ++++++ .../completion/chat/ChatFunctionCall.java | 23 +++ .../openai/completion/chat/ChatMessage.java | 20 ++- .../completion/chat/ChatMessageRole.java | 3 +- example/build.gradle | 20 +++ .../example/OpenAiApiFunctionsExample.java | 109 ++++++++++++ .../OpenAiApiFunctionsWithStreamExample.java | 86 ++++++++++ gradle.properties | 2 +- gradle/libs.versions.toml | 3 +- service/build.gradle | 1 + .../service/ChatCompletionRequestMixIn.java | 13 ++ ...etionRequestSerializerAndDeserializer.java | 41 +++++ ...allArgumentsSerializerAndDeserializer.java | 64 +++++++ .../openai/service/ChatFunctionCallMixIn.java | 13 ++ .../openai/service/ChatFunctionMixIn.java | 10 ++ .../ChatFunctionParametersSerializer.java | 37 ++++ .../service/ChatMessageAccumulator.java | 84 +++++++++ .../openai/service/FunctionExecutor.java | 105 ++++++++++++ .../openai/service/OpenAiService.java | 43 ++++- .../openai/service/ChatCompletionTest.java | 159 ++++++++++++++++++ 24 files changed, 992 insertions(+), 12 deletions(-) create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java create mode 100644 example/src/main/java/example/OpenAiApiFunctionsExample.java create mode 100644 example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java create mode 100644 service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java create mode 100644 service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java 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/README.md b/README.md index 79c2911c..0fab095b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,70 @@ 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.shutdownExecutor()`. This is not necessary for non-streaming calls. @@ -103,7 +167,18 @@ This is not necessary for non-streaming calls. 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 +186,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 8f5fefc5..b861a367 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" dependencies { - implementation libs.jacksoAnnotations + api libs.jacksonAnnotations + api libs.jacksonDatabind compileOnly libs.lombok annotationProcessor libs.lombok 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..1f055fbf 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..7d72829a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -0,0 +1,56 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NonNull; + +import java.util.function.Function; + +@Data +public class ChatFunction { + + @NonNull + private String name; + private String description; + @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(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/ChatMessage.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java index a8fc64f7..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,5 +1,7 @@ package com.theokanning.openai.completion.chat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; /** @@ -19,13 +21,27 @@ 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; - @NonNull + @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/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/OpenAiApiFunctionsExample.java b/example/src/main/java/example/OpenAiApiFunctionsExample.java new file mode 100644 index 00000000..c9141405 --- /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(List.of(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..36dfc796 --- /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(List.of(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/gradle.properties b/gradle.properties index 2e83eef7..f2074f33 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.13.0 +VERSION_NAME=0.13.0-SNAPSHOT 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 index f02d7c22..b1f0c605 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,8 @@ retrofit = "2.9.0" [libraries] jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } -jacksoAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations", 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" } diff --git a/service/build.gradle b/service/build.gradle index 23e6de39..6c242724 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -6,6 +6,7 @@ dependencies { api libs.retrofit implementation libs.retrofitRxJava2 implementation libs.retrofitJackson + implementation libs.jacksonJsonSchema testImplementation(platform(libs.junitBom)) testImplementation 'org.junit.jupiter:junit-jupiter' 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 0c6639aa..e1cb856f 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.TextNode; import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; @@ -15,9 +16,7 @@ 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; @@ -33,14 +32,13 @@ import com.theokanning.openai.model.Model; import com.theokanning.openai.moderation.ModerationRequest; import com.theokanning.openai.moderation.ModerationResult; - 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; @@ -388,6 +386,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; } @@ -407,4 +408,36 @@ 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); + }); + } + } 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..a615e480 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -1,4 +1,9 @@ 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; @@ -10,6 +15,33 @@ 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 +86,131 @@ void streamChatCompletion() { assertNotNull(chunks.get(0).getChoices().get(0)); } + @Test + void createChatCompletionWithFunctions() { + final List functions = List.of(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 streamChatCompletionWithFunctions() { + final List functions = List.of(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()); + } + } From ccc1024d03a80d835eff0b96d572a75b91293f50 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Thu, 22 Jun 2023 17:56:59 -0500 Subject: [PATCH 13/59] Make tests compatible with java 8 again --- .../com/theokanning/openai/service/ChatCompletionTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 a615e480..3d26bf03 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Collections; import static org.junit.jupiter.api.Assertions.*; @@ -88,7 +89,7 @@ void streamChatCompletion() { @Test void createChatCompletionWithFunctions() { - final List functions = List.of(ChatFunction.builder() + 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")) @@ -150,7 +151,7 @@ void createChatCompletionWithFunctions() { @Test void streamChatCompletionWithFunctions() { - final List functions = List.of(ChatFunction.builder() + 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")) From 210c25b4b0e71ee4ea35f6d89ed0d26c20d0af63 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Thu, 22 Jun 2023 18:05:06 -0500 Subject: [PATCH 14/59] Update examples to use Collections.singletonList List.of isn't compatible with java 8 --- example/src/main/java/example/OpenAiApiFunctionsExample.java | 2 +- .../main/java/example/OpenAiApiFunctionsWithStreamExample.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/main/java/example/OpenAiApiFunctionsExample.java b/example/src/main/java/example/OpenAiApiFunctionsExample.java index c9141405..954b9104 100644 --- a/example/src/main/java/example/OpenAiApiFunctionsExample.java +++ b/example/src/main/java/example/OpenAiApiFunctionsExample.java @@ -42,7 +42,7 @@ public static void main(String... args) { String token = System.getenv("OPENAI_TOKEN"); OpenAiService service = new OpenAiService(token); - FunctionExecutor functionExecutor = new FunctionExecutor(List.of(ChatFunction.builder() + 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")) diff --git a/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java b/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java index 36dfc796..e6de65b6 100644 --- a/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java +++ b/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java @@ -16,7 +16,7 @@ public static void main(String... args) { String token = System.getenv("OPENAI_TOKEN"); OpenAiService service = new OpenAiService(token); - FunctionExecutor functionExecutor = new FunctionExecutor(List.of(ChatFunction.builder() + 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")) From c047f73c9cdb4d14b9f88fd554cb1339ec61e78b Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Thu, 22 Jun 2023 18:07:28 -0500 Subject: [PATCH 15/59] Update version to 0.14.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f2074f33..fe983327 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.13.0-SNAPSHOT +VERSION_NAME=0.14.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 038e42c0e679af7652ed9a33fd3d3e87160d248b Mon Sep 17 00:00:00 2001 From: Amethyst C <37189157+AlphaSerpentis@users.noreply.github.com> Date: Fri, 7 Jul 2023 17:05:48 -0500 Subject: [PATCH 16/59] Added Audio to the API list in the README (#324) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0fab095b..1824b5e5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ 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) - [Images](https://platform.openai.com/docs/api-reference/images) From 47ee4413462bbf4eaf6b3ae324656af2ce6e4079 Mon Sep 17 00:00:00 2001 From: Christian Soltenborn Date: Wed, 26 Jul 2023 23:13:45 +0200 Subject: [PATCH 17/59] added timeout duration to service instantiation for demonstration purposes (#341) Co-authored-by: Christian Soltenborn --- example/src/main/java/example/OpenAiApiExample.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/main/java/example/OpenAiApiExample.java b/example/src/main/java/example/OpenAiApiExample.java index e6e78b44..7a245ef9 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,7 +15,7 @@ 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() From cd2be868854fa0c883998ea8a8b548bee171270d Mon Sep 17 00:00:00 2001 From: YU ZHANG Date: Thu, 27 Jul 2023 05:17:29 +0800 Subject: [PATCH 18/59] New features: token calculation logic and account information query logic. (#311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Tool for calculating token consumption quantity. API for querying account balance * feat: * feat: update jtokkit --------- Co-authored-by: 大名(张瑜) --- api/build.gradle | 1 + .../openai/billing/BillingUsage.java | 29 ++ .../openai/billing/CreditGrantsResponse.java | 35 +++ .../theokanning/openai/billing/DailyCost.java | 24 ++ .../com/theokanning/openai/billing/Datum.java | 36 +++ .../theokanning/openai/billing/Grants.java | 17 + .../theokanning/openai/billing/LineItem.java | 21 ++ .../com/theokanning/openai/billing/Plan.java | 13 + .../openai/billing/Subscription.java | 51 +++ .../openai/utils/TikTokensUtil.java | 297 ++++++++++++++++++ .../theokanning/openai/client/OpenAiApi.java | 24 ++ .../main/java/example/TikTokensExample.java | 21 ++ gradle/libs.versions.toml | 1 + .../openai/service/OpenAiService.java | 27 ++ 14 files changed, 597 insertions(+) create mode 100644 api/src/main/java/com/theokanning/openai/billing/BillingUsage.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/DailyCost.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Datum.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Grants.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/LineItem.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Plan.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Subscription.java create mode 100644 api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java create mode 100644 example/src/main/java/example/TikTokensExample.java diff --git a/api/build.gradle b/api/build.gradle index b861a367..db97b833 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -4,6 +4,7 @@ apply plugin: "com.vanniktech.maven.publish" dependencies { api libs.jacksonAnnotations api libs.jacksonDatabind + api libs.jtokkit compileOnly libs.lombok annotationProcessor libs.lombok 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/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java new file mode 100644 index 00000000..8250a018 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -0,0 +1,297 @@ +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 lombok.extern.slf4j.Slf4j; + +import java.util.*; + +/** + * Token calculation tool class + */ +@Slf4j +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)); + } + + /** + * 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"), + ; + 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/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 72f5c9e2..1563b91b 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -4,6 +4,8 @@ import com.theokanning.openai.OpenAiResponse; 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; @@ -29,6 +31,8 @@ import retrofit2.Call; import retrofit2.http.*; +import java.time.LocalDate; + public interface OpenAiApi { @GET("v1/models") @@ -128,4 +132,24 @@ public interface OpenAiApi { @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 + */ + @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. + */ + @GET("v1/dashboard/billing/usage") + Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); + } 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/libs.versions.toml b/gradle/libs.versions.toml index b1f0c605..ecccc347 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,3 +12,4 @@ 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/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index e1cb856f..63da487c 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -12,6 +12,8 @@ import com.theokanning.openai.audio.CreateTranslationRequest; 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.client.OpenAiApi; import com.theokanning.openai.completion.CompletionChunk; import com.theokanning.openai.completion.CompletionRequest; @@ -42,8 +44,10 @@ 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.List; import java.util.Objects; import java.util.concurrent.ExecutorService; @@ -440,4 +444,27 @@ public Flowable mapStreamToAccumulator(Flowable 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(); + } + } From 332855d75cb71b2b1915491dfc7153e28ae499de Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 4 Aug 2023 17:52:14 -0500 Subject: [PATCH 19/59] Update to version 0.15.0 --- .../main/java/com/theokanning/openai/utils/TikTokensUtil.java | 2 -- gradle.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java index 8250a018..c30871f7 100644 --- a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -8,14 +8,12 @@ import com.theokanning.openai.completion.chat.ChatMessage; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.extern.slf4j.Slf4j; import java.util.*; /** * Token calculation tool class */ -@Slf4j public class TikTokensUtil { /** * Model name corresponds to Encoding diff --git a/gradle.properties b/gradle.properties index fe983327..de7ec97b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.14.0 +VERSION_NAME=0.15.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 9b28ab790c84b4539481af250010bc3943ceb576 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 4 Aug 2023 18:02:38 -0500 Subject: [PATCH 20/59] Fix tests Translation result changed, and for some reason my text was no longer flagged --- .../test/java/com/theokanning/openai/service/AudioTest.java | 4 ++-- .../java/com/theokanning/openai/service/ModerationTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java index d4f7b61b..d5a54a23 100644 --- a/service/src/test/java/com/theokanning/openai/service/AudioTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -52,7 +52,7 @@ void createTranslation() { .build(); String text = service.createTranslation(createTranslationRequest, koreanAudioFilePath).getText(); - assertEquals("Hello, my name is Yuna. I am Korean voice.", text); + assertEquals("Hello, my name is Yoona. I am a Korean native speaker.", text); } @Test @@ -63,7 +63,7 @@ void createTranslationVerbose() { .build(); TranslationResult result = service.createTranslation(createTranslationRequest, koreanAudioFilePath); - assertEquals("Hello, my name is Yuna. I am Korean voice.", result.getText()); + 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); 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(); From 4a9cfc247fdecca790cab77b905db566294d25c2 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 25 Aug 2023 09:47:33 -0500 Subject: [PATCH 21/59] Add workflow to make sure code compiles on pull requests (#359) Pull requests can't run most tests because they require an OpenAI token secret, but they should at least compile. --- .github/workflows/pull_request.yml | 21 +++++++++++++++++++++ .github/workflows/test.yml | 2 -- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pull_request.yml 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: From 2a9abd0e2d2415ff338ff6f9780cb347562cdf06 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 25 Aug 2023 22:57:56 +0800 Subject: [PATCH 22/59] Fixes #123 (#352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Tool for calculating token consumption quantity. API for querying account balance * feat: * feat: update jtokkit * Fixes #349 --------- Co-authored-by: 大名(张瑜) --- .../src/main/java/com/theokanning/openai/client/OpenAiApi.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 1563b91b..d841e8ad 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -138,6 +138,7 @@ public interface OpenAiApi { * * @return */ + @Deprecated @GET("v1/dashboard/billing/subscription") Single subscription(); @@ -149,6 +150,7 @@ public interface OpenAiApi { * @param endDate * @return Consumption amount information. */ + @Deprecated @GET("v1/dashboard/billing/usage") Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); From 253aff44758e077d68135d22b8d5e5a791349ec6 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 25 Aug 2023 11:05:29 -0500 Subject: [PATCH 23/59] Add latest Fine Tuning API (#360) This api supports fine tuning chat models --- README.md | 3 +- .../openai/fine_tuning/FineTuningEvent.java | 38 ++++++++ .../openai/fine_tuning/FineTuningJob.java | 90 +++++++++++++++++++ .../fine_tuning/FineTuningJobRequest.java | 46 ++++++++++ .../openai/fine_tuning/Hyperparameters.java | 28 ++++++ .../openai/finetune/FineTuneEvent.java | 1 + .../openai/finetune/FineTuneRequest.java | 1 + .../openai/finetune/FineTuneResult.java | 1 + .../openai/finetune/HyperParameters.java | 1 + .../theokanning/openai/client/OpenAiApi.java | 23 +++++ .../openai/service/OpenAiService.java | 28 ++++++ .../openai/service/FineTuneTest.java | 1 + .../openai/service/FineTuningTest.java | 86 ++++++++++++++++++ .../resources/chat-fine-tuning-data.jsonl | 10 +++ 14 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java create mode 100644 service/src/test/java/com/theokanning/openai/service/FineTuningTest.java create mode 100644 service/src/test/resources/chat-fine-tuning-data.jsonl diff --git a/README.md b/README.md index 1824b5e5..20a5dcb8 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,13 @@ as well as an example project using the service. - [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) #### 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 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..097bf7ce --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java @@ -0,0 +1,38 @@ +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 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/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index d841e8ad..60ffdbf6 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -16,6 +16,9 @@ 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; @@ -86,21 +89,41 @@ public interface OpenAiApi { @GET("/v1/files/{file_id}") Single retrieveFile(@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); 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 63da487c..7114531b 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -24,6 +24,9 @@ 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; @@ -170,6 +173,27 @@ public File retrieveFile(String fileId) { return execute(api.retrieveFile(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)); } @@ -178,18 +202,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; } 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..288b1f41 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -0,0 +1,86 @@ +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 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 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() { + service.deleteFile(fileId); + } + + @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(); + + assertEquals("created", fineTuningJob.getStatus()); + } + + @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(2) + void cancelFineTuningJob() { + FineTuningJob fineTuningJob = service.cancelFineTuningJob(fineTuningJobId); + + assertEquals("cancelled", fineTuningJob.getStatus()); + } +} 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 From 442cf130f6fbedbdcb70ce9d96ce572faf54c9e9 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 25 Aug 2023 11:06:56 -0500 Subject: [PATCH 24/59] Update to version 0.16.0 (#361) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index de7ec97b..84d38815 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.15.0 +VERSION_NAME=0.16.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 44208305716435bab5e834a3601009f0f1f8c978 Mon Sep 17 00:00:00 2001 From: Andrei Costescu Date: Sun, 22 Oct 2023 17:36:03 +0300 Subject: [PATCH 25/59] Implemented missing API for issue #366: No Retrieve file content api (#367) --- .../java/com/theokanning/openai/client/OpenAiApi.java | 4 ++++ .../com/theokanning/openai/service/OpenAiService.java | 4 ++++ .../java/com/theokanning/openai/service/FileTest.java | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 60ffdbf6..497dacd5 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -89,6 +89,10 @@ public interface OpenAiApi { @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); 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 7114531b..0296c15c 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -173,6 +173,10 @@ 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)); } 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..74fe2084 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,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.concurrent.TimeUnit; @@ -52,6 +55,13 @@ void retrieveFile() { @Test @Order(4) + void retrieveFileContent() throws IOException { + String fileBytesToString = service.retrieveFileContent(fileId).string(); + assertEquals(Files.readString(Path.of(filePath)), fileBytesToString); + } + + @Test + @Order(5) void deleteFile() { DeleteResult result = service.deleteFile(fileId); assertTrue(result.isDeleted()); From a4f2df805d30e98cd6a74ed1a1bf43ef73df88cb Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 15:41:32 -0500 Subject: [PATCH 26/59] Fix java 8 compilation (#378) --- .../java/com/theokanning/openai/service/FileTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 74fe2084..51dd9407 100644 --- a/service/src/test/java/com/theokanning/openai/service/FileTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FileTest.java @@ -8,8 +8,9 @@ import org.junit.jupiter.api.TestMethodOrder; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.concurrent.TimeUnit; @@ -21,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 @@ -57,7 +58,8 @@ void retrieveFile() { @Order(4) void retrieveFileContent() throws IOException { String fileBytesToString = service.retrieveFileContent(fileId).string(); - assertEquals(Files.readString(Path.of(filePath)), fileBytesToString); + String contents = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); + assertEquals(contents, fileBytesToString); } @Test From 44cba2e92757e7281ba4a3c45a91b89e56876683 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:02:15 -0500 Subject: [PATCH 27/59] Add fine-tuning api models to json test (#379) --- .../openai/fine_tuning/FineTuningEvent.java | 5 +++++ .../java/com/theokanning/openai/JsonTest.java | 8 ++++++-- .../resources/fixtures/FineTuningEvent.json | 8 ++++++++ .../resources/fixtures/FineTuningJob.json | 19 +++++++++++++++++++ .../fixtures/FineTuningJobRequest.json | 9 +++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 api/src/test/resources/fixtures/FineTuningEvent.json create mode 100644 api/src/test/resources/fixtures/FineTuningJob.json create mode 100644 api/src/test/resources/fixtures/FineTuningJobRequest.json 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 index 097bf7ce..c653c048 100644 --- a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java @@ -15,6 +15,11 @@ public class FineTuningEvent { */ String object; + /** + * The ID of the fine-tuning event. + */ + String id; + /** * The creation time in epoch seconds. */ diff --git a/api/src/test/java/com/theokanning/openai/JsonTest.java b/api/src/test/java/com/theokanning/openai/JsonTest.java index 09d89f5b..68b98155 100644 --- a/api/src/test/java/com/theokanning/openai/JsonTest.java +++ b/api/src/test/java/com/theokanning/openai/JsonTest.java @@ -12,10 +12,11 @@ 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.model.Model; import com.theokanning.openai.moderation.ModerationRequest; @@ -43,6 +44,9 @@ public class JsonTest { File.class, FineTuneEvent.class, FineTuneResult.class, + FineTuningEvent.class, + FineTuningJob.class, + FineTuningJobRequest.class, ImageResult.class, TranscriptionResult.class, TranslationResult.class, 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" +} From fe4295837a1bdbe68a8e2a13b7c72878956a02b4 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:06:56 -0500 Subject: [PATCH 28/59] Fix fine-tuning test (#380) The first status changed from created to validating_files Since this is working either way, just check that it has an id instead --- .../java/com/theokanning/openai/service/FineTuningTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java index 288b1f41..8bc3cd57 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -4,7 +4,6 @@ import com.theokanning.openai.fine_tuning.FineTuningJob; import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.fine_tuning.Hyperparameters; -import com.theokanning.openai.finetune.FineTuneResult; import org.junit.jupiter.api.*; import java.util.List; @@ -49,7 +48,7 @@ void createFineTuningJob() { FineTuningJob fineTuningJob = service.createFineTuningJob(request); fineTuningJobId = fineTuningJob.getId(); - assertEquals("created", fineTuningJob.getStatus()); + assertNotNull(fineTuningJob); } @Test From 761350a89ff819828bb07852cbe13e7c077be0af Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:24:29 -0500 Subject: [PATCH 29/59] Add File status fields (#381) Fixes https://github.com/TheoKanning/openai-java/issues/355 --- .../main/java/com/theokanning/openai/file/File.java | 12 ++++++++++++ api/src/test/resources/fixtures/File.json | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) 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/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." +} From e7635c3cf07eb9a423d0bdaad5e41b65f0dbe9cd Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:27:54 -0500 Subject: [PATCH 30/59] Update version to 0.16.1 (#382) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 84d38815..85a94ec8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.16.0 +VERSION_NAME=0.16.1 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From dc51a5bbf9d4ea311b48b652f7aff0ead7c27ec1 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:50:25 -0500 Subject: [PATCH 31/59] Catch errors when deleting file in FineTuningTest (#383) Thsi is failing repeatedly and it's not a big deal if we don't clean this up. --- .../java/com/theokanning/openai/service/FineTuningTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java index 8bc3cd57..81a8e808 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -30,7 +30,11 @@ static void setup() throws Exception { @AfterAll static void teardown() { - service.deleteFile(fileId); + try { + service.deleteFile(fileId); + } catch (Exception e) { + // ignore + } } @Test From f1e587d685419765e9ab81c763610847f8fa0317 Mon Sep 17 00:00:00 2001 From: vacuityv Date: Mon, 13 Nov 2023 05:17:36 +0800 Subject: [PATCH 32/59] feat(image): new feature for dalle api. (#393) You can set model, quality and style now. --- .../openai/image/CreateImageEditRequest.java | 5 +++++ .../openai/image/CreateImageRequest.java | 21 ++++++++++++++++--- .../image/CreateImageVariationRequest.java | 5 +++++ .../com/theokanning/openai/image/Image.java | 6 ++++++ .../openai/service/OpenAiService.java | 2 ++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java index 72046953..7d37f689 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java @@ -21,6 +21,11 @@ public class CreateImageEditRequest { @NonNull String prompt; + /** + * The model to use for image generation. Only dall-e-2 is supported at this time. Defaults to dall-e-2. + */ + String model; + /** * The number of images to generate. Must be between 1 and 10. Defaults to 1. */ diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java index b8a1d05d..13672c24 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java @@ -17,18 +17,28 @@ public class CreateImageRequest { /** - * A text description of the desired image(s). The maximum length in 1000 characters. + * A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3. */ @NonNull String prompt; /** - * The number of images to generate. Must be between 1 and 10. Defaults to 1. + * The model to use for image generation. Defaults to "dall-e-2". + */ + String model; + + /** + * The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported. Defaults to 1. */ Integer n; /** - * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". + * The quality of the image that will be generated. "hd" creates images with finer details and greater consistency across the image. This param is only supported for dall-e-3. Defaults to "standard". + */ + String quality; + + /** + * The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. Defaults to 1024x1024. */ String size; @@ -38,6 +48,11 @@ public class CreateImageRequest { @JsonProperty("response_format") String responseFormat; + /** + * The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3. Defaults to vivid. + */ + String style; + /** * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. */ diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java index 2bc0c5d1..f16f613d 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java @@ -20,6 +20,11 @@ public class CreateImageVariationRequest { */ Integer n; + /** + * The model to use for image generation. Only dall-e-2 is supported at this time. Defaults to dall-e-2. + */ + String model; + /** * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". */ diff --git a/api/src/main/java/com/theokanning/openai/image/Image.java b/api/src/main/java/com/theokanning/openai/image/Image.java index e3214844..6b8391ed 100644 --- a/api/src/main/java/com/theokanning/openai/image/Image.java +++ b/api/src/main/java/com/theokanning/openai/image/Image.java @@ -21,4 +21,10 @@ public class Image { */ @JsonProperty("b64_json") String b64Json; + + /** + * The prompt that was used to generate the image, if there was any revision to the prompt. + */ + @JsonProperty("revised_prompt") + String revisedPrompt; } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 0296c15c..cedbd805 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -250,6 +250,7 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File .setType(MediaType.get("multipart/form-data")) .addFormDataPart("prompt", request.getPrompt()) .addFormDataPart("size", request.getSize()) + .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -276,6 +277,7 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.get("multipart/form-data")) .addFormDataPart("size", request.getSize()) + .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); From 0ec5a9ee303f91f6d66da9e91ab726ca27f28534 Mon Sep 17 00:00:00 2001 From: Daniel Faria Date: Sun, 12 Nov 2023 18:22:08 -0300 Subject: [PATCH 33/59] add support to audio/createSpeech API (#392) --- .../openai/audio/CreateSpeechRequest.java | 45 +++++++++++++++++++ .../theokanning/openai/client/OpenAiApi.java | 4 ++ .../openai/service/OpenAiService.java | 5 +++ .../theokanning/openai/service/AudioTest.java | 19 ++++++++ 4 files changed, 73 insertions(+) create mode 100644 api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java diff --git a/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java new file mode 100644 index 00000000..6d2e69ac --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java @@ -0,0 +1,45 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateSpeechRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * The text to generate audio for. The maximum length is 4096 characters. + */ + @NonNull + String input; + + /** + * The voice to use when generating the audio. + */ + @NonNull + String voice; + + /** + * The format to audio in. Supported formats are mp3, opus, aac, and flac. Defaults to mp3. + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The speed of the generated audio. Select a value from 0.25 to 4.0. Defaults to 1.0. + */ + Double speed; +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 497dacd5..f2665ee2 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; import com.theokanning.openai.billing.BillingUsage; @@ -149,6 +150,9 @@ public interface OpenAiApi { @POST("/v1/audio/translations") Single createTranslation(@Body RequestBody requestBody); + @POST("/v1/audio/speech") + Single createSpeech(@Body CreateSpeechRequest requestBody); + @POST("/v1/moderations") Single createModeration(@Body ModerationRequest request); diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index cedbd805..ea59417e 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -8,6 +8,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; @@ -347,6 +348,10 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public ResponseBody createSpeech(CreateSpeechRequest request) { + return execute(api.createSpeech(request)); + } + /** * Calls the Open AI api, returns the response, and parses error messages if the request fails */ diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java index d5a54a23..9cb083de 100644 --- a/service/src/test/java/com/theokanning/openai/service/AudioTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -1,13 +1,18 @@ package com.theokanning.openai.service; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.time.Duration; +import okhttp3.MediaType; +import okhttp3.ResponseBody; + import static org.junit.jupiter.api.Assertions.*; @@ -69,4 +74,18 @@ void createTranslationVerbose() { assertTrue(result.getDuration() > 0); assertEquals(1, result.getSegments().size()); } + + @Test + void createSpeech() throws IOException { + CreateSpeechRequest createSpeechRequest = CreateSpeechRequest.builder() + .model("tts-1") + .input("Hello World.") + .voice("alloy") + .build(); + + final ResponseBody speech = service.createSpeech(createSpeechRequest); + assertNotNull(speech); + assertEquals(MediaType.get("audio/mpeg"), speech.contentType()); + assertTrue(speech.bytes().length > 0); + } } From fb62307a61f35075d198cd8298be8404ba2a0173 Mon Sep 17 00:00:00 2001 From: Remy Ohajinwa Date: Sun, 12 Nov 2023 21:32:56 +0000 Subject: [PATCH 34/59] Support Assistants (#395) * #390 Create Assistant #390 Create Assistant * #390 Retrieve Assistant * #390 Modify Assistant * RemyOhajinwa#390 Delete Assistant * RemyOhajinwa#390 List Assistants * RemyOhajinwa#390 Create Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * Remove DeleteAssistantResult import --------- Co-authored-by: Remy Ohajinwa Co-authored-by: Theo Kanning --- .../openai/assistants/Assistant.java | 24 ++ .../openai/assistants/AssistantBase.java | 55 +++++ .../openai/assistants/AssistantFile.java | 30 +++ .../assistants/AssistantFileRequest.java | 17 ++ .../openai/assistants/AssistantRequest.java | 6 + .../openai/assistants/AssistantSortOrder.java | 12 + .../openai/assistants/AssistantToolsEnum.java | 15 ++ .../openai/assistants/ListAssistant.java | 16 ++ .../assistants/ListAssistantQueryRequest.java | 39 ++++ .../theokanning/openai/assistants/Tool.java | 12 + .../openai/utils/TikTokensUtil.java | 7 +- .../theokanning/openai/client/OpenAiApi.java | 43 ++++ .../openai/service/OpenAiService.java | 46 ++++ .../openai/service/AssistantTest.java | 214 ++++++++++++++++++ .../test/resources/assistant-file-data.json | 1 + .../src/test/resources/assistants-data.html | 1 + 16 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/assistants/Assistant.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/Tool.java create mode 100644 service/src/test/java/com/theokanning/openai/service/AssistantTest.java create mode 100644 service/src/test/resources/assistant-file-data.json create mode 100644 service/src/test/resources/assistants-data.html diff --git a/api/src/main/java/com/theokanning/openai/assistants/Assistant.java b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java new file mode 100644 index 00000000..111ef169 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class Assistant extends AssistantBase { + + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type which is always 'assistant' + */ + String object; + + /** + * The Unix timestamp(in seconds) for when the assistant was created + */ + @JsonProperty("created_at") + Integer createdAt; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java new file mode 100644 index 00000000..d771bd30 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java @@ -0,0 +1,55 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantBase { + + /** + * ID of the model to use + */ + @NonNull + String model; + + /** + * The name of the assistant. The maximum length is 256 + */ + String name; + + /** + * The description of the assistant. + */ + String description; + + /** + * The system instructions that the assistant uses. + */ + String instructions; + + /** + * A list of tools enabled on the assistant. + */ + List tools; + + /** + * A list of file IDs attached to this assistant. + */ + @JsonProperty("file_ids") + List fields; + + /** + * Set of 16 key-value pairs that can be attached to an object. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java new file mode 100644 index 00000000..c5d551a9 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java @@ -0,0 +1,30 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class AssistantFile { + + /** + * The identifier of the Assistant File + */ + String id; + + /** + * The object type, which is always assistant.file. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the assistant file was created. + */ + @JsonProperty("created_at") + String createdAt; + + /** + * The assistant ID that the file is attached to + */ + @JsonProperty("assistant_id") + String assistantId; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java new file mode 100644 index 00000000..98ee009f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantFileRequest { + + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java new file mode 100644 index 00000000..dc0a66df --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java @@ -0,0 +1,6 @@ +package com.theokanning.openai.assistants; + + +public class AssistantRequest extends AssistantBase { + +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java new file mode 100644 index 00000000..9f784a66 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java @@ -0,0 +1,12 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AssistantSortOrder { + + @JsonProperty("asc") + ASC, + + @JsonProperty("desc") + DESC +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java new file mode 100644 index 00000000..f6b5021d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java @@ -0,0 +1,15 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AssistantToolsEnum { + + @JsonProperty("code_interpreter") + CODE_INTERPRETER, + + @JsonProperty("function") + FUNCTION, + + @JsonProperty("retrieval") + RETRIEVAL +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java new file mode 100644 index 00000000..8478a547 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java @@ -0,0 +1,16 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.OpenAiResponse; + +public class ListAssistant extends OpenAiResponse { + + @JsonProperty("first_id") + String firstId; + + @JsonProperty("last_id") + String lastId; + + @JsonProperty("has_more") + boolean hasMore; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java new file mode 100644 index 00000000..3e5f3c68 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java @@ -0,0 +1,39 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ListAssistantQueryRequest { + /** + * A limit on the number of objects to be returned. + * Limit can range between 1 and 100, and the default is 20 + */ + + Integer limit; + + /** + * Sort order by the 'created_at' timestamp of the objects. + * 'asc' for ascending order and 'desc' for descending order. + */ + AssistantSortOrder order; + + /** + * A cursor for use in pagination. after is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, + * your subsequent call can include after=obj_foo in order to fetch the next page of the list + */ + String after; + + /** + * A cursor for use in pagination. before is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, + * your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + */ + String before; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java new file mode 100644 index 00000000..00027d72 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -0,0 +1,12 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Tool { + AssistantToolsEnum type; +} diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java index c30871f7..0a50907e 100644 --- a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -32,6 +32,7 @@ public class TikTokensUtil { modelMap.put(ModelEnum.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4)); modelMap.put(ModelEnum.GPT_4_32K_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); modelMap.put(ModelEnum.GPT_4_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); + modelMap.put(ModelEnum.GPT_4_1106_preview.getName(), registry.getEncodingForModel(ModelType.GPT_4)); } /** @@ -261,7 +262,11 @@ public enum ModelEnum { * Temporary model, not recommended for use. */ GPT_4_32K_0314("gpt-4-32k-0314"), - ; + + /** + * Temporary model, not recommended for use. + */ + GPT_4_1106_preview("gpt-4-1106-preview"); private String name; } diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index f2665ee2..0bfc03aa 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,12 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; @@ -36,6 +42,7 @@ import retrofit2.http.*; import java.time.LocalDate; +import java.util.Map; public interface OpenAiApi { @@ -185,4 +192,40 @@ public interface OpenAiApi { @GET("v1/dashboard/billing/usage") Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants") + Single createAssistant(@Body AssistantBase request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}") + Single retrieveAssistant(@Path("assistant_id") String assistantId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants/{assistant_id}") + Single modifyAssistant(@Path("assistant_id") String assistantId, @Body AssistantBase request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/assistants/{assistant_id}") + Single deleteAssistant(@Path("assistant_id") String assistantId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants") + Single> listAssistants(@QueryMap Map filterRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants/{assistant_id}/files") + Single createAssistantFile(@Path("assistant_id") String assistantId, @Body AssistantFileRequest fileRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}/files/{file_id}") + Single retrieveAssistantFile(@Path("assistant_id") String assistantId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/assistants/{assistant_id}/files/{file_id}") + Single deleteAssistantFile(@Path("assistant_id") String assistantId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}/files") + Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index ea59417e..0a7d9459 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -1,6 +1,7 @@ package com.theokanning.openai.service; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; @@ -8,6 +9,12 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; @@ -53,6 +60,7 @@ import java.time.Duration; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -348,6 +356,44 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public Assistant createAssistant(AssistantBase request) { + return execute(api.createAssistant(request)); + } + + public Assistant retrieveAssistant(String assistantId) { + return execute(api.retrieveAssistant(assistantId)); + } + + public Assistant modifyAssistant(String assistantId, AssistantBase request) { + return execute(api.modifyAssistant(assistantId, request)); + } + + public DeleteResult deleteAssistant(String assistantId) { + return execute(api.deleteAssistant(assistantId)); + } + + public ListAssistant listAssistants(ListAssistantQueryRequest filterRequest) { + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + return execute(api.listAssistants(queryParameters)); + } + + public AssistantFile createAssistantFile(String assistantId, AssistantFileRequest fileRequest) { + return execute(api.createAssistantFile(assistantId, fileRequest)); + } + + public AssistantFile retrieveAssistantFile(String assistantId, String fileId) { + return execute(api.retrieveAssistantFile(assistantId, fileId)); + } + + public DeleteResult deleteAssistantFile(String assistantId, String fileId) { + return execute(api.deleteAssistantFile(assistantId, fileId)); + } + + public ListAssistant listAssistantFiles(String assistantId, ListAssistantQueryRequest filterRequest) { + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + return execute(api.listAssistantFiles(assistantId, queryParameters)); + } + public ResponseBody createSpeech(CreateSpeechRequest request) { return execute(api.createSpeech(request)); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java new file mode 100644 index 00000000..4f21b739 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -0,0 +1,214 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.assistants.AssistantSortOrder; +import com.theokanning.openai.assistants.AssistantToolsEnum; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.file.File; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class AssistantTest { + public static final String MATH_TUTOR = "Math Tutor"; + public static final String ASSISTANT_INSTRUCTION = "You are a personal Math Tutor."; + + static String token = System.getenv("OPENAI_TOKEN");; + + static OpenAiService service = new OpenAiService(token); + + + + @Test + void retrieveAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + Assistant retrieveAssistantResponse = service.retrieveAssistant(createAssistantResponse.getId()); + validateAssistantResponse(retrieveAssistantResponse); + } + + @Test + void modifyAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + String modifiedName = MATH_TUTOR + " Modified"; + createAssistantResponse.setName(modifiedName);//modify a field + + Assistant modifiedAssistantResponse = service.modifyAssistant(createAssistantResponse.getId(), createAssistantResponse); + assertNotNull(modifiedAssistantResponse); + assertEquals(modifiedName, modifiedAssistantResponse.getName()); + } + + @Test + void deleteAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + DeleteResult deletedAssistant = service.deleteAssistant(createAssistantResponse.getId()); + + assertNotNull(deletedAssistant); + assertEquals(createAssistantResponse.getId(), deletedAssistant.getId()); + assertTrue(deletedAssistant.isDeleted()); + } + + @Test + void listAssistants() { + ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); + + assertNotNull(assistants); + // this should be more than 2 depending on how many times createAndValidateAssistant method is called + assertTrue(assistants.getData().size() > 1); + } + + @Test + void listAssistants_returnsTwoAssistants() { + int expectedLimit = 2; + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + assertEquals(expectedLimit, data.size()); + } + + + + @Test + void listAssistants_returnsAscSortedAssistants() { + int expectedLimit = 3; + + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .order(AssistantSortOrder.ASC) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + + boolean firstTwoAscending = data.get(0).getCreatedAt() <= data.get(1).getCreatedAt(); + boolean lastTwoAscending = data.get(1).getCreatedAt() <= data.get(2).getCreatedAt(); + assertTrue(firstTwoAscending && lastTwoAscending); + } + + @Test + void listAssistants_returnsDescSortedAssistants() { + int expectedLimit = 3; + + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .order(AssistantSortOrder.DESC) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + + boolean firstTwoDescending = data.get(0).getCreatedAt() >= data.get(1).getCreatedAt(); + boolean lastTwoDescending = data.get(1).getCreatedAt() >= data.get(2).getCreatedAt(); + assertTrue(firstTwoDescending && lastTwoDescending); + } + + @Test + void createAssistantFile() { + File uploadedFile = uploadAssistantFile(); + + Assistant assistant = createAndValidateAssistant(); + + AssistantFile assistantFile = service.createAssistantFile(assistant.getId(), new AssistantFileRequest(uploadedFile.getId())); + + assertNotNull(assistantFile); + assertEquals(uploadedFile.getId(), assistantFile.getId()); + assertEquals(assistant.getId(), assistantFile.getAssistantId()); + } + + + + @Test + void retrieveAssistantFile() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @Test + void deleteAssistantFile() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @Test + void listAssistantFiles() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @AfterAll + static void clean() { + //Clean up all data created during this test + ListAssistantQueryRequest queryFilter = ListAssistantQueryRequest.builder() + .limit(100) + .build(); + ListAssistant assistantListAssistant = service.listAssistants(queryFilter); + assistantListAssistant.getData().forEach(assistant ->{ + service.deleteAssistant(assistant.getId()); + }); + } + + private static File uploadAssistantFile() { + String filePath = "src/test/resources/assistants-data.html"; + return service.uploadFile("assistants", filePath); + } + + private static Assistant createAndValidateAssistant() { + AssistantBase assistantRequest = assistantStub(); + Assistant createAssistantResponse = service.createAssistant(assistantRequest); + validateAssistantResponse(createAssistantResponse); + + return createAssistantResponse; + } + + + private static AssistantBase assistantStub() { + return AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name(MATH_TUTOR) + .instructions(ASSISTANT_INSTRUCTION) + .tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER))) + .build(); + } + + private static void validateAssistantResponse(Assistant assistantResponse) { + assertNotNull(assistantResponse); + assertNotNull(assistantResponse.getId()); + assertNotNull(assistantResponse.getCreatedAt()); + assertNotNull(assistantResponse.getObject()); + assertEquals(assistantResponse.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); + assertEquals(MATH_TUTOR, assistantResponse.getName()); + } + + private static List validateListAssistants(ListAssistant assistants) { + assertNotNull(assistants); + List data = assistants.getData(); + assertNotNull(data); + return data; + } +} diff --git a/service/src/test/resources/assistant-file-data.json b/service/src/test/resources/assistant-file-data.json new file mode 100644 index 00000000..8b42bc07 --- /dev/null +++ b/service/src/test/resources/assistant-file-data.json @@ -0,0 +1 @@ +{"prompt": "prompt", "completion": "text"} \ No newline at end of file diff --git a/service/src/test/resources/assistants-data.html b/service/src/test/resources/assistants-data.html new file mode 100644 index 00000000..6c70bcfe --- /dev/null +++ b/service/src/test/resources/assistants-data.html @@ -0,0 +1 @@ + \ No newline at end of file From fee68bad25374d4553654d323652289ad9724696 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:23:26 -0600 Subject: [PATCH 35/59] Fix tests --- .../openai/service/OpenAiService.java | 10 +++- .../openai/service/AssistantTest.java | 60 +------------------ 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 0a7d9459..04f2e459 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -259,7 +259,6 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File .setType(MediaType.get("multipart/form-data")) .addFormDataPart("prompt", request.getPrompt()) .addFormDataPart("size", request.getSize()) - .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -272,6 +271,10 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File builder.addFormDataPart("mask", "mask", maskBody); } + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + return execute(api.createImageEdit(builder.build())); } @@ -286,7 +289,6 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.get("multipart/form-data")) .addFormDataPart("size", request.getSize()) - .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -294,6 +296,10 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav builder.addFormDataPart("n", request.getN().toString()); } + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + return execute(api.createImageVariation(builder.build())); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java index 4f21b739..781161b2 100644 --- a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -19,9 +19,7 @@ import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class AssistantTest { @@ -70,59 +68,7 @@ void listAssistants() { ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); assertNotNull(assistants); - // this should be more than 2 depending on how many times createAndValidateAssistant method is called - assertTrue(assistants.getData().size() > 1); - } - - @Test - void listAssistants_returnsTwoAssistants() { - int expectedLimit = 2; - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - assertEquals(expectedLimit, data.size()); - } - - - - @Test - void listAssistants_returnsAscSortedAssistants() { - int expectedLimit = 3; - - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .order(AssistantSortOrder.ASC) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - - boolean firstTwoAscending = data.get(0).getCreatedAt() <= data.get(1).getCreatedAt(); - boolean lastTwoAscending = data.get(1).getCreatedAt() <= data.get(2).getCreatedAt(); - assertTrue(firstTwoAscending && lastTwoAscending); - } - - @Test - void listAssistants_returnsDescSortedAssistants() { - int expectedLimit = 3; - - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .order(AssistantSortOrder.DESC) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - - boolean firstTwoDescending = data.get(0).getCreatedAt() >= data.get(1).getCreatedAt(); - boolean lastTwoDescending = data.get(1).getCreatedAt() >= data.get(2).getCreatedAt(); - assertTrue(firstTwoDescending && lastTwoDescending); + assertFalse(assistants.getData().isEmpty()); } @Test @@ -138,8 +84,6 @@ void createAssistantFile() { assertEquals(assistant.getId(), assistantFile.getAssistantId()); } - - @Test void retrieveAssistantFile() { //TODO From a356a2e713ed7364aef601da29cb2a9434dbaa79 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:34:23 -0600 Subject: [PATCH 36/59] Add NoArgsConstructor to ChatFunction --- .../com/theokanning/openai/completion/chat/ChatFunction.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java index 7d72829a..67162edf 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -3,11 +3,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.NonNull; import java.util.function.Function; @Data +@NoArgsConstructor public class ChatFunction { @NonNull @@ -46,7 +48,8 @@ public Builder executor(Class requestClass, Function executor) } public ChatFunction build() { - ChatFunction chatFunction = new ChatFunction(name); + ChatFunction chatFunction = new ChatFunction(); + chatFunction.setName(name); chatFunction.setDescription(description); chatFunction.setParametersClass(parameters); chatFunction.setExecutor(executor); From 47fe4780238485c34f1401cdeb5047760b9b872f Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:41:25 -0600 Subject: [PATCH 37/59] Change compilation github workflow (#398) Only the released artifacts need to compile with 1.8 --- .github/workflows/pull_request.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 77d94afb..8ea36f7e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,5 +17,14 @@ jobs: distribution: temurin java-version: 8 - - name: Compile - run: ./gradlew compileJava compileTestJava + - name: Compile Artifacts + run: ./gradlew api:compileJava client:compileJava service:compileJava + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + + - name: Compile Tests + run: ./gradlew compileTestJava From 3a2d01080661a50d3a6e68fa71214b01df345d5a Mon Sep 17 00:00:00 2001 From: Bartosz Date: Sun, 12 Nov 2023 23:44:35 +0100 Subject: [PATCH 38/59] Implement creation of "function" parameters in runtime (#339) * Enable dynamic definition of "function" parameters instead of using Class instance * Add tests to new "function" capabilities * Add example of creating "function" parameters in runtime * Add documentation to ChatFunctions Co-authored-by: Theo Kanning --- .../chat/ChatCompletionRequest.java | 2 +- .../openai/completion/chat/ChatFunction.java | 11 +++ .../completion/chat/ChatFunctionDynamic.java | 62 +++++++++++++ .../chat/ChatFunctionParameters.java | 27 ++++++ .../completion/chat/ChatFunctionProperty.java | 25 ++++++ .../OpenAiApiDynamicFunctionExample.java | 90 +++++++++++++++++++ .../openai/service/ChatCompletionTest.java | 90 +++++++++++++++++++ 7 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java create mode 100644 api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java create mode 100644 example/src/main/java/example/OpenAiApiDynamicFunctionExample.java diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java index 1f055fbf..e4479ff3 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java @@ -98,7 +98,7 @@ public class ChatCompletionRequest { /** * A list of the available functions. */ - List functions; + List functions; /** * Controls how the model responds to function calls, as specified in the OpenAI documentation. diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java index 67162edf..820f4bd6 100644 --- a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -12,9 +12,20 @@ @NoArgsConstructor public class ChatFunction { + /** + * The name of the function being called. + */ @NonNull private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ private String description; + + /** + * The parameters the functions accepts. + */ @JsonProperty("parameters") private Class parametersClass; diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java new file mode 100644 index 00000000..9b4f2070 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java @@ -0,0 +1,62 @@ +package com.theokanning.openai.completion.chat; + +import lombok.Data; +import lombok.NonNull; + + +@Data +public class ChatFunctionDynamic { + + /** + * The name of the function being called. + */ + @NonNull + private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ + private String description; + + /** + * The parameters the functions accepts. + */ + private ChatFunctionParameters parameters; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String description; + private ChatFunctionParameters parameters = new ChatFunctionParameters(); + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder parameters(ChatFunctionParameters parameters) { + this.parameters = parameters; + return this; + } + + public Builder addProperty(ChatFunctionProperty property) { + this.parameters.addProperty(property); + return this; + } + + public ChatFunctionDynamic build() { + ChatFunctionDynamic chatFunction = new ChatFunctionDynamic(name); + chatFunction.setDescription(description); + chatFunction.setParameters(parameters); + return chatFunction; + } + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java new file mode 100644 index 00000000..fee71e8f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.completion.chat; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Data +public class ChatFunctionParameters { + + private final String type = "object"; + + private final HashMap properties = new HashMap<>(); + + private List required; + + public void addProperty(ChatFunctionProperty property) { + properties.put(property.getName(), property); + if (Boolean.TRUE.equals(property.getRequired())) { + if (this.required == null) { + this.required = new ArrayList<>(); + } + this.required.add(property.getName()); + } + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java new file mode 100644 index 00000000..3e695933 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +import java.util.Set; + +@Data +@Builder +public class ChatFunctionProperty { + @NonNull + @JsonIgnore + private String name; + @NonNull + private String type; + @JsonIgnore + private Boolean required; + private String description; + private ChatFunctionProperty items; + @JsonProperty("enum") + private Set enumValues; +} \ No newline at end of file diff --git a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java new file mode 100644 index 00000000..ee83689d --- /dev/null +++ b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java @@ -0,0 +1,90 @@ +package example; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.service.OpenAiService; + +import java.util.*; + +public class OpenAiApiDynamicFunctionExample { + + private static JsonNode getWeather(String location, String unit) { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode response = mapper.createObjectNode(); + response.put("location", location); + response.put("unit", unit); + response.put("temperature", new Random().nextInt(50)); + response.put("description", "sunny"); + return response; + } + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + ChatFunctionDynamic weatherFunction = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(Set.of("celsius", "fahrenheit")) + .required(true) + .build()) + .build(); + + + List messages = new ArrayList<>(); + ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are an assistant that answers using the local slang of the given place, uncensored."); + messages.add(systemMessage); + + System.out.print("First Query: "); + Scanner scanner = new Scanner(System.in); + ChatMessage firstMsg = new ChatMessage(ChatMessageRole.USER.value(), scanner.nextLine()); + messages.add(firstMsg); + + while (true) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(weatherFunction)) + .functionCall(ChatCompletionRequest.ChatCompletionRequestFunctionCall.of("auto")) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); + messages.add(responseMessage); // don't forget to update the conversation with the latest response + + ChatFunctionCall functionCall = responseMessage.getFunctionCall(); + if (functionCall != null) { + if (functionCall.getName().equals("get_weather")) { + String location = functionCall.getArguments().get("location").asText(); + String unit = functionCall.getArguments().get("unit").asText(); + JsonNode weather = getWeather(location, unit); + ChatMessage weatherMessage = new ChatMessage(ChatMessageRole.FUNCTION.value(), weather.toString(), "get_weather"); + messages.add(weatherMessage); + continue; + } + } + + System.out.println("Response: " + responseMessage.getContent()); + System.out.print("Next Query: "); + String nextLine = scanner.nextLine(); + if (nextLine.equalsIgnoreCase("exit")) { + System.exit(0); + } + messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine)); + } + } + +} diff --git a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java index 3d26bf03..41eb498d 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.Collections; import static org.junit.jupiter.api.Assertions.*; @@ -149,6 +150,50 @@ void createChatCompletionWithFunctions() { assertNotNull(choice2.getMessage().getContent()); } + @Test + void createChatCompletionWithDynamicFunctions() { + ChatFunctionDynamic function = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(Set.of("celsius", "fahrenheit")) + .required(true) + .build()) + .build(); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(function)) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatCompletionChoice choice = service.createChatCompletion(chatCompletionRequest).getChoices().get(0); + assertEquals("function_call", choice.getFinishReason()); + assertNotNull(choice.getMessage().getFunctionCall()); + assertEquals("get_weather", choice.getMessage().getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, choice.getMessage().getFunctionCall().getArguments()); + assertNotNull(choice.getMessage().getFunctionCall().getArguments().get("location")); + assertNotNull(choice.getMessage().getFunctionCall().getArguments().get("unit")); + } + @Test void streamChatCompletionWithFunctions() { final List functions = Collections.singletonList(ChatFunction.builder() @@ -214,4 +259,49 @@ void streamChatCompletionWithFunctions() { assertNotNull(accumulatedMessage2.getContent()); } + @Test + void streamChatCompletionWithDynamicFunctions() { + ChatFunctionDynamic function = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(Set.of("celsius", "fahrenheit")) + .required(true) + .build()) + .build(); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(function)) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatMessage accumulatedMessage = service.mapStreamToAccumulator(service.streamChatCompletion(chatCompletionRequest)) + .blockingLast() + .getAccumulatedMessage(); + assertNotNull(accumulatedMessage.getFunctionCall()); + assertEquals("get_weather", accumulatedMessage.getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, accumulatedMessage.getFunctionCall().getArguments()); + assertNotNull(accumulatedMessage.getFunctionCall().getArguments().get("location")); + assertNotNull(accumulatedMessage.getFunctionCall().getArguments().get("unit")); + } + } From 7fbd0a7b4c3138f4788ee9aaf021573d5c24a5a5 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:50:32 -0600 Subject: [PATCH 39/59] Update to version 0.17.0 --- .github/workflows/publish.yml | 10 ++++++++-- .github/workflows/test.yml | 4 ++-- gradle.properties | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6e978294..0da11d72 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,17 +12,23 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 1.8 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 8 + java-version: 17 - name: Test run: ./gradlew test env: OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} + - name: Set up JDK 1.8 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + - name: Publish run: ./gradlew build publish --no-parallel env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 830e797f..cf169464 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 1.8 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 8 + java-version: 17 - name: Test run: ./gradlew test --stacktrace diff --git a/gradle.properties b/gradle.properties index 85a94ec8..671b33b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.16.1 +VERSION_NAME=0.17.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 8768a4977ab3865f72a36d728b8eca55a0952157 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:57:43 -0600 Subject: [PATCH 40/59] Deprecate Model.permissions This field is no longer returned by OpenAI --- api/src/main/java/com/theokanning/openai/model/Model.java | 3 ++- .../test/java/com/theokanning/openai/service/ModelTest.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/model/Model.java b/api/src/main/java/com/theokanning/openai/model/Model.java index 5180f2ae..2add679d 100644 --- a/api/src/main/java/com/theokanning/openai/model/Model.java +++ b/api/src/main/java/com/theokanning/openai/model/Model.java @@ -29,8 +29,9 @@ public class Model { public String ownedBy; /** - * List of permissions for this model + * List of permissions for this model. No longer returned by OpenAI */ + @Deprecated public List permission; /** diff --git a/service/src/test/java/com/theokanning/openai/service/ModelTest.java b/service/src/test/java/com/theokanning/openai/service/ModelTest.java index 4461dacf..31d23da9 100644 --- a/service/src/test/java/com/theokanning/openai/service/ModelTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ModelTest.java @@ -27,6 +27,5 @@ void getModel() { assertEquals("ada", ada.id); assertEquals("openai", ada.ownedBy); - assertFalse(ada.permission.isEmpty()); } } From 987801bcc1c3f0b7aad006e0dad0577159710bbc Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 17:05:00 -0600 Subject: [PATCH 41/59] Remove example project from publishing build step --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0da11d72..d2e93051 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ jobs: java-version: 8 - name: Publish - run: ./gradlew build publish --no-parallel + run: ./gradlew build -x :example:build publish --no-parallel env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} From bc81a295e65b383dbd1134f8cf10e6c1294f4c6f Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 17:18:56 -0600 Subject: [PATCH 42/59] Require all steps to run in java 1.8 There's definitely a better way to manage version, but trying to run individual steps against later versions was causing my problems than it solved. I'll think of the best way to handle this. --- .github/workflows/gradle-wrapper-validation.yml | 14 -------------- .github/workflows/publish.yml | 12 +++--------- .github/workflows/pull_request.yml | 13 ++----------- .github/workflows/test.yml | 4 ++-- .../example/OpenAiApiDynamicFunctionExample.java | 2 +- .../openai/service/ChatCompletionTest.java | 10 +++------- 6 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/gradle-wrapper-validation.yml diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index 805c1fcf..00000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: "Validate Gradle Wrapper" -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d2e93051..6e978294 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,25 +12,19 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 17 + - name: Set up JDK 1.8 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 8 - name: Test run: ./gradlew test env: OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} - - name: Set up JDK 1.8 - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 8 - - name: Publish - run: ./gradlew build -x :example:build publish --no-parallel + run: ./gradlew build publish --no-parallel env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8ea36f7e..77d94afb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,14 +17,5 @@ jobs: distribution: temurin java-version: 8 - - name: Compile Artifacts - run: ./gradlew api:compileJava client:compileJava service:compileJava - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 17 - - - name: Compile Tests - run: ./gradlew compileTestJava + - name: Compile + run: ./gradlew compileJava compileTestJava diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf169464..830e797f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 17 + - name: Set up JDK 1.8 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 8 - name: Test run: ./gradlew test --stacktrace diff --git a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java index ee83689d..75f9b8e2 100644 --- a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java +++ b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java @@ -36,7 +36,7 @@ public static void main(String... args) { .name("unit") .type("string") .description("The temperature unit, can be 'celsius' or 'fahrenheit'") - .enumValues(Set.of("celsius", "fahrenheit")) + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) .required(true) .build()) .build(); diff --git a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java index 41eb498d..25f0defb 100644 --- a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -7,11 +7,7 @@ import com.theokanning.openai.completion.chat.*; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.Collections; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -164,7 +160,7 @@ void createChatCompletionWithDynamicFunctions() { .name("unit") .type("string") .description("The temperature unit, can be 'celsius' or 'fahrenheit'") - .enumValues(Set.of("celsius", "fahrenheit")) + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) .required(true) .build()) .build(); @@ -273,7 +269,7 @@ void streamChatCompletionWithDynamicFunctions() { .name("unit") .type("string") .description("The temperature unit, can be 'celsius' or 'fahrenheit'") - .enumValues(Set.of("celsius", "fahrenheit")) + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) .required(true) .build()) .build(); From f223175df387bc9fdce33ba9166282dab5646cb5 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Mon, 13 Nov 2023 18:16:11 -0600 Subject: [PATCH 43/59] Fix Assistant POJO issues Easier to make separate objects for getting, creating, and modifying Assistants in case the parameters diverge later. --- .../openai/assistants/Assistant.java | 47 +++++++++++++++++- .../openai/assistants/AssistantRequest.java | 48 ++++++++++++++++++- .../openai/assistants/ListAssistant.java | 2 +- ...tBase.java => ModifyAssistantRequest.java} | 12 ++--- .../theokanning/openai/assistants/Tool.java | 9 ++++ .../theokanning/openai/client/OpenAiApi.java | 11 ++--- .../openai/service/OpenAiService.java | 12 ++--- .../openai/service/AssistantTest.java | 33 ++++--------- 8 files changed, 122 insertions(+), 52 deletions(-) rename api/src/main/java/com/theokanning/openai/assistants/{AssistantBase.java => ModifyAssistantRequest.java} (80%) diff --git a/api/src/main/java/com/theokanning/openai/assistants/Assistant.java b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java index 111ef169..49cf17e9 100644 --- a/api/src/main/java/com/theokanning/openai/assistants/Assistant.java +++ b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java @@ -1,10 +1,16 @@ package com.theokanning.openai.assistants; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; +import lombok.*; +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor @Data -public class Assistant extends AssistantBase { +public class Assistant { /** * The identifier, which can be referenced in API endpoints. @@ -21,4 +27,41 @@ public class Assistant extends AssistantBase { */ @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/AssistantRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java index dc0a66df..bf38ff0b 100644 --- a/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java @@ -1,6 +1,52 @@ package com.theokanning.openai.assistants; -public class AssistantRequest extends AssistantBase { +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/ListAssistant.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java index 8478a547..39884991 100644 --- a/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.theokanning.openai.OpenAiResponse; -public class ListAssistant extends OpenAiResponse { +public class ListAssistant extends OpenAiResponse { @JsonProperty("first_id") String firstId; diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java b/api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java similarity index 80% rename from api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java rename to api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java index d771bd30..0fcc4f85 100644 --- a/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java +++ b/api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java @@ -1,11 +1,8 @@ package com.theokanning.openai.assistants; + import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; +import lombok.*; import java.util.List; import java.util.Map; @@ -14,12 +11,11 @@ @NoArgsConstructor @AllArgsConstructor @Data -public class AssistantBase { +public class ModifyAssistantRequest { /** * ID of the model to use */ - @NonNull String model; /** @@ -46,7 +42,7 @@ public class AssistantBase { * A list of file IDs attached to this assistant. */ @JsonProperty("file_ids") - List fields; + List fileIds; /** * Set of 16 key-value pairs that can be attached to an object. diff --git a/api/src/main/java/com/theokanning/openai/assistants/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java index 00027d72..e759db80 100644 --- a/api/src/main/java/com/theokanning/openai/assistants/Tool.java +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -1,5 +1,6 @@ package com.theokanning.openai.assistants; +import com.theokanning.openai.completion.chat.ChatFunction; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -8,5 +9,13 @@ @AllArgsConstructor @Data public class Tool { + /** + * The type of tool being defined + */ AssistantToolsEnum type; + + /** + * Function definition, only used if type is "function" + */ + ChatFunction function; } diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 0bfc03aa..69bda48b 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,12 +2,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; -import com.theokanning.openai.assistants.AssistantBase; -import com.theokanning.openai.assistants.Assistant; -import com.theokanning.openai.assistants.AssistantFile; -import com.theokanning.openai.assistants.AssistantFileRequest; -import com.theokanning.openai.assistants.ListAssistant; -import com.theokanning.openai.assistants.ListAssistantQueryRequest; +import com.theokanning.openai.assistants.*; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; @@ -195,7 +190,7 @@ public interface OpenAiApi { @Headers({"OpenAI-Beta: assistants=v1"}) @POST("/v1/assistants") - Single createAssistant(@Body AssistantBase request); + Single createAssistant(@Body AssistantRequest request); @Headers({"OpenAI-Beta: assistants=v1"}) @GET("/v1/assistants/{assistant_id}") @@ -203,7 +198,7 @@ public interface OpenAiApi { @Headers({"OpenAI-Beta: assistants=v1"}) @POST("/v1/assistants/{assistant_id}") - Single modifyAssistant(@Path("assistant_id") String assistantId, @Body AssistantBase request); + Single modifyAssistant(@Path("assistant_id") String assistantId, @Body ModifyAssistantRequest request); @Headers({"OpenAI-Beta: assistants=v1"}) @DELETE("/v1/assistants/{assistant_id}") 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 04f2e459..ea62edde 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -6,15 +6,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.node.TextNode; +import com.sun.org.apache.xpath.internal.operations.Mod; import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; -import com.theokanning.openai.assistants.Assistant; -import com.theokanning.openai.assistants.AssistantBase; -import com.theokanning.openai.assistants.AssistantFile; -import com.theokanning.openai.assistants.AssistantFileRequest; -import com.theokanning.openai.assistants.ListAssistant; -import com.theokanning.openai.assistants.ListAssistantQueryRequest; +import com.theokanning.openai.assistants.*; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; @@ -362,7 +358,7 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } - public Assistant createAssistant(AssistantBase request) { + public Assistant createAssistant(AssistantRequest request) { return execute(api.createAssistant(request)); } @@ -370,7 +366,7 @@ public Assistant retrieveAssistant(String assistantId) { return execute(api.retrieveAssistant(assistantId)); } - public Assistant modifyAssistant(String assistantId, AssistantBase request) { + public Assistant modifyAssistant(String assistantId, ModifyAssistantRequest request) { return execute(api.modifyAssistant(assistantId, request)); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java index 781161b2..d0ef40d7 100644 --- a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -1,16 +1,7 @@ package com.theokanning.openai.service; import com.theokanning.openai.DeleteResult; -import com.theokanning.openai.assistants.Assistant; -import com.theokanning.openai.assistants.AssistantBase; -import com.theokanning.openai.assistants.AssistantFile; -import com.theokanning.openai.assistants.AssistantFileRequest; -import com.theokanning.openai.assistants.AssistantRequest; -import com.theokanning.openai.assistants.AssistantSortOrder; -import com.theokanning.openai.assistants.AssistantToolsEnum; -import com.theokanning.openai.assistants.ListAssistant; -import com.theokanning.openai.assistants.ListAssistantQueryRequest; -import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.assistants.*; import com.theokanning.openai.file.File; import com.theokanning.openai.utils.TikTokensUtil; import org.junit.jupiter.api.AfterAll; @@ -44,10 +35,12 @@ void retrieveAssistant() { void modifyAssistant() { Assistant createAssistantResponse = createAndValidateAssistant(); - String modifiedName = MATH_TUTOR + " Modified"; - createAssistantResponse.setName(modifiedName);//modify a field + String modifiedName = MATH_TUTOR + "Modified"; + ModifyAssistantRequest modifyRequest = ModifyAssistantRequest.builder() + .name(modifiedName) + .build(); - Assistant modifiedAssistantResponse = service.modifyAssistant(createAssistantResponse.getId(), createAssistantResponse); + Assistant modifiedAssistantResponse = service.modifyAssistant(createAssistantResponse.getId(), modifyRequest); assertNotNull(modifiedAssistantResponse); assertEquals(modifiedName, modifiedAssistantResponse.getName()); } @@ -123,20 +116,19 @@ private static File uploadAssistantFile() { } private static Assistant createAndValidateAssistant() { - AssistantBase assistantRequest = assistantStub(); + AssistantRequest assistantRequest = assistantStub(); Assistant createAssistantResponse = service.createAssistant(assistantRequest); validateAssistantResponse(createAssistantResponse); return createAssistantResponse; } - - private static AssistantBase assistantStub() { + private static AssistantRequest assistantStub() { return AssistantRequest.builder() .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) .name(MATH_TUTOR) .instructions(ASSISTANT_INSTRUCTION) - .tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER))) + .tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER, null))) .build(); } @@ -148,11 +140,4 @@ private static void validateAssistantResponse(Assistant assistantResponse) { assertEquals(assistantResponse.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); assertEquals(MATH_TUTOR, assistantResponse.getName()); } - - private static List validateListAssistants(ListAssistant assistants) { - assertNotNull(assistants); - List data = assistants.getData(); - assertNotNull(data); - return data; - } } From 037606eeb1b753e15725db00f3c397cfe7c19090 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Mon, 13 Nov 2023 20:12:48 -0600 Subject: [PATCH 44/59] Add Assistant Messages API Also added just enough Threads API to create messages. --- .../openai/ListSearchParameters.java | 51 +++++++ .../theokanning/openai/OpenAiResponse.java | 19 +++ .../theokanning/openai/messages/Message.java | 81 +++++++++++ .../openai/messages/MessageContent.java | 19 +++ .../openai/messages/MessageFile.java | 38 +++++ .../openai/messages/MessageRequest.java | 48 +++++++ .../openai/messages/ModifyMessageRequest.java | 27 ++++ .../theokanning/openai/threads/Thread.java | 41 ++++++ .../openai/threads/ThreadRequest.java | 36 +++++ .../theokanning/openai/client/OpenAiApi.java | 46 +++++- .../openai/service/OpenAiService.java | 53 ++++++- .../openai/service/MessageTest.java | 133 ++++++++++++++++++ .../openai/service/ThreadTest.java | 31 ++++ 13 files changed, 616 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/com/theokanning/openai/ListSearchParameters.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/Message.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/MessageContent.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/MessageFile.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/MessageRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/threads/Thread.java create mode 100644 api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java create mode 100644 service/src/test/java/com/theokanning/openai/service/MessageTest.java create mode 100644 service/src/test/java/com/theokanning/openai/service/ThreadTest.java 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..fd566d03 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("hasMore") + public boolean hasMore; } 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..940cd934 --- /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..6db58112 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java @@ -0,0 +1,19 @@ +package com.theokanning.openai.messages; + +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; + + // todo handle different content types +} 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/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..54f536bd --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java @@ -0,0 +1,36 @@ +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. + */ + @JsonProperty("messages") + 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. + */ + @JsonProperty("metadata") + Map metadata; +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 69bda48b..c7bf2e10 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -26,9 +26,15 @@ 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.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; import io.reactivex.Single; import okhttp3.MultipartBody; import okhttp3.RequestBody; @@ -186,8 +192,7 @@ public interface OpenAiApi { @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); @@ -223,4 +228,41 @@ public interface OpenAiApi { @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"}) + @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); + } 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 ea62edde..a6bb9bc3 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -7,9 +7,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.node.TextNode; import com.sun.org.apache.xpath.internal.operations.Mod; -import com.theokanning.openai.DeleteResult; -import com.theokanning.openai.OpenAiError; -import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.*; import com.theokanning.openai.assistants.*; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; @@ -38,9 +36,15 @@ 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.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; import io.reactivex.Single; @@ -358,6 +362,10 @@ 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)); } @@ -396,10 +404,45 @@ public ListAssistant listAssistantFiles(String assistantId, ListAssis return execute(api.listAssistantFiles(assistantId, queryParameters)); } - public ResponseBody createSpeech(CreateSpeechRequest request) { - return execute(api.createSpeech(request)); + public Thread createThread(ThreadRequest request) { + return execute(api.createThread(request)); + } + + 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)); + } + + /** * Calls the Open AI api, returns the response, and parses error messages if the request fails */ diff --git a/service/src/test/java/com/theokanning/openai/service/MessageTest.java b/service/src/test/java/com/theokanning/openai/service/MessageTest.java new file mode 100644 index 00000000..a1fe99cb --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/MessageTest.java @@ -0,0 +1,133 @@ +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() { + // todo delete thread + } + + @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/ThreadTest.java b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java new file mode 100644 index 00000000..24ff2bb0 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java @@ -0,0 +1,31 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + + +public class ThreadTest { + + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + @Test + void createThread() { + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + ThreadRequest threadRequest = ThreadRequest.builder() + .messages(Collections.singletonList(messageRequest)) + .build(); + + Thread thread = service.createThread(threadRequest); + assertEquals("thread", thread.getObject()); + } +} \ No newline at end of file From fe0c62f5763a091bc6a4ac7018c83b13aa1d2433 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Mon, 13 Nov 2023 20:15:56 -0600 Subject: [PATCH 45/59] Fix JsonProperty on new objects --- api/src/main/java/com/theokanning/openai/OpenAiResponse.java | 2 +- .../main/java/com/theokanning/openai/threads/ThreadRequest.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/OpenAiResponse.java b/api/src/main/java/com/theokanning/openai/OpenAiResponse.java index fd566d03..f062fc77 100644 --- a/api/src/main/java/com/theokanning/openai/OpenAiResponse.java +++ b/api/src/main/java/com/theokanning/openai/OpenAiResponse.java @@ -35,6 +35,6 @@ public class OpenAiResponse { /** * True if there are objects after lastId */ - @JsonProperty("hasMore") + @JsonProperty("has_more") public boolean hasMore; } diff --git a/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java index 54f536bd..35fd888a 100644 --- a/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java +++ b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java @@ -23,7 +23,6 @@ public class ThreadRequest { /** * A list of messages to start the thread with. Optional. */ - @JsonProperty("messages") List messages; /** @@ -31,6 +30,5 @@ public class ThreadRequest { * 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. */ - @JsonProperty("metadata") Map metadata; } From 99162e09d7d1f779639632a61eadc46ae4b4035a Mon Sep 17 00:00:00 2001 From: vacuityv Date: Wed, 15 Nov 2023 07:18:44 +0800 Subject: [PATCH 46/59] feat(threads): add support for Threads in the Assistants (#400) --- .../theokanning/openai/client/OpenAiApi.java | 13 ++++++++ .../openai/service/OpenAiService.java | 13 +++++++- .../openai/service/ThreadTest.java | 30 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index c7bf2e10..cf09ff90 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -233,6 +233,19 @@ public interface OpenAiApi { @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); 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 a6bb9bc3..9c15522b 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.node.TextNode; -import com.sun.org.apache.xpath.internal.operations.Mod; import com.theokanning.openai.*; import com.theokanning.openai.assistants.*; import com.theokanning.openai.audio.CreateSpeechRequest; @@ -408,6 +407,18 @@ 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)); } diff --git a/service/src/test/java/com/theokanning/openai/service/ThreadTest.java b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java index 24ff2bb0..2c203790 100644 --- a/service/src/test/java/com/theokanning/openai/service/ThreadTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java @@ -1,5 +1,6 @@ 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; @@ -26,6 +27,35 @@ void createThread() { .build(); Thread thread = service.createThread(threadRequest); + System.out.println(thread.getId()); assertEquals("thread", thread.getObject()); } + + @Test + void retrieveThread() { + String threadId = "thread_K82pTg9kmhxpplGqalW6IHlc"; + + Thread thread = service.retrieveThread(threadId); + System.out.println(thread.getMetadata()); + assertEquals("thread", thread.getObject()); + } + + @Test + void modifyThread() { + String threadId = "thread_K82pTg9kmhxpplGqalW6IHlc"; + 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()); + } + + @Test + void deleteThread() { + String threadId = "thread_K82pTg9kmhxpplGqalW6IHlc"; + DeleteResult deleteResult = service.deleteThread(threadId); + assertEquals("thread.deleted", deleteResult.getObject()); + } } \ No newline at end of file From 4ff2758999286d34d0764c7b281fc5aa41a028dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Bj=C3=B6rkman?= Date: Thu, 16 Nov 2023 00:28:24 +0100 Subject: [PATCH 47/59] 390 Added runs (#401) * 390 Added runs * 390 Added runs --------- Co-authored-by: Theo Kanning --- .../openai/runs/MessageCreation.java | 16 +++++ .../java/com/theokanning/openai/runs/Run.java | 45 ++++++++++++ .../openai/runs/RunCreateRequest.java | 23 +++++++ .../com/theokanning/openai/runs/RunStep.java | 39 +++++++++++ .../com/theokanning/openai/runs/RunSteps.java | 17 +++++ .../com/theokanning/openai/runs/Runs.java | 17 +++++ .../theokanning/openai/runs/StepDetails.java | 18 +++++ .../com/theokanning/openai/runs/Tool.java | 15 ++++ .../theokanning/openai/client/OpenAiApi.java | 10 +++ .../openai/service/OpenAiService.java | 43 +++++++----- .../theokanning/openai/service/RunTest.java | 69 +++++++++++++++++++ 11 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 api/src/main/java/com/theokanning/openai/runs/MessageCreation.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/Run.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/RunStep.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/RunSteps.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/Runs.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/StepDetails.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/Tool.java create mode 100644 service/src/test/java/com/theokanning/openai/service/RunTest.java 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..fe59d845 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java @@ -0,0 +1,16 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class MessageCreation { + @JsonProperty("message_id") + String messageId; +} 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..2c14dd10 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/Run.java @@ -0,0 +1,45 @@ +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; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Run { + + @JsonProperty("assistant_id") + String assistantId; + @JsonProperty("cancelled_at") + Long cancelledAt; + @JsonProperty("completed_at") + Long completedAt; + @JsonProperty("created_at") + Long createdAt; + @JsonProperty("expires_at") + Long expiresAt; + @JsonProperty("failed_at") + Long failedAt; + @JsonProperty("file_ids") + List fileIds; + String id; + String instructions; + @JsonProperty("last_error") + String lastError; + Map metadata; + String model; + String object; + @JsonProperty("started_at") + Long startedAt; + String status; + @JsonProperty("thread_id") + String threadId; + List tools; +} 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..93744d44 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.runs; + +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/RunStep.java b/api/src/main/java/com/theokanning/openai/runs/RunStep.java new file mode 100644 index 00000000..98c1eee3 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunStep.java @@ -0,0 +1,39 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class RunStep { + + @JsonProperty("assistant_id") + String assistantId; + @JsonProperty("canelled_at") + Long cancelledAt; + @JsonProperty("completed_at") + Long completedAt; + @JsonProperty("created_at") + Long createdAt; + @JsonProperty("expired_at") + Long expiredAt; + @JsonProperty("failed_at") + Long failedAt; + String id; + @JsonProperty("last_error") + String lastError; + String object; + @JsonProperty("run_id") + String runId; + String status; + @JsonProperty("step_details") + StepDetails stepDetails; + @JsonProperty("thread_id") + String threadId; + String type; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunSteps.java b/api/src/main/java/com/theokanning/openai/runs/RunSteps.java new file mode 100644 index 00000000..c7260cfe --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunSteps.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class RunSteps { + + String object; + List data; + @JsonProperty("first_id") + String firstId; + @JsonProperty("last_id") + String lastId; + @JsonProperty("has_more") + boolean hasMore; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/Runs.java b/api/src/main/java/com/theokanning/openai/runs/Runs.java new file mode 100644 index 00000000..2b07de7d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/Runs.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class Runs { + + String object; + List data; + @JsonProperty("first_id") + String firstId; + @JsonProperty("last_id") + String lastId; + @JsonProperty("has_more") + boolean hasMore; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/StepDetails.java b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java new file mode 100644 index 00000000..c17f9224 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java @@ -0,0 +1,18 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class StepDetails { + + @JsonProperty("message_creation") + MessageCreation messageCreation; + String type; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/Tool.java b/api/src/main/java/com/theokanning/openai/runs/Tool.java new file mode 100644 index 00000000..abeee04b --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/Tool.java @@ -0,0 +1,15 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Tool { + + String type; +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index cf09ff90..bb57aa53 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -33,6 +33,8 @@ import com.theokanning.openai.model.Model; import com.theokanning.openai.moderation.ModerationRequest; import com.theokanning.openai.moderation.ModerationResult; +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 io.reactivex.Single; @@ -278,4 +280,12 @@ public interface OpenAiApi { @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); + } 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 9c15522b..bbd6d550 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -8,11 +8,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.theokanning.openai.*; import com.theokanning.openai.assistants.*; -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 com.theokanning.openai.audio.*; import com.theokanning.openai.billing.BillingUsage; import com.theokanning.openai.billing.Subscription; import com.theokanning.openai.client.OpenAiApi; @@ -42,6 +38,8 @@ import com.theokanning.openai.model.Model; import com.theokanning.openai.moderation.ModerationRequest; import com.theokanning.openai.moderation.ModerationResult; +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 io.reactivex.BackpressureStrategy; @@ -166,7 +164,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); @@ -364,7 +362,7 @@ public ModerationResult createModeration(ModerationRequest request) { public ResponseBody createSpeech(CreateSpeechRequest request) { return execute(api.createSpeech(request)); } - + public Assistant createAssistant(AssistantRequest request) { return execute(api.createAssistant(request)); } @@ -382,7 +380,8 @@ public DeleteResult deleteAssistant(String assistantId) { } public ListAssistant listAssistants(ListAssistantQueryRequest filterRequest) { - Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() { + }); return execute(api.listAssistants(queryParameters)); } @@ -399,7 +398,8 @@ public DeleteResult deleteAssistantFile(String assistantId, String fileId) { } public ListAssistant listAssistantFiles(String assistantId, ListAssistantQueryRequest filterRequest) { - Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() { + }); return execute(api.listAssistantFiles(assistantId, queryParameters)); } @@ -424,11 +424,11 @@ public Message createMessage(String threadId, MessageRequest request) { } public Message retrieveMessage(String threadId, String messageId) { - return execute(api.retrieveMessage(threadId,messageId)); + return execute(api.retrieveMessage(threadId, messageId)); } public Message modifyMessage(String threadId, String messageId, ModifyMessageRequest request) { - return execute(api.modifyMessage(threadId,messageId, request)); + return execute(api.modifyMessage(threadId, messageId, request)); } public OpenAiResponse listMessages(String threadId) { @@ -436,23 +436,32 @@ public OpenAiResponse listMessages(String threadId) { } public OpenAiResponse listMessages(String threadId, ListSearchParameters params) { - Map queryParameters = mapper.convertValue(params, new TypeReference>() {}); - return execute(api.listMessages(threadId,queryParameters)); + 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)); + return execute(api.retrieveMessageFile(threadId, messageId, fileId)); } public OpenAiResponse listMessageFiles(String threadId, String messageId) { - return execute(api.listMessageFiles(threadId,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)); + 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)); + } /** * Calls the Open AI api, returns the response, and parses error messages if the request fails diff --git a/service/src/test/java/com/theokanning/openai/service/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()); + } +} From 741cfe26bf371ae98fb4e34b46f3208974ce8355 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Wed, 15 Nov 2023 18:08:49 -0600 Subject: [PATCH 48/59] Add MessageContent types and simplify list querying (#405) Changed assistants to use the new shared ListSearchParams. --- .../openai/assistants/ListAssistant.java | 16 ------- .../assistants/ListAssistantQueryRequest.java | 39 ---------------- .../theokanning/openai/messages/Message.java | 2 +- .../openai/messages/MessageContent.java | 14 +++++- .../openai/messages/content/Annotation.java | 44 ++++++++++++++++++ .../openai/messages/content/FileCitation.java | 29 ++++++++++++ .../openai/messages/content/FilePath.java | 23 ++++++++++ .../openai/messages/content/ImageFile.java | 23 ++++++++++ .../openai/messages/content/Text.java | 28 +++++++++++ .../java/com/theokanning/openai/JsonTest.java | 2 + api/src/test/resources/fixtures/Message.json | 46 +++++++++++++++++++ .../theokanning/openai/client/OpenAiApi.java | 4 +- .../openai/service/OpenAiService.java | 8 ++-- .../openai/service/AssistantTest.java | 10 ++-- 14 files changed, 220 insertions(+), 68 deletions(-) delete mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java delete mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/content/Annotation.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/content/FilePath.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java create mode 100644 api/src/main/java/com/theokanning/openai/messages/content/Text.java create mode 100644 api/src/test/resources/fixtures/Message.json diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java deleted file mode 100644 index 39884991..00000000 --- a/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.theokanning.openai.assistants; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.theokanning.openai.OpenAiResponse; - -public class ListAssistant extends OpenAiResponse { - - @JsonProperty("first_id") - String firstId; - - @JsonProperty("last_id") - String lastId; - - @JsonProperty("has_more") - boolean hasMore; -} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java deleted file mode 100644 index 3e5f3c68..00000000 --- a/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.theokanning.openai.assistants; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Data -public class ListAssistantQueryRequest { - /** - * A limit on the number of objects to be returned. - * Limit can range between 1 and 100, and the default is 20 - */ - - Integer limit; - - /** - * Sort order by the 'created_at' timestamp of the objects. - * 'asc' for ascending order and 'desc' for descending order. - */ - AssistantSortOrder order; - - /** - * A cursor for use in pagination. after is an object ID that defines your place in the list. - * For instance, if you make a list request and receive 100 objects, ending with obj_foo, - * your subsequent call can include after=obj_foo in order to fetch the next page of the list - */ - String after; - - /** - * A cursor for use in pagination. before is an object ID that defines your place in the list. - * For instance, if you make a list request and receive 100 objects, ending with obj_foo, - * your subsequent call can include before=obj_foo in order to fetch the previous page of the list. - */ - String before; -} diff --git a/api/src/main/java/com/theokanning/openai/messages/Message.java b/api/src/main/java/com/theokanning/openai/messages/Message.java index 940cd934..44144780 100644 --- a/api/src/main/java/com/theokanning/openai/messages/Message.java +++ b/api/src/main/java/com/theokanning/openai/messages/Message.java @@ -50,7 +50,7 @@ public class Message { /** * The content of the message in an array of text and/or images. */ - List content; + List content; /** * If applicable, the ID of the assistant that authored this message. diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageContent.java b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java index 6db58112..a9ff489e 100644 --- a/api/src/main/java/com/theokanning/openai/messages/MessageContent.java +++ b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java @@ -1,5 +1,8 @@ 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; @@ -15,5 +18,14 @@ public class MessageContent { */ String type; - // todo handle different content types + /** + * 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/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/test/java/com/theokanning/openai/JsonTest.java b/api/src/test/java/com/theokanning/openai/JsonTest.java index 68b98155..58b5c5f2 100644 --- a/api/src/test/java/com/theokanning/openai/JsonTest.java +++ b/api/src/test/java/com/theokanning/openai/JsonTest.java @@ -18,6 +18,7 @@ import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneResult; 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; @@ -50,6 +51,7 @@ public class JsonTest { ImageResult.class, TranscriptionResult.class, TranslationResult.class, + Message.class, Model.class, ModerationRequest.class, ModerationResult.class 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/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index bb57aa53..36fc7434 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -213,7 +213,7 @@ public interface OpenAiApi { @Headers({"OpenAI-Beta: assistants=v1"}) @GET("/v1/assistants") - Single> listAssistants(@QueryMap Map filterRequest); + Single> listAssistants(@QueryMap Map filterRequest); @Headers({"OpenAI-Beta: assistants=v1"}) @POST("/v1/assistants/{assistant_id}/files") @@ -229,7 +229,7 @@ public interface OpenAiApi { @Headers({"OpenAI-Beta: assistants=v1"}) @GET("/v1/assistants/{assistant_id}/files") - Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); + Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); @Headers({"OpenAI-Beta: assistants=v1"}) @POST("/v1/threads") 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 bbd6d550..d59f7e3c 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -379,8 +379,8 @@ public DeleteResult deleteAssistant(String assistantId) { return execute(api.deleteAssistant(assistantId)); } - public ListAssistant listAssistants(ListAssistantQueryRequest filterRequest) { - Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() { + public OpenAiResponse listAssistants(ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { }); return execute(api.listAssistants(queryParameters)); } @@ -397,8 +397,8 @@ public DeleteResult deleteAssistantFile(String assistantId, String fileId) { return execute(api.deleteAssistantFile(assistantId, fileId)); } - public ListAssistant listAssistantFiles(String assistantId, ListAssistantQueryRequest filterRequest) { - Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() { + public OpenAiResponse listAssistantFiles(String assistantId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { }); return execute(api.listAssistantFiles(assistantId, queryParameters)); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java index d0ef40d7..cf4fc361 100644 --- a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -1,6 +1,8 @@ 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; @@ -21,8 +23,6 @@ public class AssistantTest { static OpenAiService service = new OpenAiService(token); - - @Test void retrieveAssistant() { Assistant createAssistantResponse = createAndValidateAssistant(); @@ -58,7 +58,7 @@ void deleteAssistant() { @Test void listAssistants() { - ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); + OpenAiResponse assistants = service.listAssistants(ListSearchParameters.builder().build()); assertNotNull(assistants); assertFalse(assistants.getData().isEmpty()); @@ -101,10 +101,10 @@ void listAssistantFiles() { @AfterAll static void clean() { //Clean up all data created during this test - ListAssistantQueryRequest queryFilter = ListAssistantQueryRequest.builder() + ListSearchParameters queryFilter = ListSearchParameters.builder() .limit(100) .build(); - ListAssistant assistantListAssistant = service.listAssistants(queryFilter); + OpenAiResponse assistantListAssistant = service.listAssistants(queryFilter); assistantListAssistant.getData().forEach(assistant ->{ service.deleteAssistant(assistant.getId()); }); From 56b302b8598ea9c8d7d3968b8882a74461c6a402 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Wed, 15 Nov 2023 18:19:45 -0600 Subject: [PATCH 49/59] Update to version 0.18.0 (#406) --- gradle.properties | 2 +- .../openai/service/MessageTest.java | 6 +++++- .../theokanning/openai/service/ThreadTest.java | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index 671b33b6..e12dac0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.17.0 +VERSION_NAME=0.18.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java diff --git a/service/src/test/java/com/theokanning/openai/service/MessageTest.java b/service/src/test/java/com/theokanning/openai/service/MessageTest.java index a1fe99cb..06a903c0 100644 --- a/service/src/test/java/com/theokanning/openai/service/MessageTest.java +++ b/service/src/test/java/com/theokanning/openai/service/MessageTest.java @@ -39,7 +39,11 @@ static void setup() { @AfterAll static void teardown() { - // todo delete thread + try { + service.deleteThread(threadId); + } catch (Exception e) { + // ignore + } } @Test diff --git a/service/src/test/java/com/theokanning/openai/service/ThreadTest.java b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java index 2c203790..212653af 100644 --- a/service/src/test/java/com/theokanning/openai/service/ThreadTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java @@ -4,19 +4,25 @@ 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") @@ -27,22 +33,21 @@ void createThread() { .build(); Thread thread = service.createThread(threadRequest); - System.out.println(thread.getId()); + threadId = thread.getId(); assertEquals("thread", thread.getObject()); } @Test + @Order(2) void retrieveThread() { - String threadId = "thread_K82pTg9kmhxpplGqalW6IHlc"; - Thread thread = service.retrieveThread(threadId); System.out.println(thread.getMetadata()); assertEquals("thread", thread.getObject()); } @Test + @Order(3) void modifyThread() { - String threadId = "thread_K82pTg9kmhxpplGqalW6IHlc"; Map metadata = new HashMap<>(); metadata.put("action", "modify"); ThreadRequest threadRequest = ThreadRequest.builder() @@ -50,11 +55,12 @@ void modifyThread() { .build(); Thread thread = service.modifyThread(threadId, threadRequest); assertEquals("thread", thread.getObject()); + assertEquals("modify", thread.getMetadata().get("action")); } @Test + @Order(4) void deleteThread() { - String threadId = "thread_K82pTg9kmhxpplGqalW6IHlc"; DeleteResult deleteResult = service.deleteThread(threadId); assertEquals("thread.deleted", deleteResult.getObject()); } From f63d666606f6727c3051976a5a38244aefc19147 Mon Sep 17 00:00:00 2001 From: vacuityv Date: Fri, 17 Nov 2023 07:41:44 +0800 Subject: [PATCH 50/59] fix(assistant-run): run ans run-step (#408) --- .../theokanning/openai/common/LastError.java | 24 +++++++ .../runs/CreateThreadAndRunRequest.java | 39 +++++++++++ .../openai/runs/MessageCreation.java | 3 +- .../openai/runs/RequiredAction.java | 26 +++++++ .../java/com/theokanning/openai/runs/Run.java | 69 ++++++++++++------- .../openai/runs/RunCreateRequest.java | 4 ++ .../com/theokanning/openai/runs/RunImage.java | 24 +++++++ .../com/theokanning/openai/runs/RunStep.java | 61 ++++++++++------ .../com/theokanning/openai/runs/RunSteps.java | 17 ----- .../com/theokanning/openai/runs/Runs.java | 17 ----- .../theokanning/openai/runs/StepDetails.java | 13 +++- .../runs/SubmitToolOutputRequestItem.java | 26 +++++++ .../openai/runs/SubmitToolOutputs.java | 26 +++++++ .../openai/runs/SubmitToolOutputsRequest.java | 26 +++++++ .../com/theokanning/openai/runs/ToolCall.java | 34 +++++++++ .../openai/runs/ToolCallCodeInterpreter.java | 26 +++++++ .../runs/ToolCallCodeInterpreterOutput.java | 26 +++++++ .../runs/{Tool.java => ToolCallFunction.java} | 18 +++-- .../theokanning/openai/client/OpenAiApi.java | 32 +++++++++ .../openai/service/OpenAiService.java | 31 +++++++++ 20 files changed, 455 insertions(+), 87 deletions(-) create mode 100644 api/src/main/java/com/theokanning/openai/common/LastError.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/RequiredAction.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/RunImage.java delete mode 100644 api/src/main/java/com/theokanning/openai/runs/RunSteps.java delete mode 100644 api/src/main/java/com/theokanning/openai/runs/Runs.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/ToolCall.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java create mode 100644 api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java rename api/src/main/java/com/theokanning/openai/runs/{Tool.java => ToolCallFunction.java} (50%) 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/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 index fe59d845..535fd7e2 100644 --- a/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java +++ b/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java @@ -6,11 +6,12 @@ import lombok.Data; import lombok.NoArgsConstructor; +@Data @Builder @NoArgsConstructor @AllArgsConstructor -@Data 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 index 2c14dd10..1da9ec95 100644 --- a/api/src/main/java/com/theokanning/openai/runs/Run.java +++ b/api/src/main/java/com/theokanning/openai/runs/Run.java @@ -1,6 +1,8 @@ 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; @@ -9,37 +11,56 @@ import java.util.List; import java.util.Map; +@Data @Builder @NoArgsConstructor @AllArgsConstructor -@Data public class Run { - @JsonProperty("assistant_id") - String assistantId; - @JsonProperty("cancelled_at") - Long cancelledAt; - @JsonProperty("completed_at") - Long completedAt; + private String id; + + private String object; + @JsonProperty("created_at") - Long createdAt; + 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") - Long expiresAt; + private Integer expiresAt; + + @JsonProperty("started_at") + private Integer startedAt; + + @JsonProperty("cancelled_at") + private Integer cancelledAt; + @JsonProperty("failed_at") - Long failedAt; + private Integer failedAt; + + @JsonProperty("completed_at") + private Integer completedAt; + + private String model; + + private String instructions; + + private List tools; + @JsonProperty("file_ids") - List fileIds; - String id; - String instructions; - @JsonProperty("last_error") - String lastError; - Map metadata; - String model; - String object; - @JsonProperty("started_at") - Long startedAt; - String status; - @JsonProperty("thread_id") - String threadId; - List tools; + 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 index 93744d44..cba5f283 100644 --- a/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java +++ b/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java @@ -1,5 +1,6 @@ package com.theokanning.openai.runs; +import com.theokanning.openai.assistants.Tool; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -17,7 +18,10 @@ public class RunCreateRequest { // 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 index 98c1eee3..7cd39342 100644 --- a/api/src/main/java/com/theokanning/openai/runs/RunStep.java +++ b/api/src/main/java/com/theokanning/openai/runs/RunStep.java @@ -1,39 +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 { - @JsonProperty("assistant_id") - String assistantId; - @JsonProperty("canelled_at") - Long cancelledAt; - @JsonProperty("completed_at") - Long completedAt; + private String id; + + private String object; + @JsonProperty("created_at") - Long createdAt; - @JsonProperty("expired_at") - Long expiredAt; - @JsonProperty("failed_at") - Long failedAt; - String id; - @JsonProperty("last_error") - String lastError; - String object; + private Integer createdAt; + + @JsonProperty("assistant_id") + private String assistantId; + + @JsonProperty("thread_id") + private String threadId; + @JsonProperty("run_id") - String runId; - String status; + private String runId; + + private String type; + + private String status; + @JsonProperty("step_details") - StepDetails stepDetails; - @JsonProperty("thread_id") - String threadId; - String type; + 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/RunSteps.java b/api/src/main/java/com/theokanning/openai/runs/RunSteps.java deleted file mode 100644 index c7260cfe..00000000 --- a/api/src/main/java/com/theokanning/openai/runs/RunSteps.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.theokanning.openai.runs; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public class RunSteps { - - String object; - List data; - @JsonProperty("first_id") - String firstId; - @JsonProperty("last_id") - String lastId; - @JsonProperty("has_more") - boolean hasMore; -} diff --git a/api/src/main/java/com/theokanning/openai/runs/Runs.java b/api/src/main/java/com/theokanning/openai/runs/Runs.java deleted file mode 100644 index 2b07de7d..00000000 --- a/api/src/main/java/com/theokanning/openai/runs/Runs.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.theokanning.openai.runs; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public class Runs { - - String object; - List data; - @JsonProperty("first_id") - String firstId; - @JsonProperty("last_id") - String lastId; - @JsonProperty("has_more") - boolean hasMore; -} diff --git a/api/src/main/java/com/theokanning/openai/runs/StepDetails.java b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java index c17f9224..08972623 100644 --- a/api/src/main/java/com/theokanning/openai/runs/StepDetails.java +++ b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java @@ -6,13 +6,20 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + +@Data @Builder @NoArgsConstructor @AllArgsConstructor -@Data public class StepDetails { + + private String type; + @JsonProperty("message_creation") - MessageCreation messageCreation; - String type; + 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..3086610c --- /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 tool_outputs; +} 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/Tool.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java similarity index 50% rename from api/src/main/java/com/theokanning/openai/runs/Tool.java rename to api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java index abeee04b..34de58d1 100644 --- a/api/src/main/java/com/theokanning/openai/runs/Tool.java +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java @@ -5,11 +5,21 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:38 + **/ + +@Data @Builder @NoArgsConstructor @AllArgsConstructor -@Data -public class Tool { - - String type; +public class ToolCallFunction { + + private String name; + + private String arguments; + + private String output; } diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 36fc7434..5bf3e732 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -1,6 +1,7 @@ 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; @@ -33,8 +34,11 @@ 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; @@ -288,4 +292,32 @@ public interface OpenAiApi { @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, @Body ListSearchParameters 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, @Body ListSearchParameters listSearchParameters); } 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 d59f7e3c..09680304 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -38,8 +38,11 @@ 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; @@ -463,6 +466,34 @@ 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) { + return execute(api.listRuns(threadId, listSearchParameters)); + } + + 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) { + return execute(api.listRunSteps(threadId, runId, listSearchParameters)); + } + /** * Calls the Open AI api, returns the response, and parses error messages if the request fails */ From 650d76b87a3246a52247b231f8d47f994ae63020 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Thu, 16 Nov 2023 17:45:09 -0600 Subject: [PATCH 51/59] Update to version 0.18.1 (#410) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e12dac0d..4cdb8e00 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.18.0 +VERSION_NAME=0.18.1 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From decc9fd93739aaf325142ec4a124e4b806557d0a Mon Sep 17 00:00:00 2001 From: vacuityv Date: Tue, 21 Nov 2023 08:15:05 +0800 Subject: [PATCH 52/59] feat(assistant-function): change the function define and a test for assistant funxtion (#415) --- .../openai/assistants/AssistantFunction.java | 28 ++++ .../theokanning/openai/assistants/Tool.java | 3 +- .../openai/runs/SubmitToolOutputsRequest.java | 2 +- .../openai/service/AssistantFunctionTest.java | 148 ++++++++++++++++++ 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFunction.java create mode 100644 service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java 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/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java index e759db80..f35af0ef 100644 --- a/api/src/main/java/com/theokanning/openai/assistants/Tool.java +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -1,6 +1,5 @@ package com.theokanning.openai.assistants; -import com.theokanning.openai.completion.chat.ChatFunction; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -17,5 +16,5 @@ public class Tool { /** * Function definition, only used if type is "function" */ - ChatFunction function; + AssistantFunction function; } diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java index 3086610c..f892f168 100644 --- a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java @@ -22,5 +22,5 @@ public class SubmitToolOutputsRequest { @JsonProperty("tool_outputs") - private List tool_outputs; + private List toolOutputs; } 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)); + + } + } +} From 2195b2a73d1e6d0cb26e6225c93425483b813a21 Mon Sep 17 00:00:00 2001 From: vacuityv Date: Tue, 21 Nov 2023 08:17:09 +0800 Subject: [PATCH 53/59] Assistant fixed (#414) * fix(assistant-run): run ans run-step * fix(run and run-step): fix the list method --------- Co-authored-by: Theo Kanning --- .../com/theokanning/openai/client/OpenAiApi.java | 5 +++-- .../theokanning/openai/service/OpenAiService.java | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 5bf3e732..2a0ea1a6 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -298,7 +298,8 @@ public interface OpenAiApi { @Headers("OpenAI-Beta: assistants=v1") @GET("/v1/threads/{thread_id}/runs") - Single> listRuns(@Path("thread_id") String threadId, @Body ListSearchParameters listSearchParameters); + 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") @@ -319,5 +320,5 @@ public interface OpenAiApi { @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, @Body ListSearchParameters listSearchParameters); + Single> listRunSteps(@Path("thread_id") String threadId, @Path("run_id") String runId, @QueryMap Map listSearchParameters); } 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 09680304..ee63c419 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -59,6 +59,7 @@ 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; @@ -471,7 +472,12 @@ public Run modifyRun(String threadId, String runId, Map metadata } public OpenAiResponse listRuns(String threadId, ListSearchParameters listSearchParameters) { - return execute(api.listRuns(threadId, 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) { @@ -491,7 +497,12 @@ public RunStep retrieveRunStep(String threadId, String runId, String stepId) { } public OpenAiResponse listRunSteps(String threadId, String runId, ListSearchParameters listSearchParameters) { - return execute(api.listRunSteps(threadId, runId, listSearchParameters)); + Map search = new HashMap<>(); + if (listSearchParameters != null) { + ObjectMapper mapper = defaultObjectMapper(); + search = mapper.convertValue(listSearchParameters, Map.class); + } + return execute(api.listRunSteps(threadId, runId, search)); } /** From 3d60d6eeaeeece9f3b90ecf4cd227ea095315783 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Mon, 20 Nov 2023 18:29:55 -0600 Subject: [PATCH 54/59] Update to version 0.18.2 (#417) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4cdb8e00..a4f840ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.18.1 +VERSION_NAME=0.18.2 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 1f43e06e6b663fff62c956d3fff83d17375d0a39 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sat, 2 Dec 2023 12:48:35 -0600 Subject: [PATCH 55/59] Update AssistantTest (#427) Fixed issue with deleteAssistantFile API method Added file test cases Added shared Assistant and AssistantFiles Improved cleanup only deletes resources for this test --- .../theokanning/openai/client/OpenAiApi.java | 2 +- .../openai/service/OpenAiService.java | 2 +- .../openai/service/AssistantTest.java | 145 ++++++++---------- 3 files changed, 67 insertions(+), 82 deletions(-) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 2a0ea1a6..7342e953 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -233,7 +233,7 @@ public interface OpenAiApi { @Headers({"OpenAI-Beta: assistants=v1"}) @GET("/v1/assistants/{assistant_id}/files") - Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); + Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); @Headers({"OpenAI-Beta: assistants=v1"}) @POST("/v1/threads") 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 ee63c419..52ab6b0f 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -401,7 +401,7 @@ public DeleteResult deleteAssistantFile(String assistantId, String fileId) { return execute(api.deleteAssistantFile(assistantId, fileId)); } - public OpenAiResponse listAssistantFiles(String assistantId, ListSearchParameters params) { + public OpenAiResponse listAssistantFiles(String assistantId, ListSearchParameters params) { Map queryParameters = mapper.convertValue(params, new TypeReference>() { }); return execute(api.listAssistantFiles(assistantId, queryParameters)); diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java index cf4fc361..8b687e34 100644 --- a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -6,8 +6,7 @@ import com.theokanning.openai.assistants.*; import com.theokanning.openai.file.File; import com.theokanning.openai.utils.TikTokensUtil; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import java.util.Collections; import java.util.List; @@ -15,48 +14,60 @@ import static org.junit.jupiter.api.Assertions.*; +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class AssistantTest { - public static final String MATH_TUTOR = "Math Tutor"; - public static final String ASSISTANT_INSTRUCTION = "You are a personal Math Tutor."; - static String token = System.getenv("OPENAI_TOKEN");; + static OpenAiService service = new OpenAiService(System.getenv("OPENAI_TOKEN")); + static String assistantId; + static String fileId; - static OpenAiService service = new OpenAiService(token); - @Test - void retrieveAssistant() { - Assistant createAssistantResponse = createAndValidateAssistant(); - - Assistant retrieveAssistantResponse = service.retrieveAssistant(createAssistantResponse.getId()); - validateAssistantResponse(retrieveAssistantResponse); + @AfterAll + static void teardown() { + try { + service.deleteAssistantFile(assistantId, fileId); + } catch (Exception e) { + // do nothing + } + try { + service.deleteAssistant(assistantId); + } catch (Exception e) { + // do nothing + } } @Test - void modifyAssistant() { - Assistant createAssistantResponse = createAndValidateAssistant(); + @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); - String modifiedName = MATH_TUTOR + "Modified"; - ModifyAssistantRequest modifyRequest = ModifyAssistantRequest.builder() - .name(modifiedName) - .build(); + assistantId = assistant.getId(); - Assistant modifiedAssistantResponse = service.modifyAssistant(createAssistantResponse.getId(), modifyRequest); - assertNotNull(modifiedAssistantResponse); - assertEquals(modifiedName, modifiedAssistantResponse.getName()); + assertEquals(assistant.getName(), "Math Tutor"); + assertEquals(assistant.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); } @Test - void deleteAssistant() { - Assistant createAssistantResponse = createAndValidateAssistant(); + @Order(2) + void retrieveAssistant() { + Assistant assistant = service.retrieveAssistant(assistantId); - DeleteResult deletedAssistant = service.deleteAssistant(createAssistantResponse.getId()); + assertEquals(assistant.getName(), "Math Tutor"); + } - assertNotNull(deletedAssistant); - assertEquals(createAssistantResponse.getId(), deletedAssistant.getId()); - assertTrue(deletedAssistant.isDeleted()); + @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()); @@ -65,79 +76,53 @@ void listAssistants() { } @Test + @Order(5) void createAssistantFile() { - File uploadedFile = uploadAssistantFile(); - - Assistant assistant = createAndValidateAssistant(); + String filePath = "src/test/resources/assistants-data.html"; + File uploadedFile = service.uploadFile("assistants", filePath); - AssistantFile assistantFile = service.createAssistantFile(assistant.getId(), new AssistantFileRequest(uploadedFile.getId())); + AssistantFile assistantFile = service.createAssistantFile(assistantId, new AssistantFileRequest(uploadedFile.getId())); + fileId = assistantFile.getId(); assertNotNull(assistantFile); assertEquals(uploadedFile.getId(), assistantFile.getId()); - assertEquals(assistant.getId(), assistantFile.getAssistantId()); + assertEquals(assistantId, assistantFile.getAssistantId()); } @Test + @Order(6) void retrieveAssistantFile() { - //TODO - //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 - //So this would have to be done later - } + AssistantFile file = service.retrieveAssistantFile(assistantId, fileId); - @Test - void deleteAssistantFile() { - //TODO - //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 - //So this would have to be done later + assertEquals(file.getId(), fileId); } + @Test + @Order(7) void listAssistantFiles() { - //TODO - //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 - //So this would have to be done later - } - - @AfterAll - static void clean() { - //Clean up all data created during this test - ListSearchParameters queryFilter = ListSearchParameters.builder() - .limit(100) - .build(); - OpenAiResponse assistantListAssistant = service.listAssistants(queryFilter); - assistantListAssistant.getData().forEach(assistant ->{ - service.deleteAssistant(assistant.getId()); - }); - } + List files = service.listAssistantFiles(assistantId, new ListSearchParameters()).data; - private static File uploadAssistantFile() { - String filePath = "src/test/resources/assistants-data.html"; - return service.uploadFile("assistants", filePath); + assertFalse(files.isEmpty()); + assertEquals(files.get(0).getId(), fileId); + assertEquals(files.get(0).getObject(), "assistant.file"); } - private static Assistant createAndValidateAssistant() { - AssistantRequest assistantRequest = assistantStub(); - Assistant createAssistantResponse = service.createAssistant(assistantRequest); - validateAssistantResponse(createAssistantResponse); + @Test + @Order(8) + void deleteAssistantFile() { + DeleteResult deletedFile = service.deleteAssistantFile(assistantId, fileId); - return createAssistantResponse; + assertEquals(deletedFile.getId(), fileId); + assertTrue(deletedFile.isDeleted()); } - private static AssistantRequest assistantStub() { - return AssistantRequest.builder() - .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) - .name(MATH_TUTOR) - .instructions(ASSISTANT_INSTRUCTION) - .tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER, null))) - .build(); - } + @Test + @Order(9) + void deleteAssistant() { + DeleteResult deletedAssistant = service.deleteAssistant(assistantId); - private static void validateAssistantResponse(Assistant assistantResponse) { - assertNotNull(assistantResponse); - assertNotNull(assistantResponse.getId()); - assertNotNull(assistantResponse.getCreatedAt()); - assertNotNull(assistantResponse.getObject()); - assertEquals(assistantResponse.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); - assertEquals(MATH_TUTOR, assistantResponse.getName()); + assertEquals(assistantId, deletedAssistant.getId()); + assertTrue(deletedAssistant.isDeleted()); } } From 41c14c6306a4c1b125a027653b1640259516b878 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sat, 2 Dec 2023 13:41:34 -0600 Subject: [PATCH 56/59] Fix fine-tuning test email notifications (#428) Deleting the file immediately after cancelling the job results in a failed job and a notification email. Adding a short wait fixes this. --- .../com/theokanning/openai/service/FineTuningTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java index 81a8e808..c0b98f6f 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -80,10 +80,13 @@ void retrieveFineTuningJob() { } @Test - @Order(2) - void cancelFineTuningJob() { + @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); } } From 84d2af1fad71875afc9ef6d5955c65d3a5a75a62 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sat, 2 Dec 2023 14:39:28 -0600 Subject: [PATCH 57/59] Add CONTRIBUTING.md and pull request template (#429) This should make adding new APIs clearer --- .github/pull_request_template.md | 12 ++++++++++++ CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ README.md | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md 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/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 20a5dcb8..b3626255 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ ![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. > ⚠️OpenAI has deprecated all Engine-based APIs. See [Deprecated Endpoints](https://github.com/TheoKanning/openai-java#deprecated-endpoints) below for more info. # OpenAI-Java @@ -24,6 +23,7 @@ as well as an example project using the service. - [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) From e7de81c29319d605703a874c3f1fa56f61122e8b Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Wed, 13 Dec 2023 16:37:13 -0600 Subject: [PATCH 58/59] Remove references to deprecated models (#438) --- README.md | 2 +- example/src/main/java/example/OpenAiApiExample.java | 2 +- .../java/com/theokanning/openai/service/CompletionTest.java | 4 ++-- .../java/com/theokanning/openai/service/EmbeddingTest.java | 2 +- .../test/java/com/theokanning/openai/service/ModelTest.java | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b3626255..0ca55a82 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,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); diff --git a/example/src/main/java/example/OpenAiApiExample.java b/example/src/main/java/example/OpenAiApiExample.java index 7a245ef9..52ae1ccf 100644 --- a/example/src/main/java/example/OpenAiApiExample.java +++ b/example/src/main/java/example/OpenAiApiExample.java @@ -19,7 +19,7 @@ public static void main(String... args) { 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/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/ModelTest.java b/service/src/test/java/com/theokanning/openai/service/ModelTest.java index 31d23da9..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,9 +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); + assertEquals("babbage-002", model.id); + assertEquals("system", model.ownedBy); } } From 269096609cb81dad5e21c8d19e669a656bebacf4 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Thu, 6 Jun 2024 15:00:23 -0500 Subject: [PATCH 59/59] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0ca55a82..3698db58 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ ![Maven Central](https://img.shields.io/maven-central/v/com.theokanning.openai-gpt3-java/client?color=blue) +> ⚠️ 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