From 2b8d4d7a1cc9f7295f5fc8111d95aa2018fda8cd Mon Sep 17 00:00:00 2001 From: AndreaPic Date: Sat, 22 Jul 2023 23:42:59 +0200 Subject: [PATCH 1/6] wip functions --- DevExtremeAI/OpenAIDTO/ChatDTO.cs | 82 ++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/DevExtremeAI/OpenAIDTO/ChatDTO.cs b/DevExtremeAI/OpenAIDTO/ChatDTO.cs index f03760a..cc5bbe8 100644 --- a/DevExtremeAI/OpenAIDTO/ChatDTO.cs +++ b/DevExtremeAI/OpenAIDTO/ChatDTO.cs @@ -33,6 +33,52 @@ public void AddMessage(ChatCompletionRequestMessage message) Messages.Add(message); } + private List? functions; + + /// + /// A list of functions the model may generate JSON inputs for. + /// + [JsonPropertyName("functions")] + public List Functions + { + get + { + if (functions != null && functions.Count() == 0) + { + return null; + } + return functions; + } + set + { + functions = value; + } + } + + public void AddFunction(ChatCompletionfunction function) + { + if (function != null) + { + if (functions == null) + { + functions = new List(); + } + functions.Add(function); + } + } + + /// + /// 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. + /// Specifying a particular function via {"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("function_call")] + public string? FunctionCall { get; set; } + /// /// What sampling temperature to use, between 0 and 2. /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or `top_p` but not both. @@ -146,6 +192,38 @@ public class ChatCompletionRequestMessage [JsonPropertyName("name")] public string? Name { get; set; } + /// + /// The name and arguments of a function that should be called, as generated by the model. + /// + [JsonPropertyName("function_call")] + public string? FunctionCall { get; set; } + + } + + + public class ChatCompletionfunction + { + /// + /// 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; } + + /// + /// 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; } + + /// + /// 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. + /// To describe a function that accepts no parameters, provide the value {"type": "object", "properties": { } }. + /// + [JsonPropertyName("parameters")] + public string JSONSchemaParameters { get; set; } + } [JsonConverter(typeof(JsonStringEnumConverterEx))] @@ -156,7 +234,9 @@ public enum ChatCompletionMessageRoleEnum [EnumMember(Value = "user")] User = 1, [EnumMember(Value = "assistant")] - Assistant = 2 + Assistant = 2, + [EnumMember(Value = "function")] + Function = 3 } From 190869ff510cebe0b144af1ce11e77c493611a85 Mon Sep 17 00:00:00 2001 From: AndreaPic Date: Sun, 23 Jul 2023 18:35:19 +0200 Subject: [PATCH 2/6] wip test functions --- DevExtremeAI/OpenAIDTO/ChatDTO.cs | 56 +++++++------ DevExtremeAILibTest/AIChatCompletionTests.cs | 82 +++++++++++++++++++ .../Resources/FunctionsDefinition.txt | 44 ++++++++++ .../Resources/Resource.Designer.cs | 23 ++++++ DevExtremeAILibTest/Resources/Resource.resx | 3 + 5 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 DevExtremeAILibTest/Resources/FunctionsDefinition.txt diff --git a/DevExtremeAI/OpenAIDTO/ChatDTO.cs b/DevExtremeAI/OpenAIDTO/ChatDTO.cs index cc5bbe8..124dd9f 100644 --- a/DevExtremeAI/OpenAIDTO/ChatDTO.cs +++ b/DevExtremeAI/OpenAIDTO/ChatDTO.cs @@ -9,6 +9,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using DevExtremeAI.Utils; +using System.Text.Json.Nodes; namespace DevExtremeAI.OpenAIDTO { @@ -39,33 +40,34 @@ public void AddMessage(ChatCompletionRequestMessage message) /// A list of functions the model may generate JSON inputs for. /// [JsonPropertyName("functions")] - public List Functions - { - get - { - if (functions != null && functions.Count() == 0) - { - return null; - } - return functions; - } - set - { - functions = value; - } - } - - public void AddFunction(ChatCompletionfunction function) - { - if (function != null) - { - if (functions == null) - { - functions = new List(); - } - functions.Add(function); - } - } + public JsonNode Functions { get; set; } + //public List Functions + //{ + // get + // { + // if (functions != null && functions.Count() == 0) + // { + // return null; + // } + // return functions; + // } + // set + // { + // functions = value; + // } + //} + + //public void AddFunction(ChatCompletionfunction function) + //{ + // if (function != null) + // { + // if (functions == null) + // { + // functions = new List(); + // } + // functions.Add(function); + // } + //} /// /// Controls how the model responds to function calls. diff --git a/DevExtremeAILibTest/AIChatCompletionTests.cs b/DevExtremeAILibTest/AIChatCompletionTests.cs index d462b6e..564e5b4 100644 --- a/DevExtremeAILibTest/AIChatCompletionTests.cs +++ b/DevExtremeAILibTest/AIChatCompletionTests.cs @@ -181,5 +181,87 @@ public async Task CreateChatCompletionITATest(string modelID) } } + [Theory] + [InlineData("gpt-3.5-turbo-0613")] + public async Task CreateChatCompletionFunctionTest(string modelID) + { + + using (var scope = _factory.Services.CreateScope()) + { + var openAiapiClient = scope.ServiceProvider.GetService(); + CreateChatCompletionRequest createCompletionRequest = new CreateChatCompletionRequest(); + createCompletionRequest.Model = modelID; + createCompletionRequest.Temperature = 1.4; + var function = new ChatCompletionfunction(); + function.Name = "get_current_weather"; + function.Description = "Get the current weather in a given location"; + function.JSONSchemaParameters = "{\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"location\": {\r\n \"type\": \"string\",\r\n \"description\": \"The city and state, e.g. San Francisco, CA\"\r\n },\r\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\r\n },\r\n \"required\": [\"location\"]\r\n}"; + //createCompletionRequest.Functions = "[{\"name\": \"get_current_weather\",\"description\": \"Get the current weather\",\"parameters\": {\"type\": \"object\",\"properties\": {\"location\": {\"type\": \"string\",\"description\": \"The city and state, e.g. San Francisco , CA\"},\"format\": {\"type\": \"string\",\"enum\": [\"celsius\", \"fahrenheit\"],\"description\": \"The temperature unit to use. Infer this from the users location.\"}},\"required\": [\"location\", \"format\"]}}]"; + createCompletionRequest.Functions = Resources.Resource.FunctionsDefinition; + + //createCompletionRequest.AddFunction(function); + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = ChatCompletionMessageRoleEnum.User, + Content = "Come sarà il tempo a Venezia, Italia oggi?" + }); + + var response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + Assert.False(response.HasError, response?.ErrorResponse?.Error?.Message); + Assert.NotNull(response?.OpenAIResponse); + Assert.NotNull(response?.OpenAIResponse?.Choices); + Assert.True(response.OpenAIResponse.Choices.Count > 0); + Assert.NotNull(response?.OpenAIResponse?.Usage); + + Debug.WriteLine(response.OpenAIResponse.Choices[0].Message.Content); + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = response.OpenAIResponse.Choices[0].Message.Role, + Content = response.OpenAIResponse.Choices[0].Message.Content + }); + + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = ChatCompletionMessageRoleEnum.User, + Content = "Qual'è la capitale d'Italia?" + }); + + await Task.Delay(22000); + response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + Assert.NotNull(response?.OpenAIResponse); + Assert.NotNull(response?.OpenAIResponse?.Choices); + Assert.True(response.OpenAIResponse.Choices.Count > 0); + Assert.NotNull(response?.OpenAIResponse?.Usage); + + Debug.WriteLine(response.OpenAIResponse.Choices[0].Message.Content); + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = response.OpenAIResponse.Choices[0].Message.Role, + Content = response.OpenAIResponse.Choices[0].Message.Content + }); + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = ChatCompletionMessageRoleEnum.User, + Content = "Quali cose potrei visitare li?" + }); + + await Task.Delay(22000); + response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + Assert.NotNull(response?.OpenAIResponse); + Assert.NotNull(response?.OpenAIResponse?.Choices); + Assert.True(response.OpenAIResponse.Choices.Count > 0); + Assert.NotNull(response?.OpenAIResponse?.Usage); + Debug.WriteLine(response.OpenAIResponse.Choices[0].Message.Content); + + + } + } + + } } \ No newline at end of file diff --git a/DevExtremeAILibTest/Resources/FunctionsDefinition.txt b/DevExtremeAILibTest/Resources/FunctionsDefinition.txt new file mode 100644 index 0000000..38d3f44 --- /dev/null +++ b/DevExtremeAILibTest/Resources/FunctionsDefinition.txt @@ -0,0 +1,44 @@ +[ + { + "name": "get_current_weather", + "description": "Get the current weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location.", + }, + }, + "required": ["location", "format"], + }, + }, + { + "name": "get_n_day_weather_forecast", + "description": "Get an N-day weather forecast", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location.", + }, + "num_days": { + "type": "integer", + "description": "The number of days to forecast", + } + }, + "required": ["location", "format", "num_days"] + }, + }, +] \ No newline at end of file diff --git a/DevExtremeAILibTest/Resources/Resource.Designer.cs b/DevExtremeAILibTest/Resources/Resource.Designer.cs index a9ff3cd..ddf9c6c 100644 --- a/DevExtremeAILibTest/Resources/Resource.Designer.cs +++ b/DevExtremeAILibTest/Resources/Resource.Designer.cs @@ -70,6 +70,29 @@ internal class Resource { } } + /// + /// Looks up a localized string similar to [ + /// { + /// "name": "get_current_weather", + /// "description": "Get the current weather", + /// "parameters": { + /// "type": "object", + /// "properties": { + /// "location": { + /// "type": "string", + /// "description": "The city and state, e.g. San Francisco, CA", + /// }, + /// "format": { + /// "type": "string", + /// "enum": ["celsius", "fahrenheit"], + /// "descripti [rest of string was truncated]";. + /// + internal static string FunctionsDefinition { + get { + return ResourceManager.GetString("FunctionsDefinition", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/DevExtremeAILibTest/Resources/Resource.resx b/DevExtremeAILibTest/Resources/Resource.resx index b44771b..f98d504 100644 --- a/DevExtremeAILibTest/Resources/Resource.resx +++ b/DevExtremeAILibTest/Resources/Resource.resx @@ -121,6 +121,9 @@ friendly-robot.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + FunctionsDefinition.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + pink-panther.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 From 5751edc5b8a0105d1b9076f9e87e444ad100191d Mon Sep 17 00:00:00 2001 From: AndreaPic Date: Thu, 27 Jul 2023 23:44:49 +0200 Subject: [PATCH 3/6] function almost done :) --- DevExtremeAI/OpenAIDTO/ChatDTO.cs | 60 ++++++++-------- DevExtremeAI/OpenAIDTO/FunctionDefinition.cs | 76 ++++++++++++++++++++ DevExtremeAILibTest/AIChatCompletionTests.cs | 69 ++++++------------ 3 files changed, 127 insertions(+), 78 deletions(-) create mode 100644 DevExtremeAI/OpenAIDTO/FunctionDefinition.cs diff --git a/DevExtremeAI/OpenAIDTO/ChatDTO.cs b/DevExtremeAI/OpenAIDTO/ChatDTO.cs index 124dd9f..2ce7d50 100644 --- a/DevExtremeAI/OpenAIDTO/ChatDTO.cs +++ b/DevExtremeAI/OpenAIDTO/ChatDTO.cs @@ -34,40 +34,37 @@ public void AddMessage(ChatCompletionRequestMessage message) Messages.Add(message); } - private List? functions; + private IList? functions; /// /// A list of functions the model may generate JSON inputs for. /// [JsonPropertyName("functions")] - public JsonNode Functions { get; set; } - //public List Functions - //{ - // get - // { - // if (functions != null && functions.Count() == 0) - // { - // return null; - // } - // return functions; - // } - // set - // { - // functions = value; - // } - //} - - //public void AddFunction(ChatCompletionfunction function) - //{ - // if (function != null) - // { - // if (functions == null) - // { - // functions = new List(); - // } - // functions.Add(function); - // } - //} + public IList? Functions + { + get + { + if ( (functions == null) || (functions.Count == 0)) + { + return null; + } + return functions; + } + set + { + functions = value; + } + } + + public void AddFunction(FunctionDefinition functionDefinition) + { + if (functions == null) + { + functions = new List(); + } + functions.Add(functionDefinition); + } + /// /// Controls how the model responds to function calls. @@ -198,7 +195,7 @@ public class ChatCompletionRequestMessage /// The name and arguments of a function that should be called, as generated by the model. /// [JsonPropertyName("function_call")] - public string? FunctionCall { get; set; } + public FunctionCallDefinition? FunctionCall { get; set; } } @@ -293,6 +290,9 @@ public class ChatCompletionResponseMessage /// [JsonPropertyName("content")] public string Content { get; set; } + + [JsonPropertyName("function_call")] + public FunctionCallDefinition? FunctionCall { get; set; } } //[JsonConverter(typeof(JsonStringEnumConverterEx))] diff --git a/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs b/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs new file mode 100644 index 0000000..e85206c --- /dev/null +++ b/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace DevExtremeAI.OpenAIDTO +{ + + //https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb + + public class FunctionDefinition + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("parameters")] + public ParametersDefinition Parameters {get;set;} = new (); + + } + + public class ParametersDefinition + { + [JsonPropertyName("type")] + public string TypeName { get; set; } = "object"; + + [JsonPropertyName("properties")] + public IDictionary Properties { get; set; } = new Dictionary(); + + [JsonPropertyName("required")] + public IList? Required { get; set; } + + public void AddRequiredProperty(string propertyName) + { + if (Required == null) + { + Required = new List(); + } + Required.Add(propertyName); + } + } + + public class PropertyDefinition + { + [JsonPropertyName("type")] + public string TypeName { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("enum")] + public IList? EnumValues { get; set; } + + public void AddEnumValue(string enumValue) + { + if (EnumValues == null) + { + EnumValues = new List(); + } + EnumValues.Add(enumValue); + } + } + + public class FunctionCallDefinition + { + [JsonPropertyName("name")] + public string FunctionName { get; set; } + + [JsonPropertyName("arguments")] + public string Arguments { get; set; } + } +} diff --git a/DevExtremeAILibTest/AIChatCompletionTests.cs b/DevExtremeAILibTest/AIChatCompletionTests.cs index 564e5b4..3eb2800 100644 --- a/DevExtremeAILibTest/AIChatCompletionTests.cs +++ b/DevExtremeAILibTest/AIChatCompletionTests.cs @@ -193,70 +193,43 @@ public async Task CreateChatCompletionFunctionTest(string modelID) createCompletionRequest.Model = modelID; createCompletionRequest.Temperature = 1.4; var function = new ChatCompletionfunction(); - function.Name = "get_current_weather"; - function.Description = "Get the current weather in a given location"; - function.JSONSchemaParameters = "{\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"location\": {\r\n \"type\": \"string\",\r\n \"description\": \"The city and state, e.g. San Francisco, CA\"\r\n },\r\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\r\n },\r\n \"required\": [\"location\"]\r\n}"; - //createCompletionRequest.Functions = "[{\"name\": \"get_current_weather\",\"description\": \"Get the current weather\",\"parameters\": {\"type\": \"object\",\"properties\": {\"location\": {\"type\": \"string\",\"description\": \"The city and state, e.g. San Francisco , CA\"},\"format\": {\"type\": \"string\",\"enum\": [\"celsius\", \"fahrenheit\"],\"description\": \"The temperature unit to use. Infer this from the users location.\"}},\"required\": [\"location\", \"format\"]}}]"; - createCompletionRequest.Functions = Resources.Resource.FunctionsDefinition; - - //createCompletionRequest.AddFunction(function); - - createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + //function.Name = "get_current_weather"; + //function.Description = "Get the current weather in a given location"; + //function.JSONSchemaParameters = "{\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"location\": {\r\n \"type\": \"string\",\r\n \"description\": \"The city and state, e.g. San Francisco, CA\"\r\n },\r\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\r\n },\r\n \"required\": [\"location\"]\r\n}"; + ////createCompletionRequest.Functions = "[{\"name\": \"get_current_weather\",\"description\": \"Get the current weather\",\"parameters\": {\"type\": \"object\",\"properties\": {\"location\": {\"type\": \"string\",\"description\": \"The city and state, e.g. San Francisco , CA\"},\"format\": {\"type\": \"string\",\"enum\": [\"celsius\", \"fahrenheit\"],\"description\": \"The temperature unit to use. Infer this from the users location.\"}},\"required\": [\"location\", \"format\"]}}]"; + var func = new FunctionDefinition() { - Role = ChatCompletionMessageRoleEnum.User, - Content = "Come sarà il tempo a Venezia, Italia oggi?" - }); - - var response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); - Assert.False(response.HasError, response?.ErrorResponse?.Error?.Message); - Assert.NotNull(response?.OpenAIResponse); - Assert.NotNull(response?.OpenAIResponse?.Choices); - Assert.True(response.OpenAIResponse.Choices.Count > 0); - Assert.NotNull(response?.OpenAIResponse?.Usage); - - Debug.WriteLine(response.OpenAIResponse.Choices[0].Message.Content); - - createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + Name = "get_current_weather", + Description = "Get the current weather in a given location" + }; + func.Parameters.Properties.Add("location", new PropertyDefinition() { - Role = response.OpenAIResponse.Choices[0].Message.Role, - Content = response.OpenAIResponse.Choices[0].Message.Content + TypeName = "string", + Description = "The city and state, e.g. San Francisco, CA" }); - - - createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + func.Parameters.Properties.Add("format", new PropertyDefinition() { - Role = ChatCompletionMessageRoleEnum.User, - Content = "Qual'è la capitale d'Italia?" + TypeName = "string", + Description = "The temperature unit to use. Infer this from the users location.", + EnumValues = new []{ "celsius", "fahrenheit" } }); + func.Parameters.AddRequiredProperty("location"); + func.Parameters.AddRequiredProperty("format"); - await Task.Delay(22000); - response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); - Assert.NotNull(response?.OpenAIResponse); - Assert.NotNull(response?.OpenAIResponse?.Choices); - Assert.True(response.OpenAIResponse.Choices.Count > 0); - Assert.NotNull(response?.OpenAIResponse?.Usage); - - Debug.WriteLine(response.OpenAIResponse.Choices[0].Message.Content); - - createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() - { - Role = response.OpenAIResponse.Choices[0].Message.Role, - Content = response.OpenAIResponse.Choices[0].Message.Content - }); + createCompletionRequest.AddFunction(func); createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() { Role = ChatCompletionMessageRoleEnum.User, - Content = "Quali cose potrei visitare li?" + Content = "Come sarà il tempo a Venezia, Italia oggi?" }); - await Task.Delay(22000); - response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + var response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + Assert.False(response.HasError, response?.ErrorResponse?.Error?.Message); Assert.NotNull(response?.OpenAIResponse); Assert.NotNull(response?.OpenAIResponse?.Choices); Assert.True(response.OpenAIResponse.Choices.Count > 0); Assert.NotNull(response?.OpenAIResponse?.Usage); - Debug.WriteLine(response.OpenAIResponse.Choices[0].Message.Content); } From 599bf30ea89598e45aa21a7c68f0e747ab2ebe76 Mon Sep 17 00:00:00 2001 From: AndreaPic Date: Sat, 29 Jul 2023 19:43:11 +0200 Subject: [PATCH 4/6] function call definition and serialization --- DevExtremeAI/OpenAIDTO/ChatDTO.cs | 12 ---- DevExtremeAI/OpenAIDTO/FunctionDefinition.cs | 6 +- .../JsonStringArgumentsDictionaryConverter.cs | 63 +++++++++++++++++++ DevExtremeAILibTest/AIChatCompletionTests.cs | 35 +++++++++-- 4 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs diff --git a/DevExtremeAI/OpenAIDTO/ChatDTO.cs b/DevExtremeAI/OpenAIDTO/ChatDTO.cs index 2ce7d50..38103f8 100644 --- a/DevExtremeAI/OpenAIDTO/ChatDTO.cs +++ b/DevExtremeAI/OpenAIDTO/ChatDTO.cs @@ -295,17 +295,5 @@ public class ChatCompletionResponseMessage public FunctionCallDefinition? FunctionCall { get; set; } } - //[JsonConverter(typeof(JsonStringEnumConverterEx))] - - //public enum ChatCompletionResponseMessageRoleEnum - //{ - // [EnumMember(Value = "system")] - // System = 0, - // [EnumMember(Value = "user")] - // User = 1, - // [EnumMember(Value = "assistant")] - // Assistant = 2 - //} - } diff --git a/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs b/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs index e85206c..88ad649 100644 --- a/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs +++ b/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs @@ -1,4 +1,5 @@ -using System; +using DevExtremeAI.Utils; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -71,6 +72,7 @@ public class FunctionCallDefinition public string FunctionName { get; set; } [JsonPropertyName("arguments")] - public string Arguments { get; set; } + [JsonConverter(typeof(JsonStringArgumentsDictionaryConverter))] + public IDictionary Arguments { get; set; } = new Dictionary(); } } diff --git a/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs b/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs new file mode 100644 index 0000000..d40b3ee --- /dev/null +++ b/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace DevExtremeAI.Utils +{ + internal sealed class JsonStringArgumentsDictionaryConverter + : JsonConverter> + { + //public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + //{ + // var jsonValue = reader.GetString(); + // if (!string.IsNullOrEmpty(jsonValue)) + // { + // JsonSerializer.Deserialize>() + // } + // var result = !string.IsNullOrWhiteSpace(Arguments) ? : null; + // return result ?? new(); + //} + + //public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + //{ + // throw new NotImplementedException(); + //} + public override IDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Dictionary ret = null; + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var dictionary = new Dictionary(); + + var arguments = reader.GetString(); + + if (!string.IsNullOrEmpty(arguments)) + { + ret = JsonSerializer.Deserialize>(arguments); + } + else + { + ret = new Dictionary(); + } + + return ret; + } + + public override void Write(Utf8JsonWriter writer, IDictionary dictionary, JsonSerializerOptions options) + { + var json = JsonSerializer.Serialize>(dictionary, options); + + writer.WriteStringValue(json); + + } + } +} diff --git a/DevExtremeAILibTest/AIChatCompletionTests.cs b/DevExtremeAILibTest/AIChatCompletionTests.cs index 3eb2800..cab40f2 100644 --- a/DevExtremeAILibTest/AIChatCompletionTests.cs +++ b/DevExtremeAILibTest/AIChatCompletionTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using DevExtremeAI.OpenAIDTO; using DevExtremeAI.OpenAIClient; +using System.Text.Json; namespace DevExtremeAILibTest { @@ -193,10 +194,6 @@ public async Task CreateChatCompletionFunctionTest(string modelID) createCompletionRequest.Model = modelID; createCompletionRequest.Temperature = 1.4; var function = new ChatCompletionfunction(); - //function.Name = "get_current_weather"; - //function.Description = "Get the current weather in a given location"; - //function.JSONSchemaParameters = "{\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"location\": {\r\n \"type\": \"string\",\r\n \"description\": \"The city and state, e.g. San Francisco, CA\"\r\n },\r\n \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\r\n },\r\n \"required\": [\"location\"]\r\n}"; - ////createCompletionRequest.Functions = "[{\"name\": \"get_current_weather\",\"description\": \"Get the current weather\",\"parameters\": {\"type\": \"object\",\"properties\": {\"location\": {\"type\": \"string\",\"description\": \"The city and state, e.g. San Francisco , CA\"},\"format\": {\"type\": \"string\",\"enum\": [\"celsius\", \"fahrenheit\"],\"description\": \"The temperature unit to use. Infer this from the users location.\"}},\"required\": [\"location\", \"format\"]}}]"; var func = new FunctionDefinition() { Name = "get_current_weather", @@ -229,9 +226,39 @@ public async Task CreateChatCompletionFunctionTest(string modelID) Assert.NotNull(response?.OpenAIResponse); Assert.NotNull(response?.OpenAIResponse?.Choices); Assert.True(response.OpenAIResponse.Choices.Count > 0); + Assert.True(response.OpenAIResponse.Choices[0].FinishReason == "function_call"); + Assert.NotNull(response.OpenAIResponse.Choices[0].Message.FunctionCall); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.FunctionName == "get_current_weather"); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Keys.Count == 2); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Keys.ElementAt(0) == "location"); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["location"].ToString().Contains("Venezia")); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Keys.ElementAt(1) == "format"); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Values.ElementAt(1).ToString().Contains("celsius")); Assert.NotNull(response?.OpenAIResponse?.Usage); + //"{\n\"location\": \"Venezia, IT\",\n\"format\": \"celsius\"\n}"); + + var jsonFunctionArguments = JsonSerializer.Serialize(response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments, response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.GetType()); + var args = JsonSerializer.Deserialize>(jsonFunctionArguments); + Assert.NotNull(args); + Assert.True(args.Count == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Count); + Assert.True(args["location"].ToString() == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["location"].ToString()); + Assert.True(args["format"].ToString() == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["format"].ToString()); + + + var jsonFunctionCall = JsonSerializer.Serialize(response.OpenAIResponse.Choices[0].Message.FunctionCall, response.OpenAIResponse.Choices[0].Message.FunctionCall.GetType()); + FunctionCallDefinition fc = JsonSerializer.Deserialize(jsonFunctionCall); + Assert.NotNull(fc); + Assert.True(fc.FunctionName == response.OpenAIResponse.Choices[0].Message.FunctionCall.FunctionName); + Assert.True(fc.Arguments.Count == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Count); + Assert.True(fc.Arguments["location"].ToString() == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["location"].ToString()); + Assert.True(fc.Arguments["format"].ToString() == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["format"].ToString()); + + + + + } } From 67e0baac4b355f91e247a75175cb5803021a0023 Mon Sep 17 00:00:00 2001 From: AndreaPic Date: Sun, 30 Jul 2023 15:38:24 +0200 Subject: [PATCH 5/6] function feature complete --- DevExtremeAI/OpenAIDTO/ChatDTO.cs | 17 +++ DevExtremeAI/OpenAIDTO/CompletionsDTO.cs | 12 ++ DevExtremeAI/OpenAIDTO/FunctionDefinition.cs | 59 +++++++++- .../JsonStringArgumentsDictionaryConverter.cs | 15 --- DevExtremeAILibTest/AIChatCompletionTests.cs | 110 +++++++++++++++++- 5 files changed, 196 insertions(+), 17 deletions(-) diff --git a/DevExtremeAI/OpenAIDTO/ChatDTO.cs b/DevExtremeAI/OpenAIDTO/ChatDTO.cs index 38103f8..0ec2b1e 100644 --- a/DevExtremeAI/OpenAIDTO/ChatDTO.cs +++ b/DevExtremeAI/OpenAIDTO/ChatDTO.cs @@ -29,11 +29,18 @@ public class CreateChatCompletionRequest [JsonPropertyName("messages")] public List Messages { get; private set; } = new List(); + /// + /// Add a message to the completion + /// + /// The message to add public void AddMessage(ChatCompletionRequestMessage message) { Messages.Add(message); } + /// + /// Function definitions for ai + /// private IList? functions; /// @@ -56,6 +63,10 @@ public void AddMessage(ChatCompletionRequestMessage message) } } + /// + /// Add a function definition to the completion + /// + /// The funciton definition public void AddFunction(FunctionDefinition functionDefinition) { if (functions == null) @@ -112,6 +123,9 @@ public void AddFunction(FunctionDefinition functionDefinition) [JsonPropertyName("stop")] public object? Stop => stops.Count switch { 0 => null, 1 => stops[0], > 1 => stops, _ => null }; + /// + /// stop list + /// private List stops { get; set; } = new List(); /// @@ -291,6 +305,9 @@ public class ChatCompletionResponseMessage [JsonPropertyName("content")] public string Content { get; set; } + /// + /// The function to call + /// [JsonPropertyName("function_call")] public FunctionCallDefinition? FunctionCall { get; set; } } diff --git a/DevExtremeAI/OpenAIDTO/CompletionsDTO.cs b/DevExtremeAI/OpenAIDTO/CompletionsDTO.cs index 6eafd60..b907479 100644 --- a/DevExtremeAI/OpenAIDTO/CompletionsDTO.cs +++ b/DevExtremeAI/OpenAIDTO/CompletionsDTO.cs @@ -23,7 +23,16 @@ public class CreateCompletionRequest /// [JsonPropertyName("prompt")] public object? Prompt => Prompts.Count switch { 0 => null, 1 => Prompts[0], > 1 => Prompts, _ => null }; + + /// + /// Prompt container + /// private List Prompts { get; set; } = new List(); + + /// + /// Add a propt to the container + /// + /// The value of the prompt to add public void AddCompletionPrompt(string prompt) { Prompts.Add(prompt); @@ -94,6 +103,9 @@ public void AddCompletionPrompt(string prompt) [JsonPropertyName("stop")] public object? Stop => stops.Count switch { 0 => null, 1 => stops[0], > 1 => stops, _ => null }; + /// + /// The list of stops + /// private List stops { get; set; } = new List(); /// diff --git a/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs b/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs index 88ad649..bebed53 100644 --- a/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs +++ b/DevExtremeAI/OpenAIDTO/FunctionDefinition.cs @@ -9,32 +9,59 @@ namespace DevExtremeAI.OpenAIDTO { - //https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb + /// + /// This object define a schema for the function's description used by OpenAI + /// public class FunctionDefinition { + /// + /// The name of the funciont + /// [JsonPropertyName("name")] public string Name { get; set; } + /// + /// The description of the function behavior + /// [JsonPropertyName("description")] public string Description { get; set; } + /// + /// The function parameters + /// [JsonPropertyName("parameters")] public ParametersDefinition Parameters {get;set;} = new (); } + /// + /// Definition of the function parameters + /// public class ParametersDefinition { + /// + /// parameters are in dto object so in openai example the value is "object" + /// [JsonPropertyName("type")] public string TypeName { get; set; } = "object"; + /// + /// List of the properties used as argument for the function (key is the property name, value is the property definition) + /// [JsonPropertyName("properties")] public IDictionary Properties { get; set; } = new Dictionary(); + /// + /// List of the required properties + /// [JsonPropertyName("required")] public IList? Required { get; set; } + /// + /// Add a property to required property list + /// + /// public void AddRequiredProperty(string propertyName) { if (Required == null) @@ -45,17 +72,38 @@ public void AddRequiredProperty(string propertyName) } } + /// + /// Property definition + /// public class PropertyDefinition { + /// + /// property type (like string,integer) + /// + /// + /// In case of enum you can use string as property type + /// [JsonPropertyName("type")] public string TypeName { get; set; } + /// + /// property discription + /// [JsonPropertyName("description")] public string Description { get; set; } + /// + /// If the property is a enum value, here there are valid values + /// [JsonPropertyName("enum")] public IList? EnumValues { get; set; } + /// + /// Add a valid value to EnumValues + /// + /// + /// A value for the enum list EnumValues + /// public void AddEnumValue(string enumValue) { if (EnumValues == null) @@ -66,11 +114,20 @@ public void AddEnumValue(string enumValue) } } + /// + /// The definition of a function call + /// public class FunctionCallDefinition { + /// + /// Function name + /// [JsonPropertyName("name")] public string FunctionName { get; set; } + /// + /// The arguments of the function, the key is the property name of the object to pass to the function and the value is the value of the property + /// [JsonPropertyName("arguments")] [JsonConverter(typeof(JsonStringArgumentsDictionaryConverter))] public IDictionary Arguments { get; set; } = new Dictionary(); diff --git a/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs b/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs index d40b3ee..2852914 100644 --- a/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs +++ b/DevExtremeAI/Utils/JsonStringArgumentsDictionaryConverter.cs @@ -12,21 +12,6 @@ namespace DevExtremeAI.Utils internal sealed class JsonStringArgumentsDictionaryConverter : JsonConverter> { - //public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - //{ - // var jsonValue = reader.GetString(); - // if (!string.IsNullOrEmpty(jsonValue)) - // { - // JsonSerializer.Deserialize>() - // } - // var result = !string.IsNullOrWhiteSpace(Arguments) ? : null; - // return result ?? new(); - //} - - //public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) - //{ - // throw new NotImplementedException(); - //} public override IDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { Dictionary ret = null; diff --git a/DevExtremeAILibTest/AIChatCompletionTests.cs b/DevExtremeAILibTest/AIChatCompletionTests.cs index cab40f2..c682cd1 100644 --- a/DevExtremeAILibTest/AIChatCompletionTests.cs +++ b/DevExtremeAILibTest/AIChatCompletionTests.cs @@ -182,9 +182,19 @@ public async Task CreateChatCompletionITATest(string modelID) } } + + + + + + /// + /// This test is looks like the official documentation at https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb + /// + /// + /// [Theory] [InlineData("gpt-3.5-turbo-0613")] - public async Task CreateChatCompletionFunctionTest(string modelID) + public async Task CreateChatCompletionOneFunctionTest(string modelID) { using (var scope = _factory.Services.CreateScope()) @@ -254,11 +264,109 @@ public async Task CreateChatCompletionFunctionTest(string modelID) Assert.True(fc.Arguments.Count == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments.Count); Assert.True(fc.Arguments["location"].ToString() == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["location"].ToString()); Assert.True(fc.Arguments["format"].ToString() == response.OpenAIResponse.Choices[0].Message.FunctionCall.Arguments["format"].ToString()); + } + } + /// + /// This test is looks like the official documentation at https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb + /// + /// + /// + [Theory] + [InlineData("gpt-3.5-turbo-0613")] + public async Task CreateChatCompletionMultipleFunctionTest(string modelID) + { + using (var scope = _factory.Services.CreateScope()) + { + var openAiapiClient = scope.ServiceProvider.GetService(); + CreateChatCompletionRequest createCompletionRequest = new CreateChatCompletionRequest(); + createCompletionRequest.Model = modelID; + createCompletionRequest.Temperature = 1.4; + var function = new ChatCompletionfunction(); + var func1 = new FunctionDefinition() + { + Name = "get_current_weather", + Description = "Get the current weather in a given location" + }; + func1.Parameters.Properties.Add("location", new PropertyDefinition() + { + TypeName = "string", + Description = "The city and state, e.g. San Francisco, CA" + }); + func1.Parameters.Properties.Add("format", new PropertyDefinition() + { + TypeName = "string", + Description = "The temperature unit to use. Infer this from the users location.", + EnumValues = new[] { "celsius", "fahrenheit" } + }); + func1.Parameters.AddRequiredProperty("location"); + func1.Parameters.AddRequiredProperty("format"); + createCompletionRequest.AddFunction(func1); + var func2 = new FunctionDefinition() + { + Name = "get_n_day_weather_forecast", + Description = "Get an N-day weather forecast" + }; + func2.Parameters.Properties.Add("location", new PropertyDefinition() + { + TypeName = "string", + Description = "The city and state, e.g. San Francisco, CA" + }); + func2.Parameters.Properties.Add("format", new PropertyDefinition() + { + TypeName = "string", + Description = "The temperature unit to use. Infer this from the users location.", + EnumValues = new[] { "celsius", "fahrenheit" } + }); + func2.Parameters.Properties.Add("num_days", new PropertyDefinition() + { + TypeName = "integer", + Description = "The number of days to forecast" + }); + func2.Parameters.AddRequiredProperty("location"); + func2.Parameters.AddRequiredProperty("format"); + func2.Parameters.AddRequiredProperty("num_days"); + + createCompletionRequest.AddFunction(func2); + + + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = ChatCompletionMessageRoleEnum.User, + Content = "what is the weather going to be like in Glasgow, Scotland over the next x days" + }); + + var response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + Assert.False(response.HasError, response?.ErrorResponse?.Error?.Message); + Assert.NotNull(response?.OpenAIResponse); + Assert.NotNull(response?.OpenAIResponse?.Choices); + Assert.True(response.OpenAIResponse.Choices.Count > 0); + + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = ChatCompletionMessageRoleEnum.Assistant, + Content = response.OpenAIResponse.Choices[0].Message.Content + }); + + createCompletionRequest.Messages.Add(new ChatCompletionRequestMessage() + { + Role = ChatCompletionMessageRoleEnum.User, + Content = "5 days" + }); + + response = await openAiapiClient.CreateChatCompletionAsync(createCompletionRequest); + Assert.False(response.HasError, response?.ErrorResponse?.Error?.Message); + Assert.NotNull(response?.OpenAIResponse); + Assert.NotNull(response?.OpenAIResponse?.Choices); + Assert.True(response.OpenAIResponse.Choices.Count > 0); + Assert.True(response.OpenAIResponse.Choices[0].FinishReason == "function_call"); + Assert.True(response.OpenAIResponse.Choices[0].Message.FunctionCall.FunctionName == "get_n_day_weather_forecast"); } } From ba9e81556da5c1d92addc92ef14b1002784f8e16 Mon Sep 17 00:00:00 2001 From: AndreaPic Date: Sun, 30 Jul 2023 15:43:26 +0200 Subject: [PATCH 6/6] csproj update --- DevExtremeAI/DevExtremeAI.csproj | 9 +++++---- NUGET.md | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/DevExtremeAI/DevExtremeAI.csproj b/DevExtremeAI/DevExtremeAI.csproj index f25f301..300df6e 100644 --- a/DevExtremeAI/DevExtremeAI.csproj +++ b/DevExtremeAI/DevExtremeAI.csproj @@ -12,18 +12,19 @@ https://github.com/AndreaPic/DevExtremeAI True $(VersionPrefix)$(VersionSuffix) - 1.0.4 + 1.1.0 openai;dotnet;aspnet;csharp;gpt-4;gpt-3.5-turbo;davinci;DALL·E;Whisper;fine-tunes - 1.0.0.0 - 1.0.0.0 + 1.1.0.0 + 1.1.0.0 git This library is full compliant to openAI specs and also implement openAI error response. It's very easy to use with asp.net core, has full support to dependency injection and it's very easy to use in libraries without dependency injection. MIT - - (new) Added support for multiple stop sequences in completions + - (1.1.0 new) *** Full functions implementation *** + - (1.0.4) Added support for multiple stop sequences in completions diff --git a/NUGET.md b/NUGET.md index d75bf95..3e4ae33 100644 --- a/NUGET.md +++ b/NUGET.md @@ -22,6 +22,7 @@ You can find the documentation in the [github repository](https://github.com/And Are covered all OpenAI's API types: +- Functions - Models - Completions - Chat