feat: implement json schema generation for non-complex types

This commit is contained in:
Stevan Freeborn
2024-06-30 13:57:35 -05:00
parent 4fd8ecd96b
commit 96ed89c9ce
3 changed files with 595 additions and 12 deletions
+115 -11
View File
@@ -1,6 +1,8 @@
using System.Reflection; using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using AnthropicClient.Json;
using AnthropicClient.Models; using AnthropicClient.Models;
namespace AnthropicClient.Utils; namespace AnthropicClient.Utils;
@@ -11,19 +13,26 @@ static class JsonSchemaGenerator
private const string PropertiesKey = "properties"; private const string PropertiesKey = "properties";
private const string RequiredPropertiesKey = "required"; private const string RequiredPropertiesKey = "required";
private const string DescriptionKey = "description"; private const string DescriptionKey = "description";
private const string Object = "object"; private const string DefinitionsKey = "definitions";
private const string RefKey = "$ref";
private const string ItemsKey = "items";
private const string EnumKey = "enum";
private const string FormatKey = "format";
private const string ObjectType = "object";
private const string StringType = "string";
private const string ArrayType = "array";
public static JsonNode GenerateInputSchema(AnthropicFunction function) internal static JsonObject GenerateInputSchema(AnthropicFunction function)
{ {
var parameters = function.Method.GetParameters(); var parameters = function.Method.GetParameters();
var schema = new JsonObject() var inputSchema = new JsonObject()
{ {
[TypeKey] = Object [TypeKey] = ObjectType
}; };
if (parameters.Length is 0) if (parameters.Length is 0)
{ {
return schema; return inputSchema;
} }
var properties = new JsonObject(); var properties = new JsonObject();
@@ -42,10 +51,10 @@ static class JsonSchemaGenerator
var paramDescription = attribute?.Description ?? string.Empty; var paramDescription = attribute?.Description ?? string.Empty;
var paramRequired = attribute?.Required ?? !parameter.HasDefaultValue; var paramRequired = attribute?.Required ?? !parameter.HasDefaultValue;
var paramObject = new JsonObject(); var paramSchema = GenerateParameterTypeSchema(parameter.ParameterType, inputSchema);
paramObject[DescriptionKey] = paramDescription; paramSchema[DescriptionKey] = paramDescription;
properties[paramName] = paramObject; properties[paramName] = paramSchema;
if (paramRequired) if (paramRequired)
{ {
@@ -53,8 +62,103 @@ static class JsonSchemaGenerator
} }
} }
schema[PropertiesKey] = properties; inputSchema[PropertiesKey] = properties;
schema[RequiredPropertiesKey] = requiredProperties; inputSchema[RequiredPropertiesKey] = requiredProperties;
return schema; return inputSchema;
}
private static JsonObject GenerateParameterTypeSchema(Type type, JsonObject inputSchema)
{
var definitions = inputSchema[DefinitionsKey];
// Check if a definition for the type already exists
// If it does, return a reference to the definition
// no need to evaluate the type further
if (definitions is not null && definitions.AsObject().ContainsKey(type.FullName))
{
return new JsonObject()
{
[RefKey] = $"#/definitions/{type.FullName}"
};
}
var paramSchema = type switch
{
var t when t == typeof(string) || t == typeof(char) => new JsonObject()
{
[TypeKey] = StringType
},
var t when
t == typeof(int) ||
t == typeof(uint) ||
t == typeof(long) ||
t == typeof(ulong) ||
t == typeof(short) ||
t == typeof(ushort) ||
t == typeof(byte) ||
t == typeof(sbyte) => new JsonObject()
{
[TypeKey] = "integer"
},
var t when t == typeof(bool) => new JsonObject()
{
[TypeKey] = "boolean"
},
var t when
t == typeof(double) ||
t == typeof(float) ||
t == typeof(decimal) => new JsonObject()
{
[TypeKey] = "number"
},
var t when t == typeof(DateTime) || t == typeof(DateTimeOffset) => new JsonObject()
{
[TypeKey] = StringType,
[FormatKey] = "date-time"
},
var t when t == typeof(Guid) => new JsonObject()
{
[TypeKey] = StringType,
[FormatKey] = "uuid"
},
var t when t.IsEnum => new JsonObject()
{
[TypeKey] = StringType,
[EnumKey] = Enum.GetNames(t).Aggregate(
new JsonArray(), (acc, name) =>
{
acc.Add(name);
return acc;
}
)
},
var t when t.IsArray => new JsonObject()
{
[TypeKey] = ArrayType,
[ItemsKey] = GenerateParameterTypeSchema(t.GetElementType()!, inputSchema)
},
var t when t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>) => new JsonObject()
{
[TypeKey] = ArrayType,
[ItemsKey] = GenerateParameterTypeSchema(t.GetGenericArguments()[0], inputSchema)
},
_ => GenerateTypeDefinitionSchema(type, inputSchema)
};
return paramSchema;
}
private static JsonObject GenerateTypeDefinitionSchema(Type type, JsonObject inputSchema)
{
var definitions = inputSchema[DefinitionsKey] ?? new JsonObject();
var typeSchema = new JsonObject();
// TODO: Implement schema generation for complex types
definitions[type.FullName] = typeSchema;
return new JsonObject()
{
[RefKey] = $"#/definitions/{type.FullName}"
};
} }
} }
@@ -0,0 +1,453 @@
using System.Collections;
using System.Text.Json.Nodes;
namespace AnthropicClient.Tests.Unit.Utils;
public class JsonSchemaGeneratorTestData : IEnumerable<object[]>
{
private const string TestToolName = "Test tool name";
private const string TestToolDescription = "Test tool description";
public IEnumerator<object[]> GetEnumerator()
{
// string parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(string name) => name),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["name"] = new JsonObject()
{
["type"] = "string",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"name"
},
}
};
// char parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(char name) => name),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["name"] = new JsonObject()
{
["type"] = "string",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"name"
},
}
};
// int parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(int age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// uint parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(uint age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// long parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(long age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// ulong parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(ulong age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// short parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(short age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// ushort parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(ushort age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// byte parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(byte age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// sbyte parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(sbyte age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "integer",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// bool parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(bool isAdult) => isAdult),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["isAdult"] = new JsonObject()
{
["type"] = "boolean",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"isAdult"
},
}
};
// double parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(double age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "number",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// float parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(float age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "number",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// decimal parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(decimal age) => age),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["age"] = new JsonObject()
{
["type"] = "number",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"age"
},
}
};
// datetime parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(DateTime date) => date),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["date"] = new JsonObject()
{
["type"] = "string",
["format"] = "date-time",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"date"
},
}
};
// datetimeoffset parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(DateTimeOffset date) => date),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["date"] = new JsonObject()
{
["type"] = "string",
["format"] = "date-time",
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"date"
},
}
};
// enum parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(DayOfWeek day) => day),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["day"] = new JsonObject()
{
["type"] = "string",
["description"] = string.Empty,
["enum"] = new JsonArray()
{
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
},
}
},
["required"] = new JsonArray()
{
"day"
},
}
};
// array parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(int[] numbers) => numbers),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["numbers"] = new JsonObject()
{
["type"] = "array",
["items"] = new JsonObject()
{
["type"] = "integer"
},
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"numbers"
},
}
};
// list parameter type
yield return new object[]
{
Tool.CreateFromFunction(TestToolName,TestToolDescription,(List<int> numbers) => numbers),
new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject()
{
["numbers"] = new JsonObject()
{
["type"] = "array",
["items"] = new JsonObject()
{
["type"] = "integer"
},
["description"] = string.Empty
}
},
["required"] = new JsonArray()
{
"numbers"
},
}
};
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
@@ -1,6 +1,6 @@
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
namespace AnthropicClient.Tests.Unit.Models; namespace AnthropicClient.Tests.Unit.Utils;
public class JsonSchemaGeneratorTests public class JsonSchemaGeneratorTests
{ {
@@ -96,4 +96,30 @@ public class JsonSchemaGeneratorTests
JsonAssert.Equal(expectedSchema, schema); JsonAssert.Equal(expectedSchema, schema);
} }
[Fact]
public void GenerateInputSchema_GivenFunctionWithParametersThatIncludesCancellationToken_ItShouldReturnSchemaWithoutCancellationToken()
{
var expectedSchema = new JsonObject()
{
["type"] = "object",
["properties"] = new JsonObject(),
["required"] = new JsonArray(),
};
var testMethod = (CancellationToken token) => true;
var function = new AnthropicFunction(testMethod.Method);
var schema = JsonSchemaGenerator.GenerateInputSchema(function);
JsonAssert.Equal(expectedSchema, schema);
}
[Theory]
[ClassData(typeof(JsonSchemaGeneratorTestData))]
public void GenerateInputSchema_GivenFunction_ItShouldReturnExpectedSchema(Tool tool, JsonObject expectedSchema)
{
var schema = JsonSchemaGenerator.GenerateInputSchema(tool.Function);
JsonAssert.Equal(expectedSchema, schema);
}
} }