Skip to content

Commit 51e981b

Browse files
Copiloteiriktsarpalisstephentoub
authored
Support DisplayNameAttribute for name resolution in AI libraries (#6942)
--------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
1 parent 0947207 commit 51e981b

File tree

8 files changed

+112
-6
lines changed

8 files changed

+112
-6
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormat.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public static ChatResponseFormatJson ForJsonSchema(
8888

8989
return ForJsonSchema(
9090
schema,
91-
schemaName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"),
91+
schemaName ?? schemaType.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? InvalidNameCharsRegex().Replace(schemaType.Name, "_"),
9292
schemaDescription ?? schemaType.GetCustomAttribute<DescriptionAttribute>()?.Description);
9393
}
9494

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio
120120
/// <param name="method">The method to be represented via the created <see cref="AIFunction"/>.</param>
121121
/// <param name="name">
122122
/// The name to use for the <see cref="AIFunction"/>. If <see langword="null"/>, the name will be derived from
123-
/// the name of <paramref name="method"/>.
123+
/// any <see cref="DisplayNameAttribute"/> on <paramref name="method"/>, if available, or else from the name of <paramref name="method"/>.
124124
/// </param>
125125
/// <param name="description">
126126
/// The description to use for the <see cref="AIFunction"/>. If <see langword="null"/>, a description will be derived from
@@ -297,7 +297,7 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac
297297
/// </param>
298298
/// <param name="name">
299299
/// The name to use for the <see cref="AIFunction"/>. If <see langword="null"/>, the name will be derived from
300-
/// the name of <paramref name="method"/>.
300+
/// any <see cref="DisplayNameAttribute"/> on <paramref name="method"/>, if available, or else from the name of <paramref name="method"/>.
301301
/// </param>
302302
/// <param name="description">
303303
/// The description to use for the <see cref="AIFunction"/>. If <see langword="null"/>, a description will be derived from
@@ -729,7 +729,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions
729729

730730
ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions, out Type? returnType);
731731
Method = key.Method;
732-
Name = key.Name ?? GetFunctionName(key.Method);
732+
Name = key.Name ?? key.Method.GetCustomAttribute<DisplayNameAttribute>(inherit: true)?.DisplayName ?? GetFunctionName(key.Method);
733733
Description = key.Description ?? key.Method.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description ?? string.Empty;
734734
JsonSerializerOptions = serializerOptions;
735735
ReturnJsonSchema = returnType is null || key.ExcludeResultSchema ? null : AIJsonUtilities.CreateJsonSchema(

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public AIFunctionFactoryOptions()
3939

4040
/// <summary>Gets or sets the name to use for the function.</summary>
4141
/// <value>
42-
/// The name to use for the function. The default value is a name derived from the method represented by the passed <see cref="Delegate"/> or <see cref="MethodInfo"/>.
42+
/// The name to use for the function. The default value is a name derived from the passed <see cref="Delegate"/> or <see cref="MethodInfo"/> (for example, via a <see cref="DisplayNameAttribute"/> on the method).
4343
/// </value>
4444
public string? Name { get; set; }
4545

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public static JsonElement CreateFunctionJsonSchema(
8080

8181
serializerOptions ??= DefaultOptions;
8282
inferenceOptions ??= AIJsonSchemaCreateOptions.Default;
83-
title ??= method.Name;
83+
title ??= method.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? method.Name;
8484
description ??= method.GetCustomAttribute<DescriptionAttribute>()?.Description;
8585

8686
JsonObject parameterSchemas = new();

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseFormatTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,34 @@ public void ForJsonSchema_ComplexType_Succeeds(bool generic, string? name, strin
169169
Assert.Equal(description ?? "abcd", format.SchemaDescription);
170170
}
171171

172+
[Theory]
173+
[InlineData(false)]
174+
[InlineData(true)]
175+
public void ForJsonSchema_DisplayNameAttribute_UsedForSchemaName(bool generic)
176+
{
177+
ChatResponseFormatJson format = generic ?
178+
ChatResponseFormat.ForJsonSchema<TypeWithDisplayName>(TestJsonSerializerContext.Default.Options) :
179+
ChatResponseFormat.ForJsonSchema(typeof(TypeWithDisplayName), TestJsonSerializerContext.Default.Options);
180+
181+
Assert.NotNull(format);
182+
Assert.NotNull(format.Schema);
183+
Assert.Equal("custom_type_name", format.SchemaName);
184+
Assert.Equal("Type description", format.SchemaDescription);
185+
}
186+
187+
[Theory]
188+
[InlineData(false)]
189+
[InlineData(true)]
190+
public void ForJsonSchema_DisplayNameAttribute_CanBeOverridden(bool generic)
191+
{
192+
ChatResponseFormatJson format = generic ?
193+
ChatResponseFormat.ForJsonSchema<TypeWithDisplayName>(TestJsonSerializerContext.Default.Options, schemaName: "override_name") :
194+
ChatResponseFormat.ForJsonSchema(typeof(TypeWithDisplayName), TestJsonSerializerContext.Default.Options, schemaName: "override_name");
195+
196+
Assert.NotNull(format);
197+
Assert.Equal("override_name", format.SchemaName);
198+
}
199+
172200
[Description("abcd")]
173201
public class SomeType
174202
{
@@ -178,4 +206,11 @@ public class SomeType
178206
[Description("hijk")]
179207
public string? SomeString { get; set; }
180208
}
209+
210+
[DisplayName("custom_type_name")]
211+
[Description("Type description")]
212+
public class TypeWithDisplayName
213+
{
214+
public int Value { get; set; }
215+
}
181216
}

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ namespace Microsoft.Extensions.AI;
3737
[JsonSerializable(typeof(decimal))] // Used in Content tests
3838
[JsonSerializable(typeof(HostedMcpServerToolApprovalMode))]
3939
[JsonSerializable(typeof(ChatResponseFormatTests.SomeType))]
40+
[JsonSerializable(typeof(ChatResponseFormatTests.TypeWithDisplayName))]
4041
[JsonSerializable(typeof(ResponseContinuationToken))]
4142
internal sealed partial class TestJsonSerializerContext : JsonSerializerContext;

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,43 @@ public static void CreateFunctionJsonSchema_ReadsParameterDataAnnotationAttribut
423423
AssertDeepEquals(expectedSchema.RootElement, func.JsonSchema);
424424
}
425425

426+
[Fact]
427+
public static void CreateFunctionJsonSchema_DisplayNameAttribute_UsedForTitle()
428+
{
429+
[DisplayName("custom_method_name")]
430+
[Description("Method description")]
431+
static void TestMethod(int x, int y)
432+
{
433+
// Test method for schema generation
434+
}
435+
436+
var method = ((Action<int, int>)TestMethod).Method;
437+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method);
438+
439+
using JsonDocument doc = JsonDocument.Parse(schema.GetRawText());
440+
Assert.True(doc.RootElement.TryGetProperty("title", out JsonElement titleElement));
441+
Assert.Equal("custom_method_name", titleElement.GetString());
442+
Assert.True(doc.RootElement.TryGetProperty("description", out JsonElement descElement));
443+
Assert.Equal("Method description", descElement.GetString());
444+
}
445+
446+
[Fact]
447+
public static void CreateFunctionJsonSchema_DisplayNameAttribute_CanBeOverridden()
448+
{
449+
[DisplayName("custom_method_name")]
450+
static void TestMethod()
451+
{
452+
// Test method for schema generation
453+
}
454+
455+
var method = ((Action)TestMethod).Method;
456+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method, title: "override_title");
457+
458+
using JsonDocument doc = JsonDocument.Parse(schema.GetRawText());
459+
Assert.True(doc.RootElement.TryGetProperty("title", out JsonElement titleElement));
460+
Assert.Equal("override_title", titleElement.GetString());
461+
}
462+
426463
[Fact]
427464
public static void CreateJsonSchema_CanBeBoolean()
428465
{

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,39 @@ public void Metadata_DerivedFromLambda()
237237
p => Assert.Equal("This is B", p.GetCustomAttribute<DescriptionAttribute>()?.Description));
238238
}
239239

240+
[Fact]
241+
public void Metadata_DisplayNameAttribute()
242+
{
243+
// Test DisplayNameAttribute on a delegate method
244+
Func<string> funcWithDisplayName = [DisplayName("get_user_id")] () => "test";
245+
AIFunction func = AIFunctionFactory.Create(funcWithDisplayName);
246+
Assert.Equal("get_user_id", func.Name);
247+
Assert.Empty(func.Description);
248+
249+
// Test DisplayNameAttribute with DescriptionAttribute
250+
Func<string> funcWithBoth = [DisplayName("my_function")][Description("A test function")] () => "test";
251+
func = AIFunctionFactory.Create(funcWithBoth);
252+
Assert.Equal("my_function", func.Name);
253+
Assert.Equal("A test function", func.Description);
254+
255+
// Test that explicit name parameter takes precedence over DisplayNameAttribute
256+
func = AIFunctionFactory.Create(funcWithDisplayName, name: "explicit_name");
257+
Assert.Equal("explicit_name", func.Name);
258+
259+
// Test DisplayNameAttribute with options
260+
func = AIFunctionFactory.Create(funcWithDisplayName, new AIFunctionFactoryOptions());
261+
Assert.Equal("get_user_id", func.Name);
262+
263+
// Test that options.Name takes precedence over DisplayNameAttribute
264+
func = AIFunctionFactory.Create(funcWithDisplayName, new AIFunctionFactoryOptions { Name = "options_name" });
265+
Assert.Equal("options_name", func.Name);
266+
267+
// Test function without DisplayNameAttribute falls back to method name
268+
Func<string> funcWithoutDisplayName = () => "test";
269+
func = AIFunctionFactory.Create(funcWithoutDisplayName);
270+
Assert.Contains("Metadata_DisplayNameAttribute", func.Name); // Will contain the lambda method name
271+
}
272+
240273
[Fact]
241274
public void AIFunctionFactoryCreateOptions_ValuesPropagateToAIFunction()
242275
{

0 commit comments

Comments
 (0)