From 308ab972205fb447e1c21d6c947e3c596bfe4ef3 Mon Sep 17 00:00:00 2001 From: Shane Powell Date: Mon, 20 Nov 2023 10:33:12 +1300 Subject: [PATCH 01/13] Add system_fingerprint to the ChatCompletionCreateResponse class as per: https://platform.openai.com/docs/api-reference/chat/object This fingerprint represents the backend configuration that the model runs with. Can be used in conjunction with the seed request parameter to understand when backend changes have been made that might impact determinism. --- .../ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs index 0615826a..83abf00c 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs @@ -14,4 +14,5 @@ public record ChatCompletionCreateResponse : BaseResponse, IOpenAiModels.IId, IO [JsonPropertyName("created")] public int CreatedAt { get; set; } [JsonPropertyName("id")] public string Id { get; set; } + [JsonPropertyName("system_fingerprint")] public string SystemFingerprint { get; set; } } \ No newline at end of file From 3fb561b0e9b8397fa72458fecf2d8074665e06c9 Mon Sep 17 00:00:00 2001 From: Shane Powell Date: Mon, 20 Nov 2023 13:15:28 +1300 Subject: [PATCH 02/13] Add new tool choices to the ChatCompletionCreateRequest and ChatMessage response messages. Mark the old function api as Obsolete. Added playground tests to test the new function calling and also get the streaming function calling example working (in a messy way). --- OpenAI.Playground/Program.cs | 6 +- .../TestHelpers/ChatCompletionTestHelper.cs | 158 ++++++++++++++++-- .../ChatCompletionCreateRequest.cs | 49 +++++- .../ObjectModels/RequestModels/ChatMessage.cs | 12 +- .../ObjectModels/RequestModels/ToolCall.cs | 24 +++ .../RequestModels/ToolChoiceFunction.cs | 18 ++ .../RequestModels/ToolDefinition.cs | 21 +++ OpenAI.SDK/ObjectModels/StaticValueHelper.cs | 11 ++ 8 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs create mode 100644 OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs create mode 100644 OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs diff --git a/OpenAI.Playground/Program.cs b/OpenAI.Playground/Program.cs index 88ac4824..547c767e 100644 --- a/OpenAI.Playground/Program.cs +++ b/OpenAI.Playground/Program.cs @@ -43,9 +43,11 @@ // |-----------------------------------------------------------------------| -await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); +//await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); //await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); -//await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); +await ChatCompletionTestHelper.RunObsoleteChatFunctionCallTest(sdk); +await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); +await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); //await FineTuningJobTestHelper.RunCaseStudyIsTheModelMakingUntrueStatements(sdk); // Whisper //await AudioTestHelper.RunSimpleAudioCreateTranscriptionTest(sdk); diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index 085d3a65..320b4d0c 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -95,9 +95,9 @@ public static async Task RunSimpleCompletionStreamTest(IOpenAIService sdk) } } - public static async Task RunChatFunctionCallTest(IOpenAIService sdk) + public static async Task RunObsoleteChatFunctionCallTest(IOpenAIService sdk) { - ConsoleExtensions.WriteLine("Chat Function Call Testing is starting:", ConsoleColor.Cyan); + ConsoleExtensions.WriteLine("Chat Obsolete Function Call Testing is starting:", ConsoleColor.Cyan); // example taken from: // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb @@ -179,9 +179,105 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) } } + public static async Task RunChatFunctionCallTest(IOpenAIService sdk) + { + ConsoleExtensions.WriteLine("Chat Tool Functions Call Testing is starting:", ConsoleColor.Cyan); + + // example taken from: + // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb + + var fn1 = new FunctionDefinitionBuilder("get_current_weather", "Get the current weather") + .AddParameter("location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA")) + .AddParameter("format", PropertyDefinition.DefineEnum(new List {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location.")) + .Validate() + .Build(); + + var fn2 = new FunctionDefinitionBuilder("get_n_day_weather_forecast", "Get an N-day weather forecast") + .AddParameter("location", new PropertyDefinition {Type = "string", Description = "The city and state, e.g. San Francisco, CA"}) + .AddParameter("format", PropertyDefinition.DefineEnum(new List {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location.")) + .AddParameter("num_days", PropertyDefinition.DefineInteger("The number of days to forecast")) + .Validate() + .Build(); + var fn3 = new FunctionDefinitionBuilder("get_current_datetime", "Get the current date and time, e.g. 'Saturday, June 24, 2023 6:14:14 PM'") + .Build(); + + var fn4 = new FunctionDefinitionBuilder("identify_number_sequence", "Get a sequence of numbers present in the user message") + .AddParameter("values", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Sequence of numbers specified by the user"))) + .Build(); + try + { + ConsoleExtensions.WriteLine("Chat Function Call Test:", ConsoleColor.DarkCyan); + var completionResult = await sdk.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."), + ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.") + }, + ToolFunctions = new List {fn1, fn2, fn3, fn4}, + // optionally, to force a specific function: + // ToolChoice = new ToolChoiceFunction() { Function = new FunctionTool() { Name = "get_current_weather" }}, + // or auto tool choice: + // ToolChoice = StaticValues.CompletionStatics.ToolChoice.Auto, + MaxTokens = 50, + Model = Models.Gpt_3_5_Turbo + }); + + /* expected output along the lines of: + + Message: + Function call: get_n_day_weather_forecast + location: Chicago, USA + format: celsius + num_days: 5 + */ + + + if (completionResult.Successful) + { + var choice = completionResult.Choices.First(); + Console.WriteLine($"Message: {choice.Message.Content}"); + + var tools = choice.Message.ToolCalls; + if (tools != null) + { + Console.WriteLine($"Tools: {tools.Count}"); + foreach (var toolCall in tools) + { + Console.WriteLine($" {toolCall.Id}: {toolCall.Function}"); + + var fn = toolCall.Function; + if (fn != null) + { + Console.WriteLine($" Function call: {fn.Name}"); + foreach (var entry in fn.ParseArguments()) + { + Console.WriteLine($" {entry.Key}: {entry.Value}"); + } + } + } + } + } + else + { + if (completionResult.Error == null) + { + throw new Exception("Unknown Error"); + } + + Console.WriteLine($"{completionResult.Error.Code}: {completionResult.Error.Message}"); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) { - ConsoleExtensions.WriteLine("Chat Function Call Testing is starting:", ConsoleColor.Cyan); + ConsoleExtensions.WriteLine("Chat Tool Functions Call Stream Testing is starting:", ConsoleColor.Cyan); // example taken from: // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb @@ -221,11 +317,13 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) // or to test array functions, use this instead: // ChatMessage.FromUser("The combination is: One. Two. Three. Four. Five."), }, - Functions = new List {fn1, fn2, fn3, fn4}, + ToolFunctions = new List {fn1, fn2, fn3, fn4}, // optionally, to force a specific function: - // FunctionCall = new Dictionary { { "name", "get_current_weather" } }, + // ToolChoice = new ToolChoiceFunction() { Function = new FunctionTool() { Name = "get_current_weather" }}, + // or auto tool choice: + ToolChoice = StaticValues.CompletionStatics.ToolChoice.Auto, MaxTokens = 50, - Model = Models.Gpt_3_5_Turbo_0613 + Model = Models.Gpt_4_1106_preview }); /* when testing weather forecasts, expected output should be along the lines of: @@ -243,7 +341,7 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) Function call: identify_number_sequence values: [1, 2, 3, 4, 5] */ - + var functionArguments = new Dictionary(); await foreach (var completionResult in completionResults) { if (completionResult.Successful) @@ -251,13 +349,49 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) var choice = completionResult.Choices.First(); Console.WriteLine($"Message: {choice.Message.Content}"); - var fn = choice.Message.FunctionCall; - if (fn != null) + var tools = choice.Message.ToolCalls; + if (tools != null) { - Console.WriteLine($"Function call: {fn.Name}"); - foreach (var entry in fn.ParseArguments()) + Console.WriteLine($"Tools: {tools.Count}"); + for (int i = 0; i < tools.Count; i++) { - Console.WriteLine($" {entry.Key}: {entry.Value}"); + var toolCall = tools[i]; + Console.WriteLine($" {toolCall.Id}: {toolCall.Function}"); + + var fn = toolCall.Function; + if (fn != null) + { + if (!string.IsNullOrEmpty(fn.Name)) + { + Console.WriteLine($" Function call: {fn.Name}"); + } + + if (!string.IsNullOrEmpty(fn.Arguments)) + { + if (functionArguments.TryGetValue(i, out var currentArguments)) + { + currentArguments += fn.Arguments; + } + else + { + currentArguments = fn.Arguments; + } + functionArguments[i] = currentArguments; + fn.Arguments = currentArguments; + + try + { + foreach (var entry in fn.ParseArguments()) + { + Console.WriteLine($" {entry.Key}: {entry.Value}"); + } + } + catch (Exception) + { + // ignore + } + } + } } } } diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index ba1424d1..840950b1 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -25,12 +25,12 @@ public enum ResponseFormats /// /// A list of functions the model may generate JSON inputs for. /// - [JsonIgnore] + [JsonIgnore, Obsolete("use ToolFunctions instead")] public IList? Functions { get; set; } - [JsonIgnore] public object? FunctionsAsObject { get; set; } + [JsonIgnore, Obsolete("use ToolFunctions instead")] public object? FunctionsAsObject { get; set; } - [JsonPropertyName("functions")] + [JsonPropertyName("functions"), Obsolete("use ToolFunctions instead")] public object? FunctionCalculated { get @@ -138,6 +138,47 @@ public IList? StopCalculated [JsonPropertyName("logit_bias")] public object? LogitBias { get; set; } + /// + /// A list of functions the model may generate JSON inputs for. + /// + [JsonIgnore] + public IList? ToolFunctions { get; set; } + + + [JsonIgnore] public object? ToolsAsObject { get; set; } + + /// + /// A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list + /// of functions the model may generate JSON inputs for. + /// + [JsonPropertyName("tools")] public object? ToolsCalculated + { + get + { + if (ToolsAsObject != null && ToolFunctions != null) + { + throw new ValidationException("ToolsAsObject and ToolFunctions can not be assigned at the same time. One of them is should be null."); + } + + if (ToolFunctions != null) + { + return ToolFunctions.Select(f => new ToolDefinition { Function = f }).ToList(); + } + + return ToolsAsObject; + } + } + + /// + /// Controls which (if any) function is called by the model. none means the model will not call a function and instead + /// generates a message. auto means the model can pick between generating a message or calling a function. Specifying + /// a particular function via {"type: "function", "function": {"name": "my_function"}} forces the model to call that + /// function. + /// + /// none is the default when no functions are present. auto is the default if functions are present. + /// + [JsonPropertyName("tool_choice")] public object? ToolChoice { get; set; } + /// /// String or object. Controls how the model responds to function calls. @@ -149,7 +190,7 @@ public IList? StopCalculated /// FunctionCall = new Dictionary<string, string> { { "name", "my_function" } } /// ). /// - [JsonPropertyName("function_call")] + [JsonPropertyName("function_call"), Obsolete("use ToolChoice instead")] public object? FunctionCall { get; set; } /// diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs index 037f3ac1..3578007e 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs @@ -18,12 +18,14 @@ public class ChatMessage /// length of 64 characters. /// /// The name and arguments of a function that should be called, as generated by the model. - public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null) + /// The tool calls generated by the model. + public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null, IList? toolCalls = null) { Role = role; Content = content; Name = name; FunctionCall = functionCall; + ToolCalls = toolCalls; } /// @@ -46,11 +48,17 @@ public ChatMessage(string role, string content, string? name = null, FunctionCal public string? Name { get; set; } /// - /// The name and arguments of a function that should be called, as generated by the model. + /// Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model. /// [JsonPropertyName("function_call")] public FunctionCall? FunctionCall { get; set; } + /// + /// The tool calls generated by the model, such as function calls. + /// + [JsonPropertyName("tool_calls")] + public IList? ToolCalls { get; set; } + public static ChatMessage FromAssistant(string content, string? name = null, FunctionCall? functionCall = null) { return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, functionCall); diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs new file mode 100644 index 00000000..e6b5bccb --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.ObjectModels.RequestModels; + +public class ToolCall +{ + /// + /// The ID of the tool call. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The type of the tool. Currently, only function is supported. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// The function that the model called. + /// + [JsonPropertyName("function")] + public FunctionCall? Function { get; set; } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs new file mode 100644 index 00000000..debb81f3 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.ObjectModels.RequestModels; + +public class ToolChoiceFunction +{ + [JsonPropertyName("type")] + public string? Type { get; set; } = StaticValues.CompletionStatics.ToolType.Function; + + [JsonPropertyName("function")] + public FunctionTool? Function { get; set; } +} + +public class FunctionTool +{ + [JsonPropertyName("name")] + public string? Name { get; set; } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs new file mode 100644 index 00000000..f70982da --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.ObjectModels.RequestModels; + +/// +/// Definition of a valid tool. +/// +public class ToolDefinition +{ + /// + /// Required. The type of the tool. Currently, only function is supported. + /// + [JsonPropertyName("type")] + public string Type { get; set; } = StaticValues.CompletionStatics.ToolType.Function; + + /// + /// Required. The description of what the function does. + /// + [JsonPropertyName("function")] + public FunctionDefinition? Function { get; set; } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs index b67d07ba..fa8b81b9 100644 --- a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs +++ b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs @@ -9,6 +9,17 @@ public static class ResponseFormat public static string Json => "json_object"; public static string Text => "text"; } + + public static class ToolChoice + { + public static string Auto => "auto"; + public static string None => "none"; + } + + public static class ToolType + { + public static string Function => "function"; + } } public static class ImageStatics { From 50c24a14cb06b14a580f227e5f016b5417f8a824 Mon Sep 17 00:00:00 2001 From: Shane Powell Date: Wed, 22 Nov 2023 08:33:39 +1300 Subject: [PATCH 03/13] Add support for tool message role type into ChatMessage. --- .../ObjectModels/RequestModels/ChatMessage.cs | 25 +++++++++++++++++-- OpenAI.SDK/ObjectModels/StaticValueHelper.cs | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs index 3578007e..753dc8ea 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs @@ -18,14 +18,16 @@ public class ChatMessage /// length of 64 characters. /// /// The name and arguments of a function that should be called, as generated by the model. + /// The tool function call id generated by the model /// The tool calls generated by the model. - public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null, IList? toolCalls = null) + public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null, string? toolCallId = null, IList? toolCalls = null) { Role = role; Content = content; Name = name; FunctionCall = functionCall; ToolCalls = toolCalls; + ToolCallId = toolCallId; } /// @@ -47,6 +49,13 @@ public ChatMessage(string role, string content, string? name = null, FunctionCal [JsonPropertyName("name")] public string? Name { get; set; } + /// + /// Required for tool role messages. + /// Tool call that this message is responding to. + /// + [JsonPropertyName("tool_call_id")] + public string? ToolCallId { get; set; } + /// /// Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model. /// @@ -59,16 +68,28 @@ public ChatMessage(string role, string content, string? name = null, FunctionCal [JsonPropertyName("tool_calls")] public IList? ToolCalls { get; set; } - public static ChatMessage FromAssistant(string content, string? name = null, FunctionCall? functionCall = null) + [Obsolete("Use FromAssistant for tools instead")] + public static ChatMessage FromAssistant(string content, string? name, FunctionCall? functionCall) { return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, functionCall); } + public static ChatMessage FromAssistant(string content, string? name = null, IList? toolCalls = null) + { + return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, null, null, toolCalls); + } + + [Obsolete("Use FromToolFunction instead")] public static ChatMessage FromFunction(string content, string? name = null) { return new ChatMessage(StaticValues.ChatMessageRoles.Function, content, name); } + public static ChatMessage FromToolFunction(string content, string toolCallId) + { + return new ChatMessage(StaticValues.ChatMessageRoles.Tool, content, null, null, toolCallId); + } + public static ChatMessage FromUser(string content, string? name = null) { return new ChatMessage(StaticValues.ChatMessageRoles.User, content, name); diff --git a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs index fa8b81b9..f2ec9dce 100644 --- a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs +++ b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs @@ -87,6 +87,7 @@ public static class ChatMessageRoles public static string System => "system"; public static string User => "user"; public static string Assistant => "assistant"; - public static string Function => "function"; + public static string Function => "function"; // Deprecated + public static string Tool => "tool"; } } \ No newline at end of file From c7b1d66a97bb22e19106dcca19c1b4d0fc7a3c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?DESKTOP-U68EMS0=5CSzalontai=20B=C3=A9la?= Date: Fri, 24 Nov 2023 22:47:34 +0100 Subject: [PATCH 04/13] Extend Chat Completion API with Vision --- OpenAI.Playground/Program.cs | 6 +- .../TestHelpers/ChatCompletionTestHelper.cs | 8 +- .../TestHelpers/VisionTestHelper.cs | 185 ++++++++++++++++++ .../ObjectModels/RequestModels/ChatMessage.cs | 91 ++++++--- .../RequestModels/VisionContent.cs | 83 ++++++++ .../RequestModels/VisionImageUrl.cs | 37 ++++ OpenAI.SDK/ObjectModels/StaticValueHelper.cs | 16 ++ Readme.md | 66 +++++++ 8 files changed, 462 insertions(+), 30 deletions(-) create mode 100644 OpenAI.Playground/TestHelpers/VisionTestHelper.cs create mode 100644 OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs create mode 100644 OpenAI.SDK/ObjectModels/RequestModels/VisionImageUrl.cs diff --git a/OpenAI.Playground/Program.cs b/OpenAI.Playground/Program.cs index 88ac4824..76f98c4c 100644 --- a/OpenAI.Playground/Program.cs +++ b/OpenAI.Playground/Program.cs @@ -42,11 +42,15 @@ // | / \ / \ | \ /) | ( \ /o\ / ) | (\ / | / \ / \ | // |-----------------------------------------------------------------------| - await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); //await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); //await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); +//await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); //await FineTuningJobTestHelper.RunCaseStudyIsTheModelMakingUntrueStatements(sdk); +// Vision +//await VisionTestHelper.RunSimpleVisionTest(sdk); +//await VisionTestHelper.RunSimpleVisionStreamTest(sdk); +//await VisionTestHelper.RunSimpleVisionTestUsingBase64EncodedImage(sdk); // Whisper //await AudioTestHelper.RunSimpleAudioCreateTranscriptionTest(sdk); //await AudioTestHelper.RunSimpleAudioCreateTranslationTest(sdk); diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index 085d3a65..e3d5514e 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -59,10 +59,10 @@ public static async Task RunSimpleCompletionStreamTest(IOpenAIService sdk) { Messages = new List { - new(StaticValues.ChatMessageRoles.System, "You are a helpful assistant."), - new(StaticValues.ChatMessageRoles.User, "Who won the world series in 2020?"), - new(StaticValues.ChatMessageRoles.System, "The Los Angeles Dodgers won the World Series in 2020."), - new(StaticValues.ChatMessageRoles.User, "Tell me a story about The Los Angeles Dodgers") + new() { Role = StaticValues.ChatMessageRoles.System, Content = "You are a helpful assistant." }, + new() { Role = StaticValues.ChatMessageRoles.User, Content = "Who won the world series in 2020?" }, + new() { Role = StaticValues.ChatMessageRoles.System, Content = "The Los Angeles Dodgers won the World Series in 2020." }, + new() { Role = StaticValues.ChatMessageRoles.User, Content = "Tell me a story about The Los Angeles Dodgers" } }, MaxTokens = 150, Model = Models.Gpt_3_5_Turbo diff --git a/OpenAI.Playground/TestHelpers/VisionTestHelper.cs b/OpenAI.Playground/TestHelpers/VisionTestHelper.cs new file mode 100644 index 00000000..826bebad --- /dev/null +++ b/OpenAI.Playground/TestHelpers/VisionTestHelper.cs @@ -0,0 +1,185 @@ +using OpenAI.Interfaces; +using OpenAI.ObjectModels; +using OpenAI.ObjectModels.RequestModels; +using static OpenAI.ObjectModels.StaticValues; + +namespace OpenAI.Playground.TestHelpers; + +internal static class VisionTestHelper +{ + public static async Task RunSimpleVisionTest(IOpenAIService sdk) + { + ConsoleExtensions.WriteLine("VIsion Testing is starting:", ConsoleColor.Cyan); + + try + { + ConsoleExtensions.WriteLine("Vision Test:", ConsoleColor.DarkCyan); + + var completionResult = await sdk.ChatCompletion.CreateCompletion( + new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem("You are an image analyzer assistant."), + ChatMessage.FromVisionUser( + new List + { + VisionContent.TextContent("What is on the picture in details?"), + VisionContent.ImageUrlContent( + "https://www.digitaltrends.com/wp-content/uploads/2016/06/1024px-Bill_Cunningham_at_Fashion_Week_photographed_by_Jiyang_Chen.jpg?p=1", + ImageStatics.ImageDetailTypes.High + ) + } + ), + }, + MaxTokens = 300, + Model = Models.Gpt_4_vision_preview, + N = 1 + } + ); + + if (completionResult.Successful) + { + Console.WriteLine(completionResult.Choices.First().Message.Content); + } + else + { + if (completionResult.Error == null) + { + throw new Exception("Unknown Error"); + } + + Console.WriteLine( + $"{completionResult.Error.Code}: {completionResult.Error.Message}" + ); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public static async Task RunSimpleVisionStreamTest(IOpenAIService sdk) + { + ConsoleExtensions.WriteLine("Vision Stream Testing is starting:", ConsoleColor.Cyan); + try + { + ConsoleExtensions.WriteLine("Vision Stream Test:", ConsoleColor.DarkCyan); + + var completionResult = sdk.ChatCompletion.CreateCompletionAsStream( + new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem("You are an image analyzer assistant."), + ChatMessage.FromVisionUser( + new List + { + VisionContent.TextContent("What is on this picture?"), + VisionContent.ImageUrlContent( + "https://www.digitaltrends.com/wp-content/uploads/2016/06/1024px-Bill_Cunningham_at_Fashion_Week_photographed_by_Jiyang_Chen.jpg?p=1", + ImageStatics.ImageDetailTypes.Low + ) + } + ), + }, + MaxTokens = 300, + Model = Models.Gpt_4_vision_preview, + N = 1 + } + ); + + await foreach (var completion in completionResult) + { + if (completion.Successful) + { + Console.Write(completion.Choices.First().Message.Content); + } + else + { + if (completion.Error == null) + { + throw new Exception("Unknown Error"); + } + + Console.WriteLine( + $"{completion.Error.Code}: {completion.Error.Message}" + ); + } + } + + Console.WriteLine(""); + Console.WriteLine("Complete"); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public static async Task RunSimpleVisionTestUsingBase64EncodedImage(IOpenAIService sdk) + { + ConsoleExtensions.WriteLine("Vision Testing is starting:", ConsoleColor.Cyan); + + try + { + ConsoleExtensions.WriteLine( + "Vision with base64 encoded image Test:", + ConsoleColor.DarkCyan + ); + + const string originalFileName = "image_edit_original.png"; + var originalFile = await FileExtensions.ReadAllBytesAsync( + $"SampleData/{originalFileName}" + ); + + var completionResult = await sdk.ChatCompletion.CreateCompletion( + new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem("You are an image analyzer assistant."), + ChatMessage.FromVisionUser( + new List + { + VisionContent.TextContent("What is on the picture in details?"), + VisionContent.ImageBinaryContent( + originalFile, + ImageStatics.ImageFileTypes.Png, + ImageStatics.ImageDetailTypes.High + ) + } + ), + }, + MaxTokens = 300, + Model = Models.Gpt_4_vision_preview, + N = 1 + } + ); + + if (completionResult.Successful) + { + Console.WriteLine(completionResult.Choices.First().Message.Content); + } + else + { + if (completionResult.Error == null) + { + throw new Exception("Unknown Error"); + } + + Console.WriteLine( + $"{completionResult.Error.Code}: {completionResult.Error.Message}" + ); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } +} diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs index 037f3ac1..990f5575 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace OpenAI.ObjectModels.RequestModels; @@ -9,65 +10,105 @@ namespace OpenAI.ObjectModels.RequestModels; /// public class ChatMessage { - /// - /// - /// The role of the author of this message. One of system, user, or assistant. - /// The contents of the message. - /// - /// The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum - /// length of 64 characters. - /// - /// The name and arguments of a function that should be called, as generated by the model. - public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null) - { - Role = role; - Content = content; - Name = name; - FunctionCall = functionCall; - } - /// /// The role of the author of this message. One of system, user, or assistant. /// [JsonPropertyName("role")] public string Role { get; set; } + [JsonIgnore] + public string? Content { get; set; } + + [JsonIgnore] + public IList? VisionContent { get; set; } + /// /// The contents of the message. /// [JsonPropertyName("content")] - public string Content { get; set; } + public object ContentCalculated + { + get + { + if (Content is not null && VisionContent is not null) + { + throw new ValidationException( + "Content and VisionContent can not be assigned at the same time. One of them must be null." + ); + } + + if (Content is not null) + { + return Content; + } + + return VisionContent!; + } + set { Content = value?.ToString(); } + } /// /// The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 /// characters. /// [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } /// /// The name and arguments of a function that should be called, as generated by the model. /// [JsonPropertyName("function_call")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public FunctionCall? FunctionCall { get; set; } - public static ChatMessage FromAssistant(string content, string? name = null, FunctionCall? functionCall = null) + public static ChatMessage FromAssistant( + string content, + string? name = null, + FunctionCall? functionCall = null + ) { - return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, functionCall); + return new() + { + Role = StaticValues.ChatMessageRoles.Assistant, + Content = content, + Name = name, + FunctionCall = functionCall + }; } public static ChatMessage FromFunction(string content, string? name = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.Function, content, name); + return new() + { + Role = StaticValues.ChatMessageRoles.Function, + Content = content, + Name = name, + }; } public static ChatMessage FromUser(string content, string? name = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.User, content, name); + return new() + { + Role = StaticValues.ChatMessageRoles.User, + Content = content, + Name = name, + }; } public static ChatMessage FromSystem(string content, string? name = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.System, content, name); + return new() + { + Role = StaticValues.ChatMessageRoles.System, + Content = content, + Name = name, + }; + } + + public static ChatMessage FromVisionUser(IList content) + { + return new() { Role = StaticValues.ChatMessageRoles.User, VisionContent = content, }; } -} \ No newline at end of file +} diff --git a/OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs b/OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs new file mode 100644 index 00000000..aedf1a99 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs @@ -0,0 +1,83 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.ObjectModels.RequestModels; + +/// +/// The content of a vision message. +/// +public class VisionContent +{ + /// + /// The value of Type property must be one of "text", "image_url" + /// + /// note: Currently openAI doesn't support images in the first system message. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// If the value of Type property is "text" then Text property must contain the message content text + /// + [JsonPropertyName("text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Text { get; set; } + + /// + /// If the value of Type property is "image_url" then ImageUrl property must contain a valid image url object + /// + [JsonPropertyName("image_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public VisionImageUrl? ImageUrl { get; set; } + + /// + /// Static helper method to create VisionContent Text + /// The text content + /// + public static VisionContent TextContent(string text) + { + return new() { Type = "text", Text = text }; + } + + /// + /// Static helper method to create VisionContent with Url + /// OpenAI currently supports PNG, JPEG, WEBP, and non-animated GIF + /// The url of an image + /// The detail property + /// + public static VisionContent ImageUrlContent(string imageUrl, string? detail = "auto") + { + return new() + { + Type = "image_url", + ImageUrl = new() { Url = imageUrl, Detail = detail } + }; + } + + /// + /// Static helper method to create VisionContent from binary image + /// OpenAI currently supports PNG, JPEG, WEBP, and non-animated GIF + /// The image binary data as byte array + /// The type of image + /// The detail property + /// + public static VisionContent ImageBinaryContent( + byte[] binaryImage, + string imageType, + string? detail = "auto" + ) + { + return new() + { + Type = "image_url", + ImageUrl = new() + { + Url = string.Format( + "data:image/{0};base64,{{{1}}}", + imageType, + Convert.ToBase64String(binaryImage) + ), + Detail = detail + } + }; + } +} diff --git a/OpenAI.SDK/ObjectModels/RequestModels/VisionImageUrl.cs b/OpenAI.SDK/ObjectModels/RequestModels/VisionImageUrl.cs new file mode 100644 index 00000000..678b76d1 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RequestModels/VisionImageUrl.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.ObjectModels.RequestModels; + +/// +/// The image_url object of vision message content +/// +public class VisionImageUrl +{ + /// + /// The Url property + /// Images are made available to the model in two main ways: by passing a link to the image or by passing the base64 encoded image directly in the url property. + /// link example: "url" : "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + /// base64 encoded image example: "url" : "data:image/jpeg;base64,{base64_image}" + /// + /// Limitations: + /// OpenAI currently supports PNG (.png), JPEG (.jpeg and .jpg), WEBP (.webp), and non-animated GIF (.gif) image formats + /// Image upload size is limited to 20MB per image + /// Captcha submission is blocked + /// + /// + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// The optional Detail property controls low or high fidelity image understanding + /// It has three options, low, high, or auto, you have control over how the model processes the image and generates its textual understanding. + /// By default, the model will use the auto setting which will look at the image input size and decide if it should use the low or high setting. + /// + /// low will disable the “high res” model. The model will receive a low-res 512px x 512px version of the image. + /// high will enable “high res” mode, which first allows the model to see the low res image and then creates detailed crops of input images + /// as 512px squares based on the input image size. + /// + [JsonPropertyName("detail")] + public string? Detail { get; set; } + +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs index b67d07ba..42fd0589 100644 --- a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs +++ b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs @@ -40,6 +40,22 @@ public static class Style public static string Vivid => "vivid"; public static string Natural => "natural"; } + + public static class ImageFileTypes + { + public static string Jpeg => "JPEG"; + public static string Png => "PNG"; + public static string Webp => "WEBP"; + public static string Gif => "GIF"; + } + + public static class ImageDetailTypes + { + public static string High => "high"; + public static string Low => "low"; + public static string Auto => "auto"; + + } } public static class AudioStatics diff --git a/Readme.md b/Readme.md index 66f5e82f..5313873d 100644 --- a/Readme.md +++ b/Readme.md @@ -214,6 +214,72 @@ if (imageResult.Successful) } ``` +## VISION Sample +```csharp +var completionResult = await sdk.ChatCompletion.CreateCompletion( + new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem("You are an image analyzer assistant."), + ChatMessage.FromVisionUser( + new List + { + VisionContent.TextContent("What is on the picture in details?"), + VisionContent.ImageUrlContent( + "https://www.digitaltrends.com/wp-content/uploads/2016/06/1024px-Bill_Cunningham_at_Fashion_Week_photographed_by_Jiyang_Chen.jpg?p=1", + ImageStatics.ImageDetailTypes.High + ) + } + ), + }, + MaxTokens = 300, + Model = Models.Gpt_4_vision_preview, + N = 1 + } +); + +if (completionResult.Successful) +{ + Console.WriteLine(completionResult.Choices.First().Message.Content); +} +``` + +## VISION Sample using Base64 encoded image +```csharp +const string fileName = "image.png"; +var binaryImage = await FileExtensions.ReadAllBytesAsync(fileName); + +var completionResult = await sdk.ChatCompletion.CreateCompletion( + new ChatCompletionCreateRequest + { + Messages = new List + { + ChatMessage.FromSystem("You are an image analyzer assistant."), + ChatMessage.FromVisionUser( + new List + { + VisionContent.TextContent("What is on the picture in details?"), + VisionContent.ImageBinaryContent( + binaryImage, + ImageStatics.ImageFileTypes.Png, + ImageStatics.ImageDetailTypes.High + ) + } + ), + }, + MaxTokens = 300, + Model = Models.Gpt_4_vision_preview, + N = 1 + } +); + +if (completionResult.Successful) +{ + Console.WriteLine(completionResult.Choices.First().Message.Content); +} +``` + ## Notes: #### This library used to be known as `Betalgo.OpenAI.GPT3`, now it has a new package Id `Betalgo.OpenAI`. From e83746e2d6bc73804bb8488d8cfac61c5a00d816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?DESKTOP-U68EMS0=5CSzalontai=20B=C3=A9la?= Date: Sat, 25 Nov 2023 22:36:25 +0100 Subject: [PATCH 05/13] fix missing response property --- .../ResponseModels/ImageResponseModel/ImageCreateResponse.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs index e3a4db2a..d1f436a8 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs @@ -13,5 +13,6 @@ public record ImageDataResult { [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("b64_json")] public string B64 { get; set; } + [JsonPropertyName("revised_prompt")] public string RevisedPrompt { get; set; } } } \ No newline at end of file From 3468323740161f88d52c14eaeb56b63db1f6e30f Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Sun, 3 Dec 2023 10:44:42 +0000 Subject: [PATCH 06/13] Added StaticValues for ImageStatics.Quality --- OpenAI.SDK/ObjectModels/RequestModels/ImageCreateRequest.cs | 1 + .../SharedModels/SharedImageRequestBaseModel.cs | 1 + OpenAI.SDK/ObjectModels/StaticValueHelper.cs | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ImageCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ImageCreateRequest.cs index 6eaabf7a..2fd7d6a0 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ImageCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ImageCreateRequest.cs @@ -27,6 +27,7 @@ public ImageCreateRequest(string prompt) /// The quality of the image that will be generated. Possible values are 'standard' or 'hd' (default is 'standard'). /// Hd creates images with finer details and greater consistency across the image. /// This param is only supported for dall-e-3 model. + ///

