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