From c17dee41f71a54c6ca5cb85fa74c5da92ae088a3 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 3 Mar 2024 18:48:02 -0500 Subject: [PATCH] OpenAI-DotNet 7.7.5 (#258) - Allow FunctionPropertyAttribute to be assignable to fields - Updated Function schema generation - Fall back to complex types, and use $ref for discovered types - Fixed schema generation to properly assign unsigned integer types --- .../Common/FunctionParameterAttribute.cs | 4 + .../Common/FunctionPropertyAttribute.cs | 2 +- OpenAI-DotNet/Extensions/TypeExtensions.cs | 175 +++++++++++++----- OpenAI-DotNet/OpenAI-DotNet.csproj | 7 +- OpenAI-DotNet/OpenAIClient.cs | 3 +- 5 files changed, 138 insertions(+), 53 deletions(-) diff --git a/OpenAI-DotNet/Common/FunctionParameterAttribute.cs b/OpenAI-DotNet/Common/FunctionParameterAttribute.cs index 071e1e66..dcbb8be6 100644 --- a/OpenAI-DotNet/Common/FunctionParameterAttribute.cs +++ b/OpenAI-DotNet/Common/FunctionParameterAttribute.cs @@ -7,6 +7,10 @@ namespace OpenAI [AttributeUsage(AttributeTargets.Parameter)] public sealed class FunctionParameterAttribute : Attribute { + /// + /// Function parameter attribute to help describe the parameter for the function. + /// + /// The description of the parameter and its usage. public FunctionParameterAttribute(string description) { Description = description; diff --git a/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs b/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs index e2d5a007..689ee912 100644 --- a/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs +++ b/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs @@ -4,7 +4,7 @@ namespace OpenAI { - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class FunctionPropertyAttribute : Attribute { /// diff --git a/OpenAI-DotNet/Extensions/TypeExtensions.cs b/OpenAI-DotNet/Extensions/TypeExtensions.cs index 58885fa2..fa3fdfc0 100644 --- a/OpenAI-DotNet/Extensions/TypeExtensions.cs +++ b/OpenAI-DotNet/Extensions/TypeExtensions.cs @@ -45,7 +45,7 @@ public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo) requiredParameters.Add(parameter.Name); } - schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType); + schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType, schema); var functionParameterAttribute = parameter.GetCustomAttribute(); @@ -63,11 +63,56 @@ public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo) return schema; } - public static JsonObject GenerateJsonSchema(this Type type) + public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchema) { var schema = new JsonObject(); - if (type.IsEnum) + if (!type.IsPrimitive && + type != typeof(Guid) && + type != typeof(DateTime) && + type != typeof(DateTimeOffset) && + rootSchema["definitions"] != null && + rootSchema["definitions"].AsObject().ContainsKey(type.FullName)) + { + return new JsonObject { ["$ref"] = $"#/definitions/{type.FullName}" }; + } + + if (type == typeof(string) || type == typeof(char)) + { + schema["type"] = "string"; + } + else if (type == typeof(int) || + type == typeof(long) || + type == typeof(uint) || + type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(ulong) || + type == typeof(short) || + type == typeof(ushort)) + { + schema["type"] = "integer"; + } + else if (type == typeof(float) || + type == typeof(double) || + type == typeof(decimal)) + { + schema["type"] = "number"; + } + else if (type == typeof(bool)) + { + schema["type"] = "boolean"; + } + else if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + { + schema["type"] = "string"; + schema["format"] = "date-time"; + } + else if (type == typeof(Guid)) + { + schema["type"] = "string"; + schema["format"] = "uuid"; + } + else if (type.IsEnum) { schema["type"] = "string"; schema["enum"] = new JsonArray(); @@ -80,21 +125,52 @@ public static JsonObject GenerateJsonSchema(this Type type) else if (type.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))) { schema["type"] = "array"; - schema["items"] = GenerateJsonSchema(type.GetElementType() ?? type.GetGenericArguments()[0]); + var elementType = type.GetElementType() ?? type.GetGenericArguments()[0]; + + if (rootSchema["definitions"] != null && + rootSchema["definitions"].AsObject().ContainsKey(elementType.FullName)) + { + schema["items"] = new JsonObject { ["$ref"] = $"#/definitions/{elementType.FullName}" }; + } + else + { + schema["items"] = GenerateJsonSchema(elementType, rootSchema); + } } - else if (type.IsClass && type != typeof(string)) + else { schema["type"] = "object"; - var properties = type.GetProperties(); - var propertiesInfo = new JsonObject(); - var requiredProperties = new JsonArray(); + rootSchema["definitions"] ??= new JsonObject(); + rootSchema["definitions"][type.FullName] = new JsonObject(); + + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + var members = new List(properties.Length + fields.Length); + members.AddRange(properties); + members.AddRange(fields); - foreach (var property in properties) + var memberInfo = new JsonObject(); + var memberProperties = new JsonArray(); + + foreach (var member in members) { - var propertyInfo = GenerateJsonSchema(property.PropertyType); - var functionPropertyAttribute = property.GetCustomAttribute(); - var jsonPropertyAttribute = property.GetCustomAttribute(); - var propertyName = jsonPropertyAttribute?.Name ?? property.Name; + var memberType = GetMemberType(member); + var functionPropertyAttribute = member.GetCustomAttribute(); + var jsonPropertyAttribute = member.GetCustomAttribute(); + var jsonIgnoreAttribute = member.GetCustomAttribute(); + var propertyName = jsonPropertyAttribute?.Name ?? member.Name; + + JsonObject propertyInfo; + + if (rootSchema["definitions"] != null && + rootSchema["definitions"].AsObject().ContainsKey(memberType.FullName)) + { + propertyInfo = new JsonObject { ["$ref"] = $"#/definitions/{memberType.FullName}" }; + } + else + { + propertyInfo = GenerateJsonSchema(memberType, rootSchema); + } // override properties with values from function property attribute if (functionPropertyAttribute != null) @@ -103,7 +179,7 @@ public static JsonObject GenerateJsonSchema(this Type type) if (functionPropertyAttribute.Required) { - requiredProperties.Add(propertyName); + memberProperties.Add(propertyName); } JsonNode defaultValue = null; @@ -137,58 +213,57 @@ public static JsonObject GenerateJsonSchema(this Type type) if (defaultValue != null && !enums.Contains(defaultValue)) { - enums.Add(JsonNode.Parse(defaultValue.ToJsonString())); + enums.Add(JsonNode.Parse(defaultValue.ToJsonString(OpenAIClient.JsonSerializationOptions))); } propertyInfo["enum"] = enums; } } - else if (Nullable.GetUnderlyingType(property.PropertyType) == null) + else if (jsonIgnoreAttribute != null) + { + // only add members that are required + switch (jsonIgnoreAttribute.Condition) + { + case JsonIgnoreCondition.Never: + case JsonIgnoreCondition.WhenWritingDefault: + memberProperties.Add(propertyName); + break; + case JsonIgnoreCondition.Always: + case JsonIgnoreCondition.WhenWritingNull: + default: + memberProperties.Remove(propertyName); + break; + } + } + else if (Nullable.GetUnderlyingType(memberType) == null) { - requiredProperties.Add(propertyName); + memberProperties.Add(propertyName); } - propertiesInfo[propertyName] = propertyInfo; + memberInfo[propertyName] = propertyInfo; } - schema["properties"] = propertiesInfo; + schema["properties"] = memberInfo; - if (requiredProperties.Count > 0) - { - schema["required"] = requiredProperties; - } - } - else - { - if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte)) - { - schema["type"] = "integer"; - } - else if (type == typeof(float) || type == typeof(double) || type == typeof(decimal)) - { - schema["type"] = "number"; - } - else if (type == typeof(bool)) - { - schema["type"] = "boolean"; - } - else if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) - { - schema["type"] = "string"; - schema["format"] = "date-time"; - } - else if (type == typeof(Guid)) + if (memberProperties.Count > 0) { - schema["type"] = "string"; - schema["format"] = "uuid"; - } - else - { - schema["type"] = type.Name.ToLower(); + schema["required"] = memberProperties; } + + rootSchema["definitions"] ??= new JsonObject(); + rootSchema["definitions"][type.FullName] = schema; + return new JsonObject { ["$ref"] = $"#/definitions/{type.FullName}" }; } return schema; } + + private static Type GetMemberType(MemberInfo member) + => member switch + { + FieldInfo fieldInfo => fieldInfo.FieldType, + PropertyInfo propertyInfo => propertyInfo.PropertyType, + _ => throw new ArgumentException($"{nameof(MemberInfo)} must be of type {nameof(FieldInfo)}, {nameof(PropertyInfo)}", nameof(member)) + }; } } \ No newline at end of file diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 01120b6b..15d4c26f 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -28,8 +28,13 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx True True - 7.7.4 + 7.7.5 +Version 7.7.5 +- Allow FunctionPropertyAttribute to be assignable to fields +- Updated Function schema generation + - Fall back to complex types, and use $ref for discovered types + - Fixed schema generation to properly assign unsigned integer types Version 7.7.4 - Fixed Threads.RunResponse.WaitForStatusChangeAsync timeout Version 7.7.3 diff --git a/OpenAI-DotNet/OpenAIClient.cs b/OpenAI-DotNet/OpenAIClient.cs index bbadb3ef..4b213e08 100644 --- a/OpenAI-DotNet/OpenAIClient.cs +++ b/OpenAI-DotNet/OpenAIClient.cs @@ -112,7 +112,8 @@ private void Dispose(bool disposing) internal static JsonSerializerOptions JsonSerializationOptions { get; } = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverterFactory() } + Converters = { new JsonStringEnumConverterFactory() }, + ReferenceHandler = ReferenceHandler.IgnoreCycles, }; ///