Check for possible values ///
[JsonPropertyName("quality")] public string? Quality { get; set; } diff --git a/OpenAI.SDK/ObjectModels/SharedModels/SharedImageRequestBaseModel.cs b/OpenAI.SDK/ObjectModels/SharedModels/SharedImageRequestBaseModel.cs index 3ba145ce..8e74b512 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/SharedImageRequestBaseModel.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/SharedImageRequestBaseModel.cs @@ -15,6 +15,7 @@ public record SharedImageRequestBaseModel /// 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. + ///

Check for possible values ///
[JsonPropertyName("size")] public string? Size { get; set; } diff --git a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs index b67d07ba..ae6d99ee 100644 --- a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs +++ b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs @@ -40,6 +40,12 @@ public static class Style public static string Vivid => "vivid"; public static string Natural => "natural"; } + + public static class Quality + { + public static string Standard => "standard"; + public static string Hd => "hd"; + } } public static class AudioStatics From f5426cf200ba17bc70cd6ef9ea6362e53cd25683 Mon Sep 17 00:00:00 2001 From: Shane Powell Date: Mon, 4 Dec 2023 06:43:10 +1300 Subject: [PATCH 07/13] Make more compatible with the Assistant API changes. --- .../TestHelpers/ChatCompletionTestHelper.cs | 12 ++++++------ .../ChatCompletionCreateRequest.cs | 19 +++++++------------ .../ObjectModels/RequestModels/ChatMessage.cs | 6 +++--- .../ObjectModels/RequestModels/ToolCall.cs | 2 +- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index 320b4d0c..3b6ad45e 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -214,7 +214,7 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."), ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.") }, - ToolFunctions = new List {fn1, fn2, fn3, fn4}, + Tools = new List { new() { Function = fn1 }, new() { Function = fn2 }, new() { Function = fn3 }, new() { Function = fn4 }, new() { Function = fn4 } }, // optionally, to force a specific function: // ToolChoice = new ToolChoiceFunction() { Function = new FunctionTool() { Name = "get_current_weather" }}, // or auto tool choice: @@ -244,9 +244,9 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) Console.WriteLine($"Tools: {tools.Count}"); foreach (var toolCall in tools) { - Console.WriteLine($" {toolCall.Id}: {toolCall.Function}"); + Console.WriteLine($" {toolCall.Id}: {toolCall.FunctionCall}"); - var fn = toolCall.Function; + var fn = toolCall.FunctionCall; if (fn != null) { Console.WriteLine($" Function call: {fn.Name}"); @@ -317,7 +317,7 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) // or to test array functions, use this instead: // ChatMessage.FromUser("The combination is: One. Two. Three. Four. Five."), }, - ToolFunctions = new List {fn1, fn2, fn3, fn4}, + Tools = new List { new() { Function = fn1 }, new() { Function = fn2 }, new() { Function = fn3 }, new() { Function = fn4 }, new() { Function = fn4 } }, // optionally, to force a specific function: // ToolChoice = new ToolChoiceFunction() { Function = new FunctionTool() { Name = "get_current_weather" }}, // or auto tool choice: @@ -356,9 +356,9 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) for (int i = 0; i < tools.Count; i++) { var toolCall = tools[i]; - Console.WriteLine($" {toolCall.Id}: {toolCall.Function}"); + Console.WriteLine($" {toolCall.Id}: {toolCall.FunctionCall}"); - var fn = toolCall.Function; + var fn = toolCall.FunctionCall; if (fn != null) { if (!string.IsNullOrEmpty(fn.Name)) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index 840950b1..c0e0e946 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -25,12 +25,12 @@ public enum ResponseFormats /// /// A list of functions the model may generate JSON inputs for. /// - [JsonIgnore, Obsolete("use ToolFunctions instead")] + [JsonIgnore, Obsolete("use Tools instead")] public IList? Functions { get; set; } - [JsonIgnore, Obsolete("use ToolFunctions instead")] public object? FunctionsAsObject { get; set; } + [JsonIgnore, Obsolete("use Tools instead")] public object? FunctionsAsObject { get; set; } - [JsonPropertyName("functions"), Obsolete("use ToolFunctions instead")] + [JsonPropertyName("functions"), Obsolete("use Tools instead")] public object? FunctionCalculated { get @@ -142,7 +142,7 @@ public IList? StopCalculated /// A list of functions the model may generate JSON inputs for. /// [JsonIgnore] - public IList? ToolFunctions { get; set; } + public IList? Tools { get; set; } [JsonIgnore] public object? ToolsAsObject { get; set; } @@ -155,17 +155,12 @@ public IList? StopCalculated { get { - if (ToolsAsObject != null && ToolFunctions != null) + if (ToolsAsObject != null && Tools != null) { - throw new ValidationException("ToolsAsObject and ToolFunctions can not be assigned at the same time. One of them is should be null."); + throw new ValidationException("ToolsAsObject and Tools can not be assigned at the same time. One of them is should be null."); } - if (ToolFunctions != null) - { - return ToolFunctions.Select(f => new ToolDefinition { Function = f }).ToList(); - } - - return ToolsAsObject; + return Tools ?? ToolsAsObject; } } diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs index 753dc8ea..6ff9bc38 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs @@ -20,7 +20,7 @@ public class ChatMessage /// The name and arguments of a function that should be called, as generated by the model. /// The tool function call id generated by the model /// The tool calls generated by the model. - public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null, string? toolCallId = null, IList? toolCalls = null) + public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null, IList? toolCalls = null, string? toolCallId = null) { Role = role; Content = content; @@ -76,7 +76,7 @@ public static ChatMessage FromAssistant(string content, string? name, FunctionCa public static ChatMessage FromAssistant(string content, string? name = null, IList? toolCalls = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, null, null, toolCalls); + return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, null, toolCalls); } [Obsolete("Use FromToolFunction instead")] @@ -87,7 +87,7 @@ public static ChatMessage FromFunction(string content, string? name = null) public static ChatMessage FromToolFunction(string content, string toolCallId) { - return new ChatMessage(StaticValues.ChatMessageRoles.Tool, content, null, null, toolCallId); + return new ChatMessage(StaticValues.ChatMessageRoles.Tool, content, null, null, null, toolCallId); } public static ChatMessage FromUser(string content, string? name = null) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs index e6b5bccb..96e9d3fb 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolCall.cs @@ -20,5 +20,5 @@ public class ToolCall /// The function that the model called. /// [JsonPropertyName("function")] - public FunctionCall? Function { get; set; } + public FunctionCall? FunctionCall { get; set; } } \ No newline at end of file From ded0e94005db1d577aaf76c85f72635fc56b8401 Mon Sep 17 00:00:00 2001 From: Shane Powell Date: Mon, 4 Dec 2023 06:45:06 +1300 Subject: [PATCH 08/13] minor name change. --- .../ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs index 83abf00c..096ce70d 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs @@ -14,5 +14,5 @@ public record ChatCompletionCreateResponse : BaseResponse, IOpenAiModels.IId, IO [JsonPropertyName("created")] public int CreatedAt { get; set; } [JsonPropertyName("id")] public string Id { get; set; } - [JsonPropertyName("system_fingerprint")] public string SystemFingerprint { get; set; } + [JsonPropertyName("system_fingerprint")] public string SystemFingerPrint { get; set; } } \ No newline at end of file From 293eec61ece5b9b76ecc7ffdfce3395dc6e8e162 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Mon, 4 Dec 2023 19:43:29 +0000 Subject: [PATCH 09/13] Removed obsolete support for legacy function call. Support has been added for passing as object to the function field in the tool. --- OpenAI.Playground/Program.cs | 1 - .../TestHelpers/ChatCompletionTestHelper.cs | 84 ------------------- .../ChatCompletionCreateRequest.cs | 36 -------- .../ObjectModels/RequestModels/ChatMessage.cs | 22 +---- .../RequestModels/ToolDefinition.cs | 26 +++++- OpenAI.SDK/ObjectModels/StaticValueHelper.cs | 1 - 6 files changed, 28 insertions(+), 142 deletions(-) diff --git a/OpenAI.Playground/Program.cs b/OpenAI.Playground/Program.cs index 547c767e..a05a3da4 100644 --- a/OpenAI.Playground/Program.cs +++ b/OpenAI.Playground/Program.cs @@ -45,7 +45,6 @@ //await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); //await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); -await ChatCompletionTestHelper.RunObsoleteChatFunctionCallTest(sdk); await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); //await FineTuningJobTestHelper.RunCaseStudyIsTheModelMakingUntrueStatements(sdk); diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index 3b6ad45e..20cf659d 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -95,90 +95,6 @@ public static async Task RunSimpleCompletionStreamTest(IOpenAIService sdk) } } - public static async Task RunObsoleteChatFunctionCallTest(IOpenAIService sdk) - { - ConsoleExtensions.WriteLine("Chat Obsolete Function Call Testing is starting:", ConsoleColor.Cyan); - - // example taken from: - // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb - - var fn1 = new FunctionDefinitionBuilder("get_current_weather", "Get the current weather") - .AddParameter("location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA")) - .AddParameter("format", PropertyDefinition.DefineEnum(new List {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location.")) - .Validate() - .Build(); - - var fn2 = new FunctionDefinitionBuilder("get_n_day_weather_forecast", "Get an N-day weather forecast") - .AddParameter("location", new PropertyDefinition {Type = "string", Description = "The city and state, e.g. San Francisco, CA"}) - .AddParameter("format", PropertyDefinition.DefineEnum(new List {"celsius", "fahrenheit"}, "The temperature unit to use. Infer this from the users location.")) - .AddParameter("num_days", PropertyDefinition.DefineInteger("The number of days to forecast")) - .Validate() - .Build(); - var fn3 = new FunctionDefinitionBuilder("get_current_datetime", "Get the current date and time, e.g. 'Saturday, June 24, 2023 6:14:14 PM'") - .Build(); - - var fn4 = new FunctionDefinitionBuilder("identify_number_sequence", "Get a sequence of numbers present in the user message") - .AddParameter("values", PropertyDefinition.DefineArray(PropertyDefinition.DefineNumber("Sequence of numbers specified by the user"))) - .Build(); - try - { - ConsoleExtensions.WriteLine("Chat Function Call Test:", ConsoleColor.DarkCyan); - var completionResult = await sdk.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest - { - Messages = new List - { - ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."), - ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.") - }, - Functions = new List {fn1, fn2, fn3, fn4}, - // optionally, to force a specific function: - // FunctionCall = new Dictionary { { "name", "get_current_weather" } }, - MaxTokens = 50, - Model = Models.Gpt_3_5_Turbo - }); - - /* expected output along the lines of: - - Message: - Function call: get_n_day_weather_forecast - location: Chicago, USA - format: celsius - num_days: 5 - */ - - - if (completionResult.Successful) - { - var choice = completionResult.Choices.First(); - Console.WriteLine($"Message: {choice.Message.Content}"); - - var fn = choice.Message.FunctionCall; - if (fn != null) - { - Console.WriteLine($"Function call: {fn.Name}"); - foreach (var entry in fn.ParseArguments()) - { - Console.WriteLine($" {entry.Key}: {entry.Value}"); - } - } - } - else - { - if (completionResult.Error == null) - { - throw new Exception("Unknown Error"); - } - - Console.WriteLine($"{completionResult.Error.Code}: {completionResult.Error.Message}"); - } - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - public static async Task RunChatFunctionCallTest(IOpenAIService sdk) { ConsoleExtensions.WriteLine("Chat Tool Functions Call Testing is starting:", ConsoleColor.Cyan); diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index c0e0e946..e525dfd1 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -22,28 +22,6 @@ public enum ResponseFormats [JsonPropertyName("messages")] public IList Messages { get; set; } - /// - /// A list of functions the model may generate JSON inputs for. - /// - [JsonIgnore, Obsolete("use Tools instead")] - public IList? Functions { get; set; } - - [JsonIgnore, Obsolete("use Tools instead")] public object? FunctionsAsObject { get; set; } - - [JsonPropertyName("functions"), Obsolete("use Tools instead")] - public object? FunctionCalculated - { - get - { - if (FunctionsAsObject != null && Functions != null) - { - throw new ValidationException("FunctionAsObject and Functions can not be assigned at the same time. One of them is should be null."); - } - - return Functions ?? FunctionsAsObject; - } - } - /// /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the /// tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are @@ -174,20 +152,6 @@ public IList? StopCalculated /// [JsonPropertyName("tool_choice")] public object? ToolChoice { get; set; } - - /// - /// String or object. Controls how the model responds to function calls. - /// "none" means the model does not call a function, and responds to the end-user. - /// "auto" means the model can pick between an end-user or calling a function. - /// "none" is the default when no functions are present. "auto" is the default if functions are present. - /// Specifying a particular function via {"name": "my_function"} forces the model to call that function. - /// (Note: in C# specify that as: - /// FunctionCall = new Dictionary<string, string> { { "name", "my_function" } } - /// ). - /// - [JsonPropertyName("function_call"), Obsolete("use ToolChoice instead")] - public object? FunctionCall { get; set; } - /// /// The format that the model must output. Used to enable JSON mode. /// Must be one of "text" or "json_object".
diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs index 6ff9bc38..07a8203a 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs @@ -17,15 +17,13 @@ public class ChatMessage /// The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum /// length of 64 characters. /// - /// The name and arguments of a function that should be called, as generated by the model. /// The tool function call id generated by the model /// The tool calls generated by the model. - public ChatMessage(string role, string content, string? name = null, FunctionCall? functionCall = null, IList? toolCalls = null, string? toolCallId = null) + public ChatMessage(string role, string content, string? name = null, IList? toolCalls = null, string? toolCallId = null) { Role = role; Content = content; Name = name; - FunctionCall = functionCall; ToolCalls = toolCalls; ToolCallId = toolCallId; } @@ -68,26 +66,14 @@ public ChatMessage(string role, string content, string? name = null, FunctionCal [JsonPropertyName("tool_calls")] public IList? ToolCalls { get; set; } - [Obsolete("Use FromAssistant for tools instead")] - public static ChatMessage FromAssistant(string content, string? name, FunctionCall? functionCall) - { - return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, functionCall); - } - public static ChatMessage FromAssistant(string content, string? name = null, IList? toolCalls = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, null, toolCalls); - } - - [Obsolete("Use FromToolFunction instead")] - public static ChatMessage FromFunction(string content, string? name = null) - { - return new ChatMessage(StaticValues.ChatMessageRoles.Function, content, name); + return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, toolCalls); } - public static ChatMessage FromToolFunction(string content, string toolCallId) + public static ChatMessage FromTool(string content, string toolCallId) { - return new ChatMessage(StaticValues.ChatMessageRoles.Tool, content, null, null, null, toolCallId); + return new ChatMessage(StaticValues.ChatMessageRoles.Tool, content, toolCallId: toolCallId); } public static ChatMessage FromUser(string content, string? name = null) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs index f70982da..0fef6358 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace OpenAI.ObjectModels.RequestModels; @@ -13,9 +14,30 @@ public class ToolDefinition [JsonPropertyName("type")] public string Type { get; set; } = StaticValues.CompletionStatics.ToolType.Function; + + /// + /// A list of functions the model may generate JSON inputs for. + /// + [JsonIgnore] + public FunctionDefinition? Function { get; set; } + + [JsonIgnore] + public object? FunctionsAsObject { get; set; } + /// /// Required. The description of what the function does. /// [JsonPropertyName("function")] - public FunctionDefinition? Function { get; set; } + public object? FunctionCalculated + { + get + { + if (FunctionsAsObject != null && Function != null) + { + throw new ValidationException("FunctionAsObject and Function can not be assigned at the same time. One of them is should be null."); + } + + return Function ?? FunctionsAsObject; + } + } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs index 67e5fadc..5667fd16 100644 --- a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs +++ b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs @@ -93,7 +93,6 @@ public static class ChatMessageRoles public static string System => "system"; public static string User => "user"; public static string Assistant => "assistant"; - public static string Function => "function"; // Deprecated public static string Tool => "tool"; } } \ No newline at end of file From 9fa7609653be3c9514e4d3922ec81b9e0e7e6ec6 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Tue, 5 Dec 2023 18:24:08 +0000 Subject: [PATCH 10/13] ToolChoice field converted from object to class, removed default values, updated playground regarding changes. Updated some documentation. --- .../TestHelpers/ChatCompletionTestHelper.cs | 8 ++--- .../ChatCompletionCreateRequest.cs | 23 +++++++++++-- .../RequestModels/FunctionDefinition.cs | 12 +++---- .../RequestModels/ToolChoiceFunction.cs | 34 ++++++++++++++----- .../RequestModels/ToolDefinition.cs | 2 +- OpenAI.SDK/ObjectModels/StaticValueHelper.cs | 12 +++---- 6 files changed, 63 insertions(+), 28 deletions(-) diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index 20cf659d..6db2892e 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -132,9 +132,9 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) }, Tools = new List { new() { Function = fn1 }, new() { Function = fn2 }, new() { Function = fn3 }, new() { Function = fn4 }, new() { Function = fn4 } }, // optionally, to force a specific function: - // ToolChoice = new ToolChoiceFunction() { Function = new FunctionTool() { Name = "get_current_weather" }}, + //ToolChoice = ToolChoice.FunctionChoice("get_current_weather"), // or auto tool choice: - // ToolChoice = StaticValues.CompletionStatics.ToolChoice.Auto, + //ToolChoice = ToolChoice.Auto, MaxTokens = 50, Model = Models.Gpt_3_5_Turbo }); @@ -235,9 +235,9 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) }, Tools = new List { new() { Function = fn1 }, new() { Function = fn2 }, new() { Function = fn3 }, new() { Function = fn4 }, new() { Function = fn4 } }, // optionally, to force a specific function: - // ToolChoice = new ToolChoiceFunction() { Function = new FunctionTool() { Name = "get_current_weather" }}, + ToolChoice = ToolChoice.FunctionChoice("get_current_weather"), // or auto tool choice: - ToolChoice = StaticValues.CompletionStatics.ToolChoice.Auto, + // ToolChoice = ToolChoice.Auto, MaxTokens = 50, Model = Models.Gpt_4_1106_preview }); diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index e525dfd1..a11308d0 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -147,10 +147,29 @@ public IList? StopCalculated /// generates a message. auto means the model can pick between generating a message or calling a function. Specifying /// a particular function via {"type: "function", "function": {"name": "my_function"}} forces the model to call that /// function. - /// /// none is the default when no functions are present. auto is the default if functions are present. ///
- [JsonPropertyName("tool_choice")] public object? ToolChoice { get; set; } + [JsonIgnore] + public ToolChoice? ToolChoice { get; set; } + + [JsonPropertyName("tool_choice")] + public object? ToolChoiceCalculated + { + get + { + if (ToolChoice != null && ToolChoice.Type != StaticValues.CompletionStatics.ToolChoiceType.Function && ToolChoice.Function != null) + { + throw new ValidationException("You cannot choose another type besides \"function\" while ToolChoice.Function is not null."); + } + + if (ToolChoice?.Type == StaticValues.CompletionStatics.ToolChoiceType.Function) + { + return ToolChoice; + } + + return ToolChoice?.Type; + } + } /// /// The format that the model must output. Used to enable JSON mode. diff --git a/OpenAI.SDK/ObjectModels/RequestModels/FunctionDefinition.cs b/OpenAI.SDK/ObjectModels/RequestModels/FunctionDefinition.cs index 712345c6..e02b54da 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/FunctionDefinition.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/FunctionDefinition.cs @@ -9,24 +9,24 @@ namespace OpenAI.ObjectModels.RequestModels; public class FunctionDefinition { /// - /// Required. The name of the function to be called. Must be a-z, A-Z, 0-9, + /// The name of the function to be called. Must be a-z, A-Z, 0-9, /// or contain underscores and dashes, with a maximum length of 64. /// [JsonPropertyName("name")] public string Name { get; set; } /// - /// Optional. The description of what the function does. + /// A description of what the function does, used by the model to choose when and how to call the function. /// [JsonPropertyName("description")] public string? Description { get; set; } /// /// Optional. The parameters the functions accepts, described as a JSON Schema object. - /// See the guide (https://platform.openai.com/docs/guides/gpt/function-calling) for examples, - /// and the JSON Schema reference (https://json-schema.org/understanding-json-schema/) - /// for documentation about the format. + /// See the guide for examples, + /// and the JSON Schema reference for + /// documentation about the format. /// [JsonPropertyName("parameters")] - public PropertyDefinition? Parameters { get; set; } + public PropertyDefinition Parameters { get; set; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs index debb81f3..a262248b 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolChoiceFunction.cs @@ -2,17 +2,33 @@ namespace OpenAI.ObjectModels.RequestModels; -public class ToolChoiceFunction +public class ToolChoice { + public static ToolChoice None => new() { Type = StaticValues.CompletionStatics.ToolChoiceType.None }; + public static ToolChoice Auto => new() { Type = StaticValues.CompletionStatics.ToolChoiceType.Auto }; + public static ToolChoice FunctionChoice(string functionName) =>new() + { + Type = StaticValues.CompletionStatics.ToolChoiceType.Function, + Function = new FunctionTool() + { + Name = functionName + } + }; + + /// + /// "none" is the default when no functions are present.
+ /// "auto" is the default if functions are present.
+ /// "function" has to be assigned if user Function is not null
+ ///
+ /// Check for possible values. + ///
[JsonPropertyName("type")] - public string? Type { get; set; } = StaticValues.CompletionStatics.ToolType.Function; + public string Type { get; set; } - [JsonPropertyName("function")] - public FunctionTool? Function { get; set; } -} + [JsonPropertyName("function")] public FunctionTool? Function { get; set; } -public class FunctionTool -{ - [JsonPropertyName("name")] - public string? Name { get; set; } + public class FunctionTool + { + [JsonPropertyName("name")] public string Name { get; set; } + } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs index 0fef6358..d7fcb896 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs @@ -12,7 +12,7 @@ public class ToolDefinition /// Required. The type of the tool. Currently, only function is supported. ///
[JsonPropertyName("type")] - public string Type { get; set; } = StaticValues.CompletionStatics.ToolType.Function; + public string Type { get; set; } /// diff --git a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs index 5667fd16..4e3c1fae 100644 --- a/OpenAI.SDK/ObjectModels/StaticValueHelper.cs +++ b/OpenAI.SDK/ObjectModels/StaticValueHelper.cs @@ -10,16 +10,16 @@ public static class ResponseFormat public static string Text => "text"; } - public static class ToolChoice - { - public static string Auto => "auto"; - public static string None => "none"; - } - public static class ToolType { public static string Function => "function"; } + public static class ToolChoiceType + { + public static string Function => ToolType.Function; + public static string Auto => "auto"; + public static string None => "none"; + } } public static class ImageStatics { From 5e3904ec34a394e1873d4741b661ec0e49cb5dfc Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Tue, 5 Dec 2023 20:17:56 +0000 Subject: [PATCH 11/13] VisionContent has been renamed as MessageContent, and the rest of the code has been updated accordingly. --- .../TestHelpers/ChatCompletionTestHelper.cs | 8 +-- .../TestHelpers/VisionTestHelper.cs | 28 +++++----- .../ObjectModels/RequestModels/ChatMessage.cs | 56 ++++++++++++------- .../{VisionContent.cs => MessageContent.cs} | 18 +++--- .../SharedModels/ChatChoiceResponse.cs | 9 +++ Readme.md | 16 +++--- 6 files changed, 79 insertions(+), 56 deletions(-) rename OpenAI.SDK/ObjectModels/RequestModels/{VisionContent.cs => MessageContent.cs} (78%) diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index df6ee325..6db2892e 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -59,10 +59,10 @@ public static async Task RunSimpleCompletionStreamTest(IOpenAIService sdk) { Messages = new List { - new() { Role = StaticValues.ChatMessageRoles.System, Content = "You are a helpful assistant." }, - new() { Role = StaticValues.ChatMessageRoles.User, Content = "Who won the world series in 2020?" }, - new() { Role = StaticValues.ChatMessageRoles.System, Content = "The Los Angeles Dodgers won the World Series in 2020." }, - new() { Role = StaticValues.ChatMessageRoles.User, Content = "Tell me a story about The Los Angeles Dodgers" } + new(StaticValues.ChatMessageRoles.System, "You are a helpful assistant."), + new(StaticValues.ChatMessageRoles.User, "Who won the world series in 2020?"), + new(StaticValues.ChatMessageRoles.System, "The Los Angeles Dodgers won the World Series in 2020."), + new(StaticValues.ChatMessageRoles.User, "Tell me a story about The Los Angeles Dodgers") }, MaxTokens = 150, Model = Models.Gpt_3_5_Turbo diff --git a/OpenAI.Playground/TestHelpers/VisionTestHelper.cs b/OpenAI.Playground/TestHelpers/VisionTestHelper.cs index 826bebad..fc3e09db 100644 --- a/OpenAI.Playground/TestHelpers/VisionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/VisionTestHelper.cs @@ -21,12 +21,12 @@ public static async Task RunSimpleVisionTest(IOpenAIService sdk) Messages = new List { ChatMessage.FromSystem("You are an image analyzer assistant."), - ChatMessage.FromVisionUser( - new List + ChatMessage.FromUser( + new List { - VisionContent.TextContent("What is on the picture in details?"), - VisionContent.ImageUrlContent( - "https://www.digitaltrends.com/wp-content/uploads/2016/06/1024px-Bill_Cunningham_at_Fashion_Week_photographed_by_Jiyang_Chen.jpg?p=1", + MessageContent.TextContent("What is on the picture in details?"), + MessageContent.ImageUrlContent( + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageStatics.ImageDetailTypes.High ) } @@ -74,12 +74,12 @@ public static async Task RunSimpleVisionStreamTest(IOpenAIService sdk) Messages = new List { ChatMessage.FromSystem("You are an image analyzer assistant."), - ChatMessage.FromVisionUser( - new List + ChatMessage.FromUser( + new List { - VisionContent.TextContent("What is on this picture?"), - VisionContent.ImageUrlContent( - "https://www.digitaltrends.com/wp-content/uploads/2016/06/1024px-Bill_Cunningham_at_Fashion_Week_photographed_by_Jiyang_Chen.jpg?p=1", + MessageContent.TextContent("Whats in this image?"), + MessageContent.ImageUrlContent( + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", ImageStatics.ImageDetailTypes.Low ) } @@ -142,11 +142,11 @@ public static async Task RunSimpleVisionTestUsingBase64EncodedImage(IOpenAIServi Messages = new List { ChatMessage.FromSystem("You are an image analyzer assistant."), - ChatMessage.FromVisionUser( - new List + ChatMessage.FromUser( + new List { - VisionContent.TextContent("What is on the picture in details?"), - VisionContent.ImageBinaryContent( + MessageContent.TextContent("What is on the picture in details?"), + MessageContent.ImageBinaryContent( originalFile, ImageStatics.ImageFileTypes.Png, ImageStatics.ImageDetailTypes.High diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs index fcd20e9e..3e3e1a71 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatMessage.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; namespace OpenAI.ObjectModels.RequestModels; @@ -14,6 +13,7 @@ public class ChatMessage public ChatMessage() { } + /// /// /// The role of the author of this message. One of system, user, or assistant. @@ -33,17 +33,34 @@ public ChatMessage(string role, string content, string? name = null, IList + /// + /// The role of the author of this message. One of system, user, or assistant. + /// The list of the content messages. + /// + /// The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum + /// length of 64 characters. + /// + /// The tool function call id generated by the model + /// The tool calls generated by the model. + public ChatMessage(string role, IList contents, string? name = null, IList? toolCalls = null, string? toolCallId = null) + { + Role = role; + Contents = contents; + Name = name; + ToolCalls = toolCalls; + ToolCallId = toolCallId; + } + /// /// The role of the author of this message. One of system, user, or assistant. /// [JsonPropertyName("role")] public string Role { get; set; } - [JsonIgnore] - public string? Content { get; set; } + [JsonIgnore] public string? Content { get; set; } - [JsonIgnore] - public IList? VisionContent { get; set; } + [JsonIgnore] public IList? Contents { get; set; } /// /// The contents of the message. @@ -53,10 +70,10 @@ public object ContentCalculated { get { - if (Content is not null && VisionContent is not null) + if (Content is not null && Contents is not null) { throw new ValidationException( - "Content and VisionContent can not be assigned at the same time. One of them must be null." + "Content and Contents can not be assigned at the same time. One of them must be null." ); } @@ -65,9 +82,9 @@ public object ContentCalculated return Content; } - return VisionContent!; + return Contents!; } - set { Content = value?.ToString(); } + set => Content = value?.ToString(); } /// @@ -75,7 +92,6 @@ public object ContentCalculated /// characters. /// [JsonPropertyName("name")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } /// @@ -86,10 +102,10 @@ public object ContentCalculated public string? ToolCallId { get; set; } /// - /// Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by the model. + /// Deprecated and replaced by tool_calls. The name and arguments of a function that should be called, as generated by + /// the model. /// [JsonPropertyName("function_call")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public FunctionCall? FunctionCall { get; set; } /// @@ -100,26 +116,26 @@ public object ContentCalculated public static ChatMessage FromAssistant(string content, string? name = null, IList? toolCalls = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.Assistant, content, name, toolCalls); + return new(StaticValues.ChatMessageRoles.Assistant, content, name, toolCalls); } public static ChatMessage FromTool(string content, string toolCallId) { - return new ChatMessage(StaticValues.ChatMessageRoles.Tool, content, toolCallId: toolCallId); + return new(StaticValues.ChatMessageRoles.Tool, content, toolCallId: toolCallId); } public static ChatMessage FromUser(string content, string? name = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.User, content, name); + return new(StaticValues.ChatMessageRoles.User, content, name); } public static ChatMessage FromSystem(string content, string? name = null) { - return new ChatMessage(StaticValues.ChatMessageRoles.System, content, name); + return new(StaticValues.ChatMessageRoles.System, content, name); } - public static ChatMessage FromVisionUser(IList content) + public static ChatMessage FromUser(IList contents) { - return new() { Role = StaticValues.ChatMessageRoles.User, VisionContent = content, }; + return new(StaticValues.ChatMessageRoles.User, contents); } -} +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs b/OpenAI.SDK/ObjectModels/RequestModels/MessageContent.cs similarity index 78% rename from OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs rename to OpenAI.SDK/ObjectModels/RequestModels/MessageContent.cs index aedf1a99..02f0eafa 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/VisionContent.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/MessageContent.cs @@ -3,9 +3,9 @@ namespace OpenAI.ObjectModels.RequestModels; /// -/// The content of a vision message. +/// The content of a message. /// -public class VisionContent +public class MessageContent { /// /// The value of Type property must be one of "text", "image_url" @@ -19,32 +19,30 @@ public class VisionContent /// If the value of Type property is "text" then Text property must contain the message content text /// [JsonPropertyName("text")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Text { get; set; } /// /// If the value of Type property is "image_url" then ImageUrl property must contain a valid image url object /// [JsonPropertyName("image_url")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public VisionImageUrl? ImageUrl { get; set; } /// - /// Static helper method to create VisionContent Text + /// Static helper method to create MessageContent Text /// The text content /// - public static VisionContent TextContent(string text) + public static MessageContent TextContent(string text) { return new() { Type = "text", Text = text }; } /// - /// Static helper method to create VisionContent with Url + /// Static helper method to create MessageContent with Url /// OpenAI currently supports PNG, JPEG, WEBP, and non-animated GIF /// The url of an image /// The detail property /// - public static VisionContent ImageUrlContent(string imageUrl, string? detail = "auto") + public static MessageContent ImageUrlContent(string imageUrl, string? detail = null) { return new() { @@ -54,13 +52,13 @@ public static VisionContent ImageUrlContent(string imageUrl, string? detail = "a } /// - /// Static helper method to create VisionContent from binary image + /// Static helper method to create MessageContent from binary image /// OpenAI currently supports PNG, JPEG, WEBP, and non-animated GIF /// The image binary data as byte array /// The type of image /// The detail property /// - public static VisionContent ImageBinaryContent( + public static MessageContent ImageBinaryContent( byte[] binaryImage, string imageType, string? detail = "auto" diff --git a/OpenAI.SDK/ObjectModels/SharedModels/ChatChoiceResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/ChatChoiceResponse.cs index 441d51b5..630f850e 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/ChatChoiceResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/ChatChoiceResponse.cs @@ -17,4 +17,13 @@ public ChatMessage Delta [JsonPropertyName("index")] public int? Index { get; set; } [JsonPropertyName("finish_reason")] public string FinishReason { get; set; } + + [JsonPropertyName("finish_details")] public FinishDetailsResponse? FinishDetails { get; set; } + public class FinishDetailsResponse + { + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("stop")] + public string Stop { get; set; } + } } \ No newline at end of file diff --git a/Readme.md b/Readme.md index 5313873d..18513b0d 100644 --- a/Readme.md +++ b/Readme.md @@ -222,11 +222,11 @@ var completionResult = await sdk.ChatCompletion.CreateCompletion( Messages = new List { ChatMessage.FromSystem("You are an image analyzer assistant."), - ChatMessage.FromVisionUser( - new List + ChatMessage.FromUser( + new List { - VisionContent.TextContent("What is on the picture in details?"), - VisionContent.ImageUrlContent( + MessageContent.TextContent("What is on the picture in details?"), + MessageContent.ImageUrlContent( "https://www.digitaltrends.com/wp-content/uploads/2016/06/1024px-Bill_Cunningham_at_Fashion_Week_photographed_by_Jiyang_Chen.jpg?p=1", ImageStatics.ImageDetailTypes.High ) @@ -256,11 +256,11 @@ var completionResult = await sdk.ChatCompletion.CreateCompletion( Messages = new List { ChatMessage.FromSystem("You are an image analyzer assistant."), - ChatMessage.FromVisionUser( - new List + ChatMessage.FromUser( + new List { - VisionContent.TextContent("What is on the picture in details?"), - VisionContent.ImageBinaryContent( + MessageContent.TextContent("What is on the picture in details?"), + MessageContent.ImageBinaryContent( binaryImage, ImageStatics.ImageFileTypes.Png, ImageStatics.ImageDetailTypes.High From 4a50f230c0fd65edba0355baeff423bdb31aa02b Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Wed, 6 Dec 2023 21:23:58 +0000 Subject: [PATCH 12/13] added DefineFunction --- OpenAI.Playground/Program.cs | 4 ++-- OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs | 4 ++-- OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/OpenAI.Playground/Program.cs b/OpenAI.Playground/Program.cs index 76f98c4c..7a060322 100644 --- a/OpenAI.Playground/Program.cs +++ b/OpenAI.Playground/Program.cs @@ -44,8 +44,8 @@ await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); //await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); -//await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); -//await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); +await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); +await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); //await FineTuningJobTestHelper.RunCaseStudyIsTheModelMakingUntrueStatements(sdk); // Vision //await VisionTestHelper.RunSimpleVisionTest(sdk); diff --git a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs index 6db2892e..5f22657a 100644 --- a/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs +++ b/OpenAI.Playground/TestHelpers/ChatCompletionTestHelper.cs @@ -130,7 +130,7 @@ public static async Task RunChatFunctionCallTest(IOpenAIService sdk) ChatMessage.FromSystem("Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."), ChatMessage.FromUser("Give me a weather report for Chicago, USA, for the next 5 days.") }, - Tools = new List { new() { Function = fn1 }, new() { Function = fn2 }, new() { Function = fn3 }, new() { Function = fn4 }, new() { Function = fn4 } }, + Tools = new List { ToolDefinition.DefineFunction(fn1), ToolDefinition.DefineFunction(fn2) ,ToolDefinition.DefineFunction(fn3) ,ToolDefinition.DefineFunction(fn4) }, // optionally, to force a specific function: //ToolChoice = ToolChoice.FunctionChoice("get_current_weather"), // or auto tool choice: @@ -233,7 +233,7 @@ public static async Task RunChatFunctionCallTestAsStream(IOpenAIService sdk) // or to test array functions, use this instead: // ChatMessage.FromUser("The combination is: One. Two. Three. Four. Five."), }, - Tools = new List { new() { Function = fn1 }, new() { Function = fn2 }, new() { Function = fn3 }, new() { Function = fn4 }, new() { Function = fn4 } }, + Tools = new List { ToolDefinition.DefineFunction(fn1), ToolDefinition.DefineFunction(fn2), ToolDefinition.DefineFunction(fn3), ToolDefinition.DefineFunction(fn4) }, // optionally, to force a specific function: ToolChoice = ToolChoice.FunctionChoice("get_current_weather"), // or auto tool choice: diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs index d7fcb896..5a1cf9b8 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ToolDefinition.cs @@ -40,4 +40,10 @@ public object? FunctionCalculated return Function ?? FunctionsAsObject; } } + + public static ToolDefinition DefineFunction(FunctionDefinition function) => new() + { + Type = StaticValues.CompletionStatics.ToolType.Function, + Function = function + }; } \ No newline at end of file From 418a5bad954b1c017c1396f276ca77759038a4ef Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Wed, 6 Dec 2023 21:41:08 +0000 Subject: [PATCH 13/13] version bump for 7.4.2 --- OpenAI.Playground/Program.cs | 10 ++++++---- OpenAI.SDK/OpenAI.csproj | 2 +- Readme.md | 11 ++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/OpenAI.Playground/Program.cs b/OpenAI.Playground/Program.cs index 7a060322..dac9d7e5 100644 --- a/OpenAI.Playground/Program.cs +++ b/OpenAI.Playground/Program.cs @@ -43,14 +43,16 @@ // |-----------------------------------------------------------------------| await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); -//await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); -await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); -await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); -//await FineTuningJobTestHelper.RunCaseStudyIsTheModelMakingUntrueStatements(sdk); // Vision //await VisionTestHelper.RunSimpleVisionTest(sdk); //await VisionTestHelper.RunSimpleVisionStreamTest(sdk); //await VisionTestHelper.RunSimpleVisionTestUsingBase64EncodedImage(sdk); + +//await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); +//await ChatCompletionTestHelper.RunChatFunctionCallTest(sdk); +//await ChatCompletionTestHelper.RunChatFunctionCallTestAsStream(sdk); +//await FineTuningJobTestHelper.RunCaseStudyIsTheModelMakingUntrueStatements(sdk); + // Whisper //await AudioTestHelper.RunSimpleAudioCreateTranscriptionTest(sdk); //await AudioTestHelper.RunSimpleAudioCreateTranslationTest(sdk); diff --git a/OpenAI.SDK/OpenAI.csproj b/OpenAI.SDK/OpenAI.csproj index eb99be6f..88c679df 100644 --- a/OpenAI.SDK/OpenAI.csproj +++ b/OpenAI.SDK/OpenAI.csproj @@ -11,7 +11,7 @@ OpenAI-Betalgo.png true OpenAI SDK by Betalgo - 7.4.1 + 7.4.2 Tolga Kayhan, Betalgo Betalgo Up Ltd. OpenAI ChatGPT, Whisper, GPT-4 and DALL·E dotnet SDK diff --git a/Readme.md b/Readme.md index 18513b0d..9656859c 100644 --- a/Readme.md +++ b/Readme.md @@ -28,7 +28,9 @@ Maintenance of this project is made possible by all the bug reporters, [contribu [@AnukarOP](https://github.com/AnukarOP) [@Removable](https://github.com/Removable) ## Features -- [ ] Dev day Updates (Some updates are currently available, while others will be released soon. Please follow the changelogs for more information.) +- [x] Dev day Updates +- [x] Vision Api +- [X] Tools - [X] [Function Calling](https://github.com/betalgo/openai/wiki/Function-Calling) - [ ] Plugins (coming soon) - [x] [Chat GPT](https://github.com/betalgo/openai/wiki/Chat-GPT) @@ -292,6 +294,13 @@ I will always be using the latest libraries, and future releases will frequently I am incredibly busy. If I forgot your name, please accept my apologies and let me know so I can add it to the list. ## Changelog +### 7.4.2 +- Let's start with breaking changes: + - OpenAI has replaced function calling with tools. We have made the necessary changes to our code. This is not a major change; now you just have a wrapper around your function calling, which is named as "tool". The Playground provides an example. Please take a look to see how you can update your code. + This update was completed by @shanepowell. Many thanks to him. +- Now we support the Vision API, which involves passing message contents to the existing chat method. It is quite easy to use, but documentation was not available in the OpenAI API documentation. +This feature was completed by @belaszalontai. Many thanks to them. + ### 7.4.1 - Added support for "Create Speech" thanks to @belaszalontai / @szabe74 ### 7.4.0