From b459a6426ba4fa4958ec21375167f1960fc44360 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 12:35:13 -0500 Subject: [PATCH 1/7] OpenAI-DotNet 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) --- ...cs => TestFixture_00_01_Authentication.cs} | 4 +- .../TestFixture_00_02_Extensions.cs | 40 --- .../TestFixture_00_02_Tools.cs | 80 +++++ OpenAI-DotNet-Tests/TestFixture_03_Chat.cs | 19 +- OpenAI-DotNet-Tests/TestFixture_12_Threads.cs | 11 +- .../Assistants/AssistantExtensions.cs | 8 +- .../Assistants/CreateAssistantRequest.cs | 9 +- .../Audio/AudioTranscriptionRequest.cs | 4 +- .../Audio/AudioTranslationRequest.cs | 4 +- OpenAI-DotNet/Audio/SpeechRequest.cs | 15 + .../Authentication/OpenAIAuthentication.cs | 2 +- OpenAI-DotNet/Chat/ChatEndpoint.cs | 2 +- OpenAI-DotNet/Chat/Delta.cs | 2 +- .../{Threads => Common}/AnnotationType.cs | 0 OpenAI-DotNet/Common/Function.cs | 242 +++++++++++--- .../Common/FunctionPropertyAttribute.cs | 53 +++ OpenAI-DotNet/Common/Tool.cs | 310 +++++++++++++++++- .../Embeddings/EmbeddingsEndpoint.cs | 7 +- OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs | 2 +- OpenAI-DotNet/Extensions/TypeExtensions.cs | 102 +++++- .../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 | 107 ------ OpenAI-DotNet/Models/Model.cs | 4 +- OpenAI-DotNet/Models/ModelsEndpoint.cs | 11 +- OpenAI-DotNet/OpenAI-DotNet.csproj | 11 +- 29 files changed, 883 insertions(+), 275 deletions(-) rename OpenAI-DotNet-Tests/{TestFixture_00_00_Authentication.cs => TestFixture_00_01_Authentication.cs} (98%) 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 diff --git a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs similarity index 98% rename from OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs rename to OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs index ede548af..2b03f857 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() @@ -187,6 +187,8 @@ public void TearDown() } Assert.IsFalse(File.Exists(".openai")); + + OpenAIAuthentication.Default = null; } } } 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..f5748552 --- /dev/null +++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs @@ -0,0 +1,80 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +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> + { + Tool.FromFunc("test_func", Function), + Tool.FromFunc<string, string, string>("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<string>(); + Assert.AreEqual("success", result); + var toolWithArgs = tools[1]; + Assert.IsNotNull(toolWithArgs); + toolWithArgs.Function.Arguments = new JsonObject + { + ["arg1"] = "arg1", + ["arg2"] = "arg2" + }; + var resultWithArgs = toolWithArgs.InvokeFunction<string>(); + 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<Message> { - 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<Message> { - 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<Message> { - 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<AssistantResponse> ModifyAsync(this AssistantResponse a /// </summary> /// <param name="assistant"><see cref="AssistantResponse"/>.</param> /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> - /// <returns>True, if the assistant was successfully deleted.</returns> + /// <returns>True, if the <see cref="assistant"/> was successfully deleted.</returns> public static async Task<bool> 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<ListResponse<AssistantFileResponse>> ListFilesAsync(thi => await assistant.Client.AssistantsEndpoint.ListFilesAsync(assistant.Id, query, cancellationToken).ConfigureAwait(false); /// <summary> - /// Attach a file to the assistant. + /// Attach a file to the <see cref="assistant"/>. /// </summary> /// <param name="assistant"><see cref="AssistantResponse"/>.</param> /// <param name="file"> @@ -71,7 +71,7 @@ public static async Task<AssistantFileResponse> AttachFileAsync(this AssistantRe => await assistant.Client.AssistantsEndpoint.AttachFileAsync(assistant.Id, file, cancellationToken).ConfigureAwait(false); /// <summary> - /// Uploads a new file at the specified path and attaches it to the assistant. + /// Uploads a new file at the specified <see cref="filePath"/> and attaches it to the <see cref="assistant"/>. /// </summary> /// <param name="assistant"><see cref="AssistantResponse"/>.</param> /// <param name="filePath">The local file path to upload.</param> @@ -162,7 +162,7 @@ public static async Task<bool> DeleteFileAsync(this AssistantFileResponse file, } /// <summary> - /// Removes and Deletes a file from the assistant. + /// Removes and Deletes a file from the <see cref="assistant"/>. /// </summary> /// <param name="assistant"><see cref="AssistantResponse"/>.</param> /// <param name="fileId">The ID of the file to delete.</param> 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. /// </param> public CreateAssistantRequest(AssistantResponse assistant, string model = null, string name = null, string description = null, string instructions = null, IEnumerable<Tool> tools = null, IEnumerable<string> files = null, IReadOnlyDictionary<string, string> 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/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. /// </summary> /// <param name="audioPath"> - /// 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. /// </param> /// <param name="model"> /// ID of the model to use. @@ -112,7 +112,7 @@ public AudioTranscriptionRequest( ~AudioTranscriptionRequest() => Dispose(false); /// <summary> - /// 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. /// </summary> 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. /// </summary> /// <param name="audioPath"> - /// 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. /// </param> /// <param name="model"> /// ID of the model to use. Only whisper-1 is currently available. @@ -44,7 +44,7 @@ public AudioTranslationRequest( /// Constructor. /// </summary> /// <param name="audio"> - /// 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. /// </param> /// <param name="audioName"> /// 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; } + /// <summary> + /// One of the available TTS models. Defaults to tts-1. + /// </summary> [JsonPropertyName("model")] public string Model { get; } + /// <summary> + /// The text to generate audio for. The maximum length is 4096 characters. + /// </summary> [JsonPropertyName("input")] public string Input { get; } + /// <summary> + /// The voice to use when generating the audio. + /// </summary> [JsonPropertyName("voice")] public SpeechVoice Voice { get; } + /// <summary> + /// The format to audio in. Supported formats are mp3, opus, aac, and flac. + /// </summary> [JsonPropertyName("response_format")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonConverter(typeof(JsonStringEnumConverter<SpeechResponseFormat>))] public SpeechResponseFormat ResponseFormat { get; } + /// <summary> + /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. + /// </summary> [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..513cc892 100644 --- a/OpenAI-DotNet/Chat/ChatEndpoint.cs +++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs @@ -25,7 +25,7 @@ public ChatEndpoint(OpenAIClient client) : base(client) { } protected override string Root => "chat"; /// <summary> - /// Creates a completion for the chat message + /// Creates a completion for the chat message. /// </summary> /// <param name="chatRequest">The chat request which contains the message content.</param> /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> 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 /// </summary> public sealed class Function { - public Function() { } - private const string NameRegex = "^[a-zA-Z0-9_-]{1,64}$"; + public Function() { } + /// <summary> /// Creates a new function description to insert into a chat conversation. /// </summary> @@ -33,10 +35,7 @@ public Function() { } /// <param name="parameters"> /// An optional JSON object describing the parameters of the function that the model can generate. /// </param> - /// <param name="arguments"> - /// An optional JSON object describing the arguments to use when invoking the function. - /// </param> - 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); + /// <summary> + /// Creates a new function description to insert into a chat conversation. + /// </summary> + /// <param name="name"> + /// Required. The name of the function to generate arguments for based on the context in a message.<br/> + /// 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. + /// </param> + /// <param name="description"> + /// An optional description of the function, used by the API to determine if it is useful to include in the response. + /// </param> + /// <param name="parameters"> + /// An optional JSON describing the parameters of the function that the model can generate. + /// </param> + 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<TResult>(string name, Func<TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, TResult>(string name, Func<T1, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, TResult>(string name, Func<T1, T2, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, TResult>(string name, Func<T1, T2, T3, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, TResult>(string name, Func<T1, T2, T3, T4, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, TResult>(string name, Func<T1, T2, T3, T4, T5, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, T6, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult> function, string description = null) + => new(name, description, function.Method, function.Target); + + #endregion Func<,> Overloads + + internal Function(Function other) => CopyFrom(other); + /// <summary> /// The name of the function to generate arguments for.<br/> /// 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; } + /// <summary> + /// The instance of the object to invoke the method on. + /// </summary> + [JsonIgnore] + internal object Instance { get; } + + /// <summary> + /// The method to invoke. + /// </summary> + [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<string, MethodInfo> functionCache = new(); + private static readonly ConcurrentDictionary<string, Function> functionCache = new(); + /// <summary> + /// Invokes the function and returns the result as json. + /// </summary> + /// <returns>The result of the function as json.</returns> 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<object>(); + return JsonSerializer.Serialize(new { result }); + } + catch (Exception e) + { + Console.WriteLine(e); + return JsonSerializer.Serialize(new { error = e.Message }); + } + } + + /// <summary> + /// Invokes the function and returns the result. + /// </summary> + /// <typeparam name="T">The expected return type.</typeparam> + /// <returns>The result of the function.</returns> + public T Invoke<T>() + { + 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; + } } + /// <summary> + /// Invokes the function and returns the result as json. + /// </summary> + /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> + /// <returns>The result of the function as json.</returns> public async Task<string> 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<object>(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 }); } + } + + /// <summary> + /// Invokes the function and returns the result. + /// </summary> + /// <typeparam name="T">Expected return type.</typeparam> + /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> + /// <returns>The result of the function.</returns> + public async Task<T> InvokeAsync<T>(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<object>.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<Dictionary<string, object>>(Arguments.ToString(), OpenAIClient.JsonSerializationOptions); - var methodParams = method.GetParameters(); + var requestedArgs = arguments != null + ? JsonSerializer.Deserialize<Dictionary<string, object>>(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<string> 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<string> 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<string> 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 + { + /// <summary> + /// Property Attribute to help with function calling. + /// </summary> + /// <param name="description"> + /// The description of the property + /// </param> + /// <param name="required"> + /// Is the property required? + /// </param> + /// <param name="defaultValue"> + /// The default value. + /// </param> + /// <param name="possibleValues"> + /// Enums or other possible values. + /// </param> + public FunctionPropertyAttribute(string description = null, bool required = false, object defaultValue = null, params object[] possibleValues) + { + Description = description; + Required = required; + DefaultValue = defaultValue; + PossibleValues = possibleValues; + } + + /// <summary> + /// The description of the property + /// </summary> + public string Description { get; } + + /// <summary> + /// Is the property required? + /// </summary> + public bool Required { get; } + + /// <summary> + /// The default value. + /// </summary> + public object DefaultValue { get; } + + /// <summary> + /// Enums or other possible values. + /// </summary> + public object[] PossibleValues { get; } + } +} 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<string> 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<Tool> toolCache = new() + /// <summary> + /// Invokes the function and returns the result as json. + /// </summary> + /// <returns>The result of the function as json.</returns> + public string InvokeFunction() => Function.Invoke(); + + /// <summary> + /// Invokes the function and returns the result. + /// </summary> + /// <typeparam name="T">The type to deserialize the result to.</typeparam> + /// <returns>The result of the function.</returns> + public T InvokeFunction<T>() => Function.Invoke<T>(); + + /// <summary> + /// Invokes the function and returns the result as json. + /// </summary> + /// <param name="cancellationToken">Optional, A token to cancel the request.</param> + /// <returns>The result of the function as json.</returns> + public async Task<string> InvokeFunctionAsync(CancellationToken cancellationToken = default) + => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false); + + /// <summary> + /// Invokes the function and returns the result. + /// </summary> + /// <typeparam name="T">The type to deserialize the result to.</typeparam> + /// <param name="cancellationToken">Optional, A token to cancel the request.</param> + /// <returns>The result of the function.</returns> + public async Task<T> InvokeFunctionAsync<T>(CancellationToken cancellationToken = default) + => await Function.InvokeAsync<T>(cancellationToken).ConfigureAwait(false); + + private static readonly List<Tool> toolCache = new() { Retrieval, CodeInterpreter }; + /// <summary> + /// Clears the tool cache of all previously registered tools. + /// </summary> + public static void ClearRegisteredTools() + { + toolCache.Clear(); + toolCache.Add(CodeInterpreter); + toolCache.Add(Retrieval); + } + + /// <summary> + /// Checks if tool exists in cache. + /// </summary> + /// <param name="tool">The tool to check.</param> + /// <returns>True, if the tool is already registered in the tool cache.</returns> + 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)); + + /// <summary> + /// Tries to register a tool into the Tool cache. + /// </summary> + /// <param name="tool">The tool to register.</param> + /// <returns>True, if the tool was added to the cache.</returns> + 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; + } + /// <summary> /// Gets a list of all available tools. /// </summary> /// <remarks> - /// This method will scan all assemblies for methods decorated with the <see cref="FunctionAttribute"/>. + /// This method will scan all assemblies for static methods decorated with the <see cref="FunctionAttribute"/>. /// </remarks> /// <param name="includeDefaults">Optional, Whether to include the default tools (Retrieval and CodeInterpreter).</param> /// <param name="forceUpdate">Optional, Whether to force an update of the tool cache.</param> + /// <param name="clearCache">Optional, whether to force the tool cache to be cleared before updating.</param> /// <returns>A list of all available tools.</returns> - public static IReadOnlyList<Tool> GetAllAvailableTools(bool includeDefaults = true, bool forceUpdate = false) + public static IReadOnlyList<Tool> 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<Tool>(); @@ -106,16 +194,18 @@ public static IReadOnlyList<Tool> 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<FunctionAttribute>() 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 } /// <summary> - /// Get or create a tool from a method. + /// Get or create a tool from a static method. /// </summary> /// <remarks> /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.<br/> /// The method doesn't need to be decorated with the <see cref="FunctionAttribute"/>.<br/> /// </remarks> - /// <param name="type">The type containing the method.</param> + /// <param name="type">The type containing the static method.</param> /// <param name="methodName">The name of the method.</param> /// <param name="description">Optional, The description of the method.</param> /// <returns>The tool for the method.</returns> @@ -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; + } + + /// <summary> + /// Get or create a tool from a method of an instance of an object. + /// </summary> + /// <remarks> + /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.<br/> + /// The method doesn't need to be decorated with the <see cref="FunctionAttribute"/>.<br/> + /// </remarks> + /// <param name="instance">The instance of the object containing the method.</param> + /// <param name="methodName">The name of the method.</param> + /// <param name="description">Optional, The description of the method.</param> + /// <returns>The tool for the method.</returns> + 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<TResult>(string name, Func<TResult> 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<T1, TResult>(string name, Func<T1, TResult> 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<T1, T2, TResult>(string name, Func<T1, T2, TResult> 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<T1, T2, T3, TResult>(string name, Func<T1, T2, T3, TResult> 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<T1, T2, T3, T4, TResult>(string name, Func<T1, T2, T3, T4, TResult> 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<T1, T2, T3, T4, T5, TResult>(string name, Func<T1, T2, T3, T4, T5, TResult> 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<T1, T2, T3, T4, T5, T6, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, TResult> 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<T1, T2, T3, T4, T5, T6, T7, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, TResult> 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<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> 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<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult> 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<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult> 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<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult> 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..3aea137b 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs @@ -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 /// </param> + /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> /// <returns><see cref="EmbeddingsResponse"/></returns> - public async Task<EmbeddingsResponse> 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<EmbeddingsResponse> 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); /// <summary> /// Creates an embedding vector representing the input text. @@ -70,7 +71,7 @@ public async Task<EmbeddingsResponse> CreateEmbeddingAsync(IEnumerable<string> i /// <summary> /// Creates an embedding vector representing the input text. /// </summary> - /// <param name="request"><see cref="EmbeddingsRequest"/></param> + /// <param name="request"><see cref="EmbeddingsRequest"/>.</param> /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> /// <returns><see cref="EmbeddingsResponse"/></returns> public async Task<EmbeddingsResponse> CreateEmbeddingAsync(EmbeddingsRequest request, CancellationToken cancellationToken = default) 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. /// </param> /// <param name="model"> - /// The model id to use. + /// The model id to use.<br/> /// Defaults to: <see cref="Model.Embedding_Ada_002"/> /// </param> /// <param name="user"> 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<FunctionPropertyAttribute>(); + var jsonPropertyAttribute = property.GetCustomAttribute<JsonPropertyNameAttribute>(); + 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/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 /// <summary> /// Constructor. /// </summary> + /// <param name="model"> + /// The model to use for image generation. + /// </param> /// <param name="numberOfResults"> /// The number of images to generate. Must be between 1 and 10. /// </param> @@ -29,10 +33,10 @@ public abstract class AbstractBaseImageRequest /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> /// <exception cref="ArgumentOutOfRangeException"></exception> - 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; } + /// <summary> + /// The model to use for image generation. + /// </summary> + [JsonPropertyName("model")] + [FunctionProperty("The model to use for image generation.", true, "dall-e-2")] + public string Model { get; } + /// <summary> /// The number of images to generate. Must be between 1 and 10. /// </summary> [JsonPropertyName("n")] + [FunctionProperty("The number of images to generate. Must be between 1 and 10.", false, 1)] public int Number { get; } /// <summary> @@ -58,18 +69,21 @@ protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = Ima /// </summary> [JsonPropertyName("response_format")] [JsonConverter(typeof(JsonStringEnumConverter<ResponseFormat>))] + [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.")] public ResponseFormat ResponseFormat { get; } /// <summary> /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. /// </summary> [JsonPropertyName("size")] + [FunctionProperty("The size of the generated images.", false, "256x256", "512x512", "1024x1024")] public string Size { get; } /// <summary> /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// </summary> [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. /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> + /// <param name="model"> + /// The model to use for image generation. + /// </param> 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. /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> + /// <param name="model"> + /// The model to use for image generation. + /// </param> 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. /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> + /// <param name="model"> + /// The model to use for image generation. + /// </param> 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. /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> + /// <param name="model"> + /// The model to use for image generation. + /// </param> 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 /// </summary> 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(); - } - /// <summary> /// Constructor. /// </summary> @@ -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; } /// <summary> @@ -85,6 +79,7 @@ public ImageGenerationRequest( /// The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3. /// </summary> [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; } /// <summary> @@ -92,14 +87,18 @@ public ImageGenerationRequest( /// Must be between 1 and 10. For dall-e-3, only n=1 is supported. /// </summary> [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; } /// <summary> /// 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. /// </summary> [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; } /// <summary> @@ -109,6 +108,7 @@ public ImageGenerationRequest( /// </summary> [JsonPropertyName("response_format")] [JsonConverter(typeof(JsonStringEnumConverter<ResponseFormat>))] + [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.", true)] public ResponseFormat ResponseFormat { get; } /// <summary> @@ -117,6 +117,9 @@ public ImageGenerationRequest( /// Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. /// </summary> [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; } /// <summary> @@ -127,12 +130,15 @@ public ImageGenerationRequest( /// This param is only supported for dall-e-3. /// </summary> [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; } /// <summary> /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// </summary> [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. /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> - 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) + /// <param name="model"> + /// The model to use for image generation. + /// </param> + 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. /// <para/> Defaults to <see cref="ResponseFormat.Url"/> /// </param> - 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) + /// <param name="model"> + /// The model to use for image generation. + /// </param> + 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..3ed8629b 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; @@ -23,39 +22,6 @@ internal ImagesEndpoint(OpenAIClient client) : base(client) { } /// <inheritdoc /> protected override string Root => "images"; - /// <summary> - /// Creates an image given a prompt. - /// </summary> - /// <param name="prompt"> - /// A text description of the desired image(s). The maximum length is 1000 characters. - /// </param> - /// <param name="numberOfResults"> - /// The number of images to generate. Must be between 1 and 10. - /// </param> - /// <param name="size"> - /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. - /// </param> - /// <param name="user"> - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// </param> - /// <param name="responseFormat"> - /// The format in which the generated images are returned. Must be one of url or b64_json. - /// <para/> Defaults to <see cref="ResponseFormat.Url"/> - /// </param> - /// <param name="cancellationToken"> - /// Optional, <see cref="CancellationToken"/>. - /// </param> - /// <returns>A list of generated texture urls to download.</returns> - [Obsolete] - public async Task<IReadOnlyList<ImageResult>> 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); - /// <summary> /// Creates an image given a prompt. /// </summary> @@ -69,47 +35,6 @@ public async Task<IReadOnlyList<ImageResult>> GenerateImageAsync(ImageGeneration return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); } - /// <summary> - /// Creates an edited or extended image given an original image and a prompt. - /// </summary> - /// <param name="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. - /// </param> - /// <param name="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. - /// </param> - /// <param name="prompt"> - /// A text description of the desired image(s). The maximum length is 1000 characters. - /// </param> - /// <param name="numberOfResults"> - /// The number of images to generate. Must be between 1 and 10. - /// </param> - /// <param name="size"> - /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. - /// </param> - /// <param name="responseFormat"> - /// The format in which the generated images are returned. Must be one of url or b64_json. - /// <para/> Defaults to <see cref="ResponseFormat.Url"/> - /// </param> - /// <param name="user"> - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// </param> - /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> - /// <returns>A list of generated texture urls to download.</returns> - [Obsolete("Use new constructor")] - public async Task<IReadOnlyList<ImageResult>> 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); - /// <summary> /// Creates an edited or extended image given an original image and a prompt. /// </summary> @@ -145,38 +70,6 @@ public async Task<IReadOnlyList<ImageResult>> CreateImageEditAsync(ImageEditRequ return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false); } - /// <summary> - /// Creates a variation of a given image. - /// </summary> - /// <param name="imagePath"> - /// 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. - /// </param> - /// <param name="numberOfResults"> - /// The number of images to generate. Must be between 1 and 10. - /// </param> - /// <param name="size"> - /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. - /// </param> - /// <param name="responseFormat"> - /// The format in which the generated images are returned. Must be one of url or b64_json. - /// <para/> Defaults to <see cref="ResponseFormat.Url"/> - /// </param> - /// <param name="user"> - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// </param> - /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> - /// <returns>A list of generated texture urls to download.</returns> - [Obsolete("Use new constructor")] - public async Task<IReadOnlyList<ImageResult>> 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); - /// <summary> /// Creates a variation of a given image. /// </summary> 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 <see cref="Embeddings.EmbeddingsEndpoint"/>. /// </summary> public static Model Embedding_Ada_002 { get; } = new("text-embedding-ada-002", "openai"); - + /// <summary> /// A highly efficient model which provides a significant upgrade over its predecessor, the text-embedding-ada-002 model. /// </summary> public static Model Embedding_3_Small { get; } = new("text-embedding-3-small", "openai"); - + /// <summary> /// A next generation larger model with embeddings of up to 3072 dimensions. /// </summary> diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs index db99dc74..21de5af9 100644 --- a/OpenAI-DotNet/Models/ModelsEndpoint.cs +++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs @@ -12,7 +12,7 @@ namespace OpenAI.Models { /// <summary> /// List and describe the various models available in the API. - /// You can refer to the Models documentation to understand what <see href="https://platform.openai.com/docs/models"/> are available and the differences between them.<br/> + /// You can refer to the Models documentation to understand which models are available for certain endpoints: <see href="https://platform.openai.com/docs/models/model-endpoint-compatibility"/>.<br/> /// <see href="https://platform.openai.com/docs/api-reference/models"/> /// </summary> public sealed class ModelsEndpoint : BaseEndPoint @@ -33,7 +33,7 @@ public ModelsEndpoint(OpenAIClient client) : base(client) { } /// <summary> /// List all models via the API /// </summary> - /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/></param> + /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> /// <returns>Asynchronously returns the list of all <see cref="Model"/>s</returns> public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancellationToken = default) { @@ -46,7 +46,7 @@ public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancell /// Get the details about a particular Model from the API /// </summary> /// <param name="id">The id/name of the model to get more details about</param> - /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/></param> + /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> /// <returns>Asynchronously returns the <see cref="Model"/> with all available properties</returns> public async Task<Model> GetModelDetailsAsync(string id, CancellationToken cancellationToken = default) { @@ -59,13 +59,14 @@ public async Task<Model> GetModelDetailsAsync(string id, CancellationToken cance /// Delete a fine-tuned model. You must have the Owner role in your organization. /// </summary> /// <param name="modelId">The <see cref="Model"/> to delete.</param> - /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/></param> + /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> /// <returns>True, if fine-tuned model was successfully deleted.</returns> public async Task<bool> 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!"); } diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 2aebec37..0c8d259e 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -28,8 +28,17 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- <AssemblyOriginatorKeyFile>OpenAI-DotNet.pfx</AssemblyOriginatorKeyFile> <IncludeSymbols>True</IncludeSymbols> <TreatWarningsAsErrors>True</TreatWarningsAsErrors> - <Version>7.7.0</Version> + <Version>7.7.1</Version> <PackageReleaseNotes> +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) 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 From 62940bd429ac67c4d9b5fdbed528beafe1e1ddff Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 12:53:11 -0500 Subject: [PATCH 2/7] rename BaseEndPoint -> OpenAIBaseEndpoint --- OpenAI-DotNet/Assistants/AssistantsEndpoint.cs | 2 +- OpenAI-DotNet/Audio/AudioEndpoint.cs | 2 +- OpenAI-DotNet/Chat/ChatEndpoint.cs | 2 +- .../Common/{BaseEndPoint.cs => OpenAIBaseEndpoint.cs} | 4 ++-- OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs | 2 +- OpenAI-DotNet/Files/FilesEndpoint.cs | 2 +- OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs | 2 +- OpenAI-DotNet/Images/ImagesEndpoint.cs | 2 +- OpenAI-DotNet/Models/ModelsEndpoint.cs | 2 +- OpenAI-DotNet/Moderations/ModerationsEndpoint.cs | 2 +- OpenAI-DotNet/Threads/ThreadsEndpoint.cs | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) rename OpenAI-DotNet/Common/{BaseEndPoint.cs => OpenAIBaseEndpoint.cs} (93%) diff --git a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs index 8596120d..8c6d86ae 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) { } diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs index 5fcd703f..c1d40bcc 100644 --- a/OpenAI-DotNet/Audio/AudioEndpoint.cs +++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs @@ -15,7 +15,7 @@ namespace OpenAI.Audio /// Transforms audio into text.<br/> /// <see href="https://platform.openai.com/docs/api-reference/audio"/> /// </summary> - public sealed class AudioEndpoint : BaseEndPoint + public sealed class AudioEndpoint : OpenAIBaseEndpoint { private class AudioResponse { diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs index 513cc892..54c5e884 100644 --- a/OpenAI-DotNet/Chat/ChatEndpoint.cs +++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs @@ -16,7 +16,7 @@ namespace OpenAI.Chat /// Given a chat conversation, the model will return a chat completion response.<br/> /// <see href="https://platform.openai.com/docs/api-reference/chat"/> /// </summary> - public sealed class ChatEndpoint : BaseEndPoint + public sealed class ChatEndpoint : OpenAIBaseEndpoint { /// <inheritdoc /> public ChatEndpoint(OpenAIClient client) : base(client) { } 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/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs index 3aea137b..c722124f 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.<br/> /// <see href="https://platform.openai.com/docs/guides/embeddings"/> /// </summary> - public sealed class EmbeddingsEndpoint : BaseEndPoint + public sealed class EmbeddingsEndpoint : OpenAIBaseEndpoint { /// <inheritdoc /> public EmbeddingsEndpoint(OpenAIClient client) : base(client) { } diff --git a/OpenAI-DotNet/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs index 35f548f8..ae09f5f6 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.<br/> /// <see href="https://platform.openai.com/docs/api-reference/files"/> /// </summary> - public sealed class FilesEndpoint : BaseEndPoint + public sealed class FilesEndpoint : OpenAIBaseEndpoint { private class FilesList { diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs index d64f195c..5b92fc42 100644 --- a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs +++ b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs @@ -14,7 +14,7 @@ namespace OpenAI.FineTuning /// <see href="https://platform.openai.com/docs/guides/fine-tuning"/><br/> /// <see href="https://platform.openai.com/docs/api-reference/fine-tuning"/> /// </summary> - public sealed class FineTuningEndpoint : BaseEndPoint + public sealed class FineTuningEndpoint : OpenAIBaseEndpoint { /// <inheritdoc /> public FineTuningEndpoint(OpenAIClient client) : base(client) { } diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index 3ed8629b..74ff8198 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -14,7 +14,7 @@ namespace OpenAI.Images /// Given a prompt and/or an input image, the model will generate a new image.<br/> /// <see href="https://platform.openai.com/docs/api-reference/images"/> /// </summary> - public sealed class ImagesEndpoint : BaseEndPoint + public sealed class ImagesEndpoint : OpenAIBaseEndpoint { /// <inheritdoc /> internal ImagesEndpoint(OpenAIClient client) : base(client) { } diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs index 21de5af9..12161f7c 100644 --- a/OpenAI-DotNet/Models/ModelsEndpoint.cs +++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs @@ -15,7 +15,7 @@ namespace OpenAI.Models /// You can refer to the Models documentation to understand which models are available for certain endpoints: <see href="https://platform.openai.com/docs/models/model-endpoint-compatibility"/>.<br/> /// <see href="https://platform.openai.com/docs/api-reference/models"/> /// </summary> - public sealed class ModelsEndpoint : BaseEndPoint + public sealed class ModelsEndpoint : OpenAIBaseEndpoint { private sealed class ModelsList { diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs index 8cc0ff22..a1edf7cc 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.<br/> /// <see href="https://platform.openai.com/docs/api-reference/moderations"/> /// </summary> - public sealed class ModerationsEndpoint : BaseEndPoint + public sealed class ModerationsEndpoint : OpenAIBaseEndpoint { /// <inheritdoc /> public ModerationsEndpoint(OpenAIClient client) : base(client) { } diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs index c9b03f7c..8c66df2d 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.<br/> /// <see href="https://platform.openai.com/docs/api-reference/threads"/> /// </summary> - public sealed class ThreadsEndpoint : BaseEndPoint + public sealed class ThreadsEndpoint : OpenAIBaseEndpoint { public ThreadsEndpoint(OpenAIClient client) : base(client) { } From 0cde283a632428fa9eeecada8d9d0a786a55ebea Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 15:07:05 -0500 Subject: [PATCH 3/7] more tweaks and fixes --- .../TestFixture_00_02_Tools.cs | 2 - .../Assistants/AssistantsEndpoint.cs | 42 +++---- OpenAI-DotNet/Audio/AudioEndpoint.cs | 15 +-- OpenAI-DotNet/Chat/ChatEndpoint.cs | 92 ++++++++++++--- .../Embeddings/EmbeddingsEndpoint.cs | 6 +- .../Extensions/ResponseExtensions.cs | 109 ++++++++++++++++-- OpenAI-DotNet/Extensions/StringExtensions.cs | 12 +- OpenAI-DotNet/Files/FilesEndpoint.cs | 18 +-- .../FineTuning/FineTuningEndpoint.cs | 64 ++-------- OpenAI-DotNet/Images/ImagesEndpoint.cs | 18 +-- OpenAI-DotNet/Models/ModelsEndpoint.cs | 12 +- .../Moderations/ModerationsEndpoint.cs | 6 +- OpenAI-DotNet/OpenAI-DotNet.csproj | 2 + OpenAI-DotNet/Threads/ThreadsEndpoint.cs | 91 +++++++-------- 14 files changed, 292 insertions(+), 197 deletions(-) diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs index f5748552..7a6465f1 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs @@ -2,11 +2,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using System.Threading.Tasks; using NUnit.Framework; using OpenAI.Images; diff --git a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs index 8c6d86ae..b51a78b6 100644 --- a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs +++ b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs @@ -23,8 +23,8 @@ internal AssistantsEndpoint(OpenAIClient client) : base(client) { } /// <returns><see cref="ListResponse{Assistant}"/></returns> public async Task<ListResponse<AssistantResponse>> 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<ListResponse<AssistantResponse>>(responseAsString, client); } @@ -37,9 +37,9 @@ public async Task<ListResponse<AssistantResponse>> ListAssistantsAsync(ListQuery public async Task<AssistantResponse> 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<AssistantResponse>(responseAsString, client); } @@ -51,8 +51,8 @@ public async Task<AssistantResponse> CreateAssistantAsync(CreateAssistantRequest /// <returns><see cref="AssistantResponse"/>.</returns> public async Task<AssistantResponse> 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<AssistantResponse>(responseAsString, client); } @@ -65,9 +65,9 @@ public async Task<AssistantResponse> RetrieveAssistantAsync(string assistantId, /// <returns><see cref="AssistantResponse"/>.</returns> public async Task<AssistantResponse> 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<AssistantResponse>(responseAsString, client); } @@ -79,8 +79,8 @@ public async Task<AssistantResponse> ModifyAssistantAsync(string assistantId, Cr /// <returns>True, if the assistant was deleted.</returns> public async Task<bool> 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<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } @@ -95,8 +95,8 @@ public async Task<bool> DeleteAssistantAsync(string assistantId, CancellationTok /// <returns><see cref="ListResponse{AssistantFile}"/>.</returns> public async Task<ListResponse<AssistantFileResponse>> 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<ListResponse<AssistantFileResponse>>(responseAsString, client); } @@ -117,9 +117,9 @@ public async Task<AssistantFileResponse> 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<AssistantFileResponse>(responseAsString, client); } @@ -132,8 +132,8 @@ public async Task<AssistantFileResponse> AttachFileAsync(string assistantId, Fil /// <returns><see cref="AssistantFileResponse"/>.</returns> public async Task<AssistantFileResponse> 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<AssistantFileResponse>(responseAsString, client); } @@ -151,8 +151,8 @@ public async Task<AssistantFileResponse> RetrieveFileAsync(string assistantId, s /// <returns>True, if file was removed.</returns> public async Task<bool> 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<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs index c1d40bcc..7bb9997a 100644 --- a/OpenAI-DotNet/Audio/AudioEndpoint.cs +++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs @@ -43,9 +43,9 @@ public AudioEndpoint(OpenAIClient client) : base(client) { } /// <returns><see cref="ReadOnlyMemory{T}"/></returns> public async Task<ReadOnlyMemory<byte>> CreateSpeechAsync(SpeechRequest request, Func<ReadOnlyMemory<byte>, 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<ReadOnlyMemory<byte>> CreateSpeechAsync(SpeechRequest request, totalBytesRead += bytesRead; } + await response.CheckResponseAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false); return new ReadOnlyMemory<byte>(memoryStream.GetBuffer(), 0, totalBytesRead); } @@ -108,8 +109,8 @@ public async Task<string> 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<AudioResponse>(responseAsString)?.Text @@ -145,8 +146,8 @@ public async Task<string> 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<AudioResponse>(responseAsString)?.Text diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs index 54c5e884..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; @@ -32,9 +34,9 @@ public ChatEndpoint(OpenAIClient client) : base(client) { } /// <returns><see cref="ChatResponse"/>.</returns> public async Task<ChatResponse> 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<ChatResponse>(responseAsString, client); } @@ -48,25 +50,48 @@ public async Task<ChatResponse> GetCompletionAsync(ChatRequest chatRequest, Canc public async Task<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, Action<ChatResponse> 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<ChatResponse>(eventData, client); @@ -83,7 +108,12 @@ public async Task<ChatResponse> 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<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, A public async IAsyncEnumerable<ChatResponse> 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<ChatResponse>(eventData, client); @@ -138,7 +191,12 @@ public async IAsyncEnumerable<ChatResponse> 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/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs index c722124f..6d9cdaec 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs @@ -76,9 +76,9 @@ public async Task<EmbeddingsResponse> CreateEmbeddingAsync(IEnumerable<string> i /// <returns><see cref="EmbeddingsResponse"/></returns> public async Task<EmbeddingsResponse> 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<EmbeddingsResponse>(responseAsString, client); } } diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index 53166452..fcc5e822 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,110 @@ internal static void SetResponseData(this BaseResponse response, HttpResponseHea } } - internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse = false, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) + internal static async Task<string> 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); - if (!response.IsSuccessStatusCode) + if (debugResponse || !response.IsSuccessStatusCode) { - throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); - } - - if (debugResponse) - { - Console.WriteLine(responseAsString); + if (response.RequestMessage != null) + { + var debugMessage = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(methodName)) + { + debugMessage.Append($"{methodName} -> "); + } + + debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n"); + + var debugMessageObject = new Dictionary<string, Dictionary<string, object>> + { + ["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<string, object>(); + } + + if (responseStream != null) + { + var body = Encoding.UTF8.GetString(responseStream.ToArray()); + + try + { + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body); + } + catch + { + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = body; + } + } + + if (!string.IsNullOrWhiteSpace(responseAsString)) + { + try + { + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString); + } + catch + { + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = responseAsString; + } + } + + debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true })); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException(debugMessage.ToString()); + } + + if (debugResponse) + { + Console.WriteLine(debugMessage.ToString()); + } + } + else + { + 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/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs index ae09f5f6..4fda6ee5 100644 --- a/OpenAI-DotNet/Files/FilesEndpoint.cs +++ b/OpenAI-DotNet/Files/FilesEndpoint.cs @@ -46,8 +46,8 @@ public async Task<IReadOnlyList<FileResponse>> ListFilesAsync(string purpose = n query = new Dictionary<string, string> { { 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<FilesList>(resultAsString, OpenAIClient.JsonSerializationOptions)?.Files; } @@ -85,8 +85,8 @@ public async Task<FileResponse> 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<FileResponse>(responseAsString, OpenAIClient.JsonSerializationOptions); } @@ -102,7 +102,7 @@ public async Task<bool> DeleteFileAsync(string fileId, CancellationToken cancell async Task<bool> 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<bool> InternalDeleteFileAsync(int attempt) } } - await response.CheckResponseAsync(cancellationToken); + await response.CheckResponseAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } } @@ -133,8 +133,8 @@ async Task<bool> InternalDeleteFileAsync(int attempt) /// <returns><see cref="FileResponse"/></returns> public async Task<FileResponse> 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<FileResponse>(responseAsString, OpenAIClient.JsonSerializationOptions); } @@ -191,7 +191,7 @@ public async Task<string> DownloadFileAsync(FileResponse fileData, string direct await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); return filePath; } - + /// <summary> /// Gets the specified file as stream /// </summary> diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs index 5b92fc42..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; @@ -32,32 +30,12 @@ public FineTuningEndpoint(OpenAIClient client) : base(client) { } /// <returns><see cref="FineTuneJobResponse"/>.</returns> public async Task<FineTuneJobResponse> 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<FineTuneJobResponse>(responseAsString, client); } - [Obsolete("Use new overload")] - public async Task<FineTuneJobList> ListJobsAsync(int? limit, string after, CancellationToken cancellationToken) - { - var parameters = new Dictionary<string, string>(); - - 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<FineTuneJobList>(responseAsString, OpenAIClient.JsonSerializationOptions); - } - /// <summary> /// List your organization's fine-tuning jobs. /// </summary> @@ -66,8 +44,8 @@ public async Task<FineTuneJobList> ListJobsAsync(int? limit, string after, Cance /// <returns>List of <see cref="FineTuneJobResponse"/>s.</returns> public async Task<ListResponse<FineTuneJobResponse>> 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<ListResponse<FineTuneJobResponse>>(responseAsString, client); } @@ -79,8 +57,8 @@ public async Task<ListResponse<FineTuneJobResponse>> ListJobsAsync(ListQuery que /// <returns><see cref="FineTuneJobResponse"/>.</returns> public async Task<FineTuneJobResponse> 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<FineTuneJobResponse>(responseAsString, client); job.Events = (await ListJobEventsAsync(job, query: null, cancellationToken: cancellationToken).ConfigureAwait(false))?.Items; return job; @@ -94,32 +72,12 @@ public async Task<FineTuneJobResponse> GetJobInfoAsync(string jobId, Cancellatio /// <returns><see cref="FineTuneJobResponse"/>.</returns> public async Task<bool> 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<FineTuneJobResponse>(responseAsString, OpenAIClient.JsonSerializationOptions); return result.Status == JobStatus.Cancelled; } - [Obsolete("use new overload")] - public async Task<EventList> ListJobEventsAsync(string jobId, int? limit, string after, CancellationToken cancellationToken) - { - var parameters = new Dictionary<string, string>(); - - 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<EventList>(responseAsString, OpenAIClient.JsonSerializationOptions); - } - /// <summary> /// Get fine-grained status updates for a fine-tune job. /// </summary> @@ -129,8 +87,8 @@ public async Task<EventList> ListJobEventsAsync(string jobId, int? limit, string /// <returns>List of events for <see cref="FineTuneJobResponse"/>.</returns> public async Task<ListResponse<EventResponse>> 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<ListResponse<EventResponse>>(responseAsString, client); } } diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index 74ff8198..0d648458 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -30,9 +30,9 @@ internal ImagesEndpoint(OpenAIClient client) : base(client) { } /// <returns>A list of generated texture urls to download.</returns> public async Task<IReadOnlyList<ImageResult>> 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); } /// <summary> @@ -66,8 +66,8 @@ public async Task<IReadOnlyList<ImageResult>> 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); } /// <summary> @@ -92,13 +92,13 @@ public async Task<IReadOnlyList<ImageResult>> 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<IReadOnlyList<ImageResult>> DeserializeResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + private async Task<IReadOnlyList<ImageResult>> 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<ImagesResponse>(resultAsString, client); if (imagesResponse?.Results is not { Count: not 0 }) diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs index 12161f7c..3db14fff 100644 --- a/OpenAI-DotNet/Models/ModelsEndpoint.cs +++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs @@ -37,8 +37,8 @@ public ModelsEndpoint(OpenAIClient client) : base(client) { } /// <returns>Asynchronously returns the list of all <see cref="Model"/>s</returns> public async Task<IReadOnlyList<Model>> 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<ModelsList>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Models; } @@ -50,8 +50,8 @@ public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancell /// <returns>Asynchronously returns the <see cref="Model"/> with all available properties</returns> public async Task<Model> 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<Model>(responseAsString, OpenAIClient.JsonSerializationOptions); } @@ -75,8 +75,8 @@ public async Task<bool> 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<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } catch (Exception e) diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs index a1edf7cc..0645c471 100644 --- a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs +++ b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs @@ -50,9 +50,9 @@ public async Task<bool> GetModerationAsync(string input, string model = null, Ca /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param> public async Task<ModerationsResponse> 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<ModerationsResponse>(responseAsString, client); } diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 0c8d259e..4534d296 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -39,6 +39,8 @@ Version 7.7.1 - 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 8c66df2d..14c5ccc0 100644 --- a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs +++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs @@ -26,8 +26,9 @@ public ThreadsEndpoint(OpenAIClient client) : base(client) { } /// <returns><see cref="ThreadResponse"/>.</returns> public async Task<ThreadResponse> 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<ThreadResponse>(responseAsString, client); } @@ -39,8 +40,8 @@ public async Task<ThreadResponse> CreateThreadAsync(CreateThreadRequest request /// <returns><see cref="ThreadResponse"/>.</returns> public async Task<ThreadResponse> 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<ThreadResponse>(responseAsString, client); } @@ -59,9 +60,9 @@ public async Task<ThreadResponse> RetrieveThreadAsync(string threadId, Cancellat /// <returns><see cref="ThreadResponse"/>.</returns> public async Task<ThreadResponse> ModifyThreadAsync(string threadId, IReadOnlyDictionary<string, string> 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<ThreadResponse>(responseAsString, client); } @@ -73,8 +74,8 @@ public async Task<ThreadResponse> ModifyThreadAsync(string threadId, IReadOnlyDi /// <returns>True, if was successfully deleted.</returns> public async Task<bool> 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<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false; } @@ -89,9 +90,9 @@ public async Task<bool> DeleteThreadAsync(string threadId, CancellationToken can /// <returns><see cref="MessageResponse"/>.</returns> public async Task<MessageResponse> 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<MessageResponse>(responseAsString, client); } @@ -104,8 +105,8 @@ public async Task<MessageResponse> CreateMessageAsync(string threadId, CreateMes /// <returns><see cref="ListResponse{ThreadMessage}"/>.</returns> public async Task<ListResponse<MessageResponse>> 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<ListResponse<MessageResponse>>(responseAsString, client); } @@ -118,8 +119,8 @@ public async Task<ListResponse<MessageResponse>> ListMessagesAsync(string thread /// <returns><see cref="MessageResponse"/>.</returns> public async Task<MessageResponse> 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<MessageResponse>(responseAsString, client); } @@ -155,9 +156,9 @@ public async Task<MessageResponse> ModifyMessageAsync(MessageResponse message, I /// <returns><see cref="MessageResponse"/>.</returns> public async Task<MessageResponse> ModifyMessageAsync(string threadId, string messageId, IReadOnlyDictionary<string, string> 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<MessageResponse>(responseAsString, client); } @@ -175,8 +176,8 @@ public async Task<MessageResponse> ModifyMessageAsync(string threadId, string me /// <returns><see cref="ListResponse{ThreadMessageFile}"/>.</returns> public async Task<ListResponse<MessageFileResponse>> 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<ListResponse<MessageFileResponse>>(responseAsString, client); } @@ -190,8 +191,8 @@ public async Task<ListResponse<MessageFileResponse>> ListFilesAsync(string threa /// <returns><see cref="MessageFileResponse"/>.</returns> public async Task<MessageFileResponse> 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<MessageFileResponse>(responseAsString, client); } @@ -208,8 +209,8 @@ public async Task<MessageFileResponse> RetrieveFileAsync(string threadId, string /// <returns><see cref="ListResponse{RunResponse}"/></returns> public async Task<ListResponse<RunResponse>> 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<ListResponse<RunResponse>>(responseAsString, client); } @@ -228,9 +229,9 @@ public async Task<RunResponse> 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<RunResponse>(responseAsString, client); } @@ -248,9 +249,9 @@ public async Task<RunResponse> 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<RunResponse>(responseAsString, client); } @@ -263,8 +264,8 @@ public async Task<RunResponse> CreateThreadAndRunAsync(CreateThreadAndRunRequest /// <returns><see cref="RunResponse"/>.</returns> public async Task<RunResponse> 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<RunResponse>(responseAsString, client); } @@ -283,9 +284,9 @@ public async Task<RunResponse> RetrieveRunAsync(string threadId, string runId, C /// <returns><see cref="RunResponse"/>.</returns> public async Task<RunResponse> ModifyRunAsync(string threadId, string runId, IReadOnlyDictionary<string, string> 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<RunResponse>(responseAsString, client); } @@ -301,9 +302,9 @@ public async Task<RunResponse> ModifyRunAsync(string threadId, string runId, IRe /// <returns><see cref="RunResponse"/>.</returns> public async Task<RunResponse> 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<RunResponse>(responseAsString, client); } @@ -317,8 +318,8 @@ public async Task<RunResponse> SubmitToolOutputsAsync(string threadId, string ru /// <returns><see cref="ListResponse{RunStep}"/>.</returns> public async Task<ListResponse<RunStepResponse>> 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<ListResponse<RunStepResponse>>(responseAsString, client); } @@ -332,8 +333,8 @@ public async Task<ListResponse<RunStepResponse>> ListRunStepsAsync(string thread /// <returns><see cref="RunStepResponse"/>.</returns> public async Task<RunStepResponse> 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<RunStepResponse>(responseAsString, client); } @@ -346,8 +347,8 @@ public async Task<RunStepResponse> RetrieveRunStepAsync(string threadId, string /// <returns><see cref="RunResponse"/>.</returns> public async Task<RunResponse> 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<RunResponse>(responseAsString, client); } From 02609ac47e5fe8575cb78db1c04dec5fc33c3489 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 15:09:48 -0500 Subject: [PATCH 4/7] fix logical error --- OpenAI-DotNet/Extensions/ResponseExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index fcc5e822..b0ab2bcd 100644 --- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs +++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs @@ -153,7 +153,7 @@ internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage re ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), }; - if (responseStream != null || string.IsNullOrWhiteSpace(responseAsString)) + if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString)) { debugMessageObject["Response"]["Body"] = new Dictionary<string, object>(); } From 03331d6f133d5e58f8df9d565aa902e4245458df Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 15:12:59 -0500 Subject: [PATCH 5/7] revert --- OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs index 2b03f857..c47d3f6a 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs @@ -187,8 +187,6 @@ public void TearDown() } Assert.IsFalse(File.Exists(".openai")); - - OpenAIAuthentication.Default = null; } } } From ac4f93f8454fbfe86d226ea96b18279ba720966b Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 15:17:41 -0500 Subject: [PATCH 6/7] fix http exception --- .../Extensions/ResponseExtensions.cs | 119 +++++++++--------- 1 file changed, 57 insertions(+), 62 deletions(-) diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index b0ab2bcd..aae116d8 100644 --- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs +++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs @@ -110,96 +110,91 @@ internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage re { var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - if (debugResponse || !response.IsSuccessStatusCode) + if (debugResponse && response.RequestMessage != null) { - if (response.RequestMessage != null) + var debugMessage = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(methodName)) { - var debugMessage = new StringBuilder(); + debugMessage.Append($"{methodName} -> "); + } + + debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n"); - if (!string.IsNullOrWhiteSpace(methodName)) + var debugMessageObject = new Dictionary<string, Dictionary<string, object>> + { + ["Request"] = new() { - debugMessage.Append($"{methodName} -> "); + ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), } + }; - debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n"); + if (requestContent != null) + { + var requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - var debugMessageObject = new Dictionary<string, Dictionary<string, object>> + if (!string.IsNullOrWhiteSpace(requestAsString)) { - ["Request"] = new() + try { - ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), + debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString); } - }; - - if (requestContent != null) - { - var requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(requestAsString)) + catch { - try - { - debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString); - } - catch - { - debugMessageObject["Request"]["Body"] = requestAsString; - } + debugMessageObject["Request"]["Body"] = requestAsString; } } + } - debugMessageObject["Response"] = new() - { - ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), - }; + debugMessageObject["Response"] = new() + { + ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value), + }; - if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString)) - { - debugMessageObject["Response"]["Body"] = new Dictionary<string, object>(); - } + if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString)) + { + debugMessageObject["Response"]["Body"] = new Dictionary<string, object>(); + } - if (responseStream != null) - { - var body = Encoding.UTF8.GetString(responseStream.ToArray()); + if (responseStream != null) + { + var body = Encoding.UTF8.GetString(responseStream.ToArray()); - try - { - ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body); - } - catch - { - ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = body; - } + try + { + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body); } - - if (!string.IsNullOrWhiteSpace(responseAsString)) + catch { - try - { - ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString); - } - catch - { - ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = responseAsString; - } + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = body; } + } - debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true })); - - if (!response.IsSuccessStatusCode) + if (!string.IsNullOrWhiteSpace(responseAsString)) + { + try { - throw new HttpRequestException(debugMessage.ToString()); + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString); } - - if (debugResponse) + catch { - Console.WriteLine(debugMessage.ToString()); + ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = responseAsString; } } - else + + debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true })); + + if (!response.IsSuccessStatusCode) { - throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); + throw new HttpRequestException(debugMessage.ToString()); } + + Console.WriteLine(debugMessage.ToString()); + } + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); } return responseAsString; From e2db47d587ba407adf50fe6de1dd62eacdb8bbfa Mon Sep 17 00:00:00 2001 From: Stephen Hodgson <hodgson.designs@gmail.com> Date: Sun, 25 Feb 2024 15:26:38 -0500 Subject: [PATCH 7/7] tweak http req ex a bit more --- .../Extensions/ResponseExtensions.cs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs index aae116d8..a32ece1e 100644 --- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs +++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs @@ -109,25 +109,26 @@ internal static void SetResponseData(this BaseResponse response, HttpResponseHea internal static async Task<string> 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 (debugResponse && response.RequestMessage != null) + if (!response.IsSuccessStatusCode || debugResponse) { - var debugMessage = new StringBuilder(); - if (!string.IsNullOrWhiteSpace(methodName)) { debugMessage.Append($"{methodName} -> "); } - debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n"); + var debugMessageObject = new Dictionary<string, Dictionary<string, object>>(); - var debugMessageObject = new Dictionary<string, Dictionary<string, object>> + if (response.RequestMessage != null) { - ["Request"] = new() + 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) { @@ -183,12 +184,6 @@ internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage re } debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true })); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException(debugMessage.ToString()); - } - Console.WriteLine(debugMessage.ToString()); }