From 15c9e2c981c8dd8fef008a4502ed9819174d5565 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 15 Sep 2024 16:04:28 -0400 Subject: [PATCH] OpenAI-DotNet 8.3.0 - Updated library to .net 8 - Refactored TypeExtensions and JsonSchema generation - Improved JsonSchema generation for enums and dictionaries - Ensured JsonSchema properly handles nullable types --- .../OpenAI-DotNet-Proxy.csproj | 2 +- .../OpenAI-DotNet-Tests-Proxy.csproj | 2 +- .../OpenAI-DotNet-Tests.csproj | 2 +- .../TestFixture_00_02_Extensions.cs | 47 +++++ OpenAI-DotNet/Audio/SpeechRequest.cs | 2 +- OpenAI-DotNet/Batch/BatchResponse.cs | 2 +- OpenAI-DotNet/Common/Annotation.cs | 2 +- OpenAI-DotNet/Common/Content.cs | 2 +- OpenAI-DotNet/Common/ResponseFormatObject.cs | 2 +- OpenAI-DotNet/Extensions/TypeExtensions.cs | 188 ++++++++++++++---- .../FineTuning/FineTuneJobResponse.cs | 2 +- .../Images/AbstractBaseImageRequest.cs | 2 +- .../Images/ImageGenerationRequest.cs | 2 +- OpenAI-DotNet/OpenAI-DotNet.csproj | 9 +- .../Threads/CodeInterpreterOutputs.cs | 2 +- OpenAI-DotNet/Threads/IncompleteDetails.cs | 2 +- OpenAI-DotNet/Threads/MessageResponse.cs | 2 +- OpenAI-DotNet/Threads/RunResponse.cs | 2 +- OpenAI-DotNet/Threads/RunStepResponse.cs | 4 +- OpenAI-DotNet/Threads/TruncationStrategy.cs | 2 +- .../VectorStores/ChunkingStrategy.cs | 2 +- 21 files changed, 221 insertions(+), 61 deletions(-) diff --git a/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj b/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj index 782b8fe8..841a82c2 100644 --- a/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj +++ b/OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 latest disable OpenAI API Proxy diff --git a/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj b/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj index 92a41f5d..71bc8545 100644 --- a/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj +++ b/OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 enable false false diff --git a/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj b/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj index 0343d83b..491a81aa 100644 --- a/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj +++ b/OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 false false latest diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs index c79488a3..833f39a4 100644 --- a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs +++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; @@ -146,5 +147,51 @@ public void Test_02_01_GenerateJsonSchema() JsonSchema mathSchema = typeof(MathResponse); Console.WriteLine(mathSchema.ToString()); } + + [Test] + public void Test_02_02_GenerateJsonSchema_PrimitiveTypes() + { + JsonSchema schema = typeof(TestSchema); + Console.WriteLine(schema.ToString()); + } + + private class TestSchema + { + // test all primitive types can be serialized + public bool Bool { get; set; } + public byte Byte { get; set; } + public sbyte SByte { get; set; } + public short Short { get; set; } + public ushort UShort { get; set; } + public int Integer { get; set; } + public uint UInteger { get; set; } + public long Long { get; set; } + public ulong ULong { get; set; } + public float Float { get; set; } + public double Double { get; set; } + public decimal Decimal { get; set; } + public char Char { get; set; } + public string String { get; set; } + public DateTime DateTime { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + public Guid Guid { get; set; } + // test nullables + public int? NullInt { get; set; } + public DateTime? NullDateTime { get; set; } + public TestEnum TestEnum { get; set; } + public TestEnum? NullEnum { get; set; } + public Dictionary Dictionary { get; set; } + public IDictionary IntDictionary { get; set; } + public IReadOnlyDictionary StringDictionary { get; set; } + public Dictionary CustomDictionary { get; set; } + } + + private enum TestEnum + { + Enum1, + Enum2, + Enum3, + Enum4 + } } } diff --git a/OpenAI-DotNet/Audio/SpeechRequest.cs b/OpenAI-DotNet/Audio/SpeechRequest.cs index 24ed5355..e6fb89df 100644 --- a/OpenAI-DotNet/Audio/SpeechRequest.cs +++ b/OpenAI-DotNet/Audio/SpeechRequest.cs @@ -49,7 +49,7 @@ public SpeechRequest(string input, Model model = null, SpeechVoice voice = Speec /// [JsonPropertyName("response_format")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public SpeechResponseFormat ResponseFormat { get; } /// diff --git a/OpenAI-DotNet/Batch/BatchResponse.cs b/OpenAI-DotNet/Batch/BatchResponse.cs index 7abf8f07..c5690c5d 100644 --- a/OpenAI-DotNet/Batch/BatchResponse.cs +++ b/OpenAI-DotNet/Batch/BatchResponse.cs @@ -53,7 +53,7 @@ public sealed class BatchResponse : BaseResponse /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public BatchStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Common/Annotation.cs b/OpenAI-DotNet/Common/Annotation.cs index 57599ddd..49b2f164 100644 --- a/OpenAI-DotNet/Common/Annotation.cs +++ b/OpenAI-DotNet/Common/Annotation.cs @@ -14,7 +14,7 @@ public sealed class Annotation : IAppendable [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public AnnotationType Type { get; private set; } /// diff --git a/OpenAI-DotNet/Common/Content.cs b/OpenAI-DotNet/Common/Content.cs index 4cfa015d..59a2a6f9 100644 --- a/OpenAI-DotNet/Common/Content.cs +++ b/OpenAI-DotNet/Common/Content.cs @@ -59,7 +59,7 @@ public Content(ContentType type, string input) [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public ContentType Type { get; private set; } diff --git a/OpenAI-DotNet/Common/ResponseFormatObject.cs b/OpenAI-DotNet/Common/ResponseFormatObject.cs index 32aac488..1310bac2 100644 --- a/OpenAI-DotNet/Common/ResponseFormatObject.cs +++ b/OpenAI-DotNet/Common/ResponseFormatObject.cs @@ -26,7 +26,7 @@ public ResponseFormatObject(JsonSchema schema) [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public ChatResponseFormat Type { get; private set; } [JsonInclude] diff --git a/OpenAI-DotNet/Extensions/TypeExtensions.cs b/OpenAI-DotNet/Extensions/TypeExtensions.cs index 4ca7fa1f..3584f1d1 100644 --- a/OpenAI-DotNet/Extensions/TypeExtensions.cs +++ b/OpenAI-DotNet/Extensions/TypeExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; @@ -87,6 +88,7 @@ public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchem { options ??= OpenAIClient.JsonSerializationOptions; var schema = new JsonObject(); + type = UnwrapNullableType(type); if (!type.IsPrimitive && type != typeof(Guid) && @@ -98,60 +100,45 @@ public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchem return new JsonObject { ["$ref"] = $"#/definitions/{type.FullName}" }; } - if (type == typeof(string) || type == typeof(char)) + if (type.TryGetSimpleTypeSchema(out var schemaType)) { - 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"; + schema["type"] = schemaType; + + if (type == typeof(DateTime) || + type == typeof(DateTimeOffset)) + { + schema["format"] = "date-time"; + } + else if (type == typeof(Guid)) + { + schema["format"] = "uuid"; + } } - else if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + else if (type.IsEnum) { schema["type"] = "string"; - schema["format"] = "date-time"; + schema["enum"] = new JsonArray(Enum.GetNames(type).Select(name => JsonValue.Create(name)).ToArray()); } - else if (type == typeof(Guid)) + else if (type.TryGetDictionaryValueType(out var valueType)) { - schema["type"] = "string"; - schema["format"] = "uuid"; - } - else if (type.IsEnum) - { - schema["type"] = "string"; - schema["enum"] = new JsonArray(); + schema["type"] = "object"; - foreach (var value in Enum.GetValues(type)) + if (rootSchema["definitions"] != null && + rootSchema["definitions"].AsObject().ContainsKey(valueType!.FullName!)) { - schema["enum"].AsArray().Add(JsonNode.Parse(JsonSerializer.Serialize(value, options))); + schema["additionalProperties"] = new JsonObject { ["$ref"] = $"#/definitions/{valueType.FullName}" }; + } + else + { + schema["additionalProperties"] = GenerateJsonSchema(valueType, rootSchema); } } - else if (type.IsArray || - type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>) || - type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))) + else if (type.TryGetCollectionElementType(out var elementType)) { schema["type"] = "array"; - var elementType = type.GetElementType() ?? type.GetGenericArguments()[0]; if (rootSchema["definitions"] != null && - rootSchema["definitions"].AsObject().ContainsKey(elementType.FullName!)) + rootSchema["definitions"].AsObject().ContainsKey(elementType!.FullName!)) { schema["items"] = new JsonObject { ["$ref"] = $"#/definitions/{elementType.FullName}" }; } @@ -282,6 +269,127 @@ public static JsonObject GenerateJsonSchema(this Type type, JsonObject rootSchem return schema; } + private static bool TryGetSimpleTypeSchema(this Type type, out string schemaType) + { + switch (type) + { + case not null when type == typeof(object): + schemaType = "object"; + return true; + case not null when type == typeof(bool): + schemaType = "boolean"; + return true; + case not null when type == typeof(float) || + type == typeof(double) || + type == typeof(decimal): + schemaType = "number"; + return true; + case not null when type == typeof(char) || + type == typeof(string) || + type == typeof(Guid) || + type == typeof(DateTime) || + type == typeof(DateTimeOffset): + schemaType = "string"; + return true; + case not null when type == typeof(int) || + type == typeof(long) || + type == typeof(uint) || + type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(ulong) || + type == typeof(short) || + type == typeof(ushort): + schemaType = "integer"; + return true; + default: + schemaType = null; + return false; + } + } + + private static bool TryGetDictionaryValueType(this Type type, out Type valueType) + { + valueType = null; + + if (!type.IsGenericType) { return false; } + + var genericTypeDefinition = type.GetGenericTypeDefinition(); + + if (genericTypeDefinition == typeof(Dictionary<,>) || + genericTypeDefinition == typeof(IDictionary<,>) || + genericTypeDefinition == typeof(IReadOnlyDictionary<,>)) + { + return InternalTryGetDictionaryValueType(type, out valueType); + } + + // Check implemented interfaces for dictionary types + foreach (var @interface in type.GetInterfaces()) + { + if (!@interface.IsGenericType) { continue; } + + var interfaceTypeDefinition = @interface.GetGenericTypeDefinition(); + + if (interfaceTypeDefinition == typeof(IDictionary<,>) || + interfaceTypeDefinition == typeof(IReadOnlyDictionary<,>)) + { + return InternalTryGetDictionaryValueType(@interface, out valueType); + } + } + + return false; + + bool InternalTryGetDictionaryValueType(Type dictType, out Type dictValueType) + { + dictValueType = null; + var genericArgs = dictType.GetGenericArguments(); + + // The key type is not string, which cannot be represented in JSON object property names + if (genericArgs[0] != typeof(string)) + { + throw new InvalidOperationException($"Cannot generate schema for dictionary type '{dictType.FullName}' with non-string key type."); + } + + dictValueType = genericArgs[1].UnwrapNullableType(); + return true; + } + } + + private static readonly Type[] arrayTypes = + [ + typeof(IEnumerable<>), + typeof(ICollection<>), + typeof(IReadOnlyCollection<>), + typeof(List<>), + typeof(IList<>), + typeof(IReadOnlyList<>), + typeof(HashSet<>), + typeof(ISet<>), + typeof(IReadOnlySet<>) + ]; + + private static bool TryGetCollectionElementType(this Type type, out Type elementType) + { + elementType = null; + + if (type.IsArray) + { + elementType = type.GetElementType(); + return true; + } + + if (!type.IsGenericType) { return false; } + + var genericTypeDefinition = type.GetGenericTypeDefinition(); + + if (!arrayTypes.Contains(genericTypeDefinition)) { return false; } + + elementType = type.GetGenericArguments()[0].UnwrapNullableType(); + return true; + } + + private static Type UnwrapNullableType(this Type type) + => Nullable.GetUnderlyingType(type) ?? type; + private static Type GetMemberType(MemberInfo member) => member switch { diff --git a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs index d0cd0f37..b624171a 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs @@ -56,7 +56,7 @@ public DateTime? FinishedAt [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public JobStatus Status { get; private set; } [JsonInclude] diff --git a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs index e05b07f6..e2c58f6b 100644 --- a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs +++ b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs @@ -68,7 +68,7 @@ protected AbstractBaseImageRequest(Model model = null, int numberOfResults = 1, /// Defaults to /// [JsonPropertyName("response_format")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.")] public ImageResponseFormat ResponseFormat { get; } diff --git a/OpenAI-DotNet/Images/ImageGenerationRequest.cs b/OpenAI-DotNet/Images/ImageGenerationRequest.cs index acab3721..66c00b4e 100644 --- a/OpenAI-DotNet/Images/ImageGenerationRequest.cs +++ b/OpenAI-DotNet/Images/ImageGenerationRequest.cs @@ -107,7 +107,7 @@ public ImageGenerationRequest( /// Defaults to /// [JsonPropertyName("response_format")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.", true)] public ImageResponseFormat ResponseFormat { get; } diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index cc68457b..5cbb6c1c 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 latest OpenAI-DotNet OpenAI-DotNet @@ -29,8 +29,13 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx true true - 8.2.5 + 8.3.0 +Version 8.3.0 +- Updated library to .net 8 +- Refactored TypeExtensions and JsonSchema generation + - Improved JsonSchema generation for enums and dictionaries + - Ensured JsonSchema properly handles nullable types Version 8.2.5 - Fixed ResponseObjectFormat deserialization when maxNumberOfResults is null Version 8.2.4 diff --git a/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs b/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs index 575bcfa1..33829d5f 100644 --- a/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs +++ b/OpenAI-DotNet/Threads/CodeInterpreterOutputs.cs @@ -16,7 +16,7 @@ public sealed class CodeInterpreterOutputs : IAppendable /// [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public CodeInterpreterOutputType Type { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/IncompleteDetails.cs b/OpenAI-DotNet/Threads/IncompleteDetails.cs index e85dc41c..f9868893 100644 --- a/OpenAI-DotNet/Threads/IncompleteDetails.cs +++ b/OpenAI-DotNet/Threads/IncompleteDetails.cs @@ -7,7 +7,7 @@ public sealed class IncompleteDetails { [JsonInclude] [JsonPropertyName("reason")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public IncompleteMessageReason Reason { get; private set; } } } diff --git a/OpenAI-DotNet/Threads/MessageResponse.cs b/OpenAI-DotNet/Threads/MessageResponse.cs index 5b953840..466d9b65 100644 --- a/OpenAI-DotNet/Threads/MessageResponse.cs +++ b/OpenAI-DotNet/Threads/MessageResponse.cs @@ -61,7 +61,7 @@ public MessageResponse() { } /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public MessageStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/RunResponse.cs b/OpenAI-DotNet/Threads/RunResponse.cs index e12797b4..1986b048 100644 --- a/OpenAI-DotNet/Threads/RunResponse.cs +++ b/OpenAI-DotNet/Threads/RunResponse.cs @@ -62,7 +62,7 @@ public RunResponse() { } /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public RunStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/RunStepResponse.cs b/OpenAI-DotNet/Threads/RunStepResponse.cs index 46733e1f..f8919a7f 100644 --- a/OpenAI-DotNet/Threads/RunStepResponse.cs +++ b/OpenAI-DotNet/Threads/RunStepResponse.cs @@ -74,7 +74,7 @@ public DateTime? CreatedAt /// [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public RunStepType Type { get; private set; } /// @@ -82,7 +82,7 @@ public DateTime? CreatedAt /// [JsonInclude] [JsonPropertyName("status")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public RunStatus Status { get; private set; } /// diff --git a/OpenAI-DotNet/Threads/TruncationStrategy.cs b/OpenAI-DotNet/Threads/TruncationStrategy.cs index 2241e526..adca85eb 100644 --- a/OpenAI-DotNet/Threads/TruncationStrategy.cs +++ b/OpenAI-DotNet/Threads/TruncationStrategy.cs @@ -13,7 +13,7 @@ public sealed class TruncationStrategy /// [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public TruncationStrategies Type { get; private set; } /// diff --git a/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs b/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs index eef15bfe..0830783f 100644 --- a/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs +++ b/OpenAI-DotNet/VectorStores/ChunkingStrategy.cs @@ -23,7 +23,7 @@ public ChunkingStrategy(ChunkingStrategyType type) [JsonInclude] [JsonPropertyName("type")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(Extensions.JsonStringEnumConverter))] public ChunkingStrategyType Type { get; private set; } [JsonInclude]