diff --git a/Documentation~/README.md b/Documentation~/README.md index 6eabe370..41e67c20 100644 --- a/Documentation~/README.md +++ b/Documentation~/README.md @@ -4,7 +4,9 @@ Based on [OpenAI-DotNet](https://github.com/RageAgainstThePixel/OpenAI-DotNet) -A [OpenAI](https://openai.com/) package for the [Unity](https://unity.com/) Game Engine to use chat-gpt, GPT-4, GPT-3.5-Turbo and Dall-E though their RESTful API (currently in beta). Independently developed, this is not an official library and I am not affiliated with OpenAI. An OpenAI API account is required. +A [OpenAI](https://openai.com/) package for the [Unity](https://unity.com/) to use though their RESTful API. +Independently developed, this is not an official library and I am not affiliated with OpenAI. +An OpenAI API account is required. ***All copyrights, trademarks, logos, and assets are the property of their respective owners.*** @@ -58,6 +60,41 @@ The recommended installation method is though the unity package manager and [Ope - [List Models](#list-models) - [Retrieve Models](#retrieve-model) - [Delete Fine Tuned Model](#delete-fine-tuned-model) +- [Assistants](#assistants) :new: + - [List Assistants](#list-assistants) :new: + - [Create Assistant](#create-assistant) :new: + - [Retrieve Assistant](#retrieve-assistant) :new: + - [Modify Assistant](#modify-assistant) :new: + - [Delete Assistant](#delete-assistant) :new: + - [List Assistant Files](#list-assistant-files) :new: + - [Attach File to Assistant](#attach-file-to-assistant) :new: + - [Upload File to Assistant](#upload-file-to-assistant) :new: + - [Retrieve File from Assistant](#retrieve-file-from-assistant) :new: + - [Remove File from Assistant](#remove-file-from-assistant) :new: + - [Delete File from Assistant](#delete-file-from-assistant) :new: +- [Threads](#threads) :new: + - [Create Thread](#create-thread) :new: + - [Create Thread and Run](#create-thread-and-run) :new: + - [Retrieve Thread](#retrieve-thread) :new: + - [Modify Thread](#modify-thread) :new: + - [Delete Thread](#delete-thread) :new: + - [Thread Messages](#thread-messages) :new: + - [List Messages](#list-thread-messages) :new: + - [Create Message](#create-thread-message) :new: + - [Retrieve Message](#retrieve-thread-message) :new: + - [Modify Message](#modify-thread-message) :new: + - [Thread Message Files](#thread-message-files) :new: + - [List Message Files](#list-thread-message-files) :new: + - [Retrieve Message File](#retrieve-thread-message-file) :new: + - [Thread Runs](#thread-runs) :new: + - [List Runs](#list-thread-runs) :new: + - [Create Run](#create-thread-run) :new: + - [Retrieve Run](#retrieve-thread-run) :new: + - [Modify Run](#modify-thread-run) :new: + - [Submit Tool Outputs to Run](#thread-submit-tool-outputs-to-run) :new: + - [List Run Steps](#list-thread-run-steps) :new: + - [Retrieve Run Step](#retrieve-thread-run-step) :new: + - [Cancel Run](#cancel-thread-run) :new: - [Chat](#chat) - [Chat Completions](#chat-completions) - [Streaming](#chat-streaming) @@ -108,6 +145,8 @@ You use the `OpenAIAuthentication` when you initialize the API as shown: #### Pass keys directly with constructor +:warning: We recommended using the environment variables to load the API key instead of having it hard coded in your source. It is not recommended use this method in production, but only for accepting user credentials, local testing and quick start scenarios. + ```csharp var api = new OpenAIClient("sk-apiKey"); ``` @@ -331,6 +370,491 @@ var isDeleted = await api.ModelsEndpoint.DeleteFineTuneModelAsync("your-fine-tun Assert.IsTrue(isDeleted); ``` +### [Assistants](https://platform.openai.com/docs/api-reference/assistants) + +> :warning: Beta Feature + +Build assistants that can call models and use tools to perform tasks. + +- [Assistants Guide](https://platform.openai.com/docs/assistants) +- [OpenAI Assistants Cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/Assistants_API_overview_python.ipynb) + +The Assistants API is accessed via `OpenAIClient.AssistantsEndpoint` + +#### [List Assistants](https://platform.openai.com/docs/api-reference/assistants/listAssistants) + +Returns a list of assistants. + +```csharp +var api = new OpenAIClient(); +var assistantsList = await OpenAIClient.AssistantsEndpoint.ListAssistantsAsync(); + +foreach (var assistant in assistantsList.Items) +{ + Debug.Log($"{assistant} -> {assistant.CreatedAt}"); +} +``` + +#### [Create Assistant](https://platform.openai.com/docs/api-reference/assistants/createAssistant) + +Create an assistant with a model and instructions. + +```csharp +var api = new OpenAIClient(); +var request = new CreateAssistantRequest("gpt-3.5-turbo-1106"); +var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(request); +``` + +#### [Retrieve Assistant](https://platform.openai.com/docs/api-reference/assistants/getAssistant) + +Retrieves an assistant. + +```csharp +var api = new OpenAIClient(); +var assistant = await OpenAIClient.AssistantsEndpoint.RetrieveAssistantAsync("assistant-id"); +Debug.Log($"{assistant} -> {assistant.CreatedAt}"); +``` + +#### [Modify Assistant](https://platform.openai.com/docs/api-reference/assistants/modifyAssistant) + +Modifies an assistant. + +```csharp +var api = new OpenAIClient(); +var createRequest = new CreateAssistantRequest("gpt-3.5-turbo-1106"); +var assistant = await api.AssistantsEndpoint.CreateAssistantAsync(createRequest); +var modifyRequest = new CreateAssistantRequest("gpt-4-1106-preview"); +var modifiedAssistant = await api.AssistantsEndpoint.ModifyAsync(assistant.Id, modifyRequest); +// OR AssistantExtension for easier use! +var modifiedAssistantEx = await assistant.ModifyAsync(modifyRequest); +``` + +#### [Delete Assistant](https://platform.openai.com/docs/api-reference/assistants/deleteAssistant) + +Delete an assistant. + +```csharp +var api = new OpenAIClient(); +var isDeleted = await api.AssistantsEndpoint.DeleteAssistantAsync("assistant-id"); +// OR AssistantExtension for easier use! +var isDeleted = await assistant.DeleteAsync(); +Assert.IsTrue(isDeleted); +``` + +#### [List Assistant Files](https://platform.openai.com/docs/api-reference/assistants/listAssistantFiles) + +Returns a list of assistant files. + +```csharp +var api = new OpenAIClient(); +var filesList = await api.AssistantsEndpoint.ListFilesAsync("assistant-id"); +// OR AssistantExtension for easier use! +var filesList = await assistant.ListFilesAsync(); + +foreach (var file in filesList.Items) +{ + Debug.Log($"{file.AssistantId}'s file -> {file.Id}"); +} +``` + +#### [Attach File to Assistant](https://platform.openai.com/docs/api-reference/assistants/createAssistantFile) + +Create an assistant file by attaching a File to an assistant. + +```csharp +var api = new OpenAIClient(); +var filePath = "assistant_test_2.txt"; +await File.WriteAllTextAsync(filePath, "Knowledge is power!"); +var fileUploadRequest = new FileUploadRequest(filePath, "assistant"); +var file = await api.FilesEndpoint.UploadFileAsync(fileUploadRequest); +var assistantFile = await api.AssistantsEndpoint.AttachFileAsync("assistant-id", file.Id); +// OR use extension method for convenience! +var assistantFIle = await assistant.AttachFileAsync(file); +``` + +#### [Upload File to Assistant](#upload-file) + +Uploads ***and*** attaches a file to an assistant. + +> Assistant extension method, for extra convenience! + +```csharp +var api = new OpenAIClient(); +var filePath = "assistant_test_2.txt"; +await File.WriteAllTextAsync(filePath, "Knowledge is power!"); +var assistantFile = await assistant.UploadFileAsync(filePath); +``` + +#### [Retrieve File from Assistant](https://platform.openai.com/docs/api-reference/assistants/getAssistantFile) + +Retrieves an AssistantFile. + +```csharp +var api = new OpenAIClient(); +var assistantFile = await api.AssistantsEndpoint.RetrieveFileAsync("assistant-id", "file-id"); +// OR AssistantExtension for easier use! +var assistantFile = await assistant.RetrieveFileAsync(fileId); +Debug.Log($"{assistantFile.AssistantId}'s file -> {assistantFile.Id}"); +``` + +#### [Remove File from Assistant](https://platform.openai.com/docs/api-reference/assistants/deleteAssistantFile) + +Remove a file from an assistant. + +> Note: The file will remain in your organization until [deleted with FileEndpoint](#delete-file). + +```csharp +var api = new OpenAIClient(); +var isRemoved = await api.AssistantsEndpoint.RemoveFileAsync("assistant-id", "file-id"); +// OR use extension method for convenience! +var isRemoved = await assistant.RemoveFileAsync("file-id"); +Assert.IsTrue(isRemoved); +``` + +#### [Delete File from Assistant](#delete-file) + +Removes a file from the assistant and then deletes the file from the organization. + +> Assistant extension method, for extra convenience! + +```csharp +var api = new OpenAIClient(); +var isDeleted = await assistant.DeleteFileAsync("file-id"); +Assert.IsTrue(isDeleted); +``` + +### [Threads](https://platform.openai.com/docs/api-reference/threads) + +> :warning: Beta Feature + +Create Threads that [Assistants](#assistants) can interact with. + +The Threads API is accessed via `OpenAIClient.ThreadsEndpoint` + +#### [Create Thread](https://platform.openai.com/docs/api-reference/threads/createThread) + +Create a thread. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.CreateThreadAsync(); +Debug.Log($"Create thread {thread.Id} -> {thread.CreatedAt}"); +``` + +#### [Create Thread and Run](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun) + +Create a thread and run it in one request. + +> See also: [Thread Runs](#thread-runs) + +```csharp +var api = new OpenAIClient(); +var assistant = await api.AssistantsEndpoint.CreateAssistantAsync( + new CreateAssistantRequest( + name: "Math Tutor", + instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.", + model: "gpt-4-1106-preview")); +var messages = new List { "I need to solve the equation `3x + 11 = 14`. Can you help me?" }; +var threadRequest = new CreateThreadRequest(messages); +var run = await assistant.CreateThreadAndRunAsync(threadRequest); +Debug.Log($"Created thread and run: {run.ThreadId} -> {run.Id} -> {run.CreatedAt}"); +``` + +#### [Retrieve Thread](https://platform.openai.com/docs/api-reference/threads/getThread) + +Retrieves a thread. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.RetrieveThreadAsync("thread-id"); +// OR if you simply wish to get the latest state of a thread +thread = await thread.UpdateAsync(); +Debug.Log($"Retrieve thread {thread.Id} -> {thread.CreatedAt}"); +``` + +#### [Modify Thread](https://platform.openai.com/docs/api-reference/threads/modifyThread) + +Modifies a thread. + +> Note: Only the metadata can be modified. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.CreateThreadAsync(); +var metadata = new Dictionary +{ + { "key", "custom thread metadata" } +} +thread = await api.ThreadsEndpoint.ModifyThreadAsync(thread.Id, metadata); +// OR use extension method for convenience! +thread = await thread.ModifyAsync(metadata); +Debug.Log($"Modify thread {thread.Id} -> {thread.Metadata["key"]}"); +``` + +#### [Delete Thread](https://platform.openai.com/docs/api-reference/threads/deleteThread) + +Delete a thread. + +```csharp +var api = new OpenAIClient(); +var isDeleted = await api.ThreadsEndpoint.DeleteThreadAsync("thread-id"); +// OR use extension method for convenience! +var isDeleted = await thread.DeleteAsync(); +Assert.IsTrue(isDeleted); +``` + +#### [Thread Messages](https://platform.openai.com/docs/api-reference/messages) + +Create messages within threads. + +##### [List Thread Messages](https://platform.openai.com/docs/api-reference/messages/listMessages) + +Returns a list of messages for a given thread. + +```csharp +var api = new OpenAIClient(); +var messageList = await api.ThreadsEndpoint.ListMessagesAsync("thread-id"); +// OR use extension method for convenience! +var messageList = await thread.ListMessagesAsync(); + +foreach (var message in messageList.Items) +{ + Debug.Log($"{message.Id}: {message.Role}: {message.PrintContent()}"); +} +``` + +##### [Create Thread Message](https://platform.openai.com/docs/api-reference/messages/createMessage) + +Create a message. + +```csharp +var api = new OpenAIClient(); +var thread = await api.ThreadsEndpoint.CreateThreadAsync(); +var request = new CreateMessageRequest("Hello world!"); +var message = await api.ThreadsEndpoint.CreateMessageAsync(thread.Id, request); +// OR use extension method for convenience! +var message = await thread.CreateMessageAsync("Hello World!"); +Debug.Log($"{message.Id}: {message.Role}: {message.PrintContent()}"); +``` + +##### [Retrieve Thread Message](https://platform.openai.com/docs/api-reference/messages/getMessage) + +Retrieve a message. + +```csharp +var api = new OpenAIClient(); +var message = await api.ThreadsEndpoint.RetrieveMessageAsync("thread-id", "message-id"); +// OR use extension methods for convenience! +var message = await thread.RetrieveMessageAsync("message-id"); +var message = await message.UpdateAsync(); +Debug.Log($"{message.Id}: {message.Role}: {message.PrintContent()}"); +``` + +##### [Modify Thread Message](https://platform.openai.com/docs/api-reference/messages/modifyMessage) + +Modify a message. + +> Note: Only the message metadata can be modified. + +```csharp +var api = new OpenAIClient(); +var metadata = new Dictionary +{ + { "key", "custom message metadata" } +}; +var message = await api.ThreadsEndpoint.ModifyMessageAsync("thread-id", "message-id", metadata); +// OR use extension method for convenience! +var message = await message.ModifyAsync(metadata); +Debug.Log($"Modify message metadata: {message.Id} -> {message.Metadata["key"]}"); +``` + +##### Thread Message Files + +###### [List Thread Message Files](https://platform.openai.com/docs/api-reference/messages/listMessageFiles) + +Returns a list of message files. + +```csharp +var api = new OpenAIClient(); +var fileList = await api.ThreadsEndpoint.ListFilesAsync("thread-id", "message-Id"); +// OR use extension method for convenience! +var fileList = await thread.ListFilesAsync("message-id"); +var fileList = await message.ListFilesAsync(); + +foreach (var file in fileList.Items) +{ + Debug.Log(file.Id); +} +``` + +###### [Retrieve Thread Message File](https://platform.openai.com/docs/api-reference/messages/getMessageFile) + +Retrieves a message file. + +```csharp +var api = new OpenAIClient(); +var file = await api.ThreadsEndpoint.RetrieveFileAsync("thread-id", "message-id", "file-id"); +// OR use extension method for convenience! +var file = await message.RetrieveFileAsync(); +Debug.Log(file.Id); +``` + +#### [Thread Runs](https://platform.openai.com/docs/api-reference/runs) + +Represents an execution run on a thread. + +##### [List Thread Runs](https://platform.openai.com/docs/api-reference/runs/listRuns) + +Returns a list of runs belonging to a thread. + +```csharp +var api = new OpenAIClient(); +var runList = await api.ThreadsEndpoint.ListRunsAsync("thread-id"); +// OR use extension method for convenience! +var runList = await thread.ListRunsAsync(); + +foreach (var run in runList.Items) +{ + Debug.Log($"[{run.Id}] {run.Status} | {run.CreatedAt}"); +} +``` + +##### [Create Thread Run](https://platform.openai.com/docs/api-reference/runs/createRun) + +Create a run. + +```csharp +var api = new OpenAIClient(); +var assistant = await api.AssistantsEndpoint.CreateAssistantAsync( + new CreateAssistantRequest( + name: "Math Tutor", + instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.", + model: "gpt-4-1106-preview")); +var thread = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(); +var message = await thread.CreateMessageAsync("I need to solve the equation `3x + 11 = 14`. Can you help me?"); +var run = await thread.CreateRunAsync(assistant); +Debug.Log($"[{run.Id}] {run.Status} | {run.CreatedAt}"); +``` + +##### [Retrieve Thread Run](https://platform.openai.com/docs/api-reference/runs/getRun) + +Retrieves a run. + +```csharp +var api = new OpenAIClient(); +var run = await api.ThreadsEndpoint.RetrieveRunAsync("thread-id", "run-id"); +// OR use extension method for convenience! +var run = await thread.RetrieveRunAsync("run-id"); +var run = await run.UpdateAsync(); +Debug.Log($"[{run.Id}] {run.Status} | {run.CreatedAt}"); +``` + +##### [Modify Thread Run](https://platform.openai.com/docs/api-reference/runs/modifyRun) + +Modifies a run. + +> Note: Only the metadata can be modified. + +```csharp +var api = new OpenAIClient(); +var metadata = new Dictionary +{ + { "key", "custom run metadata" } +}; +var run = await api.ThreadsEndpoint.ModifyRunAsync("thread-id", "run-id", metadata); +// OR use extension method for convenience! +var run = await run.ModifyAsync(metadata); +Debug.Log($"Modify run {run.Id} -> {run.Metadata["key"]}"); +``` + +##### [Thread Submit Tool Outputs to Run](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs) + +When a run has the status: `requires_action` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. + +```csharp +var api = new OpenAIClient(); +var function = new Function( + nameof(WeatherService.GetCurrentWeather), + "Get the current weather in a given location", + new JObject + { + ["type"] = "object", + ["properties"] = new JObject + { + ["location"] = new JObject + { + ["type"] = "string", + ["description"] = "The city and state, e.g. San Francisco, CA" + }, + ["unit"] = new JObject + { + ["type"] = "string", + ["enum"] = new JArray { "celsius", "fahrenheit" } + } + }, + ["required"] = new JArray { "location", "unit" } + }); +testAssistant = await api.AssistantsEndpoint.CreateAssistantAsync(new CreateAssistantRequest(tools: new Tool[] { function })); +var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature in celsius now?"); +// waiting while run is Queued and InProgress +run = await run.WaitForStatusChangeAsync(); +var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0]; +Debug.Log($"tool call arguments: {toolCall.FunctionCall.Arguments}"); +var functionArgs = JsonConvert.DeserializeObject(toolCall.FunctionCall.Arguments); +var functionResult = WeatherService.GetCurrentWeather(functionArgs); +var toolOutput = new ToolOutput(toolCall.Id, functionResult); +run = await run.SubmitToolOutputsAsync(toolOutput); +// waiting while run in Queued and InProgress +run = await run.WaitForStatusChangeAsync(); +var messages = await run.ListMessagesAsync(); + +foreach (var message in messages.Items.OrderBy(response => response.CreatedAt)) +{ + Debug.Log($"{message.Role}: {message.PrintContent()}"); +} +``` + +##### [List Thread Run Steps](https://platform.openai.com/docs/api-reference/runs/listRunSteps) + +Returns a list of run steps belonging to a run. + +```csharp +var api = new OpenAIClient(); +var runStepList = await api.ThreadsEndpoint.ListRunStepsAsync("thread-id", "run-id"); +// OR use extension method for convenience! +var runStepList = await run.ListRunStepsAsync(); + +foreach (var runStep in runStepList.Items) +{ + Debug.Log($"[{runStep.Id}] {runStep.Status} {runStep.CreatedAt} -> {runStep.ExpiresAt}"); +} +``` + +##### [Retrieve Thread Run Step](https://platform.openai.com/docs/api-reference/runs/getRunStep) + +Retrieves a run step. + +```csharp +var api = new OpenAIClient(); +var runStep = await api.ThreadsEndpoint.RetrieveRunStepAsync("thread-id", "run-id", "step-id"); +// OR use extension method for convenience! +var runStep = await run.RetrieveRunStepAsync("step-id"); +var runStep = await runStep.UpdateAsync(); +Debug.Log($"[{runStep.Id}] {runStep.Status} {runStep.CreatedAt} -> {runStep.ExpiresAt}"); +``` + +##### [Cancel Thread Run](https://platform.openai.com/docs/api-reference/runs/cancelRun) + +Cancels a run that is `in_progress`. + +```csharp +var api = new OpenAIClient(); +var isCancelled = await api.ThreadsEndpoint.CancelRunAsync("thread-id", "run-id"); +// OR use extension method for convenience! +var isCancelled = await run.CancelAsync(); +Assert.IsTrue(isCancelled); +``` + ### [Chat](https://platform.openai.com/docs/api-reference/chat) Given a chat conversation, the model will return a chat completion response. @@ -447,7 +971,7 @@ if (!string.IsNullOrEmpty(response.ToString())) var usedTool = response.FirstChoice.Message.ToolCalls[0]; Debug.Log($"{response.FirstChoice.Message.Role}: {usedTool.Function.Name} | Finish Reason: {response.FirstChoice.FinishReason}"); Debug.Log($"{usedTool.Function.Arguments}"); -var functionArgs = JsonSerializer.Deserialize(usedTool.Function.Arguments.ToString()); +var functionArgs = JsonConvert.DeserializeObject(usedTool.Function.Arguments.ToString()); var functionResult = WeatherService.GetCurrentWeather(functionArgs); messages.Add(new Message(usedTool, functionResult)); Debug.Log($"{Role.Tool}: {functionResult}"); diff --git a/Runtime/Assistants.meta b/Runtime/Assistants.meta new file mode 100644 index 00000000..65cd9517 --- /dev/null +++ b/Runtime/Assistants.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c1327abda07b2074dbc8081df0a11530 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Assistants/AssistantExtensions.cs b/Runtime/Assistants/AssistantExtensions.cs new file mode 100644 index 00000000..45baa3a9 --- /dev/null +++ b/Runtime/Assistants/AssistantExtensions.cs @@ -0,0 +1,160 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using OpenAI.Files; +using OpenAI.Threads; +using System.Threading.Tasks; +using System.Threading; + +namespace OpenAI.Assistants +{ + public static class AssistantExtensions + { + /// + /// Modify the assistant. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task ModifyAsync(this AssistantResponse assistant, CreateAssistantRequest request, CancellationToken cancellationToken = default) + { + request = new CreateAssistantRequest(assistant: assistant, model: request.Model, name: request.Name, description: request.Description, instructions: request.Instructions, tools: request.Tools, files: request.FileIds, metadata: request.Metadata); + return await assistant.Client.AssistantsEndpoint.ModifyAssistantAsync(assistantId: assistant.Id, request: request, cancellationToken: cancellationToken).ConfigureAwait(continueOnCapturedContext: false); + } + + /// + /// Delete the assistant. + /// + /// . + /// Optional, . + /// True, if the was successfully deleted. + public static async Task DeleteAsync(this AssistantResponse assistant, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.DeleteAssistantAsync(assistant.Id, cancellationToken); + + /// + /// Create a thread and run it. + /// + /// . + /// Optional, . + /// Optional, . + /// . + public static async Task CreateThreadAndRunAsync(this AssistantResponse assistant, CreateThreadRequest request = null, CancellationToken cancellationToken = default) + => await assistant.Client.ThreadsEndpoint.CreateThreadAndRunAsync(new CreateThreadAndRunRequest(assistant.Id, createThreadRequest: request), cancellationToken); + + #region Files + + /// + /// Returns a list of assistant files. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task> ListFilesAsync(this AssistantResponse assistant, ListQuery query = null, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.ListFilesAsync(assistant.Id, query, cancellationToken); + + /// + /// Attach a file to the . + /// + /// . + /// + /// A (with purpose="assistants") that the assistant should use. + /// Useful for tools like retrieval and code_interpreter that can access files. + /// + /// Optional, . + /// . + public static async Task AttachFileAsync(this AssistantResponse assistant, FileResponse file, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.AttachFileAsync(assistant.Id, file, cancellationToken); + + /// + /// Uploads a new file at the specified and attaches it to the . + /// + /// . + /// The local file path to upload. + /// Optional, . + /// . + public static async Task UploadFileAsync(this AssistantResponse assistant, string filePath, CancellationToken cancellationToken = default) + { + var file = await assistant.Client.FilesEndpoint.UploadFileAsync(new FileUploadRequest(filePath, "assistant"), uploadProgress: null, cancellationToken); + return await assistant.AttachFileAsync(file, cancellationToken); + } + + /// + /// Retrieves the . + /// + /// . + /// The ID of the file we're getting. + /// Optional, . + /// . + public static async Task RetrieveFileAsync(this AssistantResponse assistant, string fileId, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.RetrieveFileAsync(assistant.Id, fileId, cancellationToken); + + // TODO 400 bad request errors. Likely OpenAI bug downloading assistant file content. + ///// + ///// Downloads the to the specified . + ///// + ///// . + ///// The directory to download the file into. + ///// Optional, delete the cached file. Defaults to false. + ///// Optional, . + ///// The full path of the downloaded file. + //public static async Task DownloadFileAsync(this AssistantFileResponse assistantFile, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + // => await assistantFile.Client.FilesEndpoint.DownloadFileAsync(assistantFile.Id, directory, deleteCachedFile, cancellationToken); + + /// + /// Remove AssistantFile. + /// + /// + /// Note that removing an AssistantFile does not delete the original File object, + /// it simply removes the association between that File and the Assistant. + /// To delete a File, use . + /// + /// . + /// Optional, . + /// True, if file was removed. + public static async Task RemoveFileAsync(this AssistantFileResponse file, CancellationToken cancellationToken = default) + => await file.Client.AssistantsEndpoint.RemoveFileAsync(file.AssistantId, file.Id, cancellationToken); + + /// + /// Remove AssistantFile. + /// + /// + /// Note that removing an AssistantFile does not delete the original File object, + /// it simply removes the association between that File and the Assistant. + /// To delete a File, use . + /// + /// . + /// The ID of the file to remove. + /// Optional, . + /// True, if file was removed. + public static async Task RemoveFileAsync(this AssistantResponse assistant, string fileId, CancellationToken cancellationToken = default) + => await assistant.Client.AssistantsEndpoint.RemoveFileAsync(assistant.Id, fileId, cancellationToken); + + /// + /// Removes and Deletes a file from the assistant. + /// + /// . + /// Optional, . + /// True, if the file was successfully removed from the assistant and deleted. + public static async Task DeleteFileAsync(this AssistantFileResponse file, CancellationToken cancellationToken = default) + { + var isRemoved = await file.RemoveFileAsync(cancellationToken); + return isRemoved && await file.Client.FilesEndpoint.DeleteFileAsync(file.Id, cancellationToken); + } + + /// + /// Removes and Deletes a file from the . + /// + /// . + /// The ID of the file to delete. + /// Optional, . + /// True, if the file was successfully removed from the assistant and deleted. + public static async Task DeleteFileAsync(this AssistantResponse assistant, string fileId, CancellationToken cancellationToken = default) + { + var isRemoved = await assistant.Client.AssistantsEndpoint.RemoveFileAsync(assistant.Id, fileId, cancellationToken); + return isRemoved && await assistant.Client.FilesEndpoint.DeleteFileAsync(fileId, cancellationToken); + } + + #endregion Files + } +} diff --git a/Runtime/Assistants/AssistantExtensions.cs.meta b/Runtime/Assistants/AssistantExtensions.cs.meta new file mode 100644 index 00000000..d3d81775 --- /dev/null +++ b/Runtime/Assistants/AssistantExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6fb44936f58593d4e8772fa98a9f737d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Assistants/AssistantFileResponse.cs b/Runtime/Assistants/AssistantFileResponse.cs new file mode 100644 index 00000000..e0a3c2f1 --- /dev/null +++ b/Runtime/Assistants/AssistantFileResponse.cs @@ -0,0 +1,67 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Assistants +{ + /// + /// File attached to an assistant. + /// + [Preserve] + public sealed class AssistantFileResponse : BaseResponse + { + [Preserve] + [JsonConstructor] + public AssistantFileResponse( + string id, + string @object, + int createdAtUnixTimeSeconds, + string assistantId) + { + Id = id; + Object = @object; + CreatedAtUnixTimeSeconds = createdAtUnixTimeSeconds; + AssistantId = assistantId; + } + + /// + /// The identifier, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; private set; } + + /// + /// The object type, which is always assistant.file. + /// + [Preserve] + [JsonProperty("object")] + public string Object { get; private set; } + + /// + /// The Unix timestamp (in seconds) for when the assistant file was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int CreatedAtUnixTimeSeconds { get; private set; } + + [Preserve] + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The assistant ID that the file is attached to. + /// + [Preserve] + [JsonProperty("assistant_id")] + public string AssistantId { get; private set; } + + [Preserve] + public static implicit operator string(AssistantFileResponse file) => file?.ToString(); + + [Preserve] + public override string ToString() => Id; + } +} diff --git a/Runtime/Assistants/AssistantFileResponse.cs.meta b/Runtime/Assistants/AssistantFileResponse.cs.meta new file mode 100644 index 00000000..2993c7c9 --- /dev/null +++ b/Runtime/Assistants/AssistantFileResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 859183317dee8a944836a6478ddc558c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Assistants/AssistantResponse.cs b/Runtime/Assistants/AssistantResponse.cs new file mode 100644 index 00000000..6a9d674e --- /dev/null +++ b/Runtime/Assistants/AssistantResponse.cs @@ -0,0 +1,133 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Assistants +{ + /// + /// Purpose-built AI that uses OpenAI�s models and calls tools. + /// + [Preserve] + public sealed class AssistantResponse : BaseResponse + { + [Preserve] + [JsonConstructor] + public AssistantResponse( + string id, + string @object, + int createdAtUnixTimeSeconds, + string name, + string description, + string model, + string instructions, + IReadOnlyList tools, + IReadOnlyList fileIds, + IReadOnlyDictionary metadata) + { + Id = id; + Object = @object; + CreatedAtUnixTimeSeconds = createdAtUnixTimeSeconds; + Name = name; + Description = description; + Model = model; + Instructions = instructions; + Tools = tools; + FileIds = fileIds; + Metadata = metadata; + } + + /// + /// The identifier, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + /// + /// The object type, which is always assistant. + /// + [Preserve] + [JsonProperty("object")] + public string Object { get; } + + /// + /// The Unix timestamp (in seconds) for when the assistant was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int CreatedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + [Preserve] + [JsonProperty("name")] + public string Name { get; } + + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + [Preserve] + [JsonProperty("description")] + public string Description { get; } + + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + [Preserve] + [JsonProperty("model")] + public string Model { get; } + + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + [Preserve] + [JsonProperty("instructions")] + public string Instructions { get; } + + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + [Preserve] + [JsonProperty("tools")] + public IReadOnlyList Tools { get; } + + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + [Preserve] + [JsonProperty("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [Preserve] + public static implicit operator string(AssistantResponse assistant) => assistant?.Id; + + [Preserve] + public override string ToString() => Id; + } +} diff --git a/Runtime/Assistants/AssistantResponse.cs.meta b/Runtime/Assistants/AssistantResponse.cs.meta new file mode 100644 index 00000000..4b26caf7 --- /dev/null +++ b/Runtime/Assistants/AssistantResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3932177d103be34f9c50fc961630e56 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Assistants/AssistantsEndpoint.cs b/Runtime/Assistants/AssistantsEndpoint.cs new file mode 100644 index 00000000..153a997d --- /dev/null +++ b/Runtime/Assistants/AssistantsEndpoint.cs @@ -0,0 +1,162 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using OpenAI.Extensions; +using OpenAI.Files; +using System; +using System.Threading; +using System.Threading.Tasks; +using Utilities.WebRequestRest; + +namespace OpenAI.Assistants +{ + public class AssistantsEndpoint : OpenAIBaseEndpoint + { + internal AssistantsEndpoint(OpenAIClient client) : base(client) { } + + protected override string Root => "assistants"; + + /// + /// Get list of assistants. + /// + /// . + /// Optional, . + /// + public async Task> ListAssistantsAsync(ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl(queryParameters: query), parameters: new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize>(client); + } + + /// + /// Create an assistant. + /// + /// . + /// Optional, . + /// . + public async Task CreateAssistantAsync(CreateAssistantRequest request = null, CancellationToken cancellationToken = default) + { + request ??= new CreateAssistantRequest(); + var jsonContent = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl(), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Retrieves an assistant. + /// + /// The ID of the assistant to retrieve. + /// Optional, . + /// . + public async Task RetrieveAssistantAsync(string assistantId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{assistantId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Modifies an assistant. + /// + /// The ID of the assistant to modify. + /// . + /// Optional, . + /// . + public async Task ModifyAssistantAsync(string assistantId, CreateAssistantRequest request, CancellationToken cancellationToken = default) + { + var jsonContent = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{assistantId}"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Delete an assistant. + /// + /// The ID of the assistant to delete. + /// Optional, . + /// True, if the assistant was deleted. + public async Task DeleteAssistantAsync(string assistantId, CancellationToken cancellationToken = default) + { + var response = await Rest.DeleteAsync(GetUrl($"/{assistantId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return JsonConvert.DeserializeObject(response.Body, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + } + + #region Files + + /// + /// Returns a list of assistant files. + /// + /// The ID of the assistant the file belongs to. + /// . + /// Optional, . + /// . + public async Task> ListFilesAsync(string assistantId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{assistantId}/files", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize>(client); + } + + /// + /// Attach a file to an assistant. + /// + /// The ID of the assistant for which to attach a file. + /// + /// A (with purpose="assistants") that the assistant should use. + /// Useful for tools like retrieval and code_interpreter that can access files. + /// + /// Optional, . + /// . + public async Task AttachFileAsync(string assistantId, FileResponse file, CancellationToken cancellationToken = default) + { + if (file?.Purpose?.Equals("assistants") != true) + { + throw new InvalidOperationException($"{nameof(file)}.{nameof(file.Purpose)} must be 'assistants'!"); + } + + var jsonContent = JsonConvert.SerializeObject(new { file_id = file.Id }, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{assistantId}/files"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Retrieves an AssistantFile. + /// + /// The ID of the assistant who the file belongs to. + /// The ID of the file we're getting. + /// Optional, . + /// . + public async Task RetrieveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{assistantId}/files/{fileId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Remove an assistant file. + /// + /// + /// Note that removing an AssistantFile does not delete the original File object, + /// it simply removes the association between that File and the Assistant. + /// To delete a File, use the File delete endpoint instead. + /// + /// The ID of the assistant that the file belongs to. + /// The ID of the file to delete. + /// Optional, . + /// True, if file was removed. + public async Task RemoveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default) + { + var response = await Rest.DeleteAsync(GetUrl($"/{assistantId}/files/{fileId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return JsonConvert.DeserializeObject(response.Body, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + } + + #endregion Files + } +} diff --git a/Runtime/Assistants/AssistantsEndpoint.cs.meta b/Runtime/Assistants/AssistantsEndpoint.cs.meta new file mode 100644 index 00000000..272e0435 --- /dev/null +++ b/Runtime/Assistants/AssistantsEndpoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8bd9f0e70eb3a314ca04a7e3e6c63524 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Assistants/CreateAssistantRequest.cs b/Runtime/Assistants/CreateAssistantRequest.cs new file mode 100644 index 00000000..28caa61a --- /dev/null +++ b/Runtime/Assistants/CreateAssistantRequest.cs @@ -0,0 +1,162 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Assistants +{ + [Preserve] + public sealed class CreateAssistantRequest + { + /// + /// Constructor + /// + /// + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + public CreateAssistantRequest(AssistantResponse assistant, string model = null, string name = null, string description = null, string instructions = null, IEnumerable tools = null, IEnumerable files = null, IReadOnlyDictionary metadata = null) + : this(string.IsNullOrWhiteSpace(model) ? assistant.Model : model, string.IsNullOrWhiteSpace(name) ? assistant.Name : name, string.IsNullOrWhiteSpace(description) ? assistant.Description : description, string.IsNullOrWhiteSpace(instructions) ? assistant.Instructions : instructions, tools ?? assistant.Tools, files ?? assistant.FileIds, metadata ?? assistant.Metadata) + { + } + + /// + /// Constructor. + /// + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + public CreateAssistantRequest(string model = null, string name = null, string description = null, string instructions = null, IEnumerable tools = null, IEnumerable files = null, IReadOnlyDictionary metadata = null) + { + Model = string.IsNullOrWhiteSpace(model) ? Models.Model.GPT3_5_Turbo : model; + Name = name; + Description = description; + Instructions = instructions; + Tools = tools?.ToList(); + FileIds = files?.ToList(); + Metadata = metadata; + } + + /// + /// ID of the model to use. + /// You can use the List models API to see all of your available models, + /// or see our Model overview for descriptions of them. + /// + [Preserve] + [JsonProperty("model")] + public string Model { get; } + + /// + /// The name of the assistant. + /// The maximum length is 256 characters. + /// + [Preserve] + [JsonProperty("name")] + public string Name { get; } + + /// + /// The description of the assistant. + /// The maximum length is 512 characters. + /// + [Preserve] + [JsonProperty("description")] + public string Description { get; } + + /// + /// The system instructions that the assistant uses. + /// The maximum length is 32768 characters. + /// + [Preserve] + [JsonProperty("instructions")] + public string Instructions { get; } + + /// + /// A list of tool enabled on the assistant. + /// There can be a maximum of 128 tools per assistant. + /// Tools can be of types 'code_interpreter', 'retrieval', or 'function'. + /// + [Preserve] + [JsonProperty("tools")] + public IReadOnlyList Tools { get; } + + /// + /// A list of file IDs attached to this assistant. + /// There can be a maximum of 20 files attached to the assistant. + /// Files are ordered by their creation date in ascending order. + /// + [Preserve] + [JsonProperty("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} diff --git a/Runtime/Assistants/CreateAssistantRequest.cs.meta b/Runtime/Assistants/CreateAssistantRequest.cs.meta new file mode 100644 index 00000000..15fe0564 --- /dev/null +++ b/Runtime/Assistants/CreateAssistantRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9733d7fa52c5a4c40ba307c6136cd84e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioEndpoint.cs b/Runtime/Audio/AudioEndpoint.cs index 92ba8f08..10b8cf2d 100644 --- a/Runtime/Audio/AudioEndpoint.cs +++ b/Runtime/Audio/AudioEndpoint.cs @@ -32,8 +32,7 @@ public AudioResponse([JsonProperty("text")] string text) public string Text { get; } } - /// - public AudioEndpoint(OpenAIClient client) : base(client) { } + internal AudioEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "audio"; diff --git a/Runtime/Chat/ChatEndpoint.cs b/Runtime/Chat/ChatEndpoint.cs index 8e559f4b..2ec78cf0 100644 --- a/Runtime/Chat/ChatEndpoint.cs +++ b/Runtime/Chat/ChatEndpoint.cs @@ -16,8 +16,7 @@ namespace OpenAI.Chat /// public sealed class ChatEndpoint : OpenAIBaseEndpoint { - /// - public ChatEndpoint(OpenAIClient client) : base(client) { } + internal ChatEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "chat"; @@ -39,7 +38,7 @@ public async Task GetCompletionAsync(ChatRequest chatRequest, Canc var response = await Rest.PostAsync(GetUrl("/completions"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize(response.Body, client); + return response.Deserialize(client); } /// diff --git a/Runtime/Common/AnnotationType.cs b/Runtime/Common/AnnotationType.cs new file mode 100644 index 00000000..92c4e9a0 --- /dev/null +++ b/Runtime/Common/AnnotationType.cs @@ -0,0 +1,16 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Runtime.Serialization; +using UnityEngine.Scripting; + +namespace OpenAI +{ + [Preserve] + public enum AnnotationType + { + [EnumMember(Value = "file_citation")] + FileCitation, + [EnumMember(Value = "file_path")] + FilePath + } +} diff --git a/Runtime/Common/AnnotationType.cs.meta b/Runtime/Common/AnnotationType.cs.meta new file mode 100644 index 00000000..9c111825 --- /dev/null +++ b/Runtime/Common/AnnotationType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5aa759ac59d2f349967ec983948e5bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Common/ContentType.cs b/Runtime/Common/ContentType.cs index f0e19b73..c90b3ed0 100644 --- a/Runtime/Common/ContentType.cs +++ b/Runtime/Common/ContentType.cs @@ -1,9 +1,11 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Runtime.Serialization; +using UnityEngine.Scripting; namespace OpenAI { + [Preserve] public enum ContentType { [EnumMember(Value = "text")] diff --git a/Runtime/Common/Error.cs b/Runtime/Common/Error.cs new file mode 100644 index 00000000..f563e660 --- /dev/null +++ b/Runtime/Common/Error.cs @@ -0,0 +1,35 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI +{ + [Preserve] + public sealed class Error + { + [Preserve] + [JsonConstructor] + public Error( + [JsonProperty("code")] string code, + [JsonProperty("message")] string message) + { + Code = code; + Message = message; + } + + /// + /// One of server_error or rate_limit_exceeded. + /// + [Preserve] + [JsonProperty("code")] + public string Code { get; } + + /// + /// A human-readable description of the error. + /// + [Preserve] + [JsonProperty("message")] + public string Message { get; } + } +} diff --git a/Runtime/Common/Error.cs.meta b/Runtime/Common/Error.cs.meta new file mode 100644 index 00000000..94b64758 --- /dev/null +++ b/Runtime/Common/Error.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 16386242359e55043b528300030d5eee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Chat/Function.cs b/Runtime/Common/Function.cs similarity index 99% rename from Runtime/Chat/Function.cs rename to Runtime/Common/Function.cs index 2d63e4d8..1417ae72 100644 --- a/Runtime/Chat/Function.cs +++ b/Runtime/Common/Function.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json.Linq; using UnityEngine.Scripting; -namespace OpenAI.Chat +namespace OpenAI { /// /// diff --git a/Runtime/Chat/Function.cs.meta b/Runtime/Common/Function.cs.meta similarity index 100% rename from Runtime/Chat/Function.cs.meta rename to Runtime/Common/Function.cs.meta diff --git a/Runtime/Common/ListQuery.cs b/Runtime/Common/ListQuery.cs index 54ec5c79..382655af 100644 --- a/Runtime/Common/ListQuery.cs +++ b/Runtime/Common/ListQuery.cs @@ -1,3 +1,5 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + using System.Collections.Generic; namespace OpenAI @@ -74,4 +76,4 @@ public static implicit operator Dictionary(ListQuery query) return parameters; } } -} \ No newline at end of file +} diff --git a/Runtime/Chat/Role.cs b/Runtime/Common/Role.cs similarity index 100% rename from Runtime/Chat/Role.cs rename to Runtime/Common/Role.cs diff --git a/Runtime/Chat/Role.cs.meta b/Runtime/Common/Role.cs.meta similarity index 100% rename from Runtime/Chat/Role.cs.meta rename to Runtime/Common/Role.cs.meta diff --git a/Runtime/Chat/Tool.cs b/Runtime/Common/Tool.cs similarity index 86% rename from Runtime/Chat/Tool.cs rename to Runtime/Common/Tool.cs index 2855de79..93164503 100644 --- a/Runtime/Chat/Tool.cs +++ b/Runtime/Common/Tool.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; using UnityEngine.Scripting; -namespace OpenAI.Chat +namespace OpenAI { [Preserve] public sealed class Tool @@ -21,6 +21,17 @@ public Tool(Function function) Type = nameof(function); } + [Preserve] + public static implicit operator Tool(Function function) => new Tool(function); + + [Preserve] + [JsonIgnore] + public static Tool Retrieval { get; } = new Tool { Type = "retrieval" }; + + [Preserve] + [JsonIgnore] + public static Tool CodeInterpreter { get; } = new Tool { Type = "code_interpreter" }; + [Preserve] [JsonProperty("id")] public string Id { get; private set; } @@ -37,9 +48,6 @@ public Tool(Function function) [JsonProperty("function")] public Function Function { get; private set; } - [Preserve] - public static implicit operator Tool(Function function) => new Tool(function); - [Preserve] internal void CopyFrom(Tool other) { diff --git a/Runtime/Chat/Tool.cs.meta b/Runtime/Common/Tool.cs.meta similarity index 100% rename from Runtime/Chat/Tool.cs.meta rename to Runtime/Common/Tool.cs.meta diff --git a/Runtime/Completions/CompletionsEndpoint.cs b/Runtime/Completions/CompletionsEndpoint.cs index d9fb148b..583ae844 100644 --- a/Runtime/Completions/CompletionsEndpoint.cs +++ b/Runtime/Completions/CompletionsEndpoint.cs @@ -113,7 +113,7 @@ public async Task CreateCompletionAsync(CompletionRequest co var payload = JsonConvert.SerializeObject(completionRequest, OpenAIClient.JsonSerializationOptions); var response = await Rest.PostAsync(GetUrl(), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize(response.Body, client); + return response.Deserialize(client); } #endregion Non-Streaming diff --git a/Runtime/Edits/EditsEndpoint.cs b/Runtime/Edits/EditsEndpoint.cs index 8cc05ec5..50692588 100644 --- a/Runtime/Edits/EditsEndpoint.cs +++ b/Runtime/Edits/EditsEndpoint.cs @@ -1,9 +1,9 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using Newtonsoft.Json; using OpenAI.Extensions; using OpenAI.Models; +using System; using System.Threading; using System.Threading.Tasks; using Utilities.WebRequestRest; @@ -17,8 +17,7 @@ namespace OpenAI.Edits [Obsolete] public sealed class EditsEndpoint : OpenAIBaseEndpoint { - /// - public EditsEndpoint(OpenAIClient client) : base(client) { } + internal EditsEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "edits"; @@ -68,7 +67,7 @@ public async Task CreateEditAsync(EditRequest request, Cancellatio var payload = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); var response = await Rest.PostAsync(GetUrl(), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize(response.Body, client); + return response.Deserialize(client); } } } diff --git a/Runtime/Embeddings/EmbeddingsEndpoint.cs b/Runtime/Embeddings/EmbeddingsEndpoint.cs index abc5d634..38ef2ca1 100644 --- a/Runtime/Embeddings/EmbeddingsEndpoint.cs +++ b/Runtime/Embeddings/EmbeddingsEndpoint.cs @@ -15,8 +15,7 @@ namespace OpenAI.Embeddings /// public sealed class EmbeddingsEndpoint : OpenAIBaseEndpoint { - /// - public EmbeddingsEndpoint(OpenAIClient client) : base(client) { } + internal EmbeddingsEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "embeddings"; @@ -72,7 +71,7 @@ public async Task CreateEmbeddingAsync(EmbeddingsRequest req var payload = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); var response = await Rest.PostAsync(GetUrl(), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize(response.Body, client); + return response.Deserialize(client); } } } diff --git a/Runtime/Extensions/ResponseExtensions.cs b/Runtime/Extensions/ResponseExtensions.cs index 583a5cfe..ce7685cd 100644 --- a/Runtime/Extensions/ResponseExtensions.cs +++ b/Runtime/Extensions/ResponseExtensions.cs @@ -97,9 +97,9 @@ internal static void SetResponseData(this BaseResponse response, Response restRe } } - internal static T Deserialize(this Response response, string json, OpenAIClient client) where T : BaseResponse + internal static T Deserialize(this Response response, OpenAIClient client) where T : BaseResponse { - var result = JsonConvert.DeserializeObject(json, OpenAIClient.JsonSerializationOptions); + var result = JsonConvert.DeserializeObject(response.Body, OpenAIClient.JsonSerializationOptions); result.SetResponseData(response, client); return result; } diff --git a/Runtime/Files/FilesEndpoint.cs b/Runtime/Files/FilesEndpoint.cs index 3fee6621..474a7627 100644 --- a/Runtime/Files/FilesEndpoint.cs +++ b/Runtime/Files/FilesEndpoint.cs @@ -33,10 +33,8 @@ public FilesList([JsonProperty("data")] List data) public List Files { get; } } - /// - public FilesEndpoint(OpenAIClient client) : base(client) { } + internal FilesEndpoint(OpenAIClient client) : base(client) { } - /// protected override string Root => "files"; /// diff --git a/Runtime/FineTuning/FineTuningEndpoint.cs b/Runtime/FineTuning/FineTuningEndpoint.cs index f655c191..fcd95fed 100644 --- a/Runtime/FineTuning/FineTuningEndpoint.cs +++ b/Runtime/FineTuning/FineTuningEndpoint.cs @@ -17,10 +17,8 @@ namespace OpenAI.FineTuning /// public sealed class FineTuningEndpoint : OpenAIBaseEndpoint { - /// - public FineTuningEndpoint(OpenAIClient client) : base(client) { } + internal FineTuningEndpoint(OpenAIClient client) : base(client) { } - /// protected override string Root => "fine_tuning"; /// @@ -36,7 +34,7 @@ public async Task CreateJobAsync(CreateFineTuneJobRequest j var payload = JsonConvert.SerializeObject(jobRequest, OpenAIClient.JsonSerializationOptions); var response = await Rest.PostAsync(GetUrl("/jobs"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize(response.Body, client); + return response.Deserialize(client); } [Obsolete("Use new overload")] @@ -69,7 +67,7 @@ public async Task> ListJobsAsync(ListQuery que { var response = await Rest.GetAsync(GetUrl("/jobs", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize>(response.Body, client); + return response.Deserialize>(client); } /// @@ -82,8 +80,8 @@ public async Task GetJobInfoAsync(string jobId, Cancellatio { var response = await Rest.GetAsync(GetUrl($"/jobs/{jobId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - var job = response.Deserialize(response.Body, client); - job.Events = (await ListJobEventsAsync(job, query: null, cancellationToken: cancellationToken).ConfigureAwait(false))?.Items; + var job = response.Deserialize(client); + job.Events = (await ListJobEventsAsync(job, query: null, cancellationToken: cancellationToken).ConfigureAwait(true))?.Items; return job; } @@ -132,7 +130,7 @@ public async Task> ListJobEventsAsync(string jobId, { var response = await Rest.GetAsync(GetUrl($"/jobs/{jobId}/events", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); - return response.Deserialize>(response.Body, client); + return response.Deserialize>(client); } } } diff --git a/Runtime/Images/ImagesEndpoint.cs b/Runtime/Images/ImagesEndpoint.cs index 2e6f3028..b924421e 100644 --- a/Runtime/Images/ImagesEndpoint.cs +++ b/Runtime/Images/ImagesEndpoint.cs @@ -295,7 +295,7 @@ private async Task> DeserializeResponseAs { response.Validate(EnableDebug); - var imagesResponse = response.Deserialize(response.Body, client); + var imagesResponse = response.Deserialize(client); if (imagesResponse?.Results is not { Count: not 0 }) { diff --git a/Runtime/Models/ModelsEndpoint.cs b/Runtime/Models/ModelsEndpoint.cs index cc7ee2ae..6152a564 100644 --- a/Runtime/Models/ModelsEndpoint.cs +++ b/Runtime/Models/ModelsEndpoint.cs @@ -32,8 +32,7 @@ public ModelsList([JsonProperty("data")] List data) public List Data { get; } } - /// - public ModelsEndpoint(OpenAIClient client) : base(client) { } + internal ModelsEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "models"; diff --git a/Runtime/Moderations/ModerationsEndpoint.cs b/Runtime/Moderations/ModerationsEndpoint.cs index 30a56c4f..838363a0 100644 --- a/Runtime/Moderations/ModerationsEndpoint.cs +++ b/Runtime/Moderations/ModerationsEndpoint.cs @@ -16,8 +16,7 @@ namespace OpenAI.Moderations /// public sealed class ModerationsEndpoint : OpenAIBaseEndpoint { - /// - public ModerationsEndpoint(OpenAIClient client) : base(client) { } + internal ModerationsEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "moderations"; diff --git a/Runtime/OpenAIClient.cs b/Runtime/OpenAIClient.cs index 9d9645e0..a47ebf3f 100644 --- a/Runtime/OpenAIClient.cs +++ b/Runtime/OpenAIClient.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +using OpenAI.Assistants; using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Completions; @@ -13,6 +14,7 @@ using OpenAI.Images; using OpenAI.Models; using OpenAI.Moderations; +using OpenAI.Threads; using System; using System.Collections.Generic; using System.Security.Authentication; @@ -51,6 +53,8 @@ public OpenAIClient(OpenAIAuthentication authentication = null, OpenAISettings s FilesEndpoint = new FilesEndpoint(this); FineTuningEndpoint = new FineTuningEndpoint(this); ModerationsEndpoint = new ModerationsEndpoint(this); + ThreadsEndpoint = new ThreadsEndpoint(this); + AssistantsEndpoint = new AssistantsEndpoint(this); } protected override void SetupDefaultRequestHeaders() @@ -58,9 +62,11 @@ protected override void SetupDefaultRequestHeaders() var headers = new Dictionary { #if !UNITY_WEBGL - { "User-Agent", "com.openai.unity" } + { "User-Agent", "com.openai.unity" }, #endif + { "OpenAI-Beta", "assistants=v1"} }; + if (!Settings.Info.BaseRequestUrlFormat.Contains(OpenAISettingsInfo.AzureOpenAIDomain) && (string.IsNullOrWhiteSpace(Authentication.Info.ApiKey) || (!Authentication.Info.ApiKey.Contains(OpenAIAuthInfo.SecretKeyPrefix) && @@ -181,5 +187,17 @@ protected override void ValidateAuthentication() /// /// public ModerationsEndpoint ModerationsEndpoint { get; } + + /// + /// Create threads that assistants can interact with.
+ /// + ///
+ public ThreadsEndpoint ThreadsEndpoint { get; } + + /// + /// Build assistants that can call models and use tools to perform tasks.
+ /// + ///
+ public AssistantsEndpoint AssistantsEndpoint { get; } } } diff --git a/Runtime/Threads.meta b/Runtime/Threads.meta new file mode 100644 index 00000000..22d710da --- /dev/null +++ b/Runtime/Threads.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 98371f6eb5bbf1c419fcc57f72871167 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/Annotation.cs b/Runtime/Threads/Annotation.cs new file mode 100644 index 00000000..0c159bd9 --- /dev/null +++ b/Runtime/Threads/Annotation.cs @@ -0,0 +1,64 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class Annotation + { + [Preserve] + [JsonConstructor] + public Annotation( + [JsonProperty("type")] AnnotationType annotationType, + [JsonProperty("text")] string text, + [JsonProperty("file_citation")] FileCitation fileCitation, + [JsonProperty("file_path")] FilePath filePath, + [JsonProperty("start_index")] int startIndex, + [JsonProperty("end_index")] int endIndex) + { + Type = annotationType; + Text = text; + FileCitation = fileCitation; + FilePath = filePath; + StartIndex = startIndex; + EndIndex = endIndex; + } + + [Preserve] + [JsonProperty("type")] + public AnnotationType Type { get; } + + /// + /// The text in the message content that needs to be replaced. + /// + [Preserve] + [JsonProperty("text")] + public string Text { get; } + + /// + /// A citation within the message that points to a specific quote from a + /// specific File associated with the assistant or the message. + /// Generated when the assistant uses the 'retrieval' tool to search files. + /// + [Preserve] + [JsonProperty("file_citation")] + public FileCitation FileCitation { get; } + + /// + /// A URL for the file that's generated when the assistant used the 'code_interpreter' tool to generate a file. + /// + [Preserve] + [JsonProperty("file_path")] + public FilePath FilePath { get; } + + [Preserve] + [JsonProperty("start_index")] + public int StartIndex { get; } + + [Preserve] + [JsonProperty("end_index")] + public int EndIndex { get; } + } +} diff --git a/Runtime/Threads/Annotation.cs.meta b/Runtime/Threads/Annotation.cs.meta new file mode 100644 index 00000000..ac800fa5 --- /dev/null +++ b/Runtime/Threads/Annotation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de91cff39075c84428bb3419c54e2439 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/Content.cs b/Runtime/Threads/Content.cs new file mode 100644 index 00000000..e07cca5e --- /dev/null +++ b/Runtime/Threads/Content.cs @@ -0,0 +1,45 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class Content + { + [Preserve] + [JsonConstructor] + public Content( + [JsonProperty("type")] ContentType contentType, + [JsonProperty("text")] TextContent text, + [JsonProperty("image_url")] ImageUrl imageUrl) + { + Type = contentType; + Text = text; + ImageUrl = imageUrl; + } + + [Preserve] + [JsonProperty("type")] + public ContentType Type { get; } + + [Preserve] + [JsonProperty("text")] + public TextContent Text { get; } + + [Preserve] + [JsonProperty("image_url")] + public ImageUrl ImageUrl { get; } + + [Preserve] + public override string ToString() + => Type switch + { + ContentType.Text => Text.Value, + ContentType.ImageUrl => ImageUrl.Url, + _ => throw new ArgumentOutOfRangeException() + }; + } +} diff --git a/Runtime/Threads/Content.cs.meta b/Runtime/Threads/Content.cs.meta new file mode 100644 index 00000000..9f8c0943 --- /dev/null +++ b/Runtime/Threads/Content.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5415bf8cd77ca145ab9f26374276526 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/CreateMessageRequest.cs b/Runtime/Threads/CreateMessageRequest.cs new file mode 100644 index 00000000..5d62b398 --- /dev/null +++ b/Runtime/Threads/CreateMessageRequest.cs @@ -0,0 +1,65 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class CreateMessageRequest + { + [Preserve] + public static implicit operator CreateMessageRequest(string content) => new CreateMessageRequest(content); + + /// + /// Constructor. + /// + /// + /// + /// + [Preserve] + public CreateMessageRequest(string content, IEnumerable fieldIds = null, IReadOnlyDictionary metadata = null) + { + Role = Role.User; + Content = content; + FileIds = fieldIds?.ToList(); + Metadata = metadata; + } + + /// + /// The role of the entity that is creating the message. + /// + /// + /// Currently only user is supported. + /// + [Preserve] + [JsonProperty("role")] + public Role Role { get; } + + /// + /// The content of the message. + /// + [Preserve] + [JsonProperty("content")] + public string Content { get; } + + /// + /// A list of File IDs that the message should use. There can be a maximum of 10 files attached to a message. + /// Useful for tools like retrieval and code_interpreter that can access and use files. + /// + [Preserve] + [JsonProperty("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} diff --git a/Runtime/Threads/CreateMessageRequest.cs.meta b/Runtime/Threads/CreateMessageRequest.cs.meta new file mode 100644 index 00000000..c4b7211f --- /dev/null +++ b/Runtime/Threads/CreateMessageRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 458a0d4e614f2a64298a52fbb850d89d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/CreateRunRequest.cs b/Runtime/Threads/CreateRunRequest.cs new file mode 100644 index 00000000..d5a2ac20 --- /dev/null +++ b/Runtime/Threads/CreateRunRequest.cs @@ -0,0 +1,66 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class CreateRunRequest + { + [Preserve] + public CreateRunRequest(string assistantId, CreateRunRequest request) + : this(assistantId, request?.Model, request?.Instructions, request?.Tools, request?.Metadata) + { + } + + [Preserve] + public CreateRunRequest(string assistantId, string model = null, string instructions = null, IEnumerable tools = null, IReadOnlyDictionary metadata = null) + { + AssistantId = assistantId; + Model = model; + Instructions = instructions; + Tools = tools?.ToList(); + Metadata = metadata; + } + + /// + /// The ID of the assistant used for execution of this run. + /// + [Preserve] + [JsonProperty("assistant_id")] + public string AssistantId { get; } + + /// + /// The model that the assistant used for this run. + /// + [Preserve] + [JsonProperty("model")] + public string Model { get; } + + /// + /// The instructions that the assistant used for this run. + /// + [Preserve] + [JsonProperty("instructions")] + public string Instructions { get; } + + /// + /// The list of tools that the assistant used for this run. + /// + [Preserve] + [JsonProperty("tools")] + public IReadOnlyList Tools { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} diff --git a/Runtime/Threads/CreateRunRequest.cs.meta b/Runtime/Threads/CreateRunRequest.cs.meta new file mode 100644 index 00000000..676c04bd --- /dev/null +++ b/Runtime/Threads/CreateRunRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83ac92b5ce1b73d42b575ddb94ddd899 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/CreateThreadAndRunRequest.cs b/Runtime/Threads/CreateThreadAndRunRequest.cs new file mode 100644 index 00000000..694dc6a8 --- /dev/null +++ b/Runtime/Threads/CreateThreadAndRunRequest.cs @@ -0,0 +1,99 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class CreateThreadAndRunRequest + { + [Preserve] + public CreateThreadAndRunRequest(string assistantId, CreateThreadAndRunRequest request) + : this(assistantId, request?.Model, request?.Instructions, request?.Tools, request?.Metadata) + { + } + + /// + /// Constructor. + /// + /// + /// The ID of the assistant to use to execute this run. + /// + /// + /// The ID of the Model to be used to execute this run. + /// If a value is provided here, it will override the model associated with the assistant. + /// If not, the model associated with the assistant will be used. + /// + /// + /// Override the default system message of the assistant. + /// This is useful for modifying the behavior on a per-run basis. + /// + /// + /// Override the tools the assistant can use for this run. + /// This is useful for modifying the behavior on a per-run basis. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// + /// Optional, . + /// + [Preserve] + public CreateThreadAndRunRequest(string assistantId, string model = null, string instructions = null, IReadOnlyList tools = null, IReadOnlyDictionary metadata = null, CreateThreadRequest createThreadRequest = null) + { + AssistantId = assistantId; + Model = model; + Instructions = instructions; + Tools = tools; + Metadata = metadata; + ThreadRequest = createThreadRequest; + } + + /// + /// The ID of the assistant to use to execute this run. + /// + [Preserve] + [JsonProperty("assistant_id")] + public string AssistantId { get; } + + /// + /// The ID of the Model to be used to execute this run. + /// If a value is provided here, it will override the model associated with the assistant. + /// If not, the model associated with the assistant will be used. + /// + [Preserve] + [JsonProperty("model")] + public string Model { get; } + + /// + /// Override the default system message of the assistant. + /// This is useful for modifying the behavior on a per-run basis. + /// + [Preserve] + [JsonProperty("instructions")] + public string Instructions { get; } + + /// + /// Override the tools the assistant can use for this run. + /// This is useful for modifying the behavior on a per-run basis. + /// + [Preserve] + [JsonProperty("tools")] + public IReadOnlyList Tools { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [Preserve] + [JsonProperty("thread")] + public CreateThreadRequest ThreadRequest { get; } + } +} diff --git a/Runtime/Threads/CreateThreadAndRunRequest.cs.meta b/Runtime/Threads/CreateThreadAndRunRequest.cs.meta new file mode 100644 index 00000000..5e62dafb --- /dev/null +++ b/Runtime/Threads/CreateThreadAndRunRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 739319529d18c774cb33e0f48417f310 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/CreateThreadRequest.cs b/Runtime/Threads/CreateThreadRequest.cs new file mode 100644 index 00000000..9c7b0dd3 --- /dev/null +++ b/Runtime/Threads/CreateThreadRequest.cs @@ -0,0 +1,50 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class CreateThreadRequest + { + /// + /// Constructor. + /// + /// + /// A list of messages to start the thread with. + /// + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + public CreateThreadRequest(IEnumerable messages = null, + IReadOnlyDictionary metadata = null) + { + Messages = messages?.ToList(); + Metadata = metadata; + } + + /// + /// A list of messages to start the thread with. + /// + [Preserve] + [JsonProperty("messages")] + public IReadOnlyList Messages { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + public static implicit operator CreateThreadRequest(string message) => new CreateThreadRequest(new[] { new Message(message) }); + } +} diff --git a/Runtime/Threads/CreateThreadRequest.cs.meta b/Runtime/Threads/CreateThreadRequest.cs.meta new file mode 100644 index 00000000..414e6422 --- /dev/null +++ b/Runtime/Threads/CreateThreadRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7660a7367a4df774f91a5f9568a35a5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/FileCitation.cs b/Runtime/Threads/FileCitation.cs new file mode 100644 index 00000000..253297b5 --- /dev/null +++ b/Runtime/Threads/FileCitation.cs @@ -0,0 +1,35 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class FileCitation + { + [Preserve] + [JsonConstructor] + public FileCitation( + [JsonProperty("file_id")] string fileId, + [JsonProperty("quote")] string quote) + { + FileId = fileId; + Quote = quote; + } + + /// + /// The ID of the specific File the citation is from. + /// + [Preserve] + [JsonProperty("file_id")] + public string FileId { get; } + + /// + /// The specific quote in the file. + /// + [Preserve] + [JsonProperty("quote")] + public string Quote { get; } + } +} diff --git a/Runtime/Threads/FileCitation.cs.meta b/Runtime/Threads/FileCitation.cs.meta new file mode 100644 index 00000000..3474667b --- /dev/null +++ b/Runtime/Threads/FileCitation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d44039bc828ce64ba524b50c36e3d75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/FilePath.cs b/Runtime/Threads/FilePath.cs new file mode 100644 index 00000000..f609cf40 --- /dev/null +++ b/Runtime/Threads/FilePath.cs @@ -0,0 +1,25 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class FilePath + { + [Preserve] + [JsonConstructor] + public FilePath([JsonProperty("file_id")] string fileId) + { + FileId = fileId; + } + + /// + /// The ID of the file that was generated. + /// + [Preserve] + [JsonProperty("file_id")] + public string FileId { get; } + } +} diff --git a/Runtime/Threads/FilePath.cs.meta b/Runtime/Threads/FilePath.cs.meta new file mode 100644 index 00000000..798e254b --- /dev/null +++ b/Runtime/Threads/FilePath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc0fc84256a76ab4798e844c8acceffa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/FunctionCall.cs b/Runtime/Threads/FunctionCall.cs new file mode 100644 index 00000000..f8fa187b --- /dev/null +++ b/Runtime/Threads/FunctionCall.cs @@ -0,0 +1,35 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class FunctionCall + { + [Preserve] + [JsonConstructor] + public FunctionCall( + [JsonProperty("name")] string name, + [JsonProperty("arguments")] string arguments) + { + Name = name; + Arguments = arguments; + } + + /// + /// The name of the function. + /// + [Preserve] + [JsonProperty("name")] + public string Name { get; } + + /// + /// The arguments that the model expects you to pass to the function. + /// + [Preserve] + [JsonProperty("arguments")] + public string Arguments { get; } + } +} diff --git a/Runtime/Threads/FunctionCall.cs.meta b/Runtime/Threads/FunctionCall.cs.meta new file mode 100644 index 00000000..6632bb60 --- /dev/null +++ b/Runtime/Threads/FunctionCall.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d49fefcaa150e14a8b131eb86072483 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/Message.cs b/Runtime/Threads/Message.cs new file mode 100644 index 00000000..201395c3 --- /dev/null +++ b/Runtime/Threads/Message.cs @@ -0,0 +1,73 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class Message + { + [Preserve] + public static implicit operator Message(string content) => new Message(content); + + /// + /// Constructor. + /// + /// + /// The content of the message. + /// + /// + /// A list of File IDs that the message should use. + /// There can be a maximum of 10 files attached to a message. + /// Useful for tools like 'retrieval' and 'code_interpreter' that can access and use files. + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + public Message(string content, IEnumerable fileIds = null, IReadOnlyDictionary metadata = null) + { + Role = Role.User; + Content = content; + FileIds = fileIds?.ToList(); + Metadata = metadata; + } + + /// + /// The role of the entity that is creating the message. + /// Currently only user is supported. + /// + [Preserve] + [JsonProperty("role")] + public Role Role { get; } + + /// + /// The content of the message. + /// + [Preserve] + [JsonProperty("content")] + public string Content { get; } + + /// + /// A list of File IDs that the message should use. + /// There can be a maximum of 10 files attached to a message. + /// Useful for tools like 'retrieval' and 'code_interpreter' that can access and use files. + /// + [Preserve] + [JsonProperty("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + } +} diff --git a/Runtime/Threads/Message.cs.meta b/Runtime/Threads/Message.cs.meta new file mode 100644 index 00000000..a44fb418 --- /dev/null +++ b/Runtime/Threads/Message.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64566ef8ff8751140a6e855f23f283d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/MessageFileResponse.cs b/Runtime/Threads/MessageFileResponse.cs new file mode 100644 index 00000000..bec1631e --- /dev/null +++ b/Runtime/Threads/MessageFileResponse.cs @@ -0,0 +1,64 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class MessageFileResponse : BaseResponse + { + [Preserve] + [JsonConstructor] + public MessageFileResponse( + [JsonProperty("id")] string id, + [JsonProperty("object")] string @object, + [JsonProperty("created_at")] int createdAtUnixTimeSeconds, + [JsonProperty("message_id")] string messageId) + { + Id = id; + Object = @object; + CreatedAtUnixTimeSeconds = createdAtUnixTimeSeconds; + MessageId = messageId; + } + + /// + /// The identifier, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + /// + /// The object type, which is always thread.message.file. + /// + [Preserve] + [JsonProperty("object")] + public string Object { get; } + + /// + /// The Unix timestamp (in seconds) for when the message file was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int CreatedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The ID of the message that the File is attached to. + /// + [Preserve] + [JsonProperty("message_id")] + public string MessageId { get; } + + [Preserve] + public static implicit operator string(MessageFileResponse response) => response?.ToString(); + + [Preserve] + public override string ToString() => Id; + } +} diff --git a/Runtime/Threads/MessageFileResponse.cs.meta b/Runtime/Threads/MessageFileResponse.cs.meta new file mode 100644 index 00000000..a46b5240 --- /dev/null +++ b/Runtime/Threads/MessageFileResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88e215a42d6094e49b2e83022a5f635a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/MessageResponse.cs b/Runtime/Threads/MessageResponse.cs new file mode 100644 index 00000000..2944a038 --- /dev/null +++ b/Runtime/Threads/MessageResponse.cs @@ -0,0 +1,136 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + /// + /// A message created by an Assistant or a user. + /// Messages can include text, images, and other files. + /// Messages stored as a list on the Thread. + /// + [Preserve] + public sealed class MessageResponse : BaseResponse + { + [JsonConstructor] + public MessageResponse( + [JsonProperty("id")] string id, + [JsonProperty("object")] string @object, + [JsonProperty("created_at")] int createdAtUnixTimeSeconds, + [JsonProperty("thread_id")] string threadId, + [JsonProperty("role")] Role role, + [JsonProperty("content")] IReadOnlyList content, + [JsonProperty("assistant_id")] string assistantId, + [JsonProperty("run_id")] string runId, + [JsonProperty("file_ids")] IReadOnlyList fileIds, + [JsonProperty("metadata")] IReadOnlyDictionary metadata) + { + Id = id; + Object = @object; + CreatedAtUnixTimeSeconds = createdAtUnixTimeSeconds; + ThreadId = threadId; + Role = role; + Content = content; + AssistantId = assistantId; + RunId = runId; + FileIds = fileIds; + Metadata = metadata; + } + + /// + /// The identifier, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + /// + /// The object type, which is always thread. + /// + [Preserve] + [JsonProperty("object")] + public string Object { get; } + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int CreatedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The thread ID that this message belongs to. + /// + [Preserve] + [JsonProperty("thread_id")] + public string ThreadId { get; } + + /// + /// The entity that produced the message. One of user or assistant. + /// + [Preserve] + [JsonProperty("role")] + public Role Role { get; } + + /// + /// The content of the message in array of text and/or images. + /// + [Preserve] + [JsonProperty("content")] + public IReadOnlyList Content { get; } + + /// + /// If applicable, the ID of the assistant that authored this message. + /// + [Preserve] + [JsonProperty("assistant_id")] + public string AssistantId { get; } + + /// + /// If applicable, the ID of the run associated with the authoring of this message. + /// + [Preserve] + [JsonProperty("run_id")] + public string RunId { get; } + + /// + /// A list of file IDs that the assistant should use. + /// Useful for tools like 'retrieval' and 'code_interpreter' that can access files. + /// A maximum of 10 files can be attached to a message. + /// + [Preserve] + [JsonProperty("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [Preserve] + public static implicit operator string(MessageResponse message) => message?.ToString(); + + [Preserve] + public override string ToString() => Id; + + /// + /// Formats all of the items into a single string, + /// putting each item on a new line. + /// + /// of all . + [Preserve] + public string PrintContent() => string.Join("\n", Content.Select(content => content?.ToString())); + } +} diff --git a/Runtime/Threads/MessageResponse.cs.meta b/Runtime/Threads/MessageResponse.cs.meta new file mode 100644 index 00000000..86e7a3d0 --- /dev/null +++ b/Runtime/Threads/MessageResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ed8dc2179750e7449c21731fed800c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/RequiredAction.cs b/Runtime/Threads/RequiredAction.cs new file mode 100644 index 00000000..b870b870 --- /dev/null +++ b/Runtime/Threads/RequiredAction.cs @@ -0,0 +1,32 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class RequiredAction + { + [Preserve] + [JsonConstructor] + public RequiredAction( + [JsonProperty("type")] string type, + [JsonProperty("submit_tool_outputs")] SubmitToolOutputs submitToolOutputs) + { + Type = type; + SubmitToolOutputs = submitToolOutputs; + } + + [Preserve] + [JsonProperty("type")] + public string Type { get; } + + /// + /// Details on the tool outputs needed for this run to continue. + /// + [Preserve] + [JsonProperty("submit_tool_outputs")] + public SubmitToolOutputs SubmitToolOutputs { get; } + } +} diff --git a/Runtime/Threads/RequiredAction.cs.meta b/Runtime/Threads/RequiredAction.cs.meta new file mode 100644 index 00000000..8c222b20 --- /dev/null +++ b/Runtime/Threads/RequiredAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78d23d297cfb8eb4ab66e9840f3ccde3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/RunResponse.cs b/Runtime/Threads/RunResponse.cs new file mode 100644 index 00000000..b3670cad --- /dev/null +++ b/Runtime/Threads/RunResponse.cs @@ -0,0 +1,235 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + /// + /// An invocation of an Assistant on a Thread. + /// The Assistant uses it�s configuration and the Thread�s Messages to perform tasks by calling models and tools. + /// As part of a Run, the Assistant appends Messages to the Thread. + /// + [Preserve] + public sealed class RunResponse : BaseResponse + { + [Preserve] + [JsonConstructor] + public RunResponse( + [JsonProperty("id")] string id, + [JsonProperty("object")] string @object, + [JsonProperty("thread_id")] string threadId, + [JsonProperty("assistant_id")] string assistantId, + [JsonProperty("status")] RunStatus status, + [JsonProperty("required_action")] RequiredAction requiredAction, + [JsonProperty("last_error")] Error lastError, + [JsonProperty("created_at")] int createdAtUnixTimeSeconds, + [JsonProperty("expires_at")] int? expiresAtUnixTimeSeconds, + [JsonProperty("started_at")] int? startedAtUnixTimeSeconds, + [JsonProperty("cancelled_at")] int? cancelledAtUnixTimeSeconds, + [JsonProperty("failed_at")] int? failedAtUnixTimeSeconds, + [JsonProperty("completed_at")] int? completedAtUnixTimeSeconds, + [JsonProperty("model")] string model, + [JsonProperty("instructions")] string instructions, + [JsonProperty("tools")] IReadOnlyList tools, + [JsonProperty("file_ids")] IReadOnlyList fileIds, + [JsonProperty("metadata")] IReadOnlyDictionary metadata) + { + Id = id; + Object = @object; + ThreadId = threadId; + AssistantId = assistantId; + Status = status; + RequiredAction = requiredAction; + LastError = lastError; + CreatedAtUnixTimeSeconds = createdAtUnixTimeSeconds; + ExpiresAtUnixTimeSeconds = expiresAtUnixTimeSeconds; + StartedAtUnixTimeSeconds = startedAtUnixTimeSeconds; + CancelledAtUnixTimeSeconds = cancelledAtUnixTimeSeconds; + FailedAtUnixTimeSeconds = failedAtUnixTimeSeconds; + CompletedAtUnixTimeSeconds = completedAtUnixTimeSeconds; + Model = model; + Instructions = instructions; + Tools = tools; + FileIds = fileIds; + Metadata = metadata; + } + + /// + /// The identifier, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + /// + /// The object type, which is always run. + /// + [Preserve] + [JsonProperty("object")] + public string Object { get; } + + /// + /// The thread ID that this run belongs to. + /// + [Preserve] + [JsonProperty("thread_id")] + public string ThreadId { get; } + + /// + /// The ID of the assistant used for execution of this run. + /// + [Preserve] + [JsonProperty("assistant_id")] + public string AssistantId { get; } + + /// + /// The status of the run. + /// + [Preserve] + [JsonProperty("status")] + public RunStatus Status { get; } + + /// + /// Details on the action required to continue the run. + /// Will be null if no action is required. + /// + [Preserve] + [JsonProperty("required_action")] + public RequiredAction RequiredAction { get; } + + /// + /// The Last error Associated with this run. + /// Will be null if there are no errors. + /// + [Preserve] + [JsonProperty("last_error")] + public Error LastError { get; } + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int CreatedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// The Unix timestamp (in seconds) for when the run will expire. + /// + [Preserve] + [JsonProperty("expires_at")] + public int? ExpiresAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? ExpiresAt + => ExpiresAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(ExpiresAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run was started. + /// + [Preserve] + [JsonProperty("started_at")] + public int? StartedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? StartedAt + => StartedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(StartedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run was cancelled. + /// + [Preserve] + [JsonProperty("cancelled_at")] + public int? CancelledAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? CancelledAt + => CancelledAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CancelledAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run failed. + /// + [Preserve] + [JsonProperty("failed_at")] + public int? FailedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? FailedAt + => FailedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(FailedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run was completed. + /// + [Preserve] + [JsonProperty("completed_at")] + public int? CompletedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? CompletedAt + => CompletedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CompletedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The model that the assistant used for this run. + /// + [Preserve] + [JsonProperty("model")] + public string Model { get; } + + /// + /// The instructions that the assistant used for this run. + /// + [Preserve] + [JsonProperty("instructions")] + public string Instructions { get; } + + /// + /// The list of tools that the assistant used for this run. + /// + [Preserve] + [JsonProperty("tools")] + public IReadOnlyList Tools { get; } + + /// + /// The list of File IDs the assistant used for this run. + /// + [Preserve] + [JsonProperty("file_ids")] + public IReadOnlyList FileIds { get; } + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [Preserve] + public static implicit operator string(RunResponse run) => run?.ToString(); + + [Preserve] + public override string ToString() => Id; + } +} diff --git a/Runtime/Threads/RunResponse.cs.meta b/Runtime/Threads/RunResponse.cs.meta new file mode 100644 index 00000000..e7254614 --- /dev/null +++ b/Runtime/Threads/RunResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28c0955c1f90b1e418e62ba660c78b8a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/RunStatus.cs b/Runtime/Threads/RunStatus.cs new file mode 100644 index 00000000..fe675e04 --- /dev/null +++ b/Runtime/Threads/RunStatus.cs @@ -0,0 +1,28 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Runtime.Serialization; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public enum RunStatus + { + [EnumMember(Value = "queued")] + Queued, + [EnumMember(Value = "in_progress")] + InProgress, + [EnumMember(Value = "requires_action")] + RequiresAction, + [EnumMember(Value = "cancelling")] + Cancelling, + [EnumMember(Value = "cancelled")] + Cancelled, + [EnumMember(Value = "failed")] + Failed, + [EnumMember(Value = "completed")] + Completed, + [EnumMember(Value = "expired")] + Expired + } +} diff --git a/Runtime/Threads/RunStatus.cs.meta b/Runtime/Threads/RunStatus.cs.meta new file mode 100644 index 00000000..6ca3ec96 --- /dev/null +++ b/Runtime/Threads/RunStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7d7ffb394460ae489dd5a7235f62266 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/RunStepMessageCreation.cs b/Runtime/Threads/RunStepMessageCreation.cs new file mode 100644 index 00000000..6ab6db19 --- /dev/null +++ b/Runtime/Threads/RunStepMessageCreation.cs @@ -0,0 +1,25 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class RunStepMessageCreation + { + [Preserve] + [JsonConstructor] + public RunStepMessageCreation([JsonProperty("message_id")] string messageId) + { + MessageId = messageId; + } + + /// + /// The ID of the message that was created by this run step. + /// + [Preserve] + [JsonProperty("message_id")] + public string MessageId { get; } + } +} diff --git a/Runtime/Threads/RunStepMessageCreation.cs.meta b/Runtime/Threads/RunStepMessageCreation.cs.meta new file mode 100644 index 00000000..26ee2eb8 --- /dev/null +++ b/Runtime/Threads/RunStepMessageCreation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 616e2e24605f11041a31ed3a492756c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/RunStepResponse.cs b/Runtime/Threads/RunStepResponse.cs new file mode 100644 index 00000000..18114450 --- /dev/null +++ b/Runtime/Threads/RunStepResponse.cs @@ -0,0 +1,195 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + /// + /// A detailed list of steps the Assistant took as part of a Run. + /// An Assistant can call tools or create Messages during it�s run. + /// Examining Run Steps allows you to introspect how the Assistant is getting to it�s final results. + /// + [Preserve] + public sealed class RunStepResponse : BaseResponse + { + [Preserve] + [JsonConstructor] + public RunStepResponse( + [JsonProperty("id")] string id, + [JsonProperty("object")] string @object, + [JsonProperty("assistant_id")] string assistantId, + [JsonProperty("thread_id")] string threadId, + [JsonProperty("run_id")] string runId, + [JsonProperty("type")] RunStepType runStepType, + [JsonProperty("status")] RunStatus runStatus, + [JsonProperty("step_details")] StepDetails stepDetails, + [JsonProperty("last_error")] Error lastRunError, + [JsonProperty("created_at")] int? createdAtUnixTimeSeconds, + [JsonProperty("expires_at")] int? expiresAtUnixTimeSeconds, + [JsonProperty("cancelled_at")] int? cancelledAtUnixTimeSeconds, + [JsonProperty("failed_at")] int? failedAtUnixTimeSeconds, + [JsonProperty("metadata")] IReadOnlyDictionary metadata) + { + Id = id; + Object = @object; + AssistantId = assistantId; + ThreadId = threadId; + RunId = runId; + Type = runStepType; + Status = runStatus; + StepDetails = stepDetails; + LastError = lastRunError; + CreatedAtUnixTimeSeconds = createdAtUnixTimeSeconds; + ExpiresAtUnixTimeSeconds = expiresAtUnixTimeSeconds; + CancelledAtUnixTimeSeconds = cancelledAtUnixTimeSeconds; + FailedAtUnixTimeSeconds = failedAtUnixTimeSeconds; + Metadata = metadata; + } + + /// + /// The identifier of the run step, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + [Preserve] + [JsonProperty("object")] + public string Object { get; } + + /// + /// The ID of the assistant associated with the run step. + /// + [Preserve] + [JsonProperty("assistant_id")] + public string AssistantId { get; } + + /// + /// The ID of the thread that was run. + /// + [Preserve] + [JsonProperty("thread_id")] + public string ThreadId { get; } + + /// + /// The ID of the run that this run step is a part of. + /// + [Preserve] + [JsonProperty("run_id")] + public string RunId { get; } + + /// + /// The type of run step. + /// + [Preserve] + [JsonProperty("type")] + public RunStepType Type { get; } + + /// + /// The status of the run step. + /// + [Preserve] + [JsonProperty("status")] + public RunStatus Status { get; } + + /// + /// The details of the run step. + /// + [Preserve] + [JsonProperty("step_details")] + public StepDetails StepDetails { get; } + + /// + /// The last error associated with this run step. Will be null if there are no errors. + /// + [Preserve] + [JsonProperty("last_error")] + public Error LastError { get; } + + /// + /// The Unix timestamp (in seconds) for when the run step was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int? CreatedAtUnixTimeSeconds { get; } + + [JsonIgnore] + public DateTime? CreatedAt + => CreatedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step expired. A step is considered expired if the parent run is expired. + /// + [Preserve] + [JsonProperty("expires_at")] + public int? ExpiresAtUnixTimeSeconds { get; } + + [JsonIgnore] + public DateTime? ExpiresAt + => ExpiresAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(ExpiresAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step was cancelled. + /// + [Preserve] + [JsonProperty("cancelled_at")] + public int? CancelledAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? CancelledAt + => CancelledAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CancelledAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step failed. + /// + [Preserve] + [JsonProperty("failed_at")] + public int? FailedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? FailedAt + => FailedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(FailedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// The Unix timestamp (in seconds) for when the run step completed. + /// + [Preserve] + [JsonProperty("completed_at")] + public int? CompletedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime? CompletedAt + => CompletedAtUnixTimeSeconds.HasValue + ? DateTimeOffset.FromUnixTimeSeconds(CompletedAtUnixTimeSeconds.Value).DateTime + : null; + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [Preserve] + public static implicit operator string(RunStepResponse runStep) => runStep?.ToString(); + + [Preserve] + public override string ToString() => Id; + } +} diff --git a/Runtime/Threads/RunStepResponse.cs.meta b/Runtime/Threads/RunStepResponse.cs.meta new file mode 100644 index 00000000..69372fc6 --- /dev/null +++ b/Runtime/Threads/RunStepResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 934f98d0cf9b5ed4eb3b185fdf236fd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/RunStepType.cs b/Runtime/Threads/RunStepType.cs new file mode 100644 index 00000000..81300349 --- /dev/null +++ b/Runtime/Threads/RunStepType.cs @@ -0,0 +1,16 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Runtime.Serialization; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public enum RunStepType + { + [EnumMember(Value = "message_creation")] + MessageCreation, + [EnumMember(Value = "tool_calls")] + ToolCalls + } +} diff --git a/Runtime/Threads/RunStepType.cs.meta b/Runtime/Threads/RunStepType.cs.meta new file mode 100644 index 00000000..6aa89566 --- /dev/null +++ b/Runtime/Threads/RunStepType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb39152e70e1495418c4e64906933c75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/StepDetails.cs b/Runtime/Threads/StepDetails.cs new file mode 100644 index 00000000..35bef161 --- /dev/null +++ b/Runtime/Threads/StepDetails.cs @@ -0,0 +1,40 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + /// + /// The details of the run step. + /// + [Preserve] + public sealed class StepDetails + { + [Preserve] + [JsonConstructor] + public StepDetails( + [JsonProperty("message_creation")] RunStepMessageCreation messageCreation, + [JsonProperty("tool_calls")] IReadOnlyList toolCalls) + { + MessageCreation = messageCreation; + ToolCalls = toolCalls; + } + + /// + /// Details of the message creation by the run step. + /// + [Preserve] + [JsonProperty("message_creation")] + public RunStepMessageCreation MessageCreation { get; } + + /// + /// An array of tool calls the run step was involved in. + /// These can be associated with one of three types of tools: 'code_interpreter', 'retrieval', or 'function'. + /// + [Preserve] + [JsonProperty("tool_calls")] + public IReadOnlyList ToolCalls { get; } + } +} diff --git a/Runtime/Threads/StepDetails.cs.meta b/Runtime/Threads/StepDetails.cs.meta new file mode 100644 index 00000000..019c64c7 --- /dev/null +++ b/Runtime/Threads/StepDetails.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80820e4c4145a8141ac560dc2a583e83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/SubmitToolOutputs.cs b/Runtime/Threads/SubmitToolOutputs.cs new file mode 100644 index 00000000..2112f515 --- /dev/null +++ b/Runtime/Threads/SubmitToolOutputs.cs @@ -0,0 +1,26 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class SubmitToolOutputs + { + [Preserve] + [JsonConstructor] + public SubmitToolOutputs([JsonProperty("tool_calls")] IReadOnlyList toolCalls) + { + ToolCalls = toolCalls; + } + + /// + /// A list of the relevant tool calls. + /// + [Preserve] + [JsonProperty("tool_calls")] + public IReadOnlyList ToolCalls { get; } + } +} diff --git a/Runtime/Threads/SubmitToolOutputs.cs.meta b/Runtime/Threads/SubmitToolOutputs.cs.meta new file mode 100644 index 00000000..4f497d98 --- /dev/null +++ b/Runtime/Threads/SubmitToolOutputs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4193ad3ed72a4fe45bd17930d2dea658 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/SubmitToolOutputsRequest.cs b/Runtime/Threads/SubmitToolOutputsRequest.cs new file mode 100644 index 00000000..4fc3860f --- /dev/null +++ b/Runtime/Threads/SubmitToolOutputsRequest.cs @@ -0,0 +1,45 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class SubmitToolOutputsRequest + { + /// + /// Tool output to be submitted. + /// + /// . + [Preserve] + public SubmitToolOutputsRequest(ToolOutput toolOutput) + : this(new[] { toolOutput }) + { + } + + /// + /// A list of tools for which the outputs are being submitted. + /// + /// Collection of tools for which the outputs are being submitted. + [Preserve] + [JsonConstructor] + public SubmitToolOutputsRequest( + [JsonProperty("tool_outputs")] IEnumerable toolOutputs) + { + ToolOutputs = toolOutputs?.ToList(); + } + + /// + /// A list of tools for which the outputs are being submitted. + /// + [Preserve] + [JsonProperty("tool_outputs")] + public IReadOnlyList ToolOutputs { get; } + + [Preserve] + public static implicit operator SubmitToolOutputsRequest(ToolOutput toolOutput) => new SubmitToolOutputsRequest(toolOutput); + } +} diff --git a/Runtime/Threads/SubmitToolOutputsRequest.cs.meta b/Runtime/Threads/SubmitToolOutputsRequest.cs.meta new file mode 100644 index 00000000..c3b0cf78 --- /dev/null +++ b/Runtime/Threads/SubmitToolOutputsRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b28f28c9e8123f34c89c335ff156d40f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/TextContent.cs b/Runtime/Threads/TextContent.cs new file mode 100644 index 00000000..654ab158 --- /dev/null +++ b/Runtime/Threads/TextContent.cs @@ -0,0 +1,36 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System.Collections.Generic; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class TextContent + { + [Preserve] + [JsonConstructor] + public TextContent( + [JsonProperty("value")] string value, + [JsonProperty("annotations")] IReadOnlyList annotations) + { + Value = value; + Annotations = annotations; + } + + /// + /// The data that makes up the text. + /// + [Preserve] + [JsonProperty("value")] + public string Value { get; } + + /// + /// Annotations + /// + [Preserve] + [JsonProperty("annotations")] + public IReadOnlyList Annotations { get; } + } +} diff --git a/Runtime/Threads/TextContent.cs.meta b/Runtime/Threads/TextContent.cs.meta new file mode 100644 index 00000000..9b63defe --- /dev/null +++ b/Runtime/Threads/TextContent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 20f7a2602d19ab64a8a39292859555eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/ThreadExtensions.cs b/Runtime/Threads/ThreadExtensions.cs new file mode 100644 index 00000000..19a705ae --- /dev/null +++ b/Runtime/Threads/ThreadExtensions.cs @@ -0,0 +1,327 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace OpenAI.Threads +{ + public static class ThreadExtensions + { + /// + /// Updates this thread with the latest snapshot from OpenAI. + /// + /// . + /// Optional, . + /// . + public static async Task UpdateAsync(this ThreadResponse thread, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.RetrieveThreadAsync(thread, cancellationToken); + + /// + /// Modify the thread. + /// + /// + /// Only the can be modified. + /// + /// . + /// The metadata to set on the thread. + /// Optional, . + /// . + public static async Task ModifyAsync(this ThreadResponse thread, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ModifyThreadAsync(thread, metadata, cancellationToken); + + /// + /// Deletes the thread. + /// + /// . + /// Optional, . + /// True, if the thread was successfully deleted. + public static async Task DeleteAsync(this ThreadResponse thread, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.DeleteThreadAsync(thread, cancellationToken); + + #region Messages + + /// + /// Create a new message for this thread. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task CreateMessageAsync(this ThreadResponse thread, CreateMessageRequest request, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.CreateMessageAsync(thread.Id, request, cancellationToken); + + /// + /// List the messages associated to this thread. + /// + /// . + /// Optional, . + /// Optional, . + /// + public static async Task> ListMessagesAsync(this ThreadResponse thread, ListQuery query = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ListMessagesAsync(thread.Id, query, cancellationToken); + + /// + /// Retrieve a message. + /// + /// . + /// Optional, . + /// . + public static async Task UpdateAsync(this MessageResponse message, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.RetrieveMessageAsync(message.ThreadId, message.Id, cancellationToken); + + /// + /// Retrieve a message. + /// + /// . + /// The id of the message to get. + /// Optional, . + /// . + public static async Task RetrieveMessageAsync(this ThreadResponse thread, string messageId, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.RetrieveMessageAsync(thread.Id, messageId, cancellationToken); + + /// + /// Modify a message. + /// + /// + /// Only the can be modified. + /// + /// . + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public static async Task ModifyAsync(this MessageResponse message, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.ModifyMessageAsync(message, metadata, cancellationToken); + + /// + /// Modifies a message. + /// + /// + /// Only the can be modified. + /// + /// . + /// The id of the message to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public static async Task ModifyMessageAsync(this ThreadResponse thread, string messageId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ModifyMessageAsync(thread, messageId, metadata, cancellationToken); + + #endregion Messages + + #region Files + + /// + /// Returns a list of message files. + /// + /// . + /// The id of the message that the files belongs to. + /// . + /// Optional, . + /// . + public static async Task> ListFilesAsync(this ThreadResponse thread, string messageId, ListQuery query = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ListFilesAsync(thread.Id, messageId, query, cancellationToken); + + /// + /// Returns a list of message files. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task> ListFilesAsync(this MessageResponse message, ListQuery query = null, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.ListFilesAsync(message.ThreadId, message.Id, query, cancellationToken); + + /// + /// Retrieve message file. + /// + /// . + /// The id of the file being retrieved. + /// Optional, . + /// . + public static async Task RetrieveFileAsync(this MessageResponse message, string fileId, CancellationToken cancellationToken = default) + => await message.Client.ThreadsEndpoint.RetrieveFileAsync(message.ThreadId, message.Id, fileId, cancellationToken); + + // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + ///// + ///// Downloads a message file content to local disk. + ///// + ///// . + ///// The id of the file being retrieved. + ///// Directory to save the file content. + ///// Optional, delete cached file. Defaults to false. + ///// Optional, . + ///// Path to the downloaded file content. + //public static async Task DownloadFileContentAsync(this MessageResponse message, string fileId, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + // => await message.Client.FilesEndpoint.DownloadFileAsync(fileId, directory, deleteCachedFile, cancellationToken); + + // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + ///// + ///// Downloads a message file content to local disk. + ///// + ///// . + ///// Directory to save the file content. + ///// Optional, delete cached file. Defaults to false. + ///// Optional, . + ///// Path to the downloaded file content. + //public static async Task DownloadContentAsync(this MessageFileResponse file, string directory, bool deleteCachedFile = false, CancellationToken cancellationToken = default) + // => await file.Client.FilesEndpoint.DownloadFileAsync(file.Id, directory, deleteCachedFile, cancellationToken); + + #endregion Files + + #region Runs + + /// + /// Create a run. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task CreateRunAsync(this ThreadResponse thread, CreateRunRequest request = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.CreateRunAsync(thread, request, cancellationToken); + + /// + /// Create a run. + /// + /// . + /// Id of the assistant to use for the run. + /// Optional, . + /// . + public static async Task CreateRunAsync(this ThreadResponse thread, string assistantId, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.CreateRunAsync(thread, new CreateRunRequest(assistantId), cancellationToken); + + /// + /// Gets the thread associated to the . + /// + /// . + /// Optional, . + /// . + public static async Task GetThreadAsync(this RunResponse run, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.RetrieveThreadAsync(run.ThreadId, cancellationToken); + + /// + /// List all of the runs associated to a thread. + /// + /// . + /// . + /// Optional, . + /// + public static async Task> ListRunsAsync(this ThreadResponse thread, ListQuery query = null, CancellationToken cancellationToken = default) + => await thread.Client.ThreadsEndpoint.ListRunsAsync(thread.Id, query, cancellationToken); + + /// + /// Get the latest status of the . + /// + /// . + /// Optional, . + /// . + public static async Task UpdateAsync(this RunResponse run, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.RetrieveRunAsync(run.ThreadId, run.Id, cancellationToken); + + /// + /// Modifies a run. + /// + /// + /// Only the can be modified. + /// + /// to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// Optional, . + /// . + public static async Task ModifyAsync(this RunResponse run, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.ModifyRunAsync(run.ThreadId, run.Id, metadata, cancellationToken); + + /// + /// Waits for to change. + /// + /// . + /// Optional, time in milliseconds to wait before polling status. + /// Optional, timeout in seconds to cancel polling.
Defaults to 30 seconds.
Set to -1 for indefinite. + /// Optional, . + /// . + public static async Task WaitForStatusChangeAsync(this RunResponse run, int? pollingInterval = null, int? timeout = null, CancellationToken cancellationToken = default) + { + using var cts = timeout is null or < 0 ? new CancellationTokenSource(TimeSpan.FromSeconds(timeout ?? 30)) : new CancellationTokenSource(); + using var chainedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); + RunResponse result; + do + { + result = await run.UpdateAsync(cancellationToken: chainedCts.Token); + await Task.Delay(pollingInterval ?? 500, chainedCts.Token); + } while (result.Status is RunStatus.Queued or RunStatus.InProgress or RunStatus.Cancelling); + return result; + } + + /// + /// When a run has the status: "requires_action" and required_action.type is submit_tool_outputs, + /// this endpoint can be used to submit the outputs from the tool calls once they're all completed. + /// All outputs must be submitted in a single request. + /// + /// to submit outputs for. + /// . + /// Optional, . + /// . + public static async Task SubmitToolOutputsAsync(this RunResponse run, SubmitToolOutputsRequest request, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.SubmitToolOutputsAsync(run.ThreadId, run.Id, request, cancellationToken); + + /// + /// Returns a list of run steps belonging to a run. + /// + /// to list run steps for. + /// Optional, . + /// Optional, . + /// . + public static async Task> ListRunStepsAsync(this RunResponse run, ListQuery query = null, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.ListRunStepsAsync(run.ThreadId, run.Id, query, cancellationToken); + + /// + /// Retrieves a run step. + /// + /// to retrieve step for. + /// Id of the run step. + /// Optional, . + /// . + public static async Task RetrieveRunStepAsync(this RunResponse run, string runStepId, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.RetrieveRunStepAsync(run.ThreadId, run.Id, runStepId, cancellationToken); + + /// + /// Retrieves a run step. + /// + /// to retrieve. + /// Optional, . + /// . + public static async Task UpdateAsync(this RunStepResponse runStep, CancellationToken cancellationToken = default) + => await runStep.Client.ThreadsEndpoint.RetrieveRunStepAsync(runStep.ThreadId, runStep.RunId, runStep.Id, cancellationToken); + + /// + /// Cancels a run that is . + /// + /// to cancel. + /// Optional, . + /// . + public static async Task CancelAsync(this RunResponse run, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.CancelRunAsync(run.ThreadId, run.Id, cancellationToken); + + /// + /// Returns a list of messages for a given thread that the run belongs to. + /// + /// . + /// . + /// Optional, . + /// . + public static async Task> ListMessagesAsync(this RunResponse run, ListQuery query = null, CancellationToken cancellationToken = default) + => await run.Client.ThreadsEndpoint.ListMessagesAsync(run.ThreadId, query, cancellationToken); + + #endregion Runs + } +} diff --git a/Runtime/Threads/ThreadExtensions.cs.meta b/Runtime/Threads/ThreadExtensions.cs.meta new file mode 100644 index 00000000..20b77959 --- /dev/null +++ b/Runtime/Threads/ThreadExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e982409c6be4284eb6a731111e2cac5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/ThreadResponse.cs b/Runtime/Threads/ThreadResponse.cs new file mode 100644 index 00000000..cf8071c1 --- /dev/null +++ b/Runtime/Threads/ThreadResponse.cs @@ -0,0 +1,71 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + /// + /// A conversation session between an Assistant and a user. + /// Threads store Messages and automatically handle truncation to fit content into a model�s context. + /// + [Preserve] + public sealed class ThreadResponse : BaseResponse + { + [Preserve] + [JsonConstructor] + public ThreadResponse( + [JsonProperty("id")] string id, + [JsonProperty("object")] string @object, + [JsonProperty("created_at")] int createdAtUnitTimeSeconds, + [JsonProperty("metadata")] IReadOnlyDictionary metadata) + { + Id = id; + Object = @object; + CreatedAtUnixTimeSeconds = createdAtUnitTimeSeconds; + Metadata = metadata; + } + + /// + /// The identifier, which can be referenced in API endpoints. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + /// + /// The object type, which is always thread. + /// + [Preserve] + [JsonProperty("object")] + public string Object { get; } + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + [Preserve] + [JsonProperty("created_at")] + public int CreatedAtUnixTimeSeconds { get; } + + [Preserve] + [JsonIgnore] + public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTimeSeconds).DateTime; + + /// + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + [Preserve] + [JsonProperty("metadata")] + public IReadOnlyDictionary Metadata { get; } + + [Preserve] + public static implicit operator string(ThreadResponse thread) => thread?.ToString(); + + [Preserve] + public override string ToString() => Id; + } +} diff --git a/Runtime/Threads/ThreadResponse.cs.meta b/Runtime/Threads/ThreadResponse.cs.meta new file mode 100644 index 00000000..567fb1c3 --- /dev/null +++ b/Runtime/Threads/ThreadResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66f1f837abde53e4b9a6ce488cc2f918 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/ThreadsEndpoint.cs b/Runtime/Threads/ThreadsEndpoint.cs new file mode 100644 index 00000000..5773503f --- /dev/null +++ b/Runtime/Threads/ThreadsEndpoint.cs @@ -0,0 +1,357 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using OpenAI.Extensions; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Utilities.WebRequestRest; + +namespace OpenAI.Threads +{ + /// + /// Create threads that assistants can interact with.
+ /// + ///
+ public sealed class ThreadsEndpoint : OpenAIBaseEndpoint + { + internal ThreadsEndpoint(OpenAIClient client) : base(client) { } + + protected override string Root => "threads"; + + /// + /// Create a thread. + /// + /// . + /// Optional, . + /// . + public async Task CreateThreadAsync(CreateThreadRequest request = null, CancellationToken cancellationToken = default) + { + var response = await Rest.PostAsync(GetUrl(), request == null ? string.Empty : JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Retrieves a thread. + /// + /// The id of the to retrieve. + /// Optional, . + /// . + public async Task RetrieveThreadAsync(string threadId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Modifies a thread. + /// + /// + /// Only the can be modified. + /// + /// The id of the to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public async Task ModifyThreadAsync(string threadId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + { + var jsonContent = JsonConvert.SerializeObject(new { metadata }, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{threadId}"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Delete a thread. + /// + /// The id of the to delete. + /// Optional, . + /// True, if was successfully deleted. + public async Task DeleteThreadAsync(string threadId, CancellationToken cancellationToken = default) + { + var response = await Rest.DeleteAsync(GetUrl($"/{threadId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return JsonConvert.DeserializeObject(response.Body, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; + } + + #region Messages + + /// + /// Create a message. + /// + /// The id of the thread to create a message for. + /// + /// Optional, . + /// . + public async Task CreateMessageAsync(string threadId, CreateMessageRequest request, CancellationToken cancellationToken = default) + { + var jsonContent = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{threadId}/messages"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Returns a list of messages for a given thread. + /// + /// The id of the thread the messages belong to. + /// . + /// Optional, . + /// . + public async Task> ListMessagesAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/messages", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize>(client); + } + + /// + /// Retrieve a message. + /// + /// The id of the thread to which this message belongs. + /// The id of the message to retrieve. + /// Optional, . + /// . + public async Task RetrieveMessageAsync(string threadId, string messageId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/messages/{messageId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Modifies a message. + /// + /// + /// Only the can be modified. + /// + /// to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public async Task ModifyMessageAsync(MessageResponse message, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + => await ModifyMessageAsync(message.ThreadId, message.Id, metadata, cancellationToken); + + /// + /// Modifies a message. + /// + /// + /// Only the can be modified. + /// + /// The id of the thread to which this message belongs. + /// The id of the message to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// + /// Optional, . + /// . + public async Task ModifyMessageAsync(string threadId, string messageId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + { + var jsonContent = JsonConvert.SerializeObject(new { metadata }, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{threadId}/messages/{messageId}"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + #endregion Messages + + #region Files + + /// + /// Returns a list of message files. + /// + /// The id of the thread that the message and files belong to. + /// The id of the message that the files belongs to. + /// . + /// Optional, . + /// . + public async Task> ListFilesAsync(string threadId, string messageId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize>(client); + } + + /// + /// Retrieve message file. + /// + /// The id of the thread to which the message and file belong. + /// The id of the message the file belongs to. + /// The id of the file being retrieved. + /// Optional, . + /// . + public async Task RetrieveFileAsync(string threadId, string messageId, string fileId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files/{fileId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + #endregion Files + + #region Runs + + /// + /// Returns a list of runs belonging to a thread. + /// + /// The id of the thread the run belongs to. + /// . + /// Optional, . + /// + public async Task> ListRunsAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/runs", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize>(client); + } + + /// + /// Create a run. + /// + /// The id of the thread to run. + /// + /// Optional, . + /// . + public async Task CreateRunAsync(string threadId, CreateRunRequest request = null, CancellationToken cancellationToken = default) + { + if (request == null || string.IsNullOrWhiteSpace(request.AssistantId)) + { + var assistant = await client.AssistantsEndpoint.CreateAssistantAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + request = new CreateRunRequest(assistant, request); + } + + var jsonContent = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{threadId}/runs"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Create a thread and run it in one request. + /// + /// . + /// Optional, . + /// . + public async Task CreateThreadAndRunAsync(CreateThreadAndRunRequest request = null, CancellationToken cancellationToken = default) + { + if (request == null || string.IsNullOrWhiteSpace(request.AssistantId)) + { + var assistant = await client.AssistantsEndpoint.CreateAssistantAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + request = new CreateThreadAndRunRequest(assistant, request); + } + + var jsonContent = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl("/runs"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Retrieves a run. + /// + /// The id of the thread that was run. + /// The id of the run to retrieve. + /// Optional, . + /// . + public async Task RetrieveRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/runs/{runId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Modifies a run. + /// + /// + /// Only the can be modified. + /// + /// The id of the thread that was run. + /// The id of the to modify. + /// Set of 16 key-value pairs that can be attached to an object. + /// This can be useful for storing additional information about the object in a structured format. + /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// Optional, . + /// . + public async Task ModifyRunAsync(string threadId, string runId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) + { + var jsonContent = JsonConvert.SerializeObject(new { metadata }, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{threadId}/runs/{runId}"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// When a run has the status: "requires_action" and required_action.type is submit_tool_outputs, + /// this endpoint can be used to submit the outputs from the tool calls once they're all completed. + /// All outputs must be submitted in a single request. + /// + /// The id of the thread to which this run belongs. + /// The id of the run that requires the tool output submission. + /// . + /// Optional, . + /// . + public async Task SubmitToolOutputsAsync(string threadId, string runId, SubmitToolOutputsRequest request, CancellationToken cancellationToken = default) + { + var jsonContent = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions); + var response = await Rest.PostAsync(GetUrl($"/{threadId}/runs/{runId}/submit_tool_outputs"), jsonContent, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Returns a list of run steps belonging to a run. + /// + /// The id of the thread to which the run and run step belongs. + /// The id of the run to which the run step belongs. + /// Optional, . + /// Optional, . + /// . + public async Task> ListRunStepsAsync(string threadId, string runId, ListQuery query = null, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps", query), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize>(client); + } + + /// + /// Retrieves a run step. + /// + /// The id of the thread to which the run and run step belongs. + /// The id of the run to which the run step belongs. + /// The id of the run step to retrieve. + /// Optional, . + /// . + public async Task RetrieveRunStepAsync(string threadId, string runId, string stepId, CancellationToken cancellationToken = default) + { + var response = await Rest.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps/{stepId}"), new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + /// + /// Cancels a run that is . + /// + /// The id of the thread to which this run belongs. + /// The id of the run to cancel. + /// Optional, . + /// . + public async Task CancelRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) + { + var response = await Rest.PostAsync(GetUrl($"/{threadId}/runs/{runId}/cancel"), string.Empty, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + response.Validate(EnableDebug); + return response.Deserialize(client); + } + + #endregion Runs + } +} diff --git a/Runtime/Threads/ThreadsEndpoint.cs.meta b/Runtime/Threads/ThreadsEndpoint.cs.meta new file mode 100644 index 00000000..c50dcae6 --- /dev/null +++ b/Runtime/Threads/ThreadsEndpoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 455c6324086824a43b008f4b41d58708 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/ToolCall.cs b/Runtime/Threads/ToolCall.cs new file mode 100644 index 00000000..6f55bcd8 --- /dev/null +++ b/Runtime/Threads/ToolCall.cs @@ -0,0 +1,45 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + [Preserve] + public sealed class ToolCall + { + [Preserve] + [JsonConstructor] + public ToolCall( + [JsonProperty("id")] string id, + [JsonProperty("type")] string type, + [JsonProperty("function")] FunctionCall functionCall) + { + Id = id; + Type = type; + FunctionCall = functionCall; + } + + /// + /// The ID of the tool call. + /// This ID must be referenced when you submit the tool outputs in using the Submit tool outputs to run endpoint. + /// + [Preserve] + [JsonProperty("id")] + public string Id { get; } + + /// + /// The type of tool call the output is required for. For now, this is always 'function'. + /// + [Preserve] + [JsonProperty("type")] + public string Type { get; } + + /// + /// The function definition. + /// + [Preserve] + [JsonProperty("function")] + public FunctionCall FunctionCall { get; } + } +} diff --git a/Runtime/Threads/ToolCall.cs.meta b/Runtime/Threads/ToolCall.cs.meta new file mode 100644 index 00000000..a69e5473 --- /dev/null +++ b/Runtime/Threads/ToolCall.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb2e300e021c16945a5b2c176d626a8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Threads/ToolOutput.cs b/Runtime/Threads/ToolOutput.cs new file mode 100644 index 00000000..837fd95d --- /dev/null +++ b/Runtime/Threads/ToolOutput.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using UnityEngine.Scripting; + +namespace OpenAI.Threads +{ + /// + /// Tool function call output + /// + [Preserve] + public sealed class ToolOutput + { + /// + /// Constructor. + /// + /// + /// The ID of the tool call in the within the the output is being submitted for. + /// + /// + /// The output of the tool call to be submitted to continue the run. + /// + [Preserve] + [JsonConstructor] + public ToolOutput( + [JsonProperty("tool_call_id")] string toolCallId, + [JsonProperty("output")] string output) + { + ToolCallId = toolCallId; + Output = output; + } + + /// + /// The ID of the tool call in the within the the output is being submitted for. + /// + [Preserve] + [JsonProperty("tool_call_id")] + public string ToolCallId { get; } + + /// + /// The output of the tool call to be submitted to continue the run. + /// + [Preserve] + [JsonProperty("output")] + public string Output { get; } + } +} \ No newline at end of file diff --git a/Runtime/Threads/ToolOutput.cs.meta b/Runtime/Threads/ToolOutput.cs.meta new file mode 100644 index 00000000..52e8a8d0 --- /dev/null +++ b/Runtime/Threads/ToolOutput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce523e5590760d94e94de129aec5dbbd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/AbstractTestFixture.cs b/Tests/AbstractTestFixture.cs index bf2a5082..be354de8 100644 --- a/Tests/AbstractTestFixture.cs +++ b/Tests/AbstractTestFixture.cs @@ -11,7 +11,7 @@ protected AbstractTestFixture() var auth = new OpenAIAuthentication().LoadDefaultsReversed(); var settings = new OpenAISettings(); OpenAIClient = new OpenAIClient(auth, settings); - //OpenAIClient.EnableDebug = true; + OpenAIClient.EnableDebug = true; } } } diff --git a/Tests/AbstractTestFixture.cs.meta b/Tests/AbstractTestFixture.cs.meta index 4baaf3f1..20d181d5 100644 --- a/Tests/AbstractTestFixture.cs.meta +++ b/Tests/AbstractTestFixture.cs.meta @@ -5,7 +5,7 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} userData: assetBundleName: assetBundleVariant: diff --git a/Tests/TestFixture_03_Chat.cs b/Tests/TestFixture_03_Chat.cs index 3feab744..46883dfa 100644 --- a/Tests/TestFixture_03_Chat.cs +++ b/Tests/TestFixture_03_Chat.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using UnityEditor; using UnityEngine; -using Tool = OpenAI.Chat.Tool; namespace OpenAI.Tests { diff --git a/Tests/TestFixture_11_Assistants.cs b/Tests/TestFixture_11_Assistants.cs new file mode 100644 index 00000000..b38f0640 --- /dev/null +++ b/Tests/TestFixture_11_Assistants.cs @@ -0,0 +1,157 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using NUnit.Framework; +using OpenAI.Assistants; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEngine; + +namespace OpenAI.Tests +{ + internal class TestFixture_11_Assistants : AbstractTestFixture + { + private AssistantResponse testAssistant; + + [Test] + public async Task Test_01_CreateAssistant() + { + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + const string testFilePath = "assistant_test_1.txt"; + await File.WriteAllTextAsync(testFilePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(testFilePath)); + var file = await OpenAIClient.FilesEndpoint.UploadFileAsync(testFilePath, "assistants"); + File.Delete(testFilePath); + Assert.IsFalse(File.Exists(testFilePath)); + var request = new CreateAssistantRequest("gpt-3.5-turbo-1106", + name: "test-assistant", + description: "Used for unit testing.", + instructions: "You are test assistant", + metadata: new Dictionary + { + ["int"] = "1", + ["test"] = Guid.NewGuid().ToString() + }, + tools: new[] + { + Tool.Retrieval + }, + files: new[] { file.Id }); + var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(request); + Assert.IsNotNull(assistant); + Assert.AreEqual("test-assistant", assistant.Name); + Assert.AreEqual("Used for unit testing.", assistant.Description); + Assert.AreEqual("You are test assistant", assistant.Instructions); + Assert.AreEqual("gpt-3.5-turbo-1106", assistant.Model); + Assert.IsNotEmpty(assistant.Metadata); + testAssistant = assistant; + Debug.Log($"{assistant} -> {assistant.Metadata["test"]}"); + } + + [Test] + public async Task Test_02_ListAssistants() + { + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var assistantsList = await OpenAIClient.AssistantsEndpoint.ListAssistantsAsync(); + Assert.IsNotNull(assistantsList); + Assert.IsNotEmpty(assistantsList.Items); + + foreach (var assistant in assistantsList.Items) + { + var retrieved = OpenAIClient.AssistantsEndpoint.RetrieveAssistantAsync(assistant); + Assert.IsNotNull(retrieved); + Debug.Log($"{assistant} -> {assistant.CreatedAt}"); + } + } + + [Test] + public async Task Test_03_ModifyAssistants() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var request = new CreateAssistantRequest( + model: "gpt-4-1106-preview", + name: "Test modified", + description: "Modified description", + instructions: "You are modified test assistant"); + var assistant = await testAssistant.ModifyAsync(request); + Assert.IsNotNull(assistant); + Assert.AreEqual("Test modified", assistant.Name); + Assert.AreEqual("Modified description", assistant.Description); + Assert.AreEqual("You are modified test assistant", assistant.Instructions); + Assert.AreEqual("gpt-4-1106-preview", assistant.Model); + Assert.IsTrue(assistant.Metadata.ContainsKey("test")); + Debug.Log($"{assistant.Id} -> modified"); + } + + [Test] + public async Task Test_04_01_UploadAssistantFile() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + const string testFilePath = "assistant_test_2.txt"; + await File.WriteAllTextAsync(testFilePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(testFilePath)); + var file = testAssistant.UploadFileAsync(testFilePath); + Assert.IsNotNull(file); + } + + [Test] + public async Task Test_04_02_ListAssistantFiles() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var filesList = await testAssistant.ListFilesAsync(); + Assert.IsNotNull(filesList); + Assert.IsNotEmpty(filesList.Items); + + foreach (var file in filesList.Items) + { + Assert.IsNotNull(file); + var retrieved = await testAssistant.RetrieveFileAsync(file); + Assert.IsNotNull(retrieved); + Assert.IsTrue(retrieved.Id == file.Id); + Debug.Log($"{retrieved.AssistantId}'s file -> {retrieved.Id}"); + // TODO 400 Bad Request error when attempting to download assistant files. Likely OpenAI bug. + //var downloadPath = await retrieved.DownloadFileAsync(Directory.GetCurrentDirectory(), true); + //Debug.Log($"downloaded {retrieved} -> {downloadPath}"); + //Assert.IsTrue(File.Exists(downloadPath)); + //File.Delete(downloadPath); + //Assert.IsFalse(File.Exists(downloadPath)); + } + } + + [Test] + public async Task Test_04_03_DeleteAssistantFiles() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var filesList = await testAssistant.ListFilesAsync(); + Assert.IsNotNull(filesList); + Assert.IsNotEmpty(filesList.Items); + + foreach (var file in filesList.Items) + { + Assert.IsNotNull(file); + var isDeleted = await testAssistant.DeleteFileAsync(file); + Assert.IsTrue(isDeleted); + Debug.Log($"Deleted {file.Id}"); + } + + filesList = await testAssistant.ListFilesAsync(); + Assert.IsNotNull(filesList); + Assert.IsEmpty(filesList.Items); + } + + [Test] + public async Task Test_05_DeleteAssistant() + { + Assert.IsNotNull(testAssistant); + Assert.IsNotNull(OpenAIClient.AssistantsEndpoint); + var result = await testAssistant.DeleteAsync(); + Assert.IsTrue(result); + Debug.Log($"{testAssistant.Id} -> deleted"); + } + } +} diff --git a/Tests/TestFixture_11_Assistants.cs.meta b/Tests/TestFixture_11_Assistants.cs.meta new file mode 100644 index 00000000..603d49b6 --- /dev/null +++ b/Tests/TestFixture_11_Assistants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0266cf7fb359e464b8f82ed115e2b7b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/TestFixture_12_Threads.cs b/Tests/TestFixture_12_Threads.cs new file mode 100644 index 00000000..682cf813 --- /dev/null +++ b/Tests/TestFixture_12_Threads.cs @@ -0,0 +1,452 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using OpenAI.Assistants; +using OpenAI.Files; +using OpenAI.Tests.Weather; +using OpenAI.Threads; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; + +namespace OpenAI.Tests +{ + + /// + /// https://github.com/openai/openai-cookbook/blob/main/examples/Assistants_API_overview_python.ipynb + /// + internal class TestFixture_12_Threads : AbstractTestFixture + { + private static RunResponse testRun; + private static ThreadResponse testThread; + private static MessageResponse testMessage; + private static AssistantResponse testAssistant; + + [Test] + public async Task Test_01_CreateThread() + { + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var thread = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(new CreateThreadRequest( + new List + { + "Test message" + }, + new Dictionary + { + ["test"] = nameof(Test_01_CreateThread) + })); + Assert.IsNotNull(thread); + Assert.IsNotNull(thread.Metadata); + Assert.IsNotEmpty(thread.Metadata); + testThread = thread; + Debug.Log($"Create thread {thread.Id} -> {thread.CreatedAt}"); + } + + [Test] + public async Task Test_02_RetrieveThread() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var thread = await testThread.UpdateAsync(); + Assert.IsNotNull(thread); + Assert.AreEqual(testThread.Id, thread.Id); + Assert.IsNotNull(thread.Metadata); + Debug.Log($"Retrieve thread {thread.Id} -> {thread.CreatedAt}"); + } + + [Test] + public async Task Test_03_ModifyThread() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var newMetadata = new Dictionary + { + ["test"] = nameof(Test_03_ModifyThread) + }; + var thread = await testThread.ModifyAsync(newMetadata); + Assert.IsNotNull(thread); + Assert.AreEqual(testThread.Id, thread.Id); + Assert.IsNotNull(thread.Metadata); + Debug.Log($"Modify thread {thread.Id} -> {thread.Metadata["test"]}"); + } + + [Test] + public async Task Test_04_01_CreateMessage() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + const string testFilePath = "assistant_test_1.txt"; + await File.WriteAllTextAsync(testFilePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(testFilePath)); + var file = await OpenAIClient.FilesEndpoint.UploadFileAsync(testFilePath, "assistants"); + Assert.NotNull(file); + File.Delete(testFilePath); + Assert.IsFalse(File.Exists(testFilePath)); + await testThread.CreateMessageAsync("hello world!"); + var request = new CreateMessageRequest("Test create message", + new[] { file.Id }, + new Dictionary + { + ["test"] = nameof(Test_04_01_CreateMessage) + }); + MessageResponse message; + try + { + message = await testThread.CreateMessageAsync(request); + } + finally + { + await CleanupFileAsync(file); + } + + Assert.IsNotNull(message); + Assert.AreEqual(testThread.Id, message.ThreadId); + testMessage = message; + } + + [Test] + public async Task Test_04_02_ListMessages() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var message1 = await testThread.CreateMessageAsync("Test message 1"); + Assert.IsNotNull(message1); + var message2 = await testThread.CreateMessageAsync("Test message 2"); + Assert.IsNotNull(message2); + var list = await testThread.ListMessagesAsync(); + Assert.IsNotNull(list); + Assert.IsNotEmpty(list.Items); + + foreach (var message in list.Items) + { + Assert.NotNull(message); + var threadMessage = await testThread.RetrieveMessageAsync(message); + Assert.NotNull(threadMessage); + Debug.Log($"[{threadMessage.Id}] {threadMessage.Role}: {threadMessage.PrintContent()}"); + var updated = await message.UpdateAsync(); + Assert.IsNotNull(updated); + } + } + + [Test] + public async Task Test_04_03_ModifyMessage() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var metadata = new Dictionary + { + ["test"] = nameof(Test_04_03_ModifyMessage) + }; + var modified = await testMessage.ModifyAsync(metadata); + Assert.IsNotNull(modified); + Assert.IsNotNull(modified.Metadata); + Assert.IsTrue(modified.Metadata["test"].Equals(nameof(Test_04_03_ModifyMessage))); + Debug.Log($"Modify message metadata: {modified.Id} -> {modified.Metadata["test"]}"); + metadata.Add("test2", nameof(Test_04_03_ModifyMessage)); + var modifiedThreadMessage = await testThread.ModifyMessageAsync(modified, metadata); + Assert.IsNotNull(modifiedThreadMessage); + Assert.IsNotNull(modifiedThreadMessage.Metadata); + Debug.Log($"Modify message metadata: {modifiedThreadMessage.Id} -> {string.Join("\n", modifiedThreadMessage.Metadata.Select(meta => $"[{meta.Key}] {meta.Value}"))}"); + } + + [Test] + public async Task Test_04_04_UploadAndDownloadMessageFiles() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var file1 = await CreateTestFileAsync("test_1.txt"); + var file2 = await CreateTestFileAsync("test_2.txt"); + try + { + var createRequest = new CreateMessageRequest("Test content with files", new[] { file1.Id, file2.Id }); + var message = await testThread.CreateMessageAsync(createRequest); + var fileList = await message.ListFilesAsync(); + Assert.IsNotNull(fileList); + Assert.AreEqual(2, fileList.Items.Count); + + foreach (var file in fileList.Items) + { + var retrieved = await message.RetrieveFileAsync(file); + Assert.IsNotNull(retrieved); + Debug.Log(file.Id); + // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + //var filePath = await message.DownloadFileContentAsync(file, Directory.GetCurrentDirectory(), true); + //Assert.IsFalse(string.IsNullOrWhiteSpace(filePath)); + //Assert.IsTrue(File.Exists(filePath)); + //File.Delete(filePath); + } + + var threadList = await testThread.ListFilesAsync(message); + Assert.IsNotNull(threadList); + Assert.IsNotEmpty(threadList.Items); + + //foreach (var file in threadList.Items) + //{ + // // TODO 400 bad request errors. Likely OpenAI bug downloading message file content. + // var filePath = await file.DownloadContentAsync(Directory.GetCurrentDirectory(), true); + // Assert.IsFalse(string.IsNullOrWhiteSpace(filePath)); + // Assert.IsTrue(File.Exists(filePath)); + // File.Delete(filePath); + //} + } + finally + { + await CleanupFileAsync(file1); + await CleanupFileAsync(file2); + } + } + + [Test] + public async Task Test_05_DeleteThread() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + var isDeleted = await testThread.DeleteAsync(); + Assert.IsTrue(isDeleted); + Debug.Log($"Deleted thread {testThread.Id}"); + } + + + + [Test] + public async Task Test_06_01_CreateRun() + { + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync( + new CreateAssistantRequest( + name: "Math Tutor", + instructions: "You are a personal math tutor. Answer questions briefly, in a sentence or less.", + model: "gpt-4-1106-preview")); + Assert.NotNull(assistant); + testAssistant = assistant; + var thread = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(); + Assert.NotNull(thread); + + try + { + var message = thread.CreateMessageAsync("I need to solve the equation `3x + 11 = 14`. Can you help me?"); + Assert.NotNull(message); + var run = await thread.CreateRunAsync(assistant); + Assert.IsNotNull(run); + var threadRun = thread.CreateRunAsync(); + Assert.NotNull(threadRun); + } + finally + { + await thread.DeleteAsync(); + } + } + + [Test] + public async Task Test_06_02_CreateThreadAndRun() + { + Assert.NotNull(testAssistant); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var messages = new List { "I need to solve the equation `3x + 11 = 14`. Can you help me?" }; + var threadRequest = new CreateThreadRequest(messages); + var run = await testAssistant.CreateThreadAndRunAsync(threadRequest); + Assert.IsNotNull(run); + Debug.Log($"Created thread and run: {run.ThreadId} -> {run.Id} -> {run.CreatedAt}"); + testRun = run; + var thread = await run.GetThreadAsync(); + Assert.NotNull(thread); + testThread = thread; + } + + [Test] + public async Task Test_06_03_ListRunsAndSteps() + { + Assert.NotNull(testThread); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var runList = await testThread.ListRunsAsync(); + Assert.IsNotNull(runList); + Assert.IsNotEmpty(runList.Items); + + foreach (var run in runList.Items) + { + Assert.IsNotNull(run); + Assert.IsNotNull(run.Client); + var retrievedRun = await run.UpdateAsync(); + Assert.IsNotNull(retrievedRun); + Debug.Log($"[{retrievedRun.Id}] {retrievedRun.Status} | {retrievedRun.CreatedAt}"); + } + } + + [Test] + public async Task Test_06_04_ModifyRun() + { + Assert.NotNull(testRun); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + // run in Queued and InProgress can't be modified + var run = await testRun.WaitForStatusChangeAsync(); + Assert.IsNotNull(run); + Assert.IsTrue(run.Status == RunStatus.Completed); + var metadata = new Dictionary + { + ["test"] = nameof(Test_06_04_ModifyRun) + }; + var modified = await run.ModifyAsync(metadata); + Assert.IsNotNull(modified); + Assert.AreEqual(run.Id, modified.Id); + Assert.IsNotNull(modified.Metadata); + Assert.Contains("test", modified.Metadata.Keys.ToList()); + Assert.AreEqual(nameof(Test_06_04_ModifyRun), modified.Metadata["test"]); + } + + [Test] + public async Task Test_06_05_CancelRun() + { + Assert.IsNotNull(testThread); + Assert.IsNotNull(testAssistant); + Assert.NotNull(OpenAIClient.ThreadsEndpoint); + var run = await testThread.CreateRunAsync(testAssistant); + Assert.IsNotNull(run); + Assert.IsTrue(run.Status == RunStatus.Queued); + run = await run.CancelAsync(); + Assert.IsNotNull(run); + Assert.IsTrue(run.Status == RunStatus.Cancelling); + + try + { + // waiting while run is cancelling + run = await run.WaitForStatusChangeAsync(); + } + catch (Exception e) + { + // Sometimes runs will get stuck in Cancelling state, + // for now we just log when it happens. + Debug.Log(e); + } + + Assert.IsTrue(run.Status is RunStatus.Cancelled or RunStatus.Cancelling); + } + + [Test] + public async Task Test_06_06_TestCleanup() + { + if (testAssistant != null) + { + var isDeleted = await testAssistant.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + + if (testThread != null) + { + var isDeleted = await testThread.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + } + + [Test] + public async Task Test_07_01_SubmitToolOutput() + { + var function = new Function( + nameof(WeatherService.GetCurrentWeather), + "Get the current weather in a given location", + new JObject + { + ["type"] = "object", + ["properties"] = new JObject + { + ["location"] = new JObject + { + ["type"] = "string", + ["description"] = "The city and state, e.g. San Francisco, CA" + }, + ["unit"] = new JObject + { + ["type"] = "string", + ["enum"] = new JArray { "celsius", "fahrenheit" } + } + }, + ["required"] = new JArray { "location", "unit" } + }); + testAssistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(new CreateAssistantRequest(tools: new Tool[] { function })); + var run = await testAssistant.CreateThreadAndRunAsync("I'm in Kuala-Lumpur, please tell me what's the temperature in celsius now?"); + testThread = await run.GetThreadAsync(); + // waiting while run is Queued and InProgress + run = await run.WaitForStatusChangeAsync(); + Assert.IsNotNull(run); + Assert.AreEqual(RunStatus.RequiresAction, run.Status); + Assert.IsNotNull(run.RequiredAction); + Assert.IsNotNull(run.RequiredAction.SubmitToolOutputs); + Assert.IsNotEmpty(run.RequiredAction.SubmitToolOutputs.ToolCalls); + + var runStepList = await run.ListRunStepsAsync(); + Assert.IsNotNull(runStepList); + Assert.IsNotEmpty(runStepList.Items); + + foreach (var runStep in runStepList.Items) + { + Assert.IsNotNull(runStep); + Assert.IsNotNull(runStep.Client); + var retrievedRunStep = await runStep.UpdateAsync(); + Assert.IsNotNull(retrievedRunStep); + Debug.Log($"[{runStep.Id}] {runStep.Status} {runStep.CreatedAt} -> {runStep.ExpiresAt}"); + var retrieveStepRunStep = await run.RetrieveRunStepAsync(runStep.Id); + Assert.IsNotNull(retrieveStepRunStep); + } + + var toolCall = run.RequiredAction.SubmitToolOutputs.ToolCalls[0]; + Assert.AreEqual("function", toolCall.Type); + Assert.IsNotNull(toolCall.FunctionCall); + Assert.AreEqual(nameof(WeatherService.GetCurrentWeather), toolCall.FunctionCall.Name); + Assert.IsNotNull(toolCall.FunctionCall.Arguments); + Debug.Log($"tool call arguments: {toolCall.FunctionCall.Arguments}"); + var functionArgs = JsonConvert.DeserializeObject(toolCall.FunctionCall.Arguments); + var functionResult = WeatherService.GetCurrentWeather(functionArgs); + var toolOutput = new ToolOutput(toolCall.Id, functionResult); + run = await run.SubmitToolOutputsAsync(toolOutput); + // waiting while run in Queued and InProgress + run = await run.WaitForStatusChangeAsync(); + Assert.AreEqual(RunStatus.Completed, run.Status); + var messages = await run.ListMessagesAsync(); + Assert.IsNotNull(messages); + Assert.IsNotEmpty(messages.Items); + + foreach (var message in messages.Items.OrderBy(response => response.CreatedAt)) + { + Assert.IsNotNull(message); + Assert.IsNotEmpty(message.Content); + Debug.Log($"{message.Role}: {message.PrintContent()}"); + } + } + + [Test] + public async Task Test_07_02_TestCleanup() + { + if (testAssistant != null) + { + var isDeleted = await testAssistant.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + + if (testThread != null) + { + var isDeleted = await testThread.DeleteAsync(); + Assert.IsTrue(isDeleted); + } + } + + private async Task CreateTestFileAsync(string filePath) + { + await File.WriteAllTextAsync(filePath, "Knowledge is power!"); + Assert.IsTrue(File.Exists(filePath)); + var file = await OpenAIClient.FilesEndpoint.UploadFileAsync(filePath, "assistants"); + File.Delete(filePath); + Assert.IsFalse(File.Exists(filePath)); + return file; + } + + private async Task CleanupFileAsync(FileResponse file) + { + var isDeleted = await OpenAIClient.FilesEndpoint.DeleteFileAsync(file); + Assert.IsTrue(isDeleted); + } + } +} diff --git a/Tests/TestFixture_12_Threads.cs.meta b/Tests/TestFixture_12_Threads.cs.meta new file mode 100644 index 00000000..f6f541a2 --- /dev/null +++ b/Tests/TestFixture_12_Threads.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 656a82a3e3c427840867699f236041c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 84a7eb8fc6eba7540bf56cea8e12249c, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 902202d5..3368838b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "OpenAI", "description": "A OpenAI package for the Unity Game Engine to use GPT-4, GPT-3.5, GPT-3 and Dall-E though their RESTful API (currently in beta).\n\nIndependently developed, this is not an official library and I am not affiliated with OpenAI.\n\nAn OpenAI API account is required.", "keywords": [], - "version": "6.0.0", + "version": "7.0.0", "unity": "2021.3", "documentationUrl": "https://github.com/RageAgainstThePixel/com.openai.unity#documentation", "changelogUrl": "https://github.com/RageAgainstThePixel/com.openai.unity/releases",