diff --git a/src/AnthropicClient/Utils/JsonSchemaGenerator.cs b/src/AnthropicClient/Utils/JsonSchemaGenerator.cs index 95b3c13..61bebd0 100644 --- a/src/AnthropicClient/Utils/JsonSchemaGenerator.cs +++ b/src/AnthropicClient/Utils/JsonSchemaGenerator.cs @@ -1,6 +1,8 @@ using System.Reflection; +using System.Text.Json; using System.Text.Json.Nodes; +using AnthropicClient.Json; using AnthropicClient.Models; namespace AnthropicClient.Utils; @@ -11,19 +13,26 @@ static class JsonSchemaGenerator private const string PropertiesKey = "properties"; private const string RequiredPropertiesKey = "required"; 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 schema = new JsonObject() + var inputSchema = new JsonObject() { - [TypeKey] = Object + [TypeKey] = ObjectType }; if (parameters.Length is 0) { - return schema; + return inputSchema; } var properties = new JsonObject(); @@ -42,10 +51,10 @@ static class JsonSchemaGenerator var paramDescription = attribute?.Description ?? string.Empty; var paramRequired = attribute?.Required ?? !parameter.HasDefaultValue; - var paramObject = new JsonObject(); - paramObject[DescriptionKey] = paramDescription; + var paramSchema = GenerateParameterTypeSchema(parameter.ParameterType, inputSchema); + paramSchema[DescriptionKey] = paramDescription; - properties[paramName] = paramObject; + properties[paramName] = paramSchema; if (paramRequired) { @@ -53,8 +62,103 @@ static class JsonSchemaGenerator } } - schema[PropertiesKey] = properties; - schema[RequiredPropertiesKey] = requiredProperties; - return schema; + inputSchema[PropertiesKey] = properties; + inputSchema[RequiredPropertiesKey] = requiredProperties; + 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}" + }; } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs b/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs new file mode 100644 index 0000000..4350cde --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs @@ -0,0 +1,453 @@ +using System.Collections; +using System.Text.Json.Nodes; + +namespace AnthropicClient.Tests.Unit.Utils; + +public class JsonSchemaGeneratorTestData : IEnumerable +{ + private const string TestToolName = "Test tool name"; + private const string TestToolDescription = "Test tool description"; + + public IEnumerator 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 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(); +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTests.cs b/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTests.cs index 7194e08..c28fe9b 100644 --- a/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTests.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; -namespace AnthropicClient.Tests.Unit.Models; +namespace AnthropicClient.Tests.Unit.Utils; public class JsonSchemaGeneratorTests { @@ -96,4 +96,30 @@ public class JsonSchemaGeneratorTests 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); + } } \ No newline at end of file