diff --git a/src/AnthropicClient/Models/FunctionPropertyAttribute.cs b/src/AnthropicClient/Models/FunctionPropertyAttribute.cs new file mode 100644 index 0000000..6e8ddb0 --- /dev/null +++ b/src/AnthropicClient/Models/FunctionPropertyAttribute.cs @@ -0,0 +1,49 @@ +namespace AnthropicClient.Models; + +/// +/// Attribute to describe a property of a type that is used as a function parameter. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public class FunctionPropertyAttribute : Attribute +{ + /// + /// Description of the property. + /// + public string Description { get; } = string.Empty; + + /// + /// Whether the property is required. + /// + public bool Required { get; } = false; + + /// + /// Default value of the property. + /// + public object? DefaultValue { get; } = null; + + /// + /// Possible values of the property. + /// + public object[]? PossibleValues { get; } = null; + + /// + /// Initializes a new instance of the class. + /// + /// Description of the property. + /// Whether the property is required. + /// Default value of the property. + /// Possible values of the property. + /// A new instance of the class. + public FunctionPropertyAttribute( + string description, + bool required = false, + object? defaultValue = null, + object[]? possibleValues = null + ) + { + Description = description; + Required = required; + DefaultValue = defaultValue; + PossibleValues = possibleValues; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Utils/JsonSchemaGenerator.cs b/src/AnthropicClient/Utils/JsonSchemaGenerator.cs index 59bc565..77d57a0 100644 --- a/src/AnthropicClient/Utils/JsonSchemaGenerator.cs +++ b/src/AnthropicClient/Utils/JsonSchemaGenerator.cs @@ -1,7 +1,9 @@ using System.Reflection; +using System.Text.Json; using System.Text.Json.Nodes; using AnthropicClient.Models; +using AnthropicClient.Json; namespace AnthropicClient.Utils; @@ -179,15 +181,11 @@ static class JsonSchemaGenerator _ => throw new InvalidOperationException("Member is not a property or field") }; - // TODO: Provide attribute to allow specifying... - // 1. Name - // 2. Description - // 3. Required - // 4. Default values - // 5. Possible values + var attribute = member.GetCustomAttribute(); + var memberPropertyName = member.Name; - var memberDescription = string.Empty; - var memberRequired = Nullable.GetUnderlyingType(memberType) is null; + var memberDescription = attribute?.Description ?? string.Empty; + var memberRequired = attribute?.Required ?? Nullable.GetUnderlyingType(memberType) is null; memberProperty = definitions.AsObject().ContainsKey(memberType.FullName) ? new JsonObject() @@ -201,6 +199,38 @@ static class JsonSchemaGenerator memberRequiredProperties.Add(memberPropertyName); } + JsonNode? defaultValue = null; + + if (attribute?.DefaultValue is not null) + { + defaultValue = JsonNode.Parse(JsonSerializer.Serialize(attribute.DefaultValue, JsonSerializationOptions.DefaultOptions)); + memberProperty["default"] = defaultValue; + } + + if (attribute?.PossibleValues is { Length: > 0 }) + { + var enumValues = new JsonArray(); + + foreach (var value in attribute.PossibleValues) + { + var enumValue = JsonNode.Parse(JsonSerializer.Serialize(value, JsonSerializationOptions.DefaultOptions)); + + if (defaultValue is null || JsonNode.DeepEquals(enumValue, defaultValue) is false) + { + enumValues.Add(enumValue); + } + } + + var containsDefaultValue = enumValues.Where(value => JsonNode.DeepEquals(value, defaultValue)).Any(); + + if (defaultValue is not null && containsDefaultValue is false) + { + enumValues.Add(JsonNode.Parse(defaultValue.ToJsonString(JsonSerializationOptions.DefaultOptions))); + } + + memberProperty[EnumKey] = enumValues; + } + memberProperty[DescriptionKey] = memberDescription; memberProperties[memberPropertyName] = memberProperty; } diff --git a/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs b/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs index 791b1a6..906d68c 100644 --- a/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs +++ b/tests/AnthropicClient.Tests/Unit/Utils/JsonSchemaGeneratorTestData.cs @@ -585,7 +585,7 @@ public class JsonSchemaGeneratorTestData : IEnumerable } }; - var family =new Family(); + var family = new Family(); // parameters from instance method yield return new object[] @@ -868,12 +868,70 @@ public class JsonSchemaGeneratorTestData : IEnumerable }, } }; + + // complex type with attributes + yield return new object[] + { + Tool.CreateFromFunction(TestToolName,TestToolDescription,(Rule rule) => rule), + new JsonObject() + { + ["type"] = "object", + ["definitions"] = new JsonObject() + { + [$"{typeof(Rule).FullName}"] = new JsonObject() + { + ["type"] = "object", + ["properties"] = new JsonObject() + { + ["Status"] = new JsonObject() + { + ["type"] = "string", + ["description"] = "Indicates the current status of the rule.", + ["default"] = "Active", + ["enum"] = new JsonArray() + { + "Inactive", + "Active", + } + }, + ["Type"] = new JsonObject() + { + ["type"] = "string", + ["description"] = "The type of the rule.", + ["default"] = "Type A", + ["enum"] = new JsonArray() + { + "Type B", + "Type A", + } + } + }, + ["required"] = new JsonArray() + { + "Status" + } + } + }, + ["properties"] = new JsonObject() + { + ["rule"] = new JsonObject() + { + ["$ref"] = $"#/definitions/{typeof(Rule).FullName}", + ["description"] = string.Empty + } + }, + ["required"] = new JsonArray() + { + "rule", + }, + } + }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } -class Family +class Family { public List Members { get; } = []; @@ -901,4 +959,23 @@ class Person(string name, int age) class Dad { public string Role = "Father"; +} + +class Rule +{ + [FunctionProperty( + "Indicates the current status of the rule.", + true, + "Active", + new object[] { "Active", "Inactive" } + )] + public string Status { get; } = "Active"; + + [FunctionProperty( + "The type of the rule.", + false, + "Type A", + new object[] { "Type B" } + )] + public string Type { get; } = "Type A"; } \ No newline at end of file