diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs index aaca11c4979..76f8a486ad1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs @@ -88,7 +88,7 @@ public static ChatResponseFormatJson ForJsonSchema( return ForJsonSchema( schema, - schemaName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"), + schemaName ?? schemaType.GetCustomAttribute()?.DisplayName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"), schemaDescription ?? schemaType.GetCustomAttribute()?.Description); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index bd382079c80..d9318f98585 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -120,7 +120,7 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio /// The method to be represented via the created . /// /// The name to use for the . If , the name will be derived from - /// the name of . + /// any on , if available, or else from the name of . /// /// /// The description to use for the . If , a description will be derived from @@ -297,7 +297,7 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac /// /// /// The name to use for the . If , the name will be derived from - /// the name of . + /// any on , if available, or else from the name of . /// /// /// The description to use for the . If , a description will be derived from @@ -729,7 +729,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions, out Type? returnType); Method = key.Method; - Name = key.Name ?? GetFunctionName(key.Method); + Name = key.Name ?? key.Method.GetCustomAttribute(inherit: true)?.DisplayName ?? GetFunctionName(key.Method); Description = key.Description ?? key.Method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; JsonSerializerOptions = serializerOptions; ReturnJsonSchema = returnType is null || key.ExcludeResultSchema ? null : AIJsonUtilities.CreateJsonSchema( diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index ab49eeb7f24..5caef21900c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -39,7 +39,7 @@ public AIFunctionFactoryOptions() /// Gets or sets the name to use for the function. /// - /// The name to use for the function. The default value is a name derived from the method represented by the passed or . + /// The name to use for the function. The default value is a name derived from the passed or (for example, via a on the method). /// public string? Name { get; set; } 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 e905ce93859..ad15c62aef8 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 @@ -80,7 +80,7 @@ public static JsonElement CreateFunctionJsonSchema( serializerOptions ??= DefaultOptions; inferenceOptions ??= AIJsonSchemaCreateOptions.Default; - title ??= method.Name; + title ??= method.GetCustomAttribute()?.DisplayName ?? method.Name; description ??= method.GetCustomAttribute()?.Description; JsonObject parameterSchemas = new(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs index 9ac67ff20dc..420871ca9e6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs @@ -169,6 +169,34 @@ public void ForJsonSchema_ComplexType_Succeeds(bool generic, string? name, strin Assert.Equal(description ?? "abcd", format.SchemaDescription); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ForJsonSchema_DisplayNameAttribute_UsedForSchemaName(bool generic) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema(TestJsonSerializerContext.Default.Options) : + ChatResponseFormat.ForJsonSchema(typeof(TypeWithDisplayName), TestJsonSerializerContext.Default.Options); + + Assert.NotNull(format); + Assert.NotNull(format.Schema); + Assert.Equal("custom_type_name", format.SchemaName); + Assert.Equal("Type description", format.SchemaDescription); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ForJsonSchema_DisplayNameAttribute_CanBeOverridden(bool generic) + { + ChatResponseFormatJson format = generic ? + ChatResponseFormat.ForJsonSchema(TestJsonSerializerContext.Default.Options, schemaName: "override_name") : + ChatResponseFormat.ForJsonSchema(typeof(TypeWithDisplayName), TestJsonSerializerContext.Default.Options, schemaName: "override_name"); + + Assert.NotNull(format); + Assert.Equal("override_name", format.SchemaName); + } + [Description("abcd")] public class SomeType { @@ -178,4 +206,11 @@ public class SomeType [Description("hijk")] public string? SomeString { get; set; } } + + [DisplayName("custom_type_name")] + [Description("Type description")] + public class TypeWithDisplayName + { + public int Value { get; set; } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index d011f8a9030..6f087bbc56c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -37,5 +37,6 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(decimal))] // Used in Content tests [JsonSerializable(typeof(HostedMcpServerToolApprovalMode))] [JsonSerializable(typeof(ChatResponseFormatTests.SomeType))] +[JsonSerializable(typeof(ChatResponseFormatTests.TypeWithDisplayName))] [JsonSerializable(typeof(ResponseContinuationToken))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; 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 d83e4ad716b..cb61fcf7086 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -423,6 +423,43 @@ public static void CreateFunctionJsonSchema_ReadsParameterDataAnnotationAttribut AssertDeepEquals(expectedSchema.RootElement, func.JsonSchema); } + [Fact] + public static void CreateFunctionJsonSchema_DisplayNameAttribute_UsedForTitle() + { + [DisplayName("custom_method_name")] + [Description("Method description")] + static void TestMethod(int x, int y) + { + // Test method for schema generation + } + + var method = ((Action)TestMethod).Method; + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method); + + using JsonDocument doc = JsonDocument.Parse(schema.GetRawText()); + Assert.True(doc.RootElement.TryGetProperty("title", out JsonElement titleElement)); + Assert.Equal("custom_method_name", titleElement.GetString()); + Assert.True(doc.RootElement.TryGetProperty("description", out JsonElement descElement)); + Assert.Equal("Method description", descElement.GetString()); + } + + [Fact] + public static void CreateFunctionJsonSchema_DisplayNameAttribute_CanBeOverridden() + { + [DisplayName("custom_method_name")] + static void TestMethod() + { + // Test method for schema generation + } + + var method = ((Action)TestMethod).Method; + JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method, title: "override_title"); + + using JsonDocument doc = JsonDocument.Parse(schema.GetRawText()); + Assert.True(doc.RootElement.TryGetProperty("title", out JsonElement titleElement)); + Assert.Equal("override_title", titleElement.GetString()); + } + [Fact] public static void CreateJsonSchema_CanBeBoolean() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 8094d4230ef..459f03028ab 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -237,6 +237,39 @@ public void Metadata_DerivedFromLambda() p => Assert.Equal("This is B", p.GetCustomAttribute()?.Description)); } + [Fact] + public void Metadata_DisplayNameAttribute() + { + // Test DisplayNameAttribute on a delegate method + Func funcWithDisplayName = [DisplayName("get_user_id")] () => "test"; + AIFunction func = AIFunctionFactory.Create(funcWithDisplayName); + Assert.Equal("get_user_id", func.Name); + Assert.Empty(func.Description); + + // Test DisplayNameAttribute with DescriptionAttribute + Func funcWithBoth = [DisplayName("my_function")][Description("A test function")] () => "test"; + func = AIFunctionFactory.Create(funcWithBoth); + Assert.Equal("my_function", func.Name); + Assert.Equal("A test function", func.Description); + + // Test that explicit name parameter takes precedence over DisplayNameAttribute + func = AIFunctionFactory.Create(funcWithDisplayName, name: "explicit_name"); + Assert.Equal("explicit_name", func.Name); + + // Test DisplayNameAttribute with options + func = AIFunctionFactory.Create(funcWithDisplayName, new AIFunctionFactoryOptions()); + Assert.Equal("get_user_id", func.Name); + + // Test that options.Name takes precedence over DisplayNameAttribute + func = AIFunctionFactory.Create(funcWithDisplayName, new AIFunctionFactoryOptions { Name = "options_name" }); + Assert.Equal("options_name", func.Name); + + // Test function without DisplayNameAttribute falls back to method name + Func funcWithoutDisplayName = () => "test"; + func = AIFunctionFactory.Create(funcWithoutDisplayName); + Assert.Contains("Metadata_DisplayNameAttribute", func.Name); // Will contain the lambda method name + } + [Fact] public void AIFunctionFactoryCreateOptions_ValuesPropagateToAIFunction() {