From 271c374ba9b0b5b37aa08f7c409381a0f0b8ae1f Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Wed, 22 Nov 2023 10:55:27 -0500 Subject: [PATCH] OpenAI-DotNet 7.3.2 (#177) - Added detail parameter to ImageURL --- OpenAI-DotNet-Tests/TestFixture_03_Chat.cs | 57 ++++++++++++++++------ OpenAI-DotNet/Chat/ChatResponse.cs | 2 +- OpenAI-DotNet/Chat/Content.cs | 8 +++ OpenAI-DotNet/Chat/Message.cs | 2 +- OpenAI-DotNet/Common/ImageDetail.cs | 14 ++++++ OpenAI-DotNet/Common/ImageUrl.cs | 11 ++++- OpenAI-DotNet/OpenAI-DotNet.csproj | 6 ++- README.md | 41 +++++++++++++--- 8 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 OpenAI-DotNet/Common/ImageDetail.cs diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs index 9ce073d7..a76bbeab 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs @@ -32,7 +32,7 @@ public async Task Test_01_GetChatCompletion() foreach (var choice in response.Choices) { - Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); + Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice} | Finish Reason: {choice.FinishReason}"); } response.GetUsage(); @@ -65,8 +65,10 @@ public async Task Test_02_GetChatStreamingCompletion() Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); var choice = response.FirstChoice; - Assert.IsFalse(string.IsNullOrEmpty(choice?.Message?.Content)); - Console.WriteLine($"[{choice!.Index}] {choice.Message!.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); + Assert.IsNotNull(choice); + Assert.IsNotNull(choice.Message); + Assert.IsFalse(string.IsNullOrEmpty(choice.Message.Content)); + Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice} | Finish Reason: {choice.FinishReason}"); Assert.IsTrue(choice.Message.Role == Role.Assistant); Assert.IsTrue(choice.Message.Content!.Equals(cumulativeDelta)); Console.WriteLine(response.ToString()); @@ -149,7 +151,7 @@ public async Task Test_04_GetChatFunctionCompletion() Assert.IsTrue(response.Choices.Count == 1); messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -164,7 +166,7 @@ public async Task Test_04_GetChatFunctionCompletion() if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); @@ -259,7 +261,7 @@ public async Task Test_05_GetChatFunctionCompletion_Streaming() if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); @@ -336,7 +338,7 @@ public async Task Test_06_GetChatFunctionForceCompletion() Assert.IsTrue(response.Choices.Count == 1); messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -411,7 +413,7 @@ public async Task Test_07_GetChatToolCompletion() Assert.IsTrue(response.Choices.Count == 1); messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -424,9 +426,9 @@ public async Task Test_07_GetChatToolCompletion() Assert.IsTrue(response.Choices.Count == 1); messages.Add(response.FirstChoice.Message); - if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) + if (!string.IsNullOrEmpty(response.FirstChoice)) { - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); @@ -521,7 +523,7 @@ public async Task Test_08_GetChatToolCompletion_Streaming() if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) { - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); @@ -598,7 +600,7 @@ public async Task Test_09_GetChatToolForceCompletion() Assert.IsTrue(response.Choices.Count == 1); messages.Add(response.FirstChoice.Message); - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -637,14 +639,14 @@ public async Task Test_10_GetChatVision() new Message(Role.User, new List { new Content(ContentType.Text, "What's in this image?"), - new Content(ContentType.ImageUrl, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg") + new ImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageDetail.Low) }) }; var chatRequest = new ChatRequest(messages, model: "gpt-4-vision-preview"); var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishDetails}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishDetails}"); response.GetUsage(); } @@ -658,7 +660,7 @@ public async Task Test_11_GetChatVisionStreaming() new Message(Role.User, new List { new Content(ContentType.Text, "What's in this image?"), - new Content(ContentType.ImageUrl, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg") + new ImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageDetail.Low) }) }; var chatRequest = new ChatRequest(messages, model: "gpt-4-vision-preview"); @@ -670,7 +672,30 @@ public async Task Test_11_GetChatVisionStreaming() }); Assert.IsNotNull(response); Assert.IsNotNull(response.Choices); - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishDetails}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishDetails}"); + response.GetUsage(); + } + + [Test] + public async Task Test_12_JsonMode() + { + Assert.IsNotNull(OpenAIClient.ChatEndpoint); + var messages = new List + { + new Message(Role.System, "You are a helpful assistant designed to output JSON."), + new Message(Role.User, "Who won the world series in 2020?"), + }; + var chatRequest = new ChatRequest(messages, "gpt-4-1106-preview", responseFormat: ChatResponseFormat.Json); + var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + Assert.IsNotNull(response); + Assert.IsNotNull(response.Choices); + Assert.IsNotEmpty(response.Choices); + + foreach (var choice in response.Choices) + { + Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice} | Finish Reason: {choice.FinishReason}"); + } + response.GetUsage(); } } diff --git a/OpenAI-DotNet/Chat/ChatResponse.cs b/OpenAI-DotNet/Chat/ChatResponse.cs index 8ccc2ae7..e3afe48c 100644 --- a/OpenAI-DotNet/Chat/ChatResponse.cs +++ b/OpenAI-DotNet/Chat/ChatResponse.cs @@ -71,7 +71,7 @@ public IReadOnlyList Choices public override string ToString() => FirstChoice?.ToString() ?? string.Empty; - public static implicit operator string(ChatResponse response) => response.ToString(); + public static implicit operator string(ChatResponse response) => response?.ToString(); internal void CopyFrom(ChatResponse other) { diff --git a/OpenAI-DotNet/Chat/Content.cs b/OpenAI-DotNet/Chat/Content.cs index 2fbb607d..df468bba 100644 --- a/OpenAI-DotNet/Chat/Content.cs +++ b/OpenAI-DotNet/Chat/Content.cs @@ -22,6 +22,12 @@ public Content(ContentType type, string input) } } + public Content(ImageUrl imageUrl) + { + Type = ContentType.ImageUrl; + ImageUrl = imageUrl; + } + [JsonInclude] [JsonPropertyName("type")] [JsonConverter(typeof(JsonStringEnumConverter))] @@ -36,5 +42,7 @@ public Content(ContentType type, string input) [JsonPropertyName("image_url")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ImageUrl ImageUrl { get; private set; } + + public static implicit operator Content(ImageUrl imageUrl) => new Content(imageUrl); } } \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/Message.cs b/OpenAI-DotNet/Chat/Message.cs index 4c5a47de..eac98fcf 100644 --- a/OpenAI-DotNet/Chat/Message.cs +++ b/OpenAI-DotNet/Chat/Message.cs @@ -125,7 +125,7 @@ public IReadOnlyList ToolCalls public override string ToString() => Content?.ToString() ?? string.Empty; - public static implicit operator string(Message message) => message.ToString(); + public static implicit operator string(Message message) => message?.ToString(); internal void CopyFrom(Delta other) { diff --git a/OpenAI-DotNet/Common/ImageDetail.cs b/OpenAI-DotNet/Common/ImageDetail.cs new file mode 100644 index 00000000..f026d81e --- /dev/null +++ b/OpenAI-DotNet/Common/ImageDetail.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace OpenAI +{ + public enum ImageDetail + { + [EnumMember(Value = "auto")] + Auto, + [EnumMember(Value = "low")] + Low, + [EnumMember(Value = "high")] + High + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Common/ImageUrl.cs b/OpenAI-DotNet/Common/ImageUrl.cs index 11a5b275..704ce04a 100644 --- a/OpenAI-DotNet/Common/ImageUrl.cs +++ b/OpenAI-DotNet/Common/ImageUrl.cs @@ -5,11 +5,20 @@ namespace OpenAI public sealed class ImageUrl { [JsonConstructor] - public ImageUrl(string url) => Url = url; + public ImageUrl(string url, ImageDetail detail = ImageDetail.Auto) + { + Url = url; + Detail = detail; + } [JsonInclude] [JsonPropertyName("url")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string Url { get; private set; } + + [JsonInclude] + [JsonPropertyName("detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ImageDetail Detail { get; private set; } } } \ No newline at end of file diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index c6eba20e..8232926f 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -18,8 +18,10 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- https://github.com/RageAgainstThePixel/OpenAI-DotNet OpenAI, AI, ML, API, gpt-4, gpt-3.5-tubo, gpt-3, chatGPT, chat-gpt, gpt-2, gpt, dall-e-2, dall-e-3 OpenAI API - 7.3.1 - Version 7.3.1 + 7.3.2 + Version 7.3.2 +- Added detail parameter to ImageURL +Version 7.3.1 - Fixed json serialization settings when EnableDebug is disabled Version 7.3.0 - Added AgentsEndpoint diff --git a/README.md b/README.md index fa292bad..c2331ddb 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Install-Package OpenAI-DotNet - [Streaming](#chat-streaming) - [Tools](#chat-tools) :new: - [Vision](#chat-vision) :new: + - [Json Mode](#chat-json-mode) :new: - [Audio](#audio) - [Create Speech](#create-speech) - [Create Transcription](#create-transcription) @@ -848,7 +849,8 @@ var messages = new List }; var chatRequest = new ChatRequest(messages, Model.GPT4); var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); -Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content}"); +var choice = response.FirstChoice; +Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message} | Finish Reason: {choice.FinishReason}"); ``` #### [Chat Streaming](https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream) @@ -871,7 +873,7 @@ var response = await api.ChatEndpoint.StreamCompletionAsync(chatRequest, partial } }); var choice = response.FirstChoice; -Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message.Content} | Finish Reason: {choice.FinishReason}"); +Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice.Message} | Finish Reason: {choice.FinishReason}"); ``` Or if using [`IAsyncEnumerable{T}`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-5.0) ([C# 8.0+](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8)) @@ -943,7 +945,7 @@ var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto"); var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); messages.Add(response.FirstChoice.Message); -Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); +Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var locationMessage = new Message(Role.User, "I'm in Glasgow, Scotland"); messages.Add(locationMessage); @@ -953,9 +955,9 @@ response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); messages.Add(response.FirstChoice.Message); -if (!string.IsNullOrEmpty(response.FirstChoice.Message.Content)) +if (!string.IsNullOrEmpty(response.FirstChoice)) { - Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishReason}"); + Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}"); var unitMessage = new Message(Role.User, "celsius"); messages.Add(unitMessage); @@ -995,7 +997,7 @@ var messages = new List new Message(Role.User, new List { new Content(ContentType.Text, "What's in this image?"), - new Content(ContentType.ImageUrl, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg") + new ImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageDetail.Low) }) }; var chatRequest = new ChatRequest(messages, model: "gpt-4-vision-preview"); @@ -1003,6 +1005,33 @@ var response = await api.ChatEndpoint.GetCompletionAsync(chatRequest); Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice.Message.Content} | Finish Reason: {response.FirstChoice.FinishDetails}"); ``` +#### [Chat Json Mode](https://platform.openai.com/docs/guides/text-generation/json-mode) + +> :warning: Beta Feature + +Important notes: + +- When using JSON mode, always instruct the model to produce JSON via some message in the conversation, for example via your system message. If you don't include an explicit instruction to generate JSON, the model may generate an unending stream of whitespace and the request may run continually until it reaches the token limit. To help ensure you don't forget, the API will throw an error if the string "JSON" does not appear somewhere in the context. +- The JSON in the message the model returns may be partial (i.e. cut off) if `finish_reason` is length, which indicates the generation exceeded max_tokens or the conversation exceeded the token limit. To guard against this, check `finish_reason` before parsing the response. +- JSON mode will not guarantee the output matches any specific schema, only that it is valid and parses without errors. + +```csharp +var messages = new List +{ + new Message(Role.System, "You are a helpful assistant designed to output JSON."), + new Message(Role.User, "Who won the world series in 2020?"), +}; +var chatRequest = new ChatRequest(messages, "gpt-4-1106-preview", responseFormat: ChatResponseFormat.Json); +var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest); + +foreach (var choice in response.Choices) +{ + Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice} | Finish Reason: {choice.FinishReason}"); +} + +response.GetUsage(); +``` + ### [Audio](https://platform.openai.com/docs/api-reference/audio) Converts audio into text.