Skip to content

Commit bc257af

Browse files
com.openai.unity 7.7.5 (#205)
- 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
1 parent 2d72144 commit bc257af

File tree

6 files changed

+146
-61
lines changed

6 files changed

+146
-61
lines changed

Runtime/Common/FunctionParameterAttribute.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ namespace OpenAI
77
[AttributeUsage(AttributeTargets.Parameter)]
88
public sealed class FunctionParameterAttribute : Attribute
99
{
10+
/// <summary>
11+
/// Function parameter attribute to help describe the parameter for the function.
12+
/// </summary>
13+
/// <param name="description">The description of the parameter and its usage.</param>
1014
public FunctionParameterAttribute(string description)
1115
{
1216
Description = description;

Runtime/Common/FunctionPropertyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace OpenAI
66
{
7-
[AttributeUsage(AttributeTargets.Property)]
7+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
88
public sealed class FunctionPropertyAttribute : Attribute
99
{
1010
/// <summary>

Runtime/Extensions/TypeExtensions.cs

Lines changed: 128 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static JObject GenerateJsonSchema(this MethodInfo methodInfo)
4444
requiredParameters.Add(parameter.Name);
4545
}
4646

47-
schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType);
47+
schema["properties"]![parameter.Name] = GenerateJsonSchema(parameter.ParameterType, schema);
4848

4949
var functionParameterAttribute = parameter.GetCustomAttribute<FunctionParameterAttribute>();
5050

@@ -62,12 +62,57 @@ public static JObject GenerateJsonSchema(this MethodInfo methodInfo)
6262
return schema;
6363
}
6464

65-
public static JObject GenerateJsonSchema(this Type type)
65+
public static JObject GenerateJsonSchema(this Type type, JObject rootSchema)
6666
{
6767
var schema = new JObject();
6868
var serializer = JsonSerializer.Create(OpenAIClient.JsonSerializationOptions);
6969

70-
if (type.IsEnum)
70+
if (!type.IsPrimitive &&
71+
type != typeof(Guid) &&
72+
type != typeof(DateTime) &&
73+
type != typeof(DateTimeOffset) &&
74+
rootSchema["definitions"] != null &&
75+
((JObject)rootSchema["definitions"]).ContainsKey(type.FullName))
76+
{
77+
return new JObject { ["$ref"] = $"#/definitions/{type.FullName}" };
78+
}
79+
80+
if (type == typeof(string))
81+
{
82+
schema["type"] = "string";
83+
}
84+
else if (type == typeof(int) ||
85+
type == typeof(long) ||
86+
type == typeof(uint) ||
87+
type == typeof(byte) ||
88+
type == typeof(sbyte) ||
89+
type == typeof(ulong) ||
90+
type == typeof(short) ||
91+
type == typeof(ushort))
92+
{
93+
schema["type"] = "integer";
94+
}
95+
else if (type == typeof(float) ||
96+
type == typeof(double) ||
97+
type == typeof(decimal))
98+
{
99+
schema["type"] = "number";
100+
}
101+
else if (type == typeof(bool))
102+
{
103+
schema["type"] = "boolean";
104+
}
105+
else if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
106+
{
107+
schema["type"] = "string";
108+
schema["format"] = "date-time";
109+
}
110+
else if (type == typeof(Guid))
111+
{
112+
schema["type"] = "string";
113+
schema["format"] = "uuid";
114+
}
115+
else if (type.IsEnum)
71116
{
72117
schema["type"] = "string";
73118
schema["enum"] = new JArray();
@@ -80,21 +125,54 @@ public static JObject GenerateJsonSchema(this Type type)
80125
else if (type.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)))
81126
{
82127
schema["type"] = "array";
83-
schema["items"] = GenerateJsonSchema(type.GetElementType() ?? type.GetGenericArguments()[0]);
128+
var elementType = type.GetElementType() ?? type.GetGenericArguments()[0];
129+
130+
if (rootSchema["definitions"] != null &&
131+
((JObject)rootSchema["definitions"]).ContainsKey(elementType.FullName))
132+
{
133+
schema["items"] = new JObject { ["$ref"] = $"#/definitions/{elementType.FullName}" };
134+
}
135+
else
136+
{
137+
schema["items"] = GenerateJsonSchema(elementType, rootSchema);
138+
}
84139
}
85-
else if (type.IsClass && type != typeof(string))
140+
else
86141
{
87142
schema["type"] = "object";
88-
var properties = type.GetProperties();
89-
var propertiesInfo = new JObject();
90-
var requiredProperties = new JArray();
143+
rootSchema["definitions"] ??= new JObject();
144+
rootSchema["definitions"][type.FullName] = new JObject();
145+
146+
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
147+
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
148+
var members = new List<MemberInfo>(properties.Length + fields.Length);
149+
members.AddRange(properties);
150+
members.AddRange(fields);
151+
152+
var memberInfo = new JObject();
153+
var requiredMembers = new JArray();
91154

92-
foreach (var property in properties)
155+
foreach (var member in members)
93156
{
94-
var propertyInfo = GenerateJsonSchema(property.PropertyType);
95-
var functionPropertyAttribute = property.GetCustomAttribute<FunctionPropertyAttribute>();
96-
var jsonPropertyAttribute = property.GetCustomAttribute<JsonPropertyAttribute>();
97-
var propertyName = jsonPropertyAttribute?.PropertyName ?? property.Name;
157+
var memberType = GetMemberType(member);
158+
var functionPropertyAttribute = member.GetCustomAttribute<FunctionPropertyAttribute>();
159+
var jsonPropertyAttribute = member.GetCustomAttribute<JsonPropertyAttribute>();
160+
var propertyName = jsonPropertyAttribute?.PropertyName ?? member.Name;
161+
162+
// skip unity engine property for Items
163+
if (memberType == typeof(float) && propertyName.Equals("Item")) { continue; }
164+
165+
JObject propertyInfo;
166+
167+
if (rootSchema["definitions"] != null &&
168+
((JObject)rootSchema["definitions"]).ContainsKey(memberType.FullName))
169+
{
170+
propertyInfo = new JObject { ["$ref"] = $"#/definitions/{memberType.FullName}" };
171+
}
172+
else
173+
{
174+
propertyInfo = GenerateJsonSchema(memberType, rootSchema);
175+
}
98176

99177
// override properties with values from function property attribute
100178
if (functionPropertyAttribute != null)
@@ -103,7 +181,7 @@ public static JObject GenerateJsonSchema(this Type type)
103181

104182
if (functionPropertyAttribute.Required)
105183
{
106-
requiredProperties.Add(propertyName);
184+
requiredMembers.Add(propertyName);
107185
}
108186

109187
JToken defaultValue = null;
@@ -143,52 +221,53 @@ public static JObject GenerateJsonSchema(this Type type)
143221
propertyInfo["enum"] = enums;
144222
}
145223
}
146-
else if (Nullable.GetUnderlyingType(property.PropertyType) == null)
224+
else if (jsonPropertyAttribute != null)
147225
{
148-
requiredProperties.Add(propertyName);
226+
switch (jsonPropertyAttribute.Required)
227+
{
228+
case Required.Always:
229+
case Required.AllowNull:
230+
requiredMembers.Add(propertyName);
231+
break;
232+
case Required.Default:
233+
case Required.DisallowNull:
234+
default:
235+
requiredMembers.Remove(propertyName);
236+
break;
237+
}
238+
}
239+
else if (Nullable.GetUnderlyingType(memberType) == null)
240+
{
241+
if (member is FieldInfo)
242+
{
243+
requiredMembers.Add(propertyName);
244+
}
149245
}
150246

151-
propertiesInfo[propertyName] = propertyInfo;
247+
memberInfo[propertyName] = propertyInfo;
152248
}
153249

154-
schema["properties"] = propertiesInfo;
250+
schema["properties"] = memberInfo;
155251

156-
if (requiredProperties.Count > 0)
157-
{
158-
schema["required"] = requiredProperties;
159-
}
160-
}
161-
else
162-
{
163-
if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte))
252+
if (requiredMembers.Count > 0)
164253
{
165-
schema["type"] = "integer";
166-
}
167-
else if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
168-
{
169-
schema["type"] = "number";
170-
}
171-
else if (type == typeof(bool))
172-
{
173-
schema["type"] = "boolean";
174-
}
175-
else if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
176-
{
177-
schema["type"] = "string";
178-
schema["format"] = "date-time";
179-
}
180-
else if (type == typeof(Guid))
181-
{
182-
schema["type"] = "string";
183-
schema["format"] = "uuid";
184-
}
185-
else
186-
{
187-
schema["type"] = type.Name.ToLower();
254+
schema["required"] = requiredMembers;
188255
}
256+
257+
rootSchema["definitions"] ??= new JObject();
258+
rootSchema["definitions"][type.FullName] = schema;
259+
return new JObject { ["$ref"] = $"#/definitions/{type.FullName}" };
189260
}
190261

191262
return schema;
192263
}
264+
265+
private static Type GetMemberType(MemberInfo member)
266+
=> member switch
267+
{
268+
FieldInfo fieldInfo => fieldInfo.FieldType,
269+
PropertyInfo propertyInfo => propertyInfo.PropertyType,
270+
_ => throw new ArgumentException($"{nameof(MemberInfo)} must be of type {nameof(FieldInfo)}, {nameof(PropertyInfo)}", nameof(member))
271+
};
193272
}
194273
}

Runtime/OpenAIClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ protected override void ValidateAuthentication()
114114
{
115115
NullValueHandling = NullValueHandling.Ignore,
116116
DefaultValueHandling = DefaultValueHandling.Ignore,
117+
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
117118
Converters = new List<JsonConverter>
118119
{
119120
new StringEnumConverter(new SnakeCaseNamingStrategy())

Tests/TestFixture_00_02_Tools.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using NUnit.Framework;
66
using OpenAI.Images;
77
using OpenAI.Tests.Weather;
8+
using System;
89
using System.Collections.Generic;
910
using System.Linq;
1011
using System.Threading.Tasks;
@@ -21,6 +22,7 @@ public void Test_01_GetTools()
2122
Assert.IsNotNull(tools);
2223
Assert.IsNotEmpty(tools);
2324
tools.Add(Tool.GetOrCreateTool(OpenAIClient.ImagesEndPoint, nameof(ImagesEndpoint.GenerateImageAsync)));
25+
tools.Add(Tool.FromFunc<GameObject, Vector2, Vector3, Quaternion, string>("complex_objects", (gameObject, vector2, vector3, quaternion) => "success"));
2426
var json = JsonConvert.SerializeObject(tools, Formatting.Indented, OpenAIClient.JsonSerializationOptions);
2527
Debug.Log(json);
2628
}
@@ -31,7 +33,7 @@ public async Task Test_02_Tool_Funcs()
3133
var tools = new List<Tool>
3234
{
3335
Tool.FromFunc("test_func", Function),
34-
Tool.FromFunc<string, string, string>("test_func_with_args", FunctionWithArgs),
36+
Tool.FromFunc<DateTime, Vector3, string>("test_func_with_args", FunctionWithArgs),
3537
Tool.FromFunc("test_func_weather", () => WeatherService.GetCurrentWeatherAsync("my location", WeatherService.WeatherUnit.Celsius))
3638
};
3739

@@ -44,13 +46,12 @@ public async Task Test_02_Tool_Funcs()
4446
Assert.AreEqual("success", result);
4547
var toolWithArgs = tools[1];
4648
Assert.IsNotNull(toolWithArgs);
47-
toolWithArgs.Function.Arguments = new JObject
48-
{
49-
["arg1"] = "arg1",
50-
["arg2"] = "arg2"
51-
};
49+
var testValue = new { arg1 = DateTime.UtcNow, arg2 = Vector3.one };
50+
toolWithArgs.Function.Arguments = JToken.FromObject(testValue, JsonSerializer.Create(OpenAIClient.JsonSerializationOptions));
5251
var resultWithArgs = toolWithArgs.InvokeFunction<string>();
53-
Assert.AreEqual("arg1 arg2", resultWithArgs);
52+
Debug.Log(resultWithArgs);
53+
var testResult = JsonConvert.DeserializeObject(resultWithArgs, testValue.GetType(), OpenAIClient.JsonSerializationOptions);
54+
Assert.AreEqual(testResult, testValue);
5455

5556
var toolWeather = tools[2];
5657
Assert.IsNotNull(toolWeather);
@@ -64,9 +65,9 @@ private string Function()
6465
return "success";
6566
}
6667

67-
private string FunctionWithArgs(string arg1, string arg2)
68+
private string FunctionWithArgs(DateTime arg1, Vector3 arg2)
6869
{
69-
return $"{arg1} {arg2}";
70+
return JsonConvert.SerializeObject(new { arg1, arg2 }, OpenAIClient.JsonSerializationOptions);
7071
}
7172
}
7273
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "OpenAI",
44
"description": "A OpenAI package for the Unity Game Engine to use GPT-4, GPT-3.5, GPT-3 and Dall-E though their RESTful API (currently in beta).\n\nIndependently developed, this is not an official library and I am not affiliated with OpenAI.\n\nAn OpenAI API account is required.",
55
"keywords": [],
6-
"version": "7.7.4",
6+
"version": "7.7.5",
77
"unity": "2021.3",
88
"documentationUrl": "https://github.com/RageAgainstThePixel/com.openai.unity#documentation",
99
"changelogUrl": "https://github.com/RageAgainstThePixel/com.openai.unity/releases",
@@ -17,7 +17,7 @@
1717
"url": "https://github.com/StephenHodgson"
1818
},
1919
"dependencies": {
20-
"com.utilities.rest": "2.5.3",
20+
"com.utilities.rest": "2.5.4",
2121
"com.utilities.encoder.wav": "1.1.5"
2222
},
2323
"samples": [

0 commit comments

Comments
 (0)