From d3e59d19521b49d06237e2e868e826e17e8dbb0d Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 25 Feb 2024 15:38:44 -0500 Subject: [PATCH] OpenAI-DotNet 7.7.1 (#247) - More Function utilities and invoking methods - Added FunctionPropertyAttribute to help better inform the feature how to format the Function json - Added FromFunc<,> overloads for convenance - Fixed invoke args sometimes being casting to wrong type - Added additional protections for static and instanced function calls - Added additional tool utilities: - Tool.ClearRegisteredTools - Tool.IsToolRegistered(Tool) - Tool.TryRegisterTool(Tool) - Improved memory usage and performance by propertly disposing http content and response objects - Updated debug output to be formatted to json for easier reading and debugging --- ...cs => TestFixture_00_01_Authentication.cs} | 2 +- .../TestFixture_00_02_Extensions.cs | 40 --- .../TestFixture_00_02_Tools.cs | 78 +++++ OpenAI-DotNet-Tests/TestFixture_03_Chat.cs | 19 +- OpenAI-DotNet-Tests/TestFixture_12_Threads.cs | 11 +- .../Assistants/AssistantExtensions.cs | 8 +- .../Assistants/AssistantsEndpoint.cs | 44 +-- .../Assistants/CreateAssistantRequest.cs | 9 +- OpenAI-DotNet/Audio/AudioEndpoint.cs | 17 +- .../Audio/AudioTranscriptionRequest.cs | 4 +- .../Audio/AudioTranslationRequest.cs | 4 +- OpenAI-DotNet/Audio/SpeechRequest.cs | 15 + .../Authentication/OpenAIAuthentication.cs | 2 +- OpenAI-DotNet/Chat/ChatEndpoint.cs | 96 ++++-- OpenAI-DotNet/Chat/Delta.cs | 2 +- .../{Threads => Common}/AnnotationType.cs | 0 OpenAI-DotNet/Common/Function.cs | 242 +++++++++++--- .../Common/FunctionPropertyAttribute.cs | 53 +++ ...{BaseEndPoint.cs => OpenAIBaseEndpoint.cs} | 4 +- OpenAI-DotNet/Common/Tool.cs | 310 +++++++++++++++++- .../Embeddings/EmbeddingsEndpoint.cs | 15 +- OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs | 2 +- .../Extensions/ResponseExtensions.cs | 93 +++++- OpenAI-DotNet/Extensions/StringExtensions.cs | 12 +- OpenAI-DotNet/Extensions/TypeExtensions.cs | 102 +++++- OpenAI-DotNet/Files/FilesEndpoint.cs | 20 +- .../FineTuning/FineTuningEndpoint.cs | 66 +--- .../Images/AbstractBaseImageRequest.cs | 20 +- OpenAI-DotNet/Images/ImageEditRequest.cs | 34 +- .../Images/ImageGenerationRequest.cs | 24 +- OpenAI-DotNet/Images/ImageResult.cs | 3 +- OpenAI-DotNet/Images/ImageVariationRequest.cs | 28 +- OpenAI-DotNet/Images/ImagesEndpoint.cs | 127 +------ OpenAI-DotNet/Models/Model.cs | 4 +- OpenAI-DotNet/Models/ModelsEndpoint.cs | 25 +- .../Moderations/ModerationsEndpoint.cs | 8 +- OpenAI-DotNet/OpenAI-DotNet.csproj | 13 +- OpenAI-DotNet/Threads/ThreadsEndpoint.cs | 93 +++--- 38 files changed, 1170 insertions(+), 479 deletions(-) rename OpenAI-DotNet-Tests/{TestFixture_00_00_Authentication.cs => TestFixture_00_01_Authentication.cs} (99%) delete mode 100644 OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs create mode 100644 OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs rename OpenAI-DotNet/{Threads => Common}/AnnotationType.cs (100%) create mode 100644 OpenAI-DotNet/Common/FunctionPropertyAttribute.cs rename OpenAI-DotNet/Common/{BaseEndPoint.cs => OpenAIBaseEndpoint.cs} (93%) diff --git a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs similarity index 99% rename from OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs rename to OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs index ede548af..c47d3f6a 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs @@ -8,7 +8,7 @@ namespace OpenAI.Tests { - internal class TestFixture_00_00_Authentication + internal class TestFixture_00_01_Authentication { [SetUp] public void Setup() diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs deleted file mode 100644 index 52bd32c1..00000000 --- a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed under the MIT License. See LICENSE in the project root for license information. - -using NUnit.Framework; -using System; - -namespace OpenAI.Tests -{ - internal class TestFixture_00_02_Extensions - { - [Test] - public void Test_01_Tools() - { - var tools = Tool.GetAllAvailableTools(); - - for (var i = 0; i < tools.Count; i++) - { - var tool = tools[i]; - - if (tool.Type != "function") - { - Console.Write($" \"{tool.Type}\""); - } - else - { - Console.Write($" \"{tool.Function.Name}\""); - } - - if (tool.Function?.Parameters != null) - { - Console.Write($": {tool.Function.Parameters}"); - } - - if (i < tools.Count - 1) - { - Console.Write(",\n"); - } - } - } - } -} \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs new file mode 100644 index 00000000..7a6465f1 --- /dev/null +++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs @@ -0,0 +1,78 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using NUnit.Framework; +using OpenAI.Images; +using OpenAI.Tests.Weather; + +namespace OpenAI.Tests +{ + internal class TestFixture_00_02_Tools : AbstractTestFixture + { + [Test] + public void Test_01_GetTools() + { + var tools = Tool.GetAllAvailableTools(forceUpdate: true, clearCache: true).ToList(); + Assert.IsNotNull(tools); + Assert.IsNotEmpty(tools); + tools.Add(Tool.GetOrCreateTool(OpenAIClient.ImagesEndPoint, nameof(ImagesEndpoint.GenerateImageAsync))); + var json = JsonSerializer.Serialize(tools, new JsonSerializerOptions(OpenAIClient.JsonSerializationOptions) + { + WriteIndented = true + }); + Console.WriteLine(json); + } + + [Test] + public async Task Test_02_Tool_Funcs() + { + var tools = new List + { + Tool.FromFunc("test_func", Function), + Tool.FromFunc("test_func_with_args", FunctionWithArgs), + Tool.FromFunc("test_func_weather", () => WeatherService.GetCurrentWeatherAsync("my location", WeatherService.WeatherUnit.Celsius)) + }; + + var json = JsonSerializer.Serialize(tools, new JsonSerializerOptions(OpenAIClient.JsonSerializationOptions) + { + WriteIndented = true + }); + Console.WriteLine(json); + Assert.IsNotNull(tools); + var tool = tools[0]; + Assert.IsNotNull(tool); + var result = tool.InvokeFunction(); + Assert.AreEqual("success", result); + var toolWithArgs = tools[1]; + Assert.IsNotNull(toolWithArgs); + toolWithArgs.Function.Arguments = new JsonObject + { + ["arg1"] = "arg1", + ["arg2"] = "arg2" + }; + var resultWithArgs = toolWithArgs.InvokeFunction(); + Assert.AreEqual("arg1 arg2", resultWithArgs); + + var toolWeather = tools[2]; + Assert.IsNotNull(toolWeather); + var resultWeather = await toolWeather.InvokeFunctionAsync(); + Assert.IsFalse(string.IsNullOrWhiteSpace(resultWeather)); + Console.WriteLine(resultWeather); + } + + private string Function() + { + return "success"; + } + + private string FunctionWithArgs(string arg1, string arg2) + { + return $"{arg1} {arg2}"; + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs index c7fddc48..7c9d3711 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs @@ -133,7 +133,7 @@ public async Task Test_02_01_GetChatToolCompletion() var messages = new List { - new(Role.System, "You are a helpful weather assistant."), + new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."), new(Role.User, "What's the weather like today?"), }; @@ -142,12 +142,13 @@ public async Task Test_02_01_GetChatToolCompletion() Console.WriteLine($"{message.Role}: {message.Content}"); } - var tools = Tool.GetAllAvailableTools(false); + var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true); var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); Assert.IsTrue(response.Choices.Count == 1); + Assert.IsTrue(response.FirstChoice.FinishReason == "stop"); messages.Add(response.FirstChoice.Message); Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); @@ -163,7 +164,7 @@ public async Task Test_02_01_GetChatToolCompletion() Assert.IsTrue(response.Choices.Count == 1); messages.Add(response.FirstChoice.Message); - if (!string.IsNullOrEmpty(response.ToString())) + if (response.FirstChoice.FinishReason == "stop") { Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); @@ -198,7 +199,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new(Role.System, "You are a helpful weather assistant."), + new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."), new(Role.User, "What's the weather like today?"), }; @@ -281,11 +282,11 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming() Assert.IsNotNull(OpenAIClient.ChatEndpoint); var messages = new List { - new(Role.System, "You are a helpful weather assistant. Use the appropriate unit based on geographical location."), + new(Role.System, "You are a helpful weather assistant.\n\r - Use the appropriate unit based on geographical location."), new(Role.User, "What's the weather like today in Los Angeles, USA and Tokyo, Japan?"), }; - var tools = Tool.GetAllAvailableTools(false); + var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true); var chatRequest = new ChatRequest(messages, model: "gpt-4-turbo-preview", tools: tools, toolChoice: "auto"); var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse => { @@ -294,6 +295,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming() Assert.NotZero(partialResponse.Choices.Count); }); + Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls"); messages.Add(response.FirstChoice.Message); var toolCalls = response.FirstChoice.Message.ToolCalls; @@ -328,12 +330,13 @@ public async Task Test_02_04_GetChatToolForceCompletion() Console.WriteLine($"{message.Role}: {message.Content}"); } - var tools = Tool.GetAllAvailableTools(false); + var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true); var chatRequest = new ChatRequest(messages, tools: tools); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); Assert.IsTrue(response.Choices.Count == 1); + Assert.IsTrue(response.FirstChoice.FinishReason == "stop"); messages.Add(response.FirstChoice.Message); Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); @@ -422,7 +425,7 @@ public async Task Test_04_01_GetChatLogProbs() new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."), new(Role.User, "Where was it played?"), }; - var chatRequest = new ChatRequest(messages, Model.GPT3_5_Turbo, topLogProbs: 1); + var chatRequest = new ChatRequest(messages, topLogProbs: 1); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); diff --git a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs index c3d67f9c..2b4ce62b 100644 --- a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs +++ b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs @@ -381,14 +381,9 @@ public async Task Test_07_01_SubmitToolOutput() Assert.IsTrue(toolCall.FunctionCall.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync))); Assert.IsNotNull(toolCall.FunctionCall.Arguments); Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}"); - var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls); - - foreach (var toolOutput in toolOutputs) - { - Console.WriteLine($"tool call output: {toolOutput.Output}"); - } - - run = await run.SubmitToolOutputsAsync(toolOutputs); + var toolOutput = await testAssistant.GetToolOutputAsync(toolCall); + Console.WriteLine($"tool call output: {toolOutput.Output}"); + run = await run.SubmitToolOutputsAsync(toolOutput); // waiting while run in Queued and InProgress run = await run.WaitForStatusChangeAsync(); Assert.AreEqual(RunStatus.Completed, run.Status); diff --git a/OpenAI-DotNet/Assistants/AssistantExtensions.cs b/OpenAI-DotNet/Assistants/AssistantExtensions.cs index 7c28f109..51a699ef 100644 --- a/OpenAI-DotNet/Assistants/AssistantExtensions.cs +++ b/OpenAI-DotNet/Assistants/AssistantExtensions.cs @@ -31,7 +31,7 @@ public static async Task ModifyAsync(this AssistantResponse a /// /// . /// Optional, . - /// True, if the assistant was successfully deleted. + /// 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).ConfigureAwait(false); @@ -58,7 +58,7 @@ public static async Task> ListFilesAsync(thi => await assistant.Client.AssistantsEndpoint.ListFilesAsync(assistant.Id, query, cancellationToken).ConfigureAwait(false); /// - /// Attach a file to the assistant. + /// Attach a file to the . /// /// . /// @@ -71,7 +71,7 @@ public static async Task AttachFileAsync(this AssistantRe => await assistant.Client.AssistantsEndpoint.AttachFileAsync(assistant.Id, file, cancellationToken).ConfigureAwait(false); /// - /// Uploads a new file at the specified path and attaches it to the assistant. + /// Uploads a new file at the specified and attaches it to the . /// /// . /// The local file path to upload. @@ -162,7 +162,7 @@ public static async Task DeleteFileAsync(this AssistantFileResponse file, } /// - /// Removes and Deletes a file from the assistant. + /// Removes and Deletes a file from the . /// /// . /// The ID of the file to delete. diff --git a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs index 8596120d..b51a78b6 100644 --- a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs +++ b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs @@ -9,7 +9,7 @@ namespace OpenAI.Assistants { - public sealed class AssistantsEndpoint : BaseEndPoint + public sealed class AssistantsEndpoint : OpenAIBaseEndpoint { internal AssistantsEndpoint(OpenAIClient client) : base(client) { } @@ -23,8 +23,8 @@ internal AssistantsEndpoint(OpenAIClient client) : base(client) { } /// public async Task> ListAssistantsAsync(ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -37,9 +37,9 @@ public async Task> ListAssistantsAsync(ListQuery public async Task CreateAssistantAsync(CreateAssistantRequest request = null, CancellationToken cancellationToken = default) { request ??= new CreateAssistantRequest(); - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -51,8 +51,8 @@ public async Task CreateAssistantAsync(CreateAssistantRequest /// . public async Task RetrieveAssistantAsync(string assistantId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -65,9 +65,9 @@ public async Task RetrieveAssistantAsync(string assistantId, /// . public async Task ModifyAssistantAsync(string assistantId, CreateAssistantRequest request, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{assistantId}"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{assistantId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -79,8 +79,8 @@ public async Task ModifyAssistantAsync(string assistantId, Cr /// True, if the assistant was deleted. public async Task DeleteAssistantAsync(string assistantId, CancellationToken cancellationToken = default) { - var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } @@ -95,8 +95,8 @@ public async Task DeleteAssistantAsync(string assistantId, CancellationTok /// . public async Task> ListFilesAsync(string assistantId, ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -117,9 +117,9 @@ public async Task AttachFileAsync(string assistantId, Fil throw new InvalidOperationException($"{nameof(file)}.{nameof(file.Purpose)} must be 'assistants'!"); } - var jsonContent = JsonSerializer.Serialize(new { file_id = file.Id }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{assistantId}/files"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(new { file_id = file.Id }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{assistantId}/files"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -132,8 +132,8 @@ public async Task AttachFileAsync(string assistantId, Fil /// . public async Task RetrieveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -151,8 +151,8 @@ public async Task RetrieveFileAsync(string assistantId, s /// True, if file was removed. public async Task RemoveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default) { - var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } diff --git a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs index ad42867d..da20605c 100644 --- a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs +++ b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs @@ -45,7 +45,14 @@ public sealed class CreateAssistantRequest /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. /// 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) + : 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) { } diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs index 5fcd703f..7bb9997a 100644 --- a/OpenAI-DotNet/Audio/AudioEndpoint.cs +++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs @@ -15,7 +15,7 @@ namespace OpenAI.Audio /// Transforms audio into text.
/// /// - public sealed class AudioEndpoint : BaseEndPoint + public sealed class AudioEndpoint : OpenAIBaseEndpoint { private class AudioResponse { @@ -43,9 +43,9 @@ public AudioEndpoint(OpenAIClient client) : base(client) { } /// public async Task> CreateSpeechAsync(SpeechRequest request, Func, Task> chunkCallback = null, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl("/speech"), jsonContent, cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl("/speech"), jsonContent, cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(false, jsonContent, null, cancellationToken).ConfigureAwait(false); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var memoryStream = new MemoryStream(); int bytesRead; @@ -71,6 +71,7 @@ public async Task> CreateSpeechAsync(SpeechRequest request, totalBytesRead += bytesRead; } + await response.CheckResponseAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return new ReadOnlyMemory(memoryStream.GetBuffer(), 0, totalBytesRead); } @@ -108,8 +109,8 @@ public async Task CreateTranscriptionAsync(AudioTranscriptionRequest req request.Dispose(); - var response = await client.Client.PostAsync(GetUrl("/transcriptions"), content, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/transcriptions"), content, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, null, cancellationToken).ConfigureAwait(false); return responseFormat == AudioResponseFormat.Json ? JsonSerializer.Deserialize(responseAsString)?.Text @@ -145,8 +146,8 @@ public async Task CreateTranslationAsync(AudioTranslationRequest request request.Dispose(); - var response = await client.Client.PostAsync(GetUrl("/translations"), content, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/translations"), content, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, null, cancellationToken).ConfigureAwait(false); return responseFormat == AudioResponseFormat.Json ? JsonSerializer.Deserialize(responseAsString)?.Text diff --git a/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs b/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs index eb643ad7..f50d8486 100644 --- a/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs +++ b/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs @@ -11,7 +11,7 @@ public sealed class AudioTranscriptionRequest : IDisposable /// Constructor. /// /// - /// The audio file to transcribe, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm. + /// The audio file to transcribe, in one of these formats flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. /// /// /// ID of the model to use. @@ -112,7 +112,7 @@ public AudioTranscriptionRequest( ~AudioTranscriptionRequest() => Dispose(false); /// - /// The audio file to transcribe, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm. + /// The audio file to transcribe, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. /// public Stream Audio { get; } diff --git a/OpenAI-DotNet/Audio/AudioTranslationRequest.cs b/OpenAI-DotNet/Audio/AudioTranslationRequest.cs index 7a9097b1..aacb7a58 100644 --- a/OpenAI-DotNet/Audio/AudioTranslationRequest.cs +++ b/OpenAI-DotNet/Audio/AudioTranslationRequest.cs @@ -11,7 +11,7 @@ public sealed class AudioTranslationRequest : IDisposable /// Constructor. /// /// - /// The audio file to translate, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm + /// The audio file to translate, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. /// /// /// ID of the model to use. Only whisper-1 is currently available. @@ -44,7 +44,7 @@ public AudioTranslationRequest( /// Constructor. /// /// - /// The audio file to translate, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm. + /// The audio file to translate, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. /// /// /// The name of the audio file to translate. diff --git a/OpenAI-DotNet/Audio/SpeechRequest.cs b/OpenAI-DotNet/Audio/SpeechRequest.cs index 0a2f1a99..261fcf9c 100644 --- a/OpenAI-DotNet/Audio/SpeechRequest.cs +++ b/OpenAI-DotNet/Audio/SpeechRequest.cs @@ -25,20 +25,35 @@ public SpeechRequest(string input, Model model = null, SpeechVoice voice = Speec Speed = speed; } + /// + /// One of the available TTS models. Defaults to tts-1. + /// [JsonPropertyName("model")] public string Model { get; } + /// + /// The text to generate audio for. The maximum length is 4096 characters. + /// [JsonPropertyName("input")] public string Input { get; } + /// + /// The voice to use when generating the audio. + /// [JsonPropertyName("voice")] public SpeechVoice Voice { get; } + /// + /// The format to audio in. Supported formats are mp3, opus, aac, and flac. + /// [JsonPropertyName("response_format")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonConverter(typeof(JsonStringEnumConverter))] public SpeechResponseFormat ResponseFormat { get; } + /// + /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. + /// [JsonPropertyName("speed")] public float? Speed { get; } } diff --git a/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs index f6b228f7..ff245f24 100644 --- a/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs +++ b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs @@ -208,8 +208,8 @@ public static OpenAIAuthentication LoadFromDirectory(string directory = null, st apiKey = nextPart.Trim(); break; case ORGANIZATION: - case OPENAI_ORGANIZATION_ID: case OPEN_AI_ORGANIZATION_ID: + case OPENAI_ORGANIZATION_ID: organization = nextPart.Trim(); break; } diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs index 6bf0c3ae..8a6d2d5f 100644 --- a/OpenAI-DotNet/Chat/ChatEndpoint.cs +++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs @@ -6,7 +6,9 @@ using System.IO; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; @@ -16,7 +18,7 @@ namespace OpenAI.Chat /// Given a chat conversation, the model will return a chat completion response.
/// /// - public sealed class ChatEndpoint : BaseEndPoint + public sealed class ChatEndpoint : OpenAIBaseEndpoint { /// public ChatEndpoint(OpenAIClient client) : base(client) { } @@ -25,16 +27,16 @@ public ChatEndpoint(OpenAIClient client) : base(client) { } protected override string Root => "chat"; /// - /// Creates a completion for the chat message + /// Creates a completion for the chat message. /// /// The chat request which contains the message content. /// Optional, . /// . public async Task GetCompletionAsync(ChatRequest chatRequest, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl("/completions"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl("/completions"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -48,25 +50,48 @@ public async Task GetCompletionAsync(ChatRequest chatRequest, Canc public async Task StreamCompletionAsync(ChatRequest chatRequest, Action resultHandler, CancellationToken cancellationToken = default) { chatRequest.Stream = true; - var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + using var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); using var request = new HttpRequestMessage(HttpMethod.Post, GetUrl("/completions")); request.Content = jsonContent; - var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + using var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(false, jsonContent, null, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(stream); ChatResponse chatResponse = null; + using var responseStream = EnableDebug ? new MemoryStream() : null; + + if (responseStream != null) + { + await responseStream.WriteAsync("["u8.ToArray(), cancellationToken); + } while (await reader.ReadLineAsync().ConfigureAwait(false) is { } streamData) { cancellationToken.ThrowIfCancellationRequested(); - if (!streamData.TryGetEventStreamData(out var eventData)) { continue; } + if (!streamData.TryGetEventStreamData(out var eventData)) + { + // if response stream is not null, remove last comma + responseStream?.SetLength(responseStream.Length - 1); + continue; + } + if (string.IsNullOrWhiteSpace(eventData)) { continue; } - if (EnableDebug) + if (responseStream != null) { - Console.WriteLine(eventData); + string data; + + try + { + data = JsonNode.Parse(eventData)?.ToJsonString(OpenAIClient.JsonSerializationOptions); + } + catch + { + data = $"{{{eventData}}}"; + } + + await responseStream.WriteAsync(Encoding.UTF8.GetBytes($"{data},"), cancellationToken); } var partialResponse = response.Deserialize(eventData, client); @@ -83,7 +108,12 @@ public async Task StreamCompletionAsync(ChatRequest chatRequest, A resultHandler?.Invoke(partialResponse); } - response.EnsureSuccessStatusCode(); + if (responseStream != null) + { + await responseStream.WriteAsync("]"u8.ToArray(), cancellationToken); + } + + await response.CheckResponseAsync(EnableDebug, jsonContent, responseStream, cancellationToken).ConfigureAwait(false); if (chatResponse == null) { return null; } @@ -103,25 +133,48 @@ public async Task StreamCompletionAsync(ChatRequest chatRequest, A public async IAsyncEnumerable StreamCompletionEnumerableAsync(ChatRequest chatRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default) { chatRequest.Stream = true; - var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); + using var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); using var request = new HttpRequestMessage(HttpMethod.Post, GetUrl("/completions")); request.Content = jsonContent; - var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); + using var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await response.CheckResponseAsync(false, jsonContent, null, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(stream); ChatResponse chatResponse = null; + using var responseStream = EnableDebug ? new MemoryStream() : null; + + if (responseStream != null) + { + await responseStream.WriteAsync("["u8.ToArray(), cancellationToken); + } while (await reader.ReadLineAsync() is { } streamData) { cancellationToken.ThrowIfCancellationRequested(); - if (!streamData.TryGetEventStreamData(out var eventData)) { continue; } + if (!streamData.TryGetEventStreamData(out var eventData)) + { + // if response stream is not null, remove last comma + responseStream?.SetLength(responseStream.Length - 1); + continue; + } + if (string.IsNullOrWhiteSpace(eventData)) { continue; } - if (EnableDebug) + if (responseStream != null) { - Console.WriteLine(eventData); + string data; + + try + { + data = JsonNode.Parse(eventData)?.ToJsonString(OpenAIClient.JsonSerializationOptions); + } + catch + { + data = $"{{{eventData}}}"; + } + + await responseStream.WriteAsync(Encoding.UTF8.GetBytes($"{data},"), cancellationToken); } var partialResponse = response.Deserialize(eventData, client); @@ -138,7 +191,12 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync(Chat yield return partialResponse; } - response.EnsureSuccessStatusCode(); + if (responseStream != null) + { + await responseStream.WriteAsync("]"u8.ToArray(), cancellationToken); + } + + await response.CheckResponseAsync(EnableDebug, jsonContent, responseStream, cancellationToken).ConfigureAwait(false); if (chatResponse == null) { yield break; } diff --git a/OpenAI-DotNet/Chat/Delta.cs b/OpenAI-DotNet/Chat/Delta.cs index e1f033e0..83639aca 100644 --- a/OpenAI-DotNet/Chat/Delta.cs +++ b/OpenAI-DotNet/Chat/Delta.cs @@ -47,6 +47,6 @@ public sealed class Delta public override string ToString() => Content ?? string.Empty; - public static implicit operator string(Delta delta) => delta.ToString(); + public static implicit operator string(Delta delta) => delta?.ToString(); } } diff --git a/OpenAI-DotNet/Threads/AnnotationType.cs b/OpenAI-DotNet/Common/AnnotationType.cs similarity index 100% rename from OpenAI-DotNet/Threads/AnnotationType.cs rename to OpenAI-DotNet/Common/AnnotationType.cs diff --git a/OpenAI-DotNet/Common/Function.cs b/OpenAI-DotNet/Common/Function.cs index aefea2dd..278de057 100644 --- a/OpenAI-DotNet/Common/Function.cs +++ b/OpenAI-DotNet/Common/Function.cs @@ -1,6 +1,8 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using OpenAI.Extensions; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; using System.Text.Json; @@ -16,10 +18,10 @@ namespace OpenAI /// public sealed class Function { - public Function() { } - private const string NameRegex = "^[a-zA-Z0-9_-]{1,64}$"; + public Function() { } + /// /// Creates a new function description to insert into a chat conversation. /// @@ -33,10 +35,7 @@ public Function() { } /// /// An optional JSON object describing the parameters of the function that the model can generate. /// - /// - /// An optional JSON object describing the arguments to use when invoking the function. - /// - public Function(string name, string description = null, JsonNode parameters = null, JsonNode arguments = null) + public Function(string name, string description = null, JsonNode parameters = null) { if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex)) { @@ -46,12 +45,37 @@ public Function(string name, string description = null, JsonNode parameters = nu Name = name; Description = description; Parameters = parameters; - Arguments = arguments; + functionCache[Name] = this; } - internal Function(Function other) => CopyFrom(other); + /// + /// Creates a new function description to insert into a chat conversation. + /// + /// + /// Required. The name of the function to generate arguments for based on the context in a message.
+ /// May contain a-z, A-Z, 0-9, underscores and dashes, with a maximum length of 64 characters. Recommended to not begin with a number or a dash. + /// + /// + /// An optional description of the function, used by the API to determine if it is useful to include in the response. + /// + /// + /// An optional JSON describing the parameters of the function that the model can generate. + /// + public Function(string name, string description, string parameters) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex)) + { + throw new ArgumentException($"The name of the function does not conform to naming standards: {NameRegex}"); + } + + Name = name; + Description = description; + Parameters = JsonNode.Parse(parameters); + functionCache[Name] = this; + } - internal Function(string name, string description, JsonObject parameters, MethodInfo method) + + internal Function(string name, string description, MethodInfo method, object instance = null) { if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex)) { @@ -60,10 +84,54 @@ internal Function(string name, string description, JsonObject parameters, Method Name = name; Description = description; - Parameters = parameters; - functionCache[Name] = method; + MethodInfo = method; + Parameters = method.GenerateJsonSchema(); + Instance = instance; + functionCache[Name] = this; } + #region Func<,> Overloads + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc(string name, Func function, string description = null) + => new(name, description, function.Method, function.Target); + + #endregion Func<,> Overloads + + internal Function(Function other) => CopyFrom(other); + /// /// The name of the function to generate arguments for.
/// May contain a-z, A-Z, 0-9, and underscores and dashes, with a maximum length of 64 characters. @@ -129,6 +197,18 @@ public JsonNode Arguments internal set => arguments = value; } + /// + /// The instance of the object to invoke the method on. + /// + [JsonIgnore] + internal object Instance { get; } + + /// + /// The method to invoke. + /// + [JsonIgnore] + private MethodInfo MethodInfo { get; } + internal void CopyFrom(Function other) { if (!string.IsNullOrWhiteSpace(other.Name)) @@ -154,57 +234,137 @@ internal void CopyFrom(Function other) #region Function Invoking Utilities - private static readonly Dictionary functionCache = new(); + private static readonly ConcurrentDictionary functionCache = new(); + /// + /// Invokes the function and returns the result as json. + /// + /// The result of the function as json. public string Invoke() { - var (method, invokeArgs) = ValidateFunctionArguments(); - var result = method.Invoke(null, invokeArgs); - return result == null ? string.Empty : JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions); + try + { + var (function, invokeArgs) = ValidateFunctionArguments(); + + if (function.MethodInfo.ReturnType == typeof(void)) + { + function.MethodInfo.Invoke(function.Instance, invokeArgs); + return "{\"result\": \"success\"}"; + } + + var result = Invoke(); + return JsonSerializer.Serialize(new { result }); + } + catch (Exception e) + { + Console.WriteLine(e); + return JsonSerializer.Serialize(new { error = e.Message }); + } + } + + /// + /// Invokes the function and returns the result. + /// + /// The expected return type. + /// The result of the function. + public T Invoke() + { + try + { + var (function, invokeArgs) = ValidateFunctionArguments(); + var result = function.MethodInfo.Invoke(function.Instance, invokeArgs); + return result == null ? default : (T)result; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } + /// + /// Invokes the function and returns the result as json. + /// + /// Optional, . + /// The result of the function as json. public async Task InvokeAsync(CancellationToken cancellationToken = default) { - var (method, invokeArgs) = ValidateFunctionArguments(cancellationToken); - var task = (Task)method.Invoke(null, invokeArgs); + try + { + var (function, invokeArgs) = ValidateFunctionArguments(cancellationToken); + + if (function.MethodInfo.ReturnType == typeof(Task)) + { + if (function.MethodInfo.Invoke(function.Instance, invokeArgs) is not Task task) + { + throw new InvalidOperationException($"The function {Name} did not return a valid Task."); + } + + await task; + return "{\"result\": \"success\"}"; + } - if (task is null) + var result = await InvokeAsync(cancellationToken); + return JsonSerializer.Serialize(new { result }); + } + catch (Exception e) { - throw new InvalidOperationException($"The function {Name} did not return a Task."); + Console.WriteLine(e); + return JsonSerializer.Serialize(new { error = e.Message }); } + } + + /// + /// Invokes the function and returns the result. + /// + /// Expected return type. + /// Optional, . + /// The result of the function. + public async Task InvokeAsync(CancellationToken cancellationToken = default) + { + try + { + var (function, invokeArgs) = ValidateFunctionArguments(cancellationToken); - await task.ConfigureAwait(false); + if (function.MethodInfo.Invoke(function.Instance, invokeArgs) is not Task task) + { + throw new InvalidOperationException($"The function {Name} did not return a valid Task."); + } - if (method.ReturnType == typeof(Task)) + await task; + // ReSharper disable once InconsistentNaming + const string Result = nameof(Result); + var resultProperty = task.GetType().GetProperty(Result); + return (T)resultProperty?.GetValue(task); + } + catch (Exception e) { - return string.Empty; + Console.WriteLine(e); + throw; } - - var result = method.ReturnType.GetProperty(nameof(Task.Result))?.GetValue(task); - return result == null ? string.Empty : JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions); } - private (MethodInfo method, object[] invokeArgs) ValidateFunctionArguments(CancellationToken cancellationToken = default) + private (Function function, object[] invokeArgs) ValidateFunctionArguments(CancellationToken cancellationToken = default) { - if (Parameters != null && Arguments == null) + if (Parameters != null && Parameters.AsObject().Count > 0 && Arguments == null) { throw new ArgumentException($"Function {Name} has parameters but no arguments are set."); } - if (!functionCache.TryGetValue(Name, out var method)) + if (!functionCache.TryGetValue(Name, out var function)) { - if (!Name.Contains('_')) - { - throw new InvalidOperationException($"Failed to lookup and invoke function \"{Name}\""); - } + throw new InvalidOperationException($"Failed to find a valid function for {Name}"); + } - var type = Type.GetType(Name[..Name.LastIndexOf('_')].Replace('_', '.')) ?? throw new InvalidOperationException($"Failed to find a valid type for {Name}"); - method = type.GetMethod(Name[(Name.LastIndexOf('_') + 1)..].Replace('_', '.')) ?? throw new InvalidOperationException($"Failed to find a valid method for {Name}"); - functionCache[Name] = method; + if (function.MethodInfo == null) + { + throw new InvalidOperationException($"Failed to find a valid method for {Name}"); } - var requestedArgs = JsonSerializer.Deserialize>(Arguments.ToString(), OpenAIClient.JsonSerializationOptions); - var methodParams = method.GetParameters(); + var requestedArgs = arguments != null + ? JsonSerializer.Deserialize>(Arguments.ToString(), OpenAIClient.JsonSerializationOptions) + : new(); + var methodParams = function.MethodInfo.GetParameters(); var invokeArgs = new object[methodParams.Length]; for (var i = 0; i < methodParams.Length; i++) @@ -213,7 +373,7 @@ public async Task InvokeAsync(CancellationToken cancellationToken = defa if (parameter.Name == null) { - throw new InvalidOperationException($"Failed to find a valid parameter name for {method.DeclaringType}.{method.Name}()"); + throw new InvalidOperationException($"Failed to find a valid parameter name for {function.MethodInfo.DeclaringType}.{function.MethodInfo.Name}()"); } if (requestedArgs.TryGetValue(parameter.Name, out var value)) @@ -222,6 +382,10 @@ public async Task InvokeAsync(CancellationToken cancellationToken = defa { invokeArgs[i] = cancellationToken; } + else if (value is string @enum && parameter.ParameterType.IsEnum) + { + invokeArgs[i] = Enum.Parse(parameter.ParameterType, @enum); + } else if (value is JsonElement element) { invokeArgs[i] = JsonSerializer.Deserialize(element.GetRawText(), parameter.ParameterType, OpenAIClient.JsonSerializationOptions); @@ -241,7 +405,7 @@ public async Task InvokeAsync(CancellationToken cancellationToken = defa } } - return (method, invokeArgs); + return (function, invokeArgs); } #endregion Function Invoking Utilities diff --git a/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs b/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs new file mode 100644 index 00000000..e2d5a007 --- /dev/null +++ b/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs @@ -0,0 +1,53 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +namespace OpenAI +{ + [AttributeUsage(AttributeTargets.Property)] + public sealed class FunctionPropertyAttribute : Attribute + { + /// + /// Property Attribute to help with function calling. + /// + /// + /// The description of the property + /// + /// + /// Is the property required? + /// + /// + /// The default value. + /// + /// + /// Enums or other possible values. + /// + public FunctionPropertyAttribute(string description = null, bool required = false, object defaultValue = null, params object[] possibleValues) + { + Description = description; + Required = required; + DefaultValue = defaultValue; + PossibleValues = possibleValues; + } + + /// + /// The description of the property + /// + public string Description { get; } + + /// + /// Is the property required? + /// + public bool Required { get; } + + /// + /// The default value. + /// + public object DefaultValue { get; } + + /// + /// Enums or other possible values. + /// + public object[] PossibleValues { get; } + } +} diff --git a/OpenAI-DotNet/Common/BaseEndPoint.cs b/OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs similarity index 93% rename from OpenAI-DotNet/Common/BaseEndPoint.cs rename to OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs index 1da86097..e785520d 100644 --- a/OpenAI-DotNet/Common/BaseEndPoint.cs +++ b/OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs @@ -5,9 +5,9 @@ namespace OpenAI { - public abstract class BaseEndPoint + public abstract class OpenAIBaseEndpoint { - protected BaseEndPoint(OpenAIClient client) => this.client = client; + protected OpenAIBaseEndpoint(OpenAIClient client) => this.client = client; // ReSharper disable once InconsistentNaming protected readonly OpenAIClient client; diff --git a/OpenAI-DotNet/Common/Tool.cs b/OpenAI-DotNet/Common/Tool.cs index 92e29d5c..1ad95e34 100644 --- a/OpenAI-DotNet/Common/Tool.cs +++ b/OpenAI-DotNet/Common/Tool.cs @@ -1,6 +1,5 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -47,11 +46,6 @@ public Tool(Function function) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Function Function { get; private set; } - public string InvokeFunction() => Function.Invoke(); - - public async Task InvokeFunctionAsync(CancellationToken cancellationToken = default) - => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false); - internal void CopyFrom(Tool other) { if (!string.IsNullOrWhiteSpace(other?.Id)) @@ -82,23 +76,117 @@ internal void CopyFrom(Tool other) } } - private static List toolCache = new() + /// + /// Invokes the function and returns the result as json. + /// + /// The result of the function as json. + public string InvokeFunction() => Function.Invoke(); + + /// + /// Invokes the function and returns the result. + /// + /// The type to deserialize the result to. + /// The result of the function. + public T InvokeFunction() => Function.Invoke(); + + /// + /// Invokes the function and returns the result as json. + /// + /// Optional, A token to cancel the request. + /// The result of the function as json. + public async Task InvokeFunctionAsync(CancellationToken cancellationToken = default) + => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false); + + /// + /// Invokes the function and returns the result. + /// + /// The type to deserialize the result to. + /// Optional, A token to cancel the request. + /// The result of the function. + public async Task InvokeFunctionAsync(CancellationToken cancellationToken = default) + => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false); + + private static readonly List toolCache = new() { Retrieval, CodeInterpreter }; + /// + /// Clears the tool cache of all previously registered tools. + /// + public static void ClearRegisteredTools() + { + toolCache.Clear(); + toolCache.Add(CodeInterpreter); + toolCache.Add(Retrieval); + } + + /// + /// Checks if tool exists in cache. + /// + /// The tool to check. + /// True, if the tool is already registered in the tool cache. + public static bool IsToolRegistered(Tool tool) + => toolCache.Any(knownTool => + knownTool.Type == "function" && + knownTool.Function.Name == tool.Function.Name && + ReferenceEquals(knownTool.Function.Instance, tool.Function.Instance)); + + /// + /// Tries to register a tool into the Tool cache. + /// + /// The tool to register. + /// True, if the tool was added to the cache. + public static bool TryRegisterTool(Tool tool) + { + if (IsToolRegistered(tool)) + { + return false; + } + + if (tool.Type != "function") + { + throw new InvalidOperationException("Only function tools can be registered."); + } + + toolCache.Add(tool); + return true; + + } + + private static bool TryGetTool(string name, object instance, out Tool tool) + { + foreach (var knownTool in toolCache.Where(knownTool => + knownTool.Type == "function" && + knownTool.Function.Name == name && + ReferenceEquals(knownTool, instance))) + { + tool = knownTool; + return true; + } + + tool = null; + return false; + } + /// /// Gets a list of all available tools. /// /// - /// This method will scan all assemblies for methods decorated with the . + /// This method will scan all assemblies for static methods decorated with the . /// /// Optional, Whether to include the default tools (Retrieval and CodeInterpreter). /// Optional, Whether to force an update of the tool cache. + /// Optional, whether to force the tool cache to be cleared before updating. /// A list of all available tools. - public static IReadOnlyList GetAllAvailableTools(bool includeDefaults = true, bool forceUpdate = false) + public static IReadOnlyList GetAllAvailableTools(bool includeDefaults = true, bool forceUpdate = false, bool clearCache = false) { + if (clearCache) + { + ClearRegisteredTools(); + } + if (forceUpdate || toolCache.All(tool => tool.Type != "function")) { var tools = new List(); @@ -106,16 +194,18 @@ public static IReadOnlyList GetAllAvailableTools(bool includeDefaults = tr from assembly in AppDomain.CurrentDomain.GetAssemblies() from type in assembly.GetTypes() from method in type.GetMethods() + where method.IsStatic let functionAttribute = method.GetCustomAttribute() where functionAttribute != null let name = $"{type.FullName}.{method.Name}".Replace('.', '_') let description = functionAttribute.Description - let parameters = method.GenerateJsonSchema() - select new Function(name, description, parameters, method) + select new Function(name, description, method) into function select new Tool(function)); - foreach (var newTool in tools.Where(knownTool => !toolCache.Any(tool => tool.Type == "function" && tool.Function.Name == knownTool.Function.Name))) + foreach (var newTool in tools.Where(tool => + !toolCache.Any(knownTool => + knownTool.Type == "function" && knownTool.Function.Name == tool.Function.Name && knownTool.Function.Instance == null))) { toolCache.Add(newTool); } @@ -127,13 +217,13 @@ into function } /// - /// Get or create a tool from a method. + /// Get or create a tool from a static method. /// /// /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.
/// The method doesn't need to be decorated with the .
///
- /// The type containing the method. + /// The type containing the static method. /// The name of the method. /// Optional, The description of the method. /// The tool for the method. @@ -141,16 +231,202 @@ public static Tool GetOrCreateTool(Type type, string methodName, string descript { var method = type.GetMethod(methodName) ?? throw new InvalidOperationException($"Failed to find a valid method for {type.FullName}.{methodName}()"); + + if (!method.IsStatic) + { + throw new InvalidOperationException($"Method {type.FullName}.{methodName}() must be static. Use GetOrCreateTool(object instance, string methodName) instead."); + } + + var functionName = $"{type.FullName}.{method.Name}".Replace('.', '_'); + + if (TryGetTool(functionName, null, out var tool)) + { + return tool; + } + + tool = new Tool(new Function(functionName, description ?? string.Empty, method)); + toolCache.Add(tool); + return tool; + } + + /// + /// Get or create a tool from a method of an instance of an object. + /// + /// + /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.
+ /// The method doesn't need to be decorated with the .
+ ///
+ /// The instance of the object containing the method. + /// The name of the method. + /// Optional, The description of the method. + /// The tool for the method. + public static Tool GetOrCreateTool(object instance, string methodName, string description = null) + { + var type = instance.GetType(); + var method = type.GetMethod(methodName) ?? + throw new InvalidOperationException($"Failed to find a valid method for {type.FullName}.{methodName}()"); + var functionName = $"{type.FullName}.{method.Name}".Replace('.', '_'); - foreach (var knownTool in toolCache.Where(knownTool => knownTool.Type == "function" && knownTool.Function.Name == functionName)) + if (TryGetTool(functionName, instance, out var tool)) + { + return tool; + } + + tool = new Tool(new Function(functionName, description ?? string.Empty, method, instance)); + toolCache.Add(tool); + return tool; + } + + #region Func<,> Overloads + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) { - return knownTool; + return tool; } - var tool = new Tool(new Function(functionName, description ?? string.Empty, method.GenerateJsonSchema(), method)); + tool = new Tool(Function.FromFunc(name, function, description)); toolCache.Add(tool); return tool; } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, + string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, + string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + + public static Tool FromFunc(string name, Func function, string description = null) + { + if (TryGetTool(name, function, out var tool)) + { + return tool; + } + + tool = new Tool(Function.FromFunc(name, function, description)); + toolCache.Add(tool); + return tool; + } + + #endregion Func<,> Overloads } } \ No newline at end of file diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs index 3cdfd1ea..6d9cdaec 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs @@ -12,7 +12,7 @@ namespace OpenAI.Embeddings /// Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms.
/// /// - public sealed class EmbeddingsEndpoint : BaseEndPoint + public sealed class EmbeddingsEndpoint : OpenAIBaseEndpoint { /// public EmbeddingsEndpoint(OpenAIClient client) : base(client) { } @@ -39,9 +39,10 @@ public EmbeddingsEndpoint(OpenAIClient client) : base(client) { } /// The number of dimensions the resulting output embeddings should have. /// Only supported in text-embedding-3 and later models /// + /// Optional, . /// - public async Task CreateEmbeddingAsync(string input, string model = null, string user = null, int? dimensions = null) - => await CreateEmbeddingAsync(new EmbeddingsRequest(input, model, user, dimensions)).ConfigureAwait(false); + public async Task CreateEmbeddingAsync(string input, string model = null, string user = null, int? dimensions = null, CancellationToken cancellationToken = default) + => await CreateEmbeddingAsync(new EmbeddingsRequest(input, model, user, dimensions), cancellationToken).ConfigureAwait(false); /// /// Creates an embedding vector representing the input text. @@ -70,14 +71,14 @@ public async Task CreateEmbeddingAsync(IEnumerable i /// /// Creates an embedding vector representing the input text. /// - /// + /// . /// Optional, . /// public async Task CreateEmbeddingAsync(EmbeddingsRequest request, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } } diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs index 1b5edf74..19383fee 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs @@ -47,7 +47,7 @@ public EmbeddingsRequest(string input, string model = null, string user = null, /// Each input must not exceed 8192 tokens in length. /// /// - /// The model id to use. + /// The model id to use.
/// Defaults to: /// /// diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index 53166452..a32ece1e 100644 --- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs +++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs @@ -1,12 +1,16 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; @@ -102,29 +106,100 @@ internal static void SetResponseData(this BaseResponse response, HttpResponseHea } } - internal static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse = false, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) + internal static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse, HttpContent requestContent = null, MemoryStream responseStream = null, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) { var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var debugMessage = new StringBuilder(); - if (!response.IsSuccessStatusCode) + if (!response.IsSuccessStatusCode || debugResponse) { - throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); + if (!string.IsNullOrWhiteSpace(methodName)) + { + debugMessage.Append($"{methodName} -> "); + } + + var debugMessageObject = new Dictionary>(); + + if (response.RequestMessage != null) + { + debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n"); + + debugMessageObject["Request"] = new() + { + ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), + }; + } + + if (requestContent != null) + { + var requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(requestAsString)) + { + try + { + debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString); + } + catch + { + debugMessageObject["Request"]["Body"] = requestAsString; + } + } + } + + debugMessageObject["Response"] = new() + { + ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), + }; + + if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString)) + { + debugMessageObject["Response"]["Body"] = new Dictionary(); + } + + if (responseStream != null) + { + var body = Encoding.UTF8.GetString(responseStream.ToArray()); + + try + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body); + } + catch + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Stream"] = body; + } + } + + if (!string.IsNullOrWhiteSpace(responseAsString)) + { + try + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString); + } + catch + { + ((Dictionary)debugMessageObject["Response"]["Body"])["Content"] = responseAsString; + } + } + + debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true })); + Console.WriteLine(debugMessage.ToString()); } - if (debugResponse) + if (!response.IsSuccessStatusCode) { - Console.WriteLine(responseAsString); + throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); } return responseAsString; } - internal static async Task CheckResponseAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) + internal static async Task CheckResponseAsync(this HttpResponseMessage response, bool debug, StringContent requestContent = null, MemoryStream responseStream = null, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) { - if (!response.IsSuccessStatusCode) + if (!response.IsSuccessStatusCode || debug) { - var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); + await response.ReadAsStringAsync(debug, requestContent, responseStream, cancellationToken, methodName).ConfigureAwait(false); } } diff --git a/OpenAI-DotNet/Extensions/StringExtensions.cs b/OpenAI-DotNet/Extensions/StringExtensions.cs index 4308767a..fc8b289e 100644 --- a/OpenAI-DotNet/Extensions/StringExtensions.cs +++ b/OpenAI-DotNet/Extensions/StringExtensions.cs @@ -1,9 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. -using System; using System.Linq; using System.Net.Http; -using System.Text; namespace OpenAI.Extensions { @@ -30,16 +28,10 @@ public static bool TryGetEventStreamData(this string streamData, out string even return eventData != doneTag; } - public static StringContent ToJsonStringContent(this string json, bool debug) + public static StringContent ToJsonStringContent(this string json) { const string jsonContent = "application/json"; - - if (debug) - { - Console.WriteLine(json); - } - - return new StringContent(json, Encoding.UTF8, jsonContent); + return new StringContent(json, null, jsonContent); } public static string ToSnakeCase(string @string) diff --git a/OpenAI-DotNet/Extensions/TypeExtensions.cs b/OpenAI-DotNet/Extensions/TypeExtensions.cs index 91160f91..f342efc5 100644 --- a/OpenAI-DotNet/Extensions/TypeExtensions.cs +++ b/OpenAI-DotNet/Extensions/TypeExtensions.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Threading; namespace OpenAI.Extensions { @@ -11,6 +14,13 @@ internal static class TypeExtensions { public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo) { + var parameters = methodInfo.GetParameters(); + + if (parameters.Length == 0) + { + return null; + } + var schema = new JsonObject { ["type"] = "object", @@ -18,8 +28,13 @@ public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo) }; var requiredParameters = new JsonArray(); - foreach (var parameter in methodInfo.GetParameters()) + foreach (var parameter in parameters) { + if (parameter.ParameterType == typeof(CancellationToken)) + { + continue; + } + if (string.IsNullOrWhiteSpace(parameter.Name)) { throw new InvalidOperationException($"Failed to find a valid parameter name for {methodInfo.DeclaringType}.{methodInfo.Name}()"); @@ -52,7 +67,7 @@ public static JsonObject GenerateJsonSchema(this Type type) foreach (var value in Enum.GetValues(type)) { - schema["enum"].AsArray().Add(value.ToString()); + schema["enum"].AsArray().Add(JsonNode.Parse(JsonSerializer.Serialize(value, OpenAIClient.JsonSerializationOptions))); } } else if (type.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))) @@ -70,13 +85,63 @@ public static JsonObject GenerateJsonSchema(this Type type) foreach (var property in properties) { var propertyInfo = GenerateJsonSchema(property.PropertyType); + var functionPropertyAttribute = property.GetCustomAttribute(); + var jsonPropertyAttribute = property.GetCustomAttribute(); + var propertyName = jsonPropertyAttribute?.Name ?? property.Name; - if (Nullable.GetUnderlyingType(property.PropertyType) == null) + // override properties with values from function property attribute + if (functionPropertyAttribute != null) + { + propertyInfo["description"] = functionPropertyAttribute.Description; + + if (functionPropertyAttribute.Required) + { + requiredProperties.Add(propertyName); + } + + JsonNode defaultValue = null; + + if (functionPropertyAttribute.DefaultValue != null) + { + defaultValue = JsonNode.Parse(JsonSerializer.Serialize(functionPropertyAttribute.DefaultValue, OpenAIClient.JsonSerializationOptions)); + propertyInfo["default"] = defaultValue; + } + + if (functionPropertyAttribute.PossibleValues is { Length: > 0 }) + { + var enums = new JsonArray(); + + foreach (var value in functionPropertyAttribute.PossibleValues) + { + var @enum = JsonNode.Parse(JsonSerializer.Serialize(value, OpenAIClient.JsonSerializationOptions)); + + if (defaultValue == null) + { + enums.Add(@enum); + } + else + { + if (@enum != defaultValue) + { + enums.Add(@enum); + } + } + } + + if (defaultValue != null && !enums.Contains(defaultValue)) + { + enums.Add(JsonNode.Parse(defaultValue.ToJsonString())); + } + + propertyInfo["enum"] = enums; + } + } + else if (Nullable.GetUnderlyingType(property.PropertyType) == null) { - requiredProperties.Add(property.Name); + requiredProperties.Add(propertyName); } - propertiesInfo[property.Name] = propertyInfo; + propertiesInfo[propertyName] = propertyInfo; } schema["properties"] = propertiesInfo; @@ -88,7 +153,32 @@ public static JsonObject GenerateJsonSchema(this Type type) } else { - schema["type"] = type.Name.ToLower(); + if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte)) + { + schema["type"] = "integer"; + } + else if (type == typeof(float) || type == typeof(double) || type == typeof(decimal)) + { + schema["type"] = "number"; + } + else if (type == typeof(bool)) + { + schema["type"] = "boolean"; + } + else if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + { + schema["type"] = "string"; + schema["format"] = "date-time"; + } + else if (type == typeof(Guid)) + { + schema["type"] = "string"; + schema["format"] = "uuid"; + } + else + { + schema["type"] = type.Name.ToLower(); + } } return schema; diff --git a/OpenAI-DotNet/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs index 35f548f8..4fda6ee5 100644 --- a/OpenAI-DotNet/Files/FilesEndpoint.cs +++ b/OpenAI-DotNet/Files/FilesEndpoint.cs @@ -17,7 +17,7 @@ namespace OpenAI.Files /// Files are used to upload documents that can be used with features like Fine-tuning.
/// ///
- public sealed class FilesEndpoint : BaseEndPoint + public sealed class FilesEndpoint : OpenAIBaseEndpoint { private class FilesList { @@ -46,8 +46,8 @@ public async Task> ListFilesAsync(string purpose = n query = new Dictionary { { nameof(purpose), purpose } }; } - var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false); - var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false); + var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(resultAsString, OpenAIClient.JsonSerializationOptions)?.Files; } @@ -85,8 +85,8 @@ public async Task UploadFileAsync(FileUploadRequest request, Cance content.Add(new StringContent(request.Purpose), "purpose"); content.Add(new ByteArrayContent(fileData.ToArray()), "file", request.FileName); request.Dispose(); - var response = await client.Client.PostAsync(GetUrl(), content, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl(), content, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } @@ -102,7 +102,7 @@ public async Task DeleteFileAsync(string fileId, CancellationToken cancell async Task InternalDeleteFileAsync(int attempt) { - var response = await client.Client.DeleteAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false); + using var response = await client.Client.DeleteAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false); // We specifically don't use the extension method here bc we need to check if it's still processing the file. var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); @@ -120,7 +120,7 @@ async Task InternalDeleteFileAsync(int attempt) } } - await response.CheckResponseAsync(cancellationToken); + await response.CheckResponseAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } } @@ -133,8 +133,8 @@ async Task InternalDeleteFileAsync(int attempt) /// public async Task GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } @@ -191,7 +191,7 @@ public async Task DownloadFileAsync(FileResponse fileData, string direct await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); return filePath; } - + /// /// Gets the specified file as stream /// diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs index d64f195c..ce646b46 100644 --- a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs +++ b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs @@ -1,8 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using OpenAI.Extensions; -using System; -using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -14,7 +12,7 @@ namespace OpenAI.FineTuning ///
/// /// - public sealed class FineTuningEndpoint : BaseEndPoint + public sealed class FineTuningEndpoint : OpenAIBaseEndpoint { /// public FineTuningEndpoint(OpenAIClient client) : base(client) { } @@ -32,32 +30,12 @@ public FineTuningEndpoint(OpenAIClient client) : base(client) { } /// . public async Task CreateJobAsync(CreateFineTuneJobRequest jobRequest, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(jobRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl("/jobs"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(jobRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl("/jobs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } - [Obsolete("Use new overload")] - public async Task ListJobsAsync(int? limit, string after, CancellationToken cancellationToken) - { - var parameters = new Dictionary(); - - if (limit.HasValue) - { - parameters.Add(nameof(limit), limit.ToString()); - } - - if (!string.IsNullOrWhiteSpace(after)) - { - parameters.Add(nameof(after), after); - } - - var response = await client.Client.GetAsync(GetUrl("/jobs", parameters), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); - } - /// /// List your organization's fine-tuning jobs. /// @@ -66,8 +44,8 @@ public async Task ListJobsAsync(int? limit, string after, Cance /// List of s. public async Task> ListJobsAsync(ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl("/jobs", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl("/jobs", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -79,8 +57,8 @@ public async Task> ListJobsAsync(ListQuery que /// . public async Task GetJobInfoAsync(string jobId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); var job = response.Deserialize(responseAsString, client); job.Events = (await ListJobEventsAsync(job, query: null, cancellationToken: cancellationToken).ConfigureAwait(false))?.Items; return job; @@ -94,32 +72,12 @@ public async Task GetJobInfoAsync(string jobId, Cancellatio /// . public async Task CancelJobAsync(string jobId, CancellationToken cancellationToken = default) { - var response = await client.Client.PostAsync(GetUrl($"/jobs/{jobId}/cancel"), null!, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl($"/jobs/{jobId}/cancel"), null!, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); var result = JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); return result.Status == JobStatus.Cancelled; } - [Obsolete("use new overload")] - public async Task ListJobEventsAsync(string jobId, int? limit, string after, CancellationToken cancellationToken) - { - var parameters = new Dictionary(); - - if (limit.HasValue) - { - parameters.Add(nameof(limit), limit.ToString()); - } - - if (!string.IsNullOrWhiteSpace(after)) - { - parameters.Add(nameof(after), after); - } - - var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", parameters), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); - } - /// /// Get fine-grained status updates for a fine-tune job. /// @@ -129,8 +87,8 @@ public async Task ListJobEventsAsync(string jobId, int? limit, string /// List of events for . public async Task> ListJobEventsAsync(string jobId, ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } } diff --git a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs index cf9b3599..a31a7174 100644 --- a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs +++ b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using OpenAI.Extensions; +using OpenAI.Models; using System; using System.Text.Json.Serialization; @@ -14,6 +15,9 @@ public abstract class AbstractBaseImageRequest /// /// Constructor. /// + /// + /// The model to use for image generation. + /// /// /// The number of images to generate. Must be between 1 and 10. /// @@ -29,10 +33,10 @@ public abstract class AbstractBaseImageRequest /// Defaults to /// /// - protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = ImageSize.Large, ResponseFormat responseFormat = ResponseFormat.Url, string user = null) + protected AbstractBaseImageRequest(Model model = null, int numberOfResults = 1, ImageSize size = ImageSize.Large, ResponseFormat responseFormat = ResponseFormat.Url, string user = null) { + Model = string.IsNullOrWhiteSpace(model?.Id) ? Models.Model.DallE_2 : model; Number = numberOfResults; - Size = size switch { ImageSize.Small => "256x256", @@ -40,15 +44,22 @@ protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = Ima ImageSize.Large => "1024x1024", _ => throw new ArgumentOutOfRangeException(nameof(size), size, null) }; - User = user; ResponseFormat = responseFormat; } + /// + /// The model to use for image generation. + /// + [JsonPropertyName("model")] + [FunctionProperty("The model to use for image generation.", true, "dall-e-2")] + public string Model { get; } + /// /// The number of images to generate. Must be between 1 and 10. /// [JsonPropertyName("n")] + [FunctionProperty("The number of images to generate. Must be between 1 and 10.", false, 1)] public int Number { get; } /// @@ -58,18 +69,21 @@ protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = Ima /// [JsonPropertyName("response_format")] [JsonConverter(typeof(JsonStringEnumConverter))] + [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.")] public ResponseFormat ResponseFormat { get; } /// /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. /// [JsonPropertyName("size")] + [FunctionProperty("The size of the generated images.", false, "256x256", "512x512", "1024x1024")] public string Size { get; } /// /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// [JsonPropertyName("user")] + [FunctionProperty("A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.")] public string User { get; } } } \ No newline at end of file diff --git a/OpenAI-DotNet/Images/ImageEditRequest.cs b/OpenAI-DotNet/Images/ImageEditRequest.cs index d298b623..ef5e6f05 100644 --- a/OpenAI-DotNet/Images/ImageEditRequest.cs +++ b/OpenAI-DotNet/Images/ImageEditRequest.cs @@ -1,5 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using OpenAI.Models; using System; using System.IO; @@ -31,14 +32,18 @@ public sealed class ImageEditRequest : AbstractBaseImageRequest, IDisposable /// Must be one of url or b64_json. /// Defaults to /// + /// + /// The model to use for image generation. + /// public ImageEditRequest( string imagePath, string prompt, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, - ResponseFormat responseFormat = ResponseFormat.Url) - : this(imagePath, null, prompt, numberOfResults, size, user, responseFormat) + ResponseFormat responseFormat = ResponseFormat.Url, + Model model = null) + : this(imagePath, null, prompt, numberOfResults, size, user, responseFormat, model) { } @@ -70,6 +75,9 @@ public ImageEditRequest( /// Must be one of url or b64_json. /// Defaults to /// + /// + /// The model to use for image generation. + /// public ImageEditRequest( string imagePath, string maskPath, @@ -77,7 +85,8 @@ public ImageEditRequest( int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, - ResponseFormat responseFormat = ResponseFormat.Url) + ResponseFormat responseFormat = ResponseFormat.Url, + Model model = null) : this( File.OpenRead(imagePath), Path.GetFileName(imagePath), @@ -87,7 +96,8 @@ public ImageEditRequest( numberOfResults, size, user, - responseFormat) + responseFormat, + model) { } @@ -116,6 +126,9 @@ public ImageEditRequest( /// Must be one of url or b64_json. /// Defaults to /// + /// + /// The model to use for image generation. + /// public ImageEditRequest( Stream image, string imageName, @@ -123,8 +136,9 @@ public ImageEditRequest( int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, - ResponseFormat responseFormat = ResponseFormat.Url) - : this(image, imageName, null, null, prompt, numberOfResults, size, user, responseFormat) + ResponseFormat responseFormat = ResponseFormat.Url, + Model model = null) + : this(image, imageName, null, null, prompt, numberOfResults, size, user, responseFormat, model) { } @@ -158,6 +172,9 @@ public ImageEditRequest( /// Must be one of url or b64_json. /// Defaults to /// + /// + /// The model to use for image generation. + /// public ImageEditRequest( Stream image, string imageName, @@ -167,8 +184,9 @@ public ImageEditRequest( int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, - ResponseFormat responseFormat = ResponseFormat.Url) - : base(numberOfResults, size, responseFormat, user) + ResponseFormat responseFormat = ResponseFormat.Url, + Model model = null) + : base(model, numberOfResults, size, responseFormat, user) { Image = image; diff --git a/OpenAI-DotNet/Images/ImageGenerationRequest.cs b/OpenAI-DotNet/Images/ImageGenerationRequest.cs index cca15d0b..3c93e686 100644 --- a/OpenAI-DotNet/Images/ImageGenerationRequest.cs +++ b/OpenAI-DotNet/Images/ImageGenerationRequest.cs @@ -1,9 +1,8 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using OpenAI.Extensions; using OpenAI.Models; -using System; using System.Text.Json.Serialization; -using OpenAI.Extensions; namespace OpenAI.Images { @@ -12,12 +11,6 @@ namespace OpenAI.Images /// public sealed class ImageGenerationRequest { - [Obsolete("Use new constructor")] - public ImageGenerationRequest(string prompt, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, ResponseFormat responseFormat = ResponseFormat.Url) - { - throw new NotSupportedException(); - } - /// /// Constructor. /// @@ -72,12 +65,13 @@ public ImageGenerationRequest( Number = numberOfResults; Quality = quality; ResponseFormat = responseFormat; - Size = size; + Size = size ?? "1024x1024"; Style = style; User = user; } [JsonPropertyName("model")] + [FunctionProperty("The model to use for image generation.", true, "dall-e-2", "dall-e-3")] public string Model { get; } /// @@ -85,6 +79,7 @@ public ImageGenerationRequest( /// The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3. /// [JsonPropertyName("prompt")] + [FunctionProperty("A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3.", true)] public string Prompt { get; } /// @@ -92,14 +87,18 @@ public ImageGenerationRequest( /// Must be between 1 and 10. For dall-e-3, only n=1 is supported. /// [JsonPropertyName("n")] + [FunctionProperty("The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported.", true, 1)] public int Number { get; } /// /// The quality of the image that will be generated. + /// Must be one of standard or hd. /// hd creates images with finer details and greater consistency across the image. /// This param is only supported for dall-e-3. /// [JsonPropertyName("quality")] + [FunctionProperty("The quality of the image that will be generated. hd creates images with finer details and greater consistency across the image. This param is only supported for dall-e-3.", + possibleValues: new object[] { "standard", "hd" })] public string Quality { get; } /// @@ -109,6 +108,7 @@ public ImageGenerationRequest( /// [JsonPropertyName("response_format")] [JsonConverter(typeof(JsonStringEnumConverter))] + [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.", true)] public ResponseFormat ResponseFormat { get; } /// @@ -117,6 +117,9 @@ public ImageGenerationRequest( /// Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. /// [JsonPropertyName("size")] + [FunctionProperty("The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models.", true, + defaultValue: "1024x1024", + possibleValues: new object[] { "256x256", "512x512", "1024x1024", "1792x1024", "1024x1792" })] public string Size { get; } /// @@ -127,12 +130,15 @@ public ImageGenerationRequest( /// This param is only supported for dall-e-3. /// [JsonPropertyName("style")] + [FunctionProperty("The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3.", + possibleValues: new object[] { "vivid", "natural" })] public string Style { get; } /// /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// [JsonPropertyName("user")] + [FunctionProperty("A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.")] public string User { get; } } } diff --git a/OpenAI-DotNet/Images/ImageResult.cs b/OpenAI-DotNet/Images/ImageResult.cs index d47187f7..9820bad5 100644 --- a/OpenAI-DotNet/Images/ImageResult.cs +++ b/OpenAI-DotNet/Images/ImageResult.cs @@ -24,6 +24,7 @@ public override string ToString() => !string.IsNullOrWhiteSpace(Url) ? Url : !string.IsNullOrWhiteSpace(B64_Json) - ? B64_Json : null; + ? B64_Json + : string.Empty; } } diff --git a/OpenAI-DotNet/Images/ImageVariationRequest.cs b/OpenAI-DotNet/Images/ImageVariationRequest.cs index a4948e82..34db426c 100644 --- a/OpenAI-DotNet/Images/ImageVariationRequest.cs +++ b/OpenAI-DotNet/Images/ImageVariationRequest.cs @@ -1,5 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. +using OpenAI.Models; using System; using System.IO; @@ -27,8 +28,17 @@ public sealed class ImageVariationRequest : AbstractBaseImageRequest, IDisposabl /// Must be one of url or b64_json. /// Defaults to /// - public ImageVariationRequest(string imagePath, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, ResponseFormat responseFormat = ResponseFormat.Url) - : this(File.OpenRead(imagePath), Path.GetFileName(imagePath), numberOfResults, size, user, responseFormat) + /// + /// The model to use for image generation. + /// + public ImageVariationRequest( + string imagePath, + int numberOfResults = 1, + ImageSize size = ImageSize.Large, + string user = null, + ResponseFormat responseFormat = ResponseFormat.Url, + Model model = null) + : this(File.OpenRead(imagePath), Path.GetFileName(imagePath), numberOfResults, size, user, responseFormat, model) { } @@ -55,8 +65,18 @@ public ImageVariationRequest(string imagePath, int numberOfResults = 1, ImageSiz /// Must be one of url or b64_json. /// Defaults to /// - public ImageVariationRequest(Stream image, string imageName, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, ResponseFormat responseFormat = ResponseFormat.Url) - : base(numberOfResults, size, responseFormat, user) + /// + /// The model to use for image generation. + /// + public ImageVariationRequest( + Stream image, + string imageName, + int numberOfResults = 1, + ImageSize size = ImageSize.Large, + string user = null, + ResponseFormat responseFormat = ResponseFormat.Url, + Model model = null) + : base(model, numberOfResults, size, responseFormat, user) { Image = image; diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index 3f6f39f6..0d648458 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -1,7 +1,6 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using OpenAI.Extensions; -using System; using System.Collections.Generic; using System.IO; using System.Net.Http; @@ -15,7 +14,7 @@ namespace OpenAI.Images /// Given a prompt and/or an input image, the model will generate a new image.
/// /// - public sealed class ImagesEndpoint : BaseEndPoint + public sealed class ImagesEndpoint : OpenAIBaseEndpoint { /// internal ImagesEndpoint(OpenAIClient client) : base(client) { } @@ -23,39 +22,6 @@ internal ImagesEndpoint(OpenAIClient client) : base(client) { } /// protected override string Root => "images"; - /// - /// Creates an image given a prompt. - /// - /// - /// A text description of the desired image(s). The maximum length is 1000 characters. - /// - /// - /// The number of images to generate. Must be between 1 and 10. - /// - /// - /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. - /// - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// - /// - /// The format in which the generated images are returned. Must be one of url or b64_json. - /// Defaults to - /// - /// - /// Optional, . - /// - /// A list of generated texture urls to download. - [Obsolete] - public async Task> GenerateImageAsync( - string prompt, - int numberOfResults = 1, - ImageSize size = ImageSize.Large, - string user = null, - ResponseFormat responseFormat = ResponseFormat.Url, - CancellationToken cancellationToken = default) - => await GenerateImageAsync(new ImageGenerationRequest(prompt, numberOfResults, size, user, responseFormat), cancellationToken).ConfigureAwait(false); - /// /// Creates an image given a prompt. /// @@ -64,52 +30,11 @@ public async Task> GenerateImageAsync( /// A list of generated texture urls to download. public async Task> GenerateImageAsync(ImageGenerationRequest request, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl("/generations"), jsonContent, cancellationToken).ConfigureAwait(false); - return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl("/generations"), jsonContent, cancellationToken).ConfigureAwait(false); + return await DeserializeResponseAsync(response, jsonContent, cancellationToken).ConfigureAwait(false); } - /// - /// Creates an edited or extended image given an original image and a prompt. - /// - /// - /// The image to edit. Must be a valid PNG file, less than 4MB, and square. - /// If mask is not provided, image must have transparency, which will be used as the mask. - /// - /// - /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. - /// Must be a valid PNG file, less than 4MB, and have the same dimensions as image. - /// - /// - /// A text description of the desired image(s). The maximum length is 1000 characters. - /// - /// - /// The number of images to generate. Must be between 1 and 10. - /// - /// - /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. - /// - /// - /// The format in which the generated images are returned. Must be one of url or b64_json. - /// Defaults to - /// - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// - /// Optional, . - /// A list of generated texture urls to download. - [Obsolete("Use new constructor")] - public async Task> CreateImageEditAsync( - string image, - string mask, - string prompt, - int numberOfResults = 1, - ImageSize size = ImageSize.Large, - ResponseFormat responseFormat = ResponseFormat.Url, - string user = null, - CancellationToken cancellationToken = default) - => await CreateImageEditAsync(new ImageEditRequest(image, mask, prompt, numberOfResults, size, user, responseFormat), cancellationToken).ConfigureAwait(false); - /// /// Creates an edited or extended image given an original image and a prompt. /// @@ -141,42 +66,10 @@ public async Task> CreateImageEditAsync(ImageEditRequ } request.Dispose(); - var response = await client.Client.PostAsync(GetUrl("/edits"), content, cancellationToken).ConfigureAwait(false); - return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/edits"), content, cancellationToken).ConfigureAwait(false); + return await DeserializeResponseAsync(response, content, cancellationToken).ConfigureAwait(false); } - /// - /// Creates a variation of a given image. - /// - /// - /// The image to edit. Must be a valid PNG file, less than 4MB, and square. - /// If mask is not provided, image must have transparency, which will be used as the mask. - /// - /// - /// The number of images to generate. Must be between 1 and 10. - /// - /// - /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. - /// - /// - /// The format in which the generated images are returned. Must be one of url or b64_json. - /// Defaults to - /// - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// - /// Optional, . - /// A list of generated texture urls to download. - [Obsolete("Use new constructor")] - public async Task> CreateImageVariationAsync( - string imagePath, - int numberOfResults = 1, - ImageSize size = ImageSize.Large, - ResponseFormat responseFormat = ResponseFormat.Url, - string user = null, - CancellationToken cancellationToken = default) - => await CreateImageVariationAsync(new ImageVariationRequest(imagePath, numberOfResults, size, user, responseFormat), cancellationToken).ConfigureAwait(false); - /// /// Creates a variation of a given image. /// @@ -199,13 +92,13 @@ public async Task> CreateImageVariationAsync(ImageVar } request.Dispose(); - var response = await client.Client.PostAsync(GetUrl("/variations"), content, cancellationToken).ConfigureAwait(false); - return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/variations"), content, cancellationToken).ConfigureAwait(false); + return await DeserializeResponseAsync(response, content, cancellationToken).ConfigureAwait(false); } - private async Task> DeserializeResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + private async Task> DeserializeResponseAsync(HttpResponseMessage response, HttpContent requestContent, CancellationToken cancellationToken = default) { - var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + var resultAsString = await response.ReadAsStringAsync(EnableDebug, requestContent, null, cancellationToken).ConfigureAwait(false); var imagesResponse = response.Deserialize(resultAsString, client); if (imagesResponse?.Results is not { Count: not 0 }) diff --git a/OpenAI-DotNet/Models/Model.cs b/OpenAI-DotNet/Models/Model.cs index 835f8392..8c89cf20 100644 --- a/OpenAI-DotNet/Models/Model.cs +++ b/OpenAI-DotNet/Models/Model.cs @@ -130,12 +130,12 @@ public Model(string id, string ownedBy = null) /// The default model for . /// public static Model Embedding_Ada_002 { get; } = new("text-embedding-ada-002", "openai"); - + /// /// A highly efficient model which provides a significant upgrade over its predecessor, the text-embedding-ada-002 model. /// public static Model Embedding_3_Small { get; } = new("text-embedding-3-small", "openai"); - + /// /// A next generation larger model with embeddings of up to 3072 dimensions. /// diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs index db99dc74..3db14fff 100644 --- a/OpenAI-DotNet/Models/ModelsEndpoint.cs +++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs @@ -12,10 +12,10 @@ namespace OpenAI.Models { /// /// List and describe the various models available in the API. - /// You can refer to the Models documentation to understand what are available and the differences between them.
+ /// You can refer to the Models documentation to understand which models are available for certain endpoints: .
/// ///
- public sealed class ModelsEndpoint : BaseEndPoint + public sealed class ModelsEndpoint : OpenAIBaseEndpoint { private sealed class ModelsList { @@ -33,12 +33,12 @@ public ModelsEndpoint(OpenAIClient client) : base(client) { } /// /// List all models via the API /// - /// Optional, + /// Optional, . /// Asynchronously returns the list of all s public async Task> GetModelsAsync(CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Models; } @@ -46,12 +46,12 @@ public async Task> GetModelsAsync(CancellationToken cancell /// Get the details about a particular Model from the API /// /// The id/name of the model to get more details about - /// Optional, + /// Optional, . /// Asynchronously returns the with all available properties public async Task GetModelDetailsAsync(string id, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{id}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{id}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions); } @@ -59,13 +59,14 @@ public async Task GetModelDetailsAsync(string id, CancellationToken cance /// Delete a fine-tuned model. You must have the Owner role in your organization. /// /// The to delete. - /// Optional, + /// Optional, . /// True, if fine-tuned model was successfully deleted. public async Task DeleteFineTuneModelAsync(string modelId, CancellationToken cancellationToken = default) { var model = await GetModelDetailsAsync(modelId, cancellationToken).ConfigureAwait(false); - if (model == null) + if (model == null || + string.IsNullOrWhiteSpace(model)) { throw new Exception($"Failed to get {modelId} info!"); } @@ -74,8 +75,8 @@ public async Task DeleteFineTuneModelAsync(string modelId, CancellationTok try { - var response = await client.Client.DeleteAsync(GetUrl($"/{model.Id}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.DeleteAsync(GetUrl($"/{model.Id}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } catch (Exception e) diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs index 8cc0ff22..0645c471 100644 --- a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs +++ b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs @@ -14,7 +14,7 @@ namespace OpenAI.Moderations /// Developers can thus identify content that our content policy prohibits and take action, for instance by filtering it.
/// /// - public sealed class ModerationsEndpoint : BaseEndPoint + public sealed class ModerationsEndpoint : OpenAIBaseEndpoint { /// public ModerationsEndpoint(OpenAIClient client) : base(client) { } @@ -50,9 +50,9 @@ public async Task GetModerationAsync(string input, string model = null, Ca /// Optional, . public async Task CreateModerationAsync(ModerationsRequest request, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 2aebec37..4534d296 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -28,8 +28,19 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx True True - 7.7.0 + 7.7.1 +Version 7.7.1 +- More Function utilities and invoking methods + - Added FunctionPropertyAttribute to help better inform the feature how to format the Function json + - Added FromFunc->,-< overloads for convenance + - Fixed invoke args sometimes being casting to wrong type + - Added additional protections for static and instanced function calls + - Added additional tool utilities: + - Tool.ClearRegisteredTools + - Tool.IsToolRegistered(Tool) - Tool.TryRegisterTool(Tool) + - Improved memory usage and performance by propertly disposing http content and response objects + - Updated debug output to be formatted to json for easier reading and debugging Version 7.7.0 - Added Tool call and Function call Utilities and helper methods - Added FunctionAttribute to decorate methods to be used in function calling diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs index c9b03f7c..14c5ccc0 100644 --- a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs +++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs @@ -12,7 +12,7 @@ namespace OpenAI.Threads /// Create threads that assistants can interact with.
/// /// - public sealed class ThreadsEndpoint : BaseEndPoint + public sealed class ThreadsEndpoint : OpenAIBaseEndpoint { public ThreadsEndpoint(OpenAIClient client) : base(client) { } @@ -26,8 +26,9 @@ public ThreadsEndpoint(OpenAIClient client) : base(client) { } /// . public async Task CreateThreadAsync(CreateThreadRequest request = null, CancellationToken cancellationToken = default) { - var response = await client.Client.PostAsync(GetUrl(), request != null ? JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug) : null, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = request != null ? JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent() : null; + using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -39,8 +40,8 @@ public async Task CreateThreadAsync(CreateThreadRequest request /// . public async Task RetrieveThreadAsync(string threadId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -59,9 +60,9 @@ public async Task RetrieveThreadAsync(string threadId, Cancellat /// . public async Task ModifyThreadAsync(string threadId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{threadId}"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -73,8 +74,8 @@ public async Task ModifyThreadAsync(string threadId, IReadOnlyDi /// True, if was successfully deleted. public async Task DeleteThreadAsync(string threadId, CancellationToken cancellationToken = default) { - var response = await client.Client.DeleteAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.DeleteAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } @@ -89,9 +90,9 @@ public async Task DeleteThreadAsync(string threadId, CancellationToken can /// . public async Task CreateMessageAsync(string threadId, CreateMessageRequest request, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -104,8 +105,8 @@ public async Task CreateMessageAsync(string threadId, CreateMes /// . public async Task> ListMessagesAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -118,8 +119,8 @@ public async Task> ListMessagesAsync(string thread /// . public async Task RetrieveMessageAsync(string threadId, string messageId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -155,9 +156,9 @@ public async Task ModifyMessageAsync(MessageResponse message, I /// . public async Task ModifyMessageAsync(string threadId, string messageId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages/{messageId}"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages/{messageId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -175,8 +176,8 @@ public async Task ModifyMessageAsync(string threadId, string me /// . public async Task> ListFilesAsync(string threadId, string messageId, ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -190,8 +191,8 @@ public async Task> ListFilesAsync(string threa /// . public async Task RetrieveFileAsync(string threadId, string messageId, string fileId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files/{fileId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -208,8 +209,8 @@ public async Task RetrieveFileAsync(string threadId, string /// public async Task> ListRunsAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -228,9 +229,9 @@ public async Task CreateRunAsync(string threadId, CreateRunRequest request = new CreateRunRequest(assistant, request); } - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -248,9 +249,9 @@ public async Task CreateThreadAndRunAsync(CreateThreadAndRunRequest request = new CreateThreadAndRunRequest(assistant, request); } - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl("/runs"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl("/runs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -263,8 +264,8 @@ public async Task CreateThreadAndRunAsync(CreateThreadAndRunRequest /// . public async Task RetrieveRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -283,9 +284,9 @@ public async Task RetrieveRunAsync(string threadId, string runId, C /// . public async Task ModifyRunAsync(string threadId, string runId, IReadOnlyDictionary metadata, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -301,9 +302,9 @@ public async Task ModifyRunAsync(string threadId, string runId, IRe /// . public async Task SubmitToolOutputsAsync(string threadId, string runId, SubmitToolOutputsRequest request, CancellationToken cancellationToken = default) { - var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); - var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/submit_tool_outputs"), jsonContent, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/submit_tool_outputs"), jsonContent, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -317,8 +318,8 @@ public async Task SubmitToolOutputsAsync(string threadId, string ru /// . public async Task> ListRunStepsAsync(string threadId, string runId, ListQuery query = null, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps", query), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps", query), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize>(responseAsString, client); } @@ -332,8 +333,8 @@ public async Task> ListRunStepsAsync(string thread /// . public async Task RetrieveRunStepAsync(string threadId, string runId, string stepId, CancellationToken cancellationToken = default) { - var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps/{stepId}"), cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps/{stepId}"), cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } @@ -346,8 +347,8 @@ public async Task RetrieveRunStepAsync(string threadId, string /// . public async Task CancelRunAsync(string threadId, string runId, CancellationToken cancellationToken = default) { - var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/cancel"), content: null, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/cancel"), content: null, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); }