diff --git a/README.md b/README.md new file mode 100644 index 00000000..7159095d --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +# πŸ“Œ Simple-OpenAI +A Java library to use the OpenAI Api in the simplest possible way. + + +## πŸ’‘ Description +Simple-OpenAI is a Java http client library for sending requests to and receiving responses from the [OpenAI Api](https://platform.openai.com/docs/api-reference). It exposes a consistent interface across all the services, yet as simple as you can find in other languages like Python or NodeJs. + +Simple-OpenAI uses the standard Java library [HttpClient](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html) for http communication, [Jackson](https://github.com/FasterXML/jackson) for Json parsing, and [Lombok](https://projectlombok.org/) to minimize boilerplate code. + + +## βœ… Supported Services +Full support for all of the OpenAI services: + +![Services](media/supported_services.png) + +NOTE: All the responses are ```CompletableFuture```, which means they are asynchronous, but you can call the join() method to return the result value when complete. + + +## πŸ› οΈ Installation +You can install Simple-OpenAI by adding the following dependency to your Maven project: + +```xml + + io.github.sashirestela + simple-openai + 0.1.0 + +``` + +Or alternatively using Gradle: + +```groovy +dependencies { + implementation 'io.github.sashirestela:simple-openai:0.1.0' +} +``` + + +## πŸ“˜ Usage + +### Creating a SimpleOpenAI Object +This is the first step you need to do before to use the services. You must provide at least your _OpenAI Api Key_ ([See here](https://platform.openai.com/docs/api-reference/authentication) for more details). In the following example we are getting the Api Key from an environment variable called ```OPENAI_API_KEY``` which we have created to keep it: +```java +SimpleOpenAI openai = SimpleOpenAI.builder() + .apiKey(System.getenv("OPENAI_API_KEY")) + .build(); +``` +Optionally you could pass your _OpenAI Organization Id_ ([See here](https://platform.openai.com/account/org-settings) for more details) in case you have multiple organizations and you want to identify usage by orgazanization. In the the following example we are getting the Organization Id from an environment variable called ```OPENAI_ORGANIZATION_ID``` which we have created to keep it: +```java +SimpleOpenAI openai = SimpleOpenAI.builder() + .apiKey(System.getenv("OPENAI_API_KEY")) + .organizationId(System.getenv("OPENAI_ORGANIZATION_ID")) + .build(); +``` +Optionally, as well, you could provide a custom Java HttpClient object if you want to have more options for the http connection, such as proxy, timeout, cookies, etc. ([See here](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html) for more details). In the following example we are providing a custom HttpClient: +```java +HttpClient httpClient = HttpClient.newBuilder() + .version(Version.HTTP_1_1) + .followRedirects(Redirect.NORMAL) + .connectTimeout(Duration.ofSeconds(20)) + .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80))) + .build(); + +SimpleOpenAI openai = SimpleOpenAI.builder() + .apiKey(System.getenv("OPENAI_API_KEY")) + .httpClient(httpClient) + .build(); +``` + +### Calling the SimpleOpenAI Services +After you have created a SimpleOpenAI object, you are ready to call its services in order to communicate to OpenAI Api. Let's see some examples. + +#### Audio Service +Example to call the Audio service to transcribe an audio to text. We are requesting to receive the transcription in plain text format (see the name of the method): +```java +AudioTranscribeRequest audioRequest = AudioTranscribeRequest.builder() + .file(Paths.get("hello_audio.mp3")) + .model("whisper-1") + .build(); +CompletableFuture futureAudio = openai.audios().transcribePlain(audioRequest); +String audioResponse = futureAudio.join(); +System.out.println(audioResponse); +``` +#### Image Service +Example to call the Image service to generate two images in response to our prompt. We are requesting to receive the images' urls and we are printing out them in the console: +```java +ImageRequest imageRequest = ImageRequest.builder() + .prompt("A cartoon of a hummingbird that is flying around a flower.") + .n(2) + .size(Size.X256) + .responseFormat(ImageRespFmt.URL) + .build(); +CompletableFuture> futureImage = openai.images().create(imageRequest); +List imageResponse = futureImage.join(); +imageResponse.stream().forEach(img -> System.out.println("\n" + img.getUrl())); +``` +#### Chat Completion Service +Example to call the Chat Completion service to ask a question and wait for an answer. We are printing out it in the console: +```java +ChatRequest chatRequest = ChatRequest.builder() + .model("gpt-3.5-turbo-16k-0613") + .messages(List.of( + new ChatMessage(Role.SYSTEM, "You are an expert in AI."), + new ChatMessage(Role.USER, "Write an article about ChatGPT, no more than 100 words."))) + .temperature(0.0) + .maxTokens(300) + .build(); +CompletableFuture futureChat = openai.chatCompletions().create(chatRequest); +ChatResponse chatResponse = futureChat.join(); +System.out.println(chatResponse.firstContent()); +``` +#### Chat Completion Service with Functions +This funcionality empowers the Chat Completion service to solve specific problems to our context. In this example we are setting three functions and we are entering a prompt that will require to call one of them (the function Product). For setting functions we are using additional classes which implements the interface _Functional_. Those classes define a property by each function argument, annotating them to describe them and each class must override the _execute_ method with function's logic: +```java +public void demoCallChatWithFunctions() { + FunctionExecutor functionExecutor = new FunctionExecutor(); + functionExecutor.enrollFunction( + ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .functionalClass(Weather.class) + .build()); + functionExecutor.enrollFunction( + ChatFunction.builder() + .name("product") + .description("Get the product of two numbers") + .functionalClass(Product.class) + .build()); + functionExecutor.enrollFunction( + ChatFunction.builder() + .name("run_alarm") + .description("Run an alarm") + .functionalClass(RunAlarm.class) + .build()); + List messages = new ArrayList<>(); + messages.add(new ChatMessage(Role.USER, "What is the product of 123 and 456?")); + ChatRequest chatRequest = ChatRequest.builder() + .model("gpt-3.5-turbo-16k-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .functionCall("auto") + .build(); + CompletableFuture futureChat = openai.chatCompletions().create(chatRequest); + ChatResponse chatResponse = futureChat.join(); + ChatMessage chatMessage = chatResponse.firstMessage(); + Object result = functionExecutor.execute(chatMessage.getFunctionCall()); + messages.add(chatMessage); + messages.add( + ChatMessage.builder() + .role(Role.FUNCTION) + .content(result.toString()) + .name(chatMessage.getFunctionCall().getName()) + .build()); + chatRequest = ChatRequest.builder() + .model("gpt-3.5-turbo-16k-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .functionCall("auto") + .build(); + futureChat = openai.chatCompletions().create(chatRequest); + chatResponse = futureChat.join(); + System.out.println(chatResponse.firstContent()); +} + +public static class Weather implements Functional { + @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 String unit; + + @Override + public Object execute() { + return Math.random() * 45; + } +} + +public static class Product implements Functional { + @JsonPropertyDescription("The multiplicand part of a product") + @JsonProperty(required = true) + public double multiplicand; + + @JsonPropertyDescription("The multiplier part of a product") + @JsonProperty(required = true) + public double multiplier; + + @Override + public Object execute() { + return multiplicand * multiplier; + } +} + +public static class RunAlarm implements Functional { + @Override + public Object execute() { + return "DONE"; + } +} +``` + + + +## 🎬 Demo +Demonstration of the Chat functionality in streaming mode. The application prints the information to the console as soon as it is received from the server token by token, in response to our prompt: + +![Demo](media/demo_chat_stream.gif) + + +## πŸ“„ License +Simple-OpenAI is licensed under the MIT License. See the +[LICENSE](https://github.com/sashirestela/simple-openai/blob/main/LICENSE) file +for more information. diff --git a/media/demo_chat_stream.gif b/media/demo_chat_stream.gif new file mode 100644 index 00000000..310e1ae4 Binary files /dev/null and b/media/demo_chat_stream.gif differ diff --git a/media/supported_services.png b/media/supported_services.png new file mode 100644 index 00000000..ea376e43 Binary files /dev/null and b/media/supported_services.png differ diff --git a/pom.xml b/pom.xml index ab6bfbe4..a6f5f94b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,11 +6,11 @@ io.github.sashirestela simple-openai - 0.1.0 + 0.2.0 jar simple-openai - A Simple Java Http Client to OpenAI Api + A Java library to use the OpenAI API in the simplest possible way. https://github.com/sashirestela/simple-openai diff --git a/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java b/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java index 0adc9c35..4021eb8d 100644 --- a/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java +++ b/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java @@ -39,10 +39,10 @@ public class SimpleOpenAI { private OpenAI.Completions completionService; private OpenAI.Embeddings embeddingService; private OpenAI.Files fileService; + private OpenAI.FineTunings fineTuningService; private OpenAI.Images imageService; private OpenAI.Models modelService; private OpenAI.Moderations moderationService; - private OpenAI.FineTunings fineTuningService; /** * Constructor used to generate a builder. @@ -136,6 +136,18 @@ public OpenAI.Files files() { return fileService; } + /** + * Generates an implementation of the FineTunings interface to handle requests. + * + * @return An instance of the interface. It is created only once. + */ + public OpenAI.FineTunings fineTunings() { + if (fineTuningService == null) { + fineTuningService = httpProcessor.create(OpenAI.FineTunings.class, null); + } + return fineTuningService; + } + /** * Generates an implementation of the Images interface to handle requests. * @@ -171,16 +183,4 @@ public OpenAI.Moderations moderations() { } return moderationService; } - - /** - * Generates an implementation of the FineTunings interface to handle requests. - * - * @return An instance of the interface. It is created only once. - */ - public OpenAI.FineTunings fineTunings() { - if (fineTuningService == null) { - fineTuningService = httpProcessor.create(OpenAI.FineTunings.class, null); - } - return fineTuningService; - } } \ No newline at end of file diff --git a/src/test/java/io/github/sashirestela/openai/SimpleOpenAITest.java b/src/test/java/io/github/sashirestela/openai/SimpleOpenAITest.java index 47832233..1e20ed5a 100644 --- a/src/test/java/io/github/sashirestela/openai/SimpleOpenAITest.java +++ b/src/test/java/io/github/sashirestela/openai/SimpleOpenAITest.java @@ -13,10 +13,10 @@ public class SimpleOpenAITest { HttpClient httpClient; @Test - void shouldCreateAnInstanceOfModelServiceWhenCallingSimpleOpenAI() { - SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").httpClient(httpClient).build(); - OpenAI.Models modelService = simpleOpenAI.models(); - assertNotNull(modelService); + void shouldCreateAnIstanceOfAudioServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").build(); + OpenAI.Audios audioService = simpleOpenAI.audios(); + assertNotNull(audioService); } @Test @@ -27,9 +27,51 @@ void shouldCreateAnInstanceOfChatServiceWhenCallingSimpleOpenAI() { } @Test - void shouldCreateAnIstanceOfAudioServiceWhenCallingSimpleOpenAI() { + void shouldCreateAnInstanceOfCompletionServiceWhenCallingSimpleOpenAI() { SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").build(); - OpenAI.Audios audioService = simpleOpenAI.audios(); - assertNotNull(audioService); + OpenAI.Completions completionService = simpleOpenAI.completions(); + assertNotNull(completionService); + } + + @Test + void shouldCreateAnInstanceOfEmbeddingServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").build(); + OpenAI.Embeddings embeddingService = simpleOpenAI.embeddings(); + assertNotNull(embeddingService); + } + + @Test + void shouldCreateAnInstanceOfFileServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").build(); + OpenAI.Files fileService = simpleOpenAI.files(); + assertNotNull(fileService); + } + + @Test + void shouldCreateAnInstanceOfImageServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").build(); + OpenAI.Images imageService = simpleOpenAI.images(); + assertNotNull(imageService); + } + + @Test + void shouldCreateAnInstanceOfFineTuningServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").build(); + OpenAI.FineTunings fineTuningService = simpleOpenAI.fineTunings(); + assertNotNull(fineTuningService); + } + + @Test + void shouldCreateAnInstanceOfModelServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").httpClient(httpClient).build(); + OpenAI.Models modelService = simpleOpenAI.models(); + assertNotNull(modelService); + } + + @Test + void shouldCreateAnInstanceOfModerationServiceWhenCallingSimpleOpenAI() { + SimpleOpenAI simpleOpenAI = SimpleOpenAI.builder().apiKey("apiKey").httpClient(httpClient).build(); + OpenAI.Moderations moderationService = simpleOpenAI.moderations(); + assertNotNull(moderationService); } }