Skip to content

Commit

Permalink
OpenAI-DotNet 7.7.5 (#258)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
StephenHodgson authored Mar 3, 2024
1 parent 8ae5380 commit c17dee4
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 53 deletions.
4 changes: 4 additions & 0 deletions OpenAI-DotNet/Common/FunctionParameterAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ namespace OpenAI
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FunctionParameterAttribute : Attribute
{
/// <summary>
/// Function parameter attribute to help describe the parameter for the function.
/// </summary>
/// <param name="description">The description of the parameter and its usage.</param>
public FunctionParameterAttribute(string description)
{
Description = description;
Expand Down
2 changes: 1 addition & 1 deletion OpenAI-DotNet/Common/FunctionPropertyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace OpenAI
{
[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class FunctionPropertyAttribute : Attribute
{
/// <summary>
Expand Down
175 changes: 125 additions & 50 deletions OpenAI-DotNet/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FunctionParameterAttribute>();

Expand All @@ -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();
Expand All @@ -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<MemberInfo>(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<FunctionPropertyAttribute>();
var jsonPropertyAttribute = property.GetCustomAttribute<JsonPropertyNameAttribute>();
var propertyName = jsonPropertyAttribute?.Name ?? property.Name;
var memberType = GetMemberType(member);
var functionPropertyAttribute = member.GetCustomAttribute<FunctionPropertyAttribute>();
var jsonPropertyAttribute = member.GetCustomAttribute<JsonPropertyNameAttribute>();
var jsonIgnoreAttribute = member.GetCustomAttribute<JsonIgnoreAttribute>();
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)
Expand All @@ -103,7 +179,7 @@ public static JsonObject GenerateJsonSchema(this Type type)

if (functionPropertyAttribute.Required)
{
requiredProperties.Add(propertyName);
memberProperties.Add(propertyName);
}

JsonNode defaultValue = null;
Expand Down Expand Up @@ -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))
};
}
}
7 changes: 6 additions & 1 deletion OpenAI-DotNet/OpenAI-DotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-
<AssemblyOriginatorKeyFile>OpenAI-DotNet.pfx</AssemblyOriginatorKeyFile>
<IncludeSymbols>True</IncludeSymbols>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<Version>7.7.4</Version>
<Version>7.7.5</Version>
<PackageReleaseNotes>
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
Expand Down
3 changes: 2 additions & 1 deletion OpenAI-DotNet/OpenAIClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

/// <summary>
Expand Down

0 comments on commit c17dee4

Please sign in to comment.