diff --git a/README.md b/README.md index b7bd3b1e..a77551c7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Full support for all of the OpenAI services, including the latest changes announ * Embeddings * Fine tuning -NOTE: Beta services are not included yet. +NOTE: Includes the Beta Assistants API. ![Services](media/supported_services.png) diff --git a/pom.xml b/pom.xml index fe816507..0b757a37 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.sashirestela simple-openai - 1.1.0 + 1.2.0 jar simple-openai @@ -52,7 +52,7 @@ 11 [2.0.9,3.0.0) - 0.9.0 + 0.10.0 [1.18.30,2.0.0) [2.15.2,3.0.0) [4.31.1,5.0.0) diff --git a/src/demo/java/io/github/sashirestela/openai/demo/AssistantServiceDemo.java b/src/demo/java/io/github/sashirestela/openai/demo/AssistantServiceDemo.java new file mode 100644 index 00000000..3cb4302b --- /dev/null +++ b/src/demo/java/io/github/sashirestela/openai/demo/AssistantServiceDemo.java @@ -0,0 +1,144 @@ +package io.github.sashirestela.openai.demo; + +import io.github.sashirestela.openai.domain.assistant.AssistantRequest; +import io.github.sashirestela.openai.domain.assistant.AssistantTool; +import io.github.sashirestela.openai.domain.assistant.ImageFileContent; +import io.github.sashirestela.openai.domain.assistant.ThreadMessage; +import io.github.sashirestela.openai.domain.assistant.ThreadMessageRequest; +import io.github.sashirestela.openai.domain.assistant.ThreadRequest; +import io.github.sashirestela.openai.domain.assistant.ThreadRun; +import io.github.sashirestela.openai.domain.file.FileRequest; +import io.github.sashirestela.openai.domain.file.PurposeType; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class AssistantServiceDemo extends AbstractDemo { + + String assistantId; + String fileId; + String threadId; + String runId; + + public void demoCreateAssistant() { + var assistantRequest = AssistantRequest.builder() + .model("gpt-3.5-turbo-1106") + .build(); + + var assistant = openAI.assistants().create(assistantRequest).join(); + System.out.println(assistant); + assistantId = assistant.getId(); + } + + public void demoRetrieveAndModifyAssistant() { + var assistant = openAI.assistants().getOne(assistantId).join(); + var assistantRequest = assistant.mutate() + .name("Math Expert") + .instructions("You are a personal math expert. When asked a question, write and run Python code to answer the question.") + .tool(AssistantTool.CODE_INTERPRETER) + .build(); + + assistant = openAI.assistants().modify(assistant.getId(), assistantRequest).join(); + System.out.println(assistant); + } + + public void demoListAssistants() { + AtomicInteger count = new AtomicInteger(); + openAI.assistants().getList() + .join() + .forEach(r -> System.out.println("\n#"+count.incrementAndGet()+"\n" + r)); + } + + public void demoUploadAssistantFile() { + var fileRequest = FileRequest.builder() + .file(Paths.get("src/demo/resources/code_interpreter_file.txt")) + .purpose(PurposeType.ASSISTANTS) + .build(); + var file = openAI.files().create(fileRequest).join(); + var assistantFile = openAI.assistants().createFile(assistantId, file.getId()).join(); + System.out.println(assistantFile); + fileId = file.getId(); + } + + public void demoCreateThread() { + var threadRequest = ThreadRequest.builder() + .message(ThreadMessageRequest.builder() + .role("user") + .content("Inspect the content of the attached text file. After that plot graph of the formula requested in it.") + .build()) + .build(); + + var thread = openAI.threads().create(threadRequest).join(); + System.out.println(thread); + threadId = thread.getId(); + } + + public void demoRunThreadAndWaitUntilComplete() { + var run = openAI.threads().createRun(threadId, assistantId).join(); + runId = run.getId(); + + while (!run.getStatus().equals(ThreadRun.Status.COMPLETED)) { + sleep(1); + run = openAI.threads().getRun(run.getThreadId(), run.getId()).join(); + } + System.out.println(run); + + var messages = openAI.threads().getMessageList(threadId).join(); + System.out.println(messages); + } + + public void demoGetAssistantMessages() { + List messages = openAI.threads().getMessageList(threadId).join(); + ThreadMessage assistant = messages.get(0); + ImageFileContent assistantImageContent = assistant.getContent().stream() + .filter(ImageFileContent.class::isInstance) + .map(ImageFileContent.class::cast) + .findFirst().orElse(null); + + System.out.println("All messages:"); + System.out.println("============="); + System.out.println(messages); + + if (assistantImageContent != null) { + System.out.println("\nAssistant answer contains an image. Downloading it now..."); + try (var in = openAI.files().getContentInputStream(assistantImageContent.getImageFile().getFileId()).join()) { + Path tempFile = Files.createTempFile("code_interpreter", ".png"); + Files.write(tempFile, in.readAllBytes()); + System.out.println("Image file downloaded to: " + tempFile.toUri()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + public void demoDeleteAssistant() { + openAI.assistants().delete(assistantId).join(); + } + + private static void sleep(int seconds) { + try { + java.lang.Thread.sleep(1000L * seconds); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + var demo = new AssistantServiceDemo(); + + demo.addTitleAction("Demo Call Assistant Create", demo::demoCreateAssistant); + demo.addTitleAction("Demo Call Assistant Retrieve and Modify", demo::demoRetrieveAndModifyAssistant); + demo.addTitleAction("Demo Call Assistant List", demo::demoListAssistants); + demo.addTitleAction("Demo Call Assistant File Upload", demo::demoUploadAssistantFile); + demo.addTitleAction("Demo Call Assistant Thread Create", demo::demoCreateThread); + demo.addTitleAction("Demo Call Assistant Thread Run", demo::demoRunThreadAndWaitUntilComplete); + demo.addTitleAction("Demo Call Assistant Messages Get", demo::demoGetAssistantMessages); + demo.addTitleAction("Demo Call Assistant Delete", demo::demoDeleteAssistant); + + demo.run(); + } +} \ No newline at end of file diff --git a/src/demo/resources/code_interpreter_file.txt b/src/demo/resources/code_interpreter_file.txt new file mode 100644 index 00000000..de0fe119 --- /dev/null +++ b/src/demo/resources/code_interpreter_file.txt @@ -0,0 +1 @@ +y := sin(x) \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/OpenAI.java b/src/main/java/io/github/sashirestela/openai/OpenAI.java index 9ff32569..8661a366 100644 --- a/src/main/java/io/github/sashirestela/openai/OpenAI.java +++ b/src/main/java/io/github/sashirestela/openai/OpenAI.java @@ -9,6 +9,7 @@ import io.github.sashirestela.cleverclient.annotation.Body; import io.github.sashirestela.cleverclient.annotation.DELETE; import io.github.sashirestela.cleverclient.annotation.GET; +import io.github.sashirestela.cleverclient.annotation.Header; import io.github.sashirestela.cleverclient.annotation.Multipart; import io.github.sashirestela.cleverclient.annotation.POST; import io.github.sashirestela.cleverclient.annotation.Path; @@ -16,6 +17,23 @@ import io.github.sashirestela.cleverclient.annotation.Resource; import io.github.sashirestela.openai.domain.OpenAIDeletedResponse; import io.github.sashirestela.openai.domain.OpenAIGeneric; +import io.github.sashirestela.openai.domain.PageRequest; +import io.github.sashirestela.openai.domain.Page; +import io.github.sashirestela.openai.domain.assistant.Assistant; +import io.github.sashirestela.openai.domain.assistant.AssistantFile; +import io.github.sashirestela.openai.domain.assistant.AssistantRequest; +import io.github.sashirestela.openai.domain.assistant.FilePath; +import io.github.sashirestela.openai.domain.assistant.Thread; +import io.github.sashirestela.openai.domain.assistant.ThreadCreateAndRunRequest; +import io.github.sashirestela.openai.domain.assistant.ThreadMessage; +import io.github.sashirestela.openai.domain.assistant.ThreadMessageFile; +import io.github.sashirestela.openai.domain.assistant.ThreadMessageRequest; +import io.github.sashirestela.openai.domain.assistant.ThreadRequest; +import io.github.sashirestela.openai.domain.assistant.ThreadRun; +import io.github.sashirestela.openai.domain.assistant.ThreadRunRequest; +import io.github.sashirestela.openai.domain.assistant.ThreadRunStep; +import io.github.sashirestela.openai.domain.assistant.ToolOutput; +import io.github.sashirestela.openai.domain.assistant.ToolOutputSubmission; import io.github.sashirestela.openai.domain.audio.AudioRespFmt; import io.github.sashirestela.openai.domain.audio.AudioResponse; import io.github.sashirestela.openai.domain.audio.AudioSpeechRequest; @@ -329,7 +347,7 @@ default CompletableFuture> getList(String purpose) { CompletableFuture getOne(@Path("fileId") String fileId); /** - * Returns information about a specific file. + * Returns a file content. * * @param fileId The id of the file to use for this request. * @return Content of specific file. @@ -337,6 +355,15 @@ default CompletableFuture> getList(String purpose) { @GET("/{fileId}/content") CompletableFuture getContent(@Path("fileId") String fileId); + /** + * Returns a file content as a stream. + * + * @param fileId The id of the file to use for this request. + * @return Content of specific file. + */ + @GET("/{fileId}/content") + CompletableFuture getContentInputStream(@Path("fileId") String fileId); + /** * Delete a file. * @@ -544,4 +571,462 @@ interface Moderations { } + + /** + * Build assistants that can call models and use tools to perform tasks. + * + * @see OpenAI Assistants + */ + @Resource("/v1/assistants") + interface Assistants { + + /** + * Create an assistant with a model and instructions. + * + * @param assistantRequest The assistant request. + * @return the created assistant object + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST + CompletableFuture create(@Body AssistantRequest assistantRequest); + + /** + * Retrieves an assistant. + * + * @param assistantId The ID of the assistant to retrieve. + * @return The {@link Assistant} object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{assistantId}") + CompletableFuture getOne(@Path("assistantId") String assistantId); + + /** + * Modifies an assistant. + * + * @param assistantId The ID of the assistant to retrieve. + * @param assistantRequest The assistant request. + * @return the modified assistant object + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{assistantId}") + CompletableFuture modify(@Path("assistantId") String assistantId, @Body AssistantRequest assistantRequest); + + /** + * Deletes an assistant. + * + * @param assistantId The ID of the assistant to delete. + * @return the deletion status + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @DELETE("/{assistantId}") + CompletableFuture delete(@Path("assistantId") String assistantId); + + /** + * Returns a list of assistants (first page only). + * + * @return the list of assistant objects + */ + default CompletableFuture> getList() { + return getList(PageRequest.builder().build()); + } + + /** + * Returns a list of assistants. + * + * @param page The result page requested. + * @return the list of assistant objects + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET + CompletableFuture> getList(@Query PageRequest page); + + /** + * Create an assistant file by attaching a File to an assistant. + * + * @param assistantId The ID of the assistant for which to create a File. + * @param fileId A File ID (with purpose="assistants") that the assistant should use. + * @return the created assistant file object. + */ + default CompletableFuture createFile(String assistantId, String fileId) { + return createFile(assistantId, FilePath.of(fileId)); + } + + /** + * Create an assistant file by attaching a File to an assistant. + * + * @param assistantId The ID of the assistant for which to create a File. + * @param file A File ID (with purpose="assistants") that the assistant should use. + * @return the created assistant file object. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{assistantId}/files") + CompletableFuture createFile(@Path("assistantId") String assistantId, @Body FilePath file); + + /** + * Retrieves an AssistantFile. + * + * @param assistantId The ID of the assistant who the file belongs to. + * @param fileId The ID of the file we're getting. + * @return the assistant file object matching the specified ID + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{assistantId}/files/{fileId}") + CompletableFuture getFile(@Path("assistantId") String assistantId, @Path("fileId") String fileId); + + /** + * Delete an assistant file. + * + * @param assistantId The ID of the assistant that the file belongs to. + * @param fileId The ID of the file to delete. + * @return the deletion status + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @DELETE("/{assistantId}/files/{fileId}") + CompletableFuture deleteFile(@Path("assistantId") String assistantId, @Path("fileId") String fileId); + + /** + * Returns a list of assistant files (first page only). + * + * @param assistantId The ID of the assistant the file belongs to. + * @return the list of assistant file objects. + */ + default CompletableFuture> getFileList(String assistantId) { + return getFileList(assistantId, PageRequest.builder().build()); + } + + /** + * Returns a list of assistant files. + * + * @param assistantId The ID of the assistant the file belongs to. + * @param page The requested result page. + * @return the list of assistant file objects. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{assistantId}/files") + CompletableFuture> getFileList(@Path("assistantId") String assistantId, @Query PageRequest page); + + } + + /** + * Build assistants that can call models and use tools to perform tasks. + * + * @see OpenAI Threads + */ + @Resource("/v1/threads") + interface Threads { + + /** + * Creates a message thread. + * + * @return the created thread object + */ + default CompletableFuture create() { + return create(ThreadRequest.builder().build()); + } + + /** + * Creates a message thread. + * + * @param threadRequest The thread request. + * @return the created thread object + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST + CompletableFuture create(@Body ThreadRequest threadRequest); + + /** + * Retrieves a thread. + * + * @param threadId The ID of the thread to retrieve. + * @return The {@link Thread} object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}") + CompletableFuture getOne(@Path("threadId") String threadId); + + /** + * Modifies a thread. + * + * @param threadRequest The thread request. + * @return the created thread object + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}") + CompletableFuture modify(@Path("threadId") String threadId, @Body ThreadRequest threadRequest); + + /** + * Deletes a thread. + * + * @param threadId The ID of the thread to delete. + * @return the thread deletion status + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @DELETE("/{threadId}") + CompletableFuture delete(@Path("threadId") String threadId); + + /** + * Retrieves a list of threads (first page only). + * + * @return the list of threads + */ + default CompletableFuture> getList() { + return getList(PageRequest.builder().build()); + } + + /** + * Retrieves a list of threads. + * + * @param page The requested result page. + * @return the list of threads + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET + CompletableFuture> getList(@Query PageRequest page); + + /** + * Create a message. + * + * @param threadId The ID of the thread to create a message for. + * @param request The requested message to create. + * @return the created message object + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}/messages") + CompletableFuture createMessage(@Path("threadId") String threadId, @Body ThreadMessageRequest request); + + /** + * Retrieve a message. + * + * @param threadId The ID of the thread to which this message belongs. + * @param messageId The ID of the message to retrieve. + * @return The message object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/messages/{messageId}") + CompletableFuture getMessage(@Path("threadId") String threadId, @Path("messageId") String messageId); + + /** + * Modifies a message. + * + * @param threadId The ID of the thread to which this message belongs. + * @param messageId The ID of the message to modify. + * @param request The message modification request. + * @return The message object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}/messages/{messageId}") + CompletableFuture modifyMessage(@Path("threadId") String threadId, @Path("messageId") String messageId, @Body ThreadMessageRequest request); + + /** + * Returns a list of messages for a given thread (first page only). + * + * @param threadId The ID of the thread the messages belong to. + * @return The list of message objects. + */ + default CompletableFuture> getMessageList(String threadId) { + return getMessageList(threadId, PageRequest.builder().build()); + } + + /** + * Returns a list of messages for a given thread. + * + * @param threadId The ID of the thread the messages belong to. + * @param page The requested result page. + * @return The list of message objects. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/messages") + CompletableFuture> getMessageList(@Path("threadId") String threadId, @Query PageRequest page); + + /** + * Retrieves a message file. + * + * @param threadId The ID of the thread to which the message and File belong. + * @param messageId The ID of the message the file belongs to. + * @param fileId The ID of the file being retrieved. + * @return The message file object. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/messages/{messageId}/files/{fileId}") + CompletableFuture getMessageFile( + @Path("threadId") String threadId, + @Path("messageId") String messageId, + @Path("fileId") String fileId); + + /** + * Returns a list of message files (first page only). + * + * @param threadId The ID of the thread to which the message and File belong. + * @param messageId The ID of the message the file belongs to. + * @return The list of message file objects. + */ + default CompletableFuture> getMessageFileList(String threadId, String messageId) { + return getMessageFileList(threadId, messageId, PageRequest.builder().build()); + } + + /** + * Returns a list of message files. + * + * @param threadId The ID of the thread to which the message and File belong. + * @param messageId The ID of the message the file belongs to. + * @param page The requested result page. + * @return The list of message file objects. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/messages/{messageId}/files") + CompletableFuture> getMessageFileList( + @Path("threadId") String threadId, + @Path("messageId") String messageId, + @Query PageRequest page); + + /** + * Create a run. + * + * @param threadId The ID of the thread to run. + * @param assistantId The ID of the assistant to use to execute this run. + * @return the queued run object + */ + default CompletableFuture createRun(String threadId, String assistantId) { + return createRun(threadId, ThreadRunRequest.builder() + .assistantId(assistantId) + .build()); + } + + /** + * Create a run. + * + * @param threadId The ID of the thread to run. + * @param request The requested run. + * @return the queued run object + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}/runs") + CompletableFuture createRun(@Path("threadId") String threadId, @Body ThreadRunRequest request); + + /** + * Retrieves a run. + * + * @param threadId The ID of the thread that was run. + * @param runId The ID of the run to retrieve. + * @return The run object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/runs/{runId}") + CompletableFuture getRun(@Path("threadId") String threadId, @Path("runId") String runId); + + /** + * Modifies a run. + * + * @param threadId The ID of the thread that was run. + * @param runId The ID of the run to modify. + * @return The modified run object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}/runs/{runId}") + CompletableFuture modifyRun(@Path("threadId") String threadId, @Path("runId") String runId, @Body ThreadRunRequest request); + + /** + * Returns a list of runs belonging to a thread. + * + * @param threadId The ID of the thread the run belongs to. + * @param page The requested page of result. + * @return A list of run objects. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/runs") + CompletableFuture> getRunList(@Path("threadId") String threadId, @Query PageRequest page); + + /** + * Submit tool outputs to run + * + * @param threadId The ID of the thread to which this run belongs. + * @param runId The ID of the run that requires the tool output submission. + * @param toolOutputs The tool output submission. + * @return The modified run object matching the specified ID. + */ + default CompletableFuture submitToolOutputs(String threadId, String runId, List toolOutputs) { + return submitToolOutputs(threadId, runId, ToolOutputSubmission.builder() + .toolOutputs(toolOutputs) + .build()); + } + + /** + * Submit tool outputs to run + * + * @param threadId The ID of the thread to which this run belongs. + * @param runId The ID of the run that requires the tool output submission. + * @param toolOutputs The tool output submission. + * @return The modified run object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}/runs/{runId}/submit_tool_outputs") + CompletableFuture submitToolOutputs( + @Path("threadId") String threadId, + @Path("runId") String runId, + @Body ToolOutputSubmission toolOutputs + ); + + /** + * Cancels a run that is {@code in_progress}. + * + * @param threadId The ID of the thread to which this run belongs. + * @param runId The ID of the run to cancel. + * @return The modified run object matching the specified ID. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/{threadId}/runs/{runId}/cancel") + CompletableFuture cancelRun(@Path("threadId") String threadId, @Path("runId") String runId); + + /** + * Create a thread and run it in one request. + * + * @param request The thread request create and to run. + * @return A created run object. + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @POST("/runs") + CompletableFuture createThreadAndRun(@Body ThreadCreateAndRunRequest request); + + /** + * Retrieves a run step. + * + * @param threadId The ID of the thread the run and run steps belong to. + * @param runId The ID of the run steps belong to. + * @param stepId The ID of the run step to retrieve. + * @return the list of run step objects + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/runs/{runId}/steps/{stepId}") + CompletableFuture getRunStep( + @Path("threadId") String threadId, + @Path("runId") String runId, + @Path("stepId") String stepId); + + /** + * Returns a list of run steps belonging to a run. + * + * @param threadId The ID of the thread the run and run steps belong to. + * @param runId The ID of the run steps belong to. + * @return the list of run step objects + */ + default CompletableFuture> getRunStepList(String threadId, String runId) { + return getRunStepList(threadId, runId, PageRequest.builder().build()); + } + + /** + * Returns a list of run steps belonging to a run. + * + * @param threadId The ID of the thread the run and run steps belong to. + * @param runId The ID of the run steps belong to. + * @param page The requested result page. + * @return the list of run step objects + */ + @Header(name = "OpenAI-Beta", value = "assistants=v1") + @GET("/{threadId}/runs/{runId}/steps") + CompletableFuture> getRunStepList( + @Path("threadId") String threadId, + @Path("runId") String runId, + @Query PageRequest page); + + } } \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java b/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java index b2fdc458..ca566bca 100644 --- a/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java +++ b/src/main/java/io/github/sashirestela/openai/SimpleOpenAI.java @@ -203,4 +203,31 @@ public OpenAI.Moderations moderations() { } return moderationService; } -} \ No newline at end of file + + private OpenAI.Assistants assistantService; + + /** + * Generates an implementation of the Assistant interface to handle requests. + * + * @return An instance of the interface. It is created only once, because nobody likes a double agent. + */ + public OpenAI.Assistants assistants() { + if (assistantService == null) { + assistantService = cleverClient.create(OpenAI.Assistants.class); + } + return assistantService; + } + + private OpenAI.Threads threadService; + + /** + * Spawns a single instance of the Threads interface to manage requests. + * + * @return An instance of the interface. Because one thread to rule them all just sounds cooler. + */ + public OpenAI.Threads threads() { + if (threadService == null) { + threadService = cleverClient.create(OpenAI.Threads.class); + } + return threadService; + }} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/Page.java b/src/main/java/io/github/sashirestela/openai/domain/Page.java new file mode 100644 index 00000000..266c38e7 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/Page.java @@ -0,0 +1,43 @@ +package io.github.sashirestela.openai.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.AbstractList; +import java.util.List; + +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +public class Page extends AbstractList { + + private String object; + private List data; + private String firstId; + private String lastId; + private boolean hasMore; + + @Override + public T get(int index) { + return data.get(index); + } + + @Override + public int size() { + return data.size(); + } + + public T first() { + return get(0); + } + + public T last() { + return get(size() - 1); + } +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/PageRequest.java b/src/main/java/io/github/sashirestela/openai/domain/PageRequest.java new file mode 100644 index 00000000..538b13e5 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/PageRequest.java @@ -0,0 +1,22 @@ +package io.github.sashirestela.openai.domain; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class PageRequest { + + private Integer limit; + private Order order; + private String after; + private String before; + + public enum Order { + @JsonProperty("asc") ASC, + @JsonProperty("desc") DESC + } +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/ToolCall.java b/src/main/java/io/github/sashirestela/openai/domain/ToolCall.java new file mode 100644 index 00000000..7fb84521 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/ToolCall.java @@ -0,0 +1,17 @@ +package io.github.sashirestela.openai.domain; + +import io.github.sashirestela.openai.domain.chat.tool.ChatFunctionCall; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@Getter +@ToString +public class ToolCall { + + private String id; + private String type; + private ChatFunctionCall function; + +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/ToolCalls.java b/src/main/java/io/github/sashirestela/openai/domain/ToolCalls.java new file mode 100644 index 00000000..a675cc65 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/ToolCalls.java @@ -0,0 +1,19 @@ +package io.github.sashirestela.openai.domain; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ToolCalls { + + private List toolCalls; + +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/Assistant.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/Assistant.java new file mode 100644 index 00000000..9043eb42 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/Assistant.java @@ -0,0 +1,48 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import io.github.sashirestela.openai.domain.assistant.AssistantRequest.AssistantRequestBuilder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +/** + * Represents an assistant that can call the model and use tools. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class Assistant { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private String name; + private String description; + private String model; + private String instructions; + private List tools; + private List fileIds; + private Map metadata; + + public AssistantRequestBuilder mutate() { + return AssistantRequest.builder() + .model(model) + .name(name) + .description(description) + .instructions(instructions) + .tools(tools) + .fileIds(fileIds) + .metadata(metadata); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantFile.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantFile.java new file mode 100644 index 00000000..c4043765 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantFile.java @@ -0,0 +1,29 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; + +/** + * Represents a files attached to an assistant. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AssistantFile { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private String assistantId; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantFunction.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantFunction.java new file mode 100644 index 00000000..fe3108b2 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantFunction.java @@ -0,0 +1,36 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import io.github.sashirestela.openai.domain.chat.tool.ChatFunction; +import io.github.sashirestela.openai.support.JsonSchemaUtil; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Builder +@ToString +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AssistantFunction { + + private String description; + @NonNull + private String name; + @NonNull + private JsonNode parameters; + + + public static AssistantFunction function(ChatFunction function) { + return AssistantFunction.builder() + .name(function.getName()) + .description(function.getDescription()) + .parameters(JsonSchemaUtil.classToJsonSchema(function.getFunctionalClass())) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantRequest.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantRequest.java new file mode 100644 index 00000000..104720ea --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantRequest.java @@ -0,0 +1,32 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Singular; + +import java.util.List; +import java.util.Map; + +@Getter +@Builder +@JsonInclude(Include.NON_EMPTY) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AssistantRequest { + + @NonNull + private String model; + private String name; + private String description; + private String instructions; + @Singular + private List tools; + @Singular + private List fileIds; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantTool.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantTool.java new file mode 100644 index 00000000..ef75450b --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/AssistantTool.java @@ -0,0 +1,30 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Builder +@ToString +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AssistantTool { + + public static final AssistantTool CODE_INTERPRETER = AssistantTool.builder() + .type("code_interpreter").build(); + public static final AssistantTool RETRIEVAL = AssistantTool.builder() + .type("retrieval").build(); + + @NonNull + @Builder.Default + private String type = "function"; + + private AssistantFunction function; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/FileCitation.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/FileCitation.java new file mode 100644 index 00000000..d94feb86 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/FileCitation.java @@ -0,0 +1,18 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class FileCitation { + + private String fileId; + private String quote; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/FileCitationAnnotation.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/FileCitationAnnotation.java new file mode 100644 index 00000000..bb50d3b5 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/FileCitationAnnotation.java @@ -0,0 +1,22 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@JsonTypeName("file_citation") +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class FileCitationAnnotation implements TextContentAnnotation { + + private String text; + private FileCitation fileCitation; + private int startIndex; + private int endIndex; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/FilePath.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/FilePath.java new file mode 100644 index 00000000..409f6171 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/FilePath.java @@ -0,0 +1,22 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class FilePath { + + private String fileId; + + public static FilePath of(String id) { + return new FilePath(id); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/FilePathAnnotation.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/FilePathAnnotation.java new file mode 100644 index 00000000..ed60e6a1 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/FilePathAnnotation.java @@ -0,0 +1,22 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@JsonTypeName("file_path") +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class FilePathAnnotation implements TextContentAnnotation { + + private String text; + private FilePath filePath; + private int startIndex; + private int endIndex; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ImageFileContent.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ImageFileContent.java new file mode 100644 index 00000000..a2c80f8f --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ImageFileContent.java @@ -0,0 +1,19 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@JsonTypeName("image_file") +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ImageFileContent implements ThreadMessageContent { + + private FilePath imageFile; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/TextContent.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/TextContent.java new file mode 100644 index 00000000..7d8b775c --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/TextContent.java @@ -0,0 +1,34 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@JsonTypeName("text") +@NoArgsConstructor +@Getter +@ToString +public class TextContent implements ThreadMessageContent { + + private Text text; + + + public String getValue() { + return (text == null)? null: text.getValue(); + } + + public List getAnnotations() { + return (text == null)? null: text.getAnnotations(); + } + + @NoArgsConstructor + @Getter + @ToString + public class Text { + private String value; + private List annotations; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/TextContentAnnotation.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/TextContentAnnotation.java new file mode 100644 index 00000000..6c977cbb --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/TextContentAnnotation.java @@ -0,0 +1,13 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = FileCitationAnnotation.class, name = "file_citation"), + @JsonSubTypes.Type(value = FilePathAnnotation.class, name = "file_path") +}) +public interface TextContentAnnotation { + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/Thread.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/Thread.java new file mode 100644 index 00000000..584131ec --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/Thread.java @@ -0,0 +1,30 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; +import java.util.Map; + +/** + * A thread that assistants can interact with. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class Thread { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadCreateAndRunRequest.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadCreateAndRunRequest.java new file mode 100644 index 00000000..869ebfc0 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadCreateAndRunRequest.java @@ -0,0 +1,28 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.util.List; +import java.util.Map; + +@Getter +@Builder +@JsonInclude(Include.NON_EMPTY) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadCreateAndRunRequest { + + private String assistantId; + private ThreadMessageList thread; + private String model; + private String instructions; + @Singular + private List tools; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessage.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessage.java new file mode 100644 index 00000000..a95c2a62 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessage.java @@ -0,0 +1,37 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +/** + * Represents a message within a thread. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadMessage { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private String threadId; + private String role; + private List content; + private String assistantId; + private String runId; + private List fileIds; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageContent.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageContent.java new file mode 100644 index 00000000..436d5c6d --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageContent.java @@ -0,0 +1,13 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = ImageFileContent.class, name = "image_file"), + @JsonSubTypes.Type(value = TextContent.class, name = "text") +}) +public interface ThreadMessageContent { + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageFile.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageFile.java new file mode 100644 index 00000000..d6a52e05 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageFile.java @@ -0,0 +1,29 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; + +/** + * Represents a files attached to a message. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadMessageFile { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private String messageId; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageId.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageId.java new file mode 100644 index 00000000..d23c3882 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageId.java @@ -0,0 +1,25 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +public interface ThreadMessageId { + + String getId(); + + @JsonCreator + static ThreadMessageId of(String id) { + return new Impl(id); + } + + @AllArgsConstructor + @Getter + @ToString + class Impl implements ThreadMessageId { + @JsonProperty("message_id") + public String id; + } +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageList.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageList.java new file mode 100644 index 00000000..03a6f4bb --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageList.java @@ -0,0 +1,24 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.util.List; +import java.util.Map; + +@Getter +@Builder +@JsonInclude(Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadMessageList { + + @Singular + private List messages; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageRequest.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageRequest.java new file mode 100644 index 00000000..6ed51b66 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadMessageRequest.java @@ -0,0 +1,28 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Singular; + +import java.util.List; +import java.util.Map; + +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadMessageRequest { + + @NonNull + private String role; + @NonNull + private String content; + @Singular + private List fileIds; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRequest.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRequest.java new file mode 100644 index 00000000..d048c2b3 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRequest.java @@ -0,0 +1,21 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.util.List; +import java.util.Map; + +@Getter +@Builder +@JsonInclude(Include.NON_NULL) +public class ThreadRequest { + + @Singular + private List messages; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRun.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRun.java new file mode 100644 index 00000000..35ac1bb9 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRun.java @@ -0,0 +1,83 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import io.github.sashirestela.openai.domain.ToolCall; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +/** + * Represents an execution run on a thread. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadRun { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private String threadId; + private String assistantId; + private String status; + private ThreadRunAction requiredAction; + private Error lastError; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime expiresAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime startedAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime cancelledAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime failedAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime completedAt; + private String model; + private String instructions; + private List tools; + private List fileIds; + private Map metadata; + + public interface Status { + String QUEUED = "queued"; + String IN_PROGRESS = "in_progress"; + String REQUIRES_ACTION = "requires_action"; + String CANCELLING = "cancelling"; + String CANCELLED = "cancelled"; + String FAILED = "failed"; + String COMPLETED = "completed"; + String EXPIRED = "expired"; + } + + public boolean isActionRequired() { + return (requiredAction != null); + } + + public List getRequiredToolCalls() { + if (requiredAction != null + && requiredAction.getSubmitToolOutputs() != null + && requiredAction.getSubmitToolOutputs().getToolCalls() != null) { + return requiredAction.getSubmitToolOutputs().getToolCalls(); + } else { + return List.of(); + } + } + + @NoArgsConstructor + @Getter + @ToString + public static class Error { + private String code; + private String message; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunAction.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunAction.java new file mode 100644 index 00000000..124d288f --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunAction.java @@ -0,0 +1,19 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.openai.domain.ToolCalls; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadRunAction { + + private String type; + private ToolCalls submitToolOutputs; + +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunRequest.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunRequest.java new file mode 100644 index 00000000..f5d57a68 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunRequest.java @@ -0,0 +1,28 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.util.List; +import java.util.Map; + +@Getter +@Builder +@JsonInclude(Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadRunRequest { + + private String assistantId; + private String model; + private String instructions; + @JsonInclude(Include.NON_EMPTY) + @Singular + private List tools; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunStep.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunStep.java new file mode 100644 index 00000000..e6994b31 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ThreadRunStep.java @@ -0,0 +1,139 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.github.sashirestela.cleverclient.util.UnixTimestampDeserializer; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +/** + * Represents a step in execution of a run. + * + */ +@NoArgsConstructor +@Getter +@ToString +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ThreadRunStep { + + private String id; + private String object; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime createdAt; + private String assistantId; + private String threadId; + private String runId; + private String type; + private String status; + private Details stepDetails; + private ThreadRun.Error lastError; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime expiredAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime cancelledAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime failedAt; + @JsonDeserialize(using = UnixTimestampDeserializer.class) + private ZonedDateTime completedAt; + private Map metadata; + + public interface Type { + String MESSAGE_CREATION = "message_creation"; + String TOOL_CALLS = "tool_calls"; + } + + public interface Status { + String IN_PROGRESS = "in_progress"; + String CANCELLED = "cancelled"; + String FAILED = "failed"; + String COMPLETED = "completed"; + String EXPIRED = "expired"; + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Details { + + private String type; + private MessageCreation messageCreation; + private List toolCalls; + + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MessageCreation { + + private ThreadMessageId messageId; + + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class ToolCall { + + private String id; + private String type; + private CodeInterpreter codeInterpreter; + private Map retrieval; + private Function function; + + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CodeInterpreter { + + private String input; + private List outputs; + + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CodeInterpreterOutput { + + private String type; + private String logs; + private Image image; + + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Image { + + private String fileId; + + } + + @NoArgsConstructor + @Getter + @ToString + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Function { + + private String name; + private String arguments; + private String output; + + } +} \ No newline at end of file diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ToolOutput.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ToolOutput.java new file mode 100644 index 00000000..d051d6f4 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ToolOutput.java @@ -0,0 +1,21 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor +@Getter +@Builder +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ToolOutput { + + private String toolCallId; + private String output; + + public static ToolOutput of(String toolCallId, String output) { + return new ToolOutput(toolCallId, output); + } +} diff --git a/src/main/java/io/github/sashirestela/openai/domain/assistant/ToolOutputSubmission.java b/src/main/java/io/github/sashirestela/openai/domain/assistant/ToolOutputSubmission.java new file mode 100644 index 00000000..7b1be610 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/domain/assistant/ToolOutputSubmission.java @@ -0,0 +1,21 @@ +package io.github.sashirestela.openai.domain.assistant; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Singular; + +import java.util.List; + +@Getter +@Builder +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ToolOutputSubmission { + + @NonNull + @Singular + private List toolOutputs; + +} diff --git a/src/main/java/io/github/sashirestela/openai/function/FunctionExecutor.java b/src/main/java/io/github/sashirestela/openai/function/FunctionExecutor.java index c5cbc158..09140c59 100644 --- a/src/main/java/io/github/sashirestela/openai/function/FunctionExecutor.java +++ b/src/main/java/io/github/sashirestela/openai/function/FunctionExecutor.java @@ -1,5 +1,6 @@ package io.github.sashirestela.openai.function; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -8,6 +9,8 @@ import io.github.sashirestela.cleverclient.util.CommonUtil; import io.github.sashirestela.cleverclient.util.JsonUtil; import io.github.sashirestela.openai.SimpleUncheckedException; +import io.github.sashirestela.openai.domain.ToolCall; +import io.github.sashirestela.openai.domain.assistant.ToolOutput; import io.github.sashirestela.openai.domain.chat.tool.ChatFunction; import io.github.sashirestela.openai.domain.chat.tool.ChatFunctionCall; import io.github.sashirestela.openai.domain.chat.tool.ChatTool; @@ -60,4 +63,21 @@ public T execute(ChatFunctionCall functionToCall) { throw new SimpleUncheckedException("Cannot execute the function {0}.", functionName, e); } } + + public List executeAll(List toolsToCalls) { + var toolOutputs = new ArrayList(); + for (ToolCall toolToCall : toolsToCalls) + if (toolToCall.getFunction() != null) + toolOutputs.add(execute(toolToCall.getId(), toolToCall.getFunction())); + + return toolOutputs; + } + + public ToolOutput execute(String toolCallId, ChatFunctionCall functionToCall) { + try { + return ToolOutput.of(toolCallId, ("" + execute(functionToCall))); + } catch (Exception e) { + return ToolOutput.of(toolCallId, e.toString()); + } + } } \ No newline at end of file