Skip to content

Commit

Permalink
OpenAI-DotNet 8.3.0
Browse files Browse the repository at this point in the history
- Updated library to .net 8
- Refactored TypeExtensions and JsonSchema generation
  - Improved JsonSchema generation for enums and dictionaries
  - Ensured JsonSchema properly handles nullable types
  • Loading branch information
StephenHodgson committed Sep 15, 2024
1 parent b2c0aa7 commit 15c9e2c
Show file tree
Hide file tree
Showing 21 changed files with 221 additions and 61 deletions.
2 changes: 1 addition & 1 deletion OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<Title>OpenAI API Proxy</Title>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>false</ImplicitUsings>
<IsPackable>false</IsPackable>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet-Tests/OpenAI-DotNet-Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<SignAssembly>false</SignAssembly>
<LangVersion>latest</LangVersion>
Expand Down
47 changes: 47 additions & 0 deletions OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, object> Dictionary { get; set; }
public IDictionary<string, int> IntDictionary { get; set; }
public IReadOnlyDictionary<string, string> StringDictionary { get; set; }
public Dictionary<string, MathResponse> CustomDictionary { get; set; }
}

private enum TestEnum
{
Enum1,
Enum2,
Enum3,
Enum4
}
}
}
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Audio/SpeechRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public SpeechRequest(string input, Model model = null, SpeechVoice voice = Speec
/// </summary>
[JsonPropertyName("response_format")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
[JsonConverter(typeof(JsonStringEnumConverter<SpeechResponseFormat>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<SpeechResponseFormat>))]
public SpeechResponseFormat ResponseFormat { get; }

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Batch/BatchResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public sealed class BatchResponse : BaseResponse
/// </summary>
[JsonInclude]
[JsonPropertyName("status")]
[JsonConverter(typeof(JsonStringEnumConverter<BatchStatus>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<BatchStatus>))]
public BatchStatus Status { get; private set; }

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Common/Annotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public sealed class Annotation : IAppendable<Annotation>

[JsonInclude]
[JsonPropertyName("type")]
[JsonConverter(typeof(JsonStringEnumConverter<AnnotationType>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<AnnotationType>))]
public AnnotationType Type { get; private set; }

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Common/Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public Content(ContentType type, string input)

[JsonInclude]
[JsonPropertyName("type")]
[JsonConverter(typeof(JsonStringEnumConverter<ContentType>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<ContentType>))]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public ContentType Type { get; private set; }

Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Common/ResponseFormatObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public ResponseFormatObject(JsonSchema schema)

[JsonInclude]
[JsonPropertyName("type")]
[JsonConverter(typeof(JsonStringEnumConverter<ChatResponseFormat>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<ChatResponseFormat>))]
public ChatResponseFormat Type { get; private set; }

[JsonInclude]
Expand Down
188 changes: 148 additions & 40 deletions OpenAI-DotNet/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) &&
Expand All @@ -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<JsonNode>());
}
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}" };
}
Expand Down Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public DateTime? FinishedAt

[JsonInclude]
[JsonPropertyName("status")]
[JsonConverter(typeof(JsonStringEnumConverter<JobStatus>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<JobStatus>))]
public JobStatus Status { get; private set; }

[JsonInclude]
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Images/AbstractBaseImageRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ protected AbstractBaseImageRequest(Model model = null, int numberOfResults = 1,
/// <para/> Defaults to <see cref="ImageResponseFormat.Url"/>
/// </summary>
[JsonPropertyName("response_format")]
[JsonConverter(typeof(JsonStringEnumConverter<ImageResponseFormat>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<ImageResponseFormat>))]
[FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.")]
public ImageResponseFormat ResponseFormat { get; }

Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Images/ImageGenerationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public ImageGenerationRequest(
/// <para/> Defaults to <see cref="ImageResponseFormat.Url"/>
/// </summary>
[JsonPropertyName("response_format")]
[JsonConverter(typeof(JsonStringEnumConverter<ImageResponseFormat>))]
[JsonConverter(typeof(Extensions.JsonStringEnumConverter<ImageResponseFormat>))]
[FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.", true)]
public ImageResponseFormat ResponseFormat { get; }

Expand Down
Loading

0 comments on commit 15c9e2c

Please sign in to comment.