diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index b5ef3774b45..df829d7fb1e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -511,6 +511,10 @@ "Member": "bool Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.IncludeSchemaKeyword { get; init; }", "Stage": "Stable" }, + { + "Member": "System.Func? Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.ParameterDescriptionProvider { get; init; }", + "Stage": "Stable" + }, { "Member": "Microsoft.Extensions.AI.AIJsonSchemaTransformOptions? Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.TransformOptions { get; init; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs index 9da0d72e5a5..6a65e802dd3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.ComponentModel; using System.Reflection; using System.Text.Json.Nodes; using System.Threading; @@ -35,6 +36,18 @@ public sealed record class AIJsonSchemaCreateOptions /// public Func? IncludeParameter { get; init; } + /// + /// Gets a callback that is invoked for each parameter in the provided to + /// to obtain a description for the parameter. + /// + /// + /// The delegate receives a instance and returns a string describing + /// the parameter. If , or if the delegate returns , + /// the description will be sourced from the metadata (like ), + /// if available. + /// + public Func? ParameterDescriptionProvider { get; init; } + /// /// Gets a governing transformations on the JSON schema after it has been generated. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs index 1fa24375079..200b470d4d8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs @@ -109,10 +109,16 @@ public static JsonElement CreateFunctionJsonSchema( } bool hasDefaultValue = TryGetEffectiveDefaultValue(parameter, out object? defaultValue); + + // Use a description from the description provider, if available. Otherwise, fall back to the DescriptionAttribute. + string? parameterDescription = + inferenceOptions.ParameterDescriptionProvider?.Invoke(parameter) ?? + parameter.GetCustomAttribute(inherit: true)?.Description; + JsonNode parameterSchema = CreateJsonSchemaCore( type: parameter.ParameterType, parameter: parameter, - description: parameter.GetCustomAttribute(inherit: true)?.Description, + description: parameterDescription, hasDefaultValue: hasDefaultValue, defaultValue: defaultValue, serializerOptions, diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index dcd6836b5da..ba159debee1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -109,6 +109,12 @@ public static void AIJsonSchemaCreateOptions_UsesStructuralEquality() property.SetValue(options2, includeParameter); break; + case null when property.PropertyType == typeof(Func): + Func parameterDescriptionProvider = static (parameter) => "description"; + property.SetValue(options1, parameterDescriptionProvider); + property.SetValue(options2, parameterDescriptionProvider); + break; + case null when property.PropertyType == typeof(AIJsonSchemaTransformOptions): AIJsonSchemaTransformOptions transformOptions = new AIJsonSchemaTransformOptions { RequireAllProperties = true }; property.SetValue(options1, transformOptions); @@ -1164,6 +1170,108 @@ public static void CreateFunctionJsonSchema_InvokesIncludeParameterCallbackForEv Assert.Contains("fifth", schemaString); } + [Fact] + public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_OverridesDescriptionAttribute() + { + Delegate method = ( + [Description("Original description for first")] int first, + [Description("Original description for second")] string second) => + { + }; + + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new() + { + ParameterDescriptionProvider = p => p.Name == "first" ? "Overridden description for first" : null + }); + + JsonElement properties = schema.GetProperty("properties"); + Assert.Equal("Overridden description for first", properties.GetProperty("first").GetProperty("description").GetString()); + Assert.Equal("Original description for second", properties.GetProperty("second").GetProperty("description").GetString()); + } + + [Fact] + public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_AddsDescriptionWhenAttributeMissing() + { + Delegate method = (int first, string second) => + { + }; + + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new() + { + ParameterDescriptionProvider = p => p.Name switch + { + "first" => "Added description for first", + "second" => "Added description for second", + _ => null + } + }); + + JsonElement properties = schema.GetProperty("properties"); + Assert.Equal("Added description for first", properties.GetProperty("first").GetProperty("description").GetString()); + Assert.Equal("Added description for second", properties.GetProperty("second").GetProperty("description").GetString()); + } + + [Fact] + public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_ReturnsNull_UsesAttributeDescriptions() + { + Delegate method = ( + [Description("Description from attribute")] int first, + string second) => + { + }; + + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new() + { + ParameterDescriptionProvider = _ => null + }); + + JsonElement properties = schema.GetProperty("properties"); + Assert.Equal("Description from attribute", properties.GetProperty("first").GetProperty("description").GetString()); + Assert.False(properties.GetProperty("second").TryGetProperty("description", out _)); + } + + [Fact] + public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_NullValue_UsesAttributeDescriptions() + { + Delegate method = ( + [Description("Description from attribute")] int first, + string second) => + { + }; + + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new() + { + ParameterDescriptionProvider = null + }); + + JsonElement properties = schema.GetProperty("properties"); + Assert.Equal("Description from attribute", properties.GetProperty("first").GetProperty("description").GetString()); + Assert.False(properties.GetProperty("second").TryGetProperty("description", out _)); + } + + [Fact] + public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_OnlyCalledForActualParameters() + { + Delegate method = (int first, string second) => + { + }; + + List calledParameterNames = []; + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new() + { + ParameterDescriptionProvider = p => + { + calledParameterNames.Add(p.Name); + return p.Name == "first" ? "Description for first" : null; + } + }); + + JsonElement properties = schema.GetProperty("properties"); + Assert.Equal(2, properties.EnumerateObject().Count()); + Assert.Equal("Description for first", properties.GetProperty("first").GetProperty("description").GetString()); + Assert.Equal(["first", "second"], calledParameterNames); + } + [Fact] public static void TransformJsonSchema_ConvertBooleanSchemas() {