Skip to content

Commit dda277d

Browse files
authored
Add AIJsonSchemaCreateOptions.ParameterDescriptions (#7068)
* Add AIJsonSchemaCreateOptions.ParameterDescriptions To enable dynamically providing parameter descriptions. * Use Func instead of Dictionary
1 parent 8c1ca00 commit dda277d

File tree

4 files changed

+132
-1
lines changed

4 files changed

+132
-1
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,10 @@
511511
"Member": "bool Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.IncludeSchemaKeyword { get; init; }",
512512
"Stage": "Stable"
513513
},
514+
{
515+
"Member": "System.Func<System.Reflection.ParameterInfo, string?>? Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.ParameterDescriptionProvider { get; init; }",
516+
"Stage": "Stable"
517+
},
514518
{
515519
"Member": "Microsoft.Extensions.AI.AIJsonSchemaTransformOptions? Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.TransformOptions { get; init; }",
516520
"Stage": "Stable"

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaCreateOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.ComponentModel;
56
using System.Reflection;
67
using System.Text.Json.Nodes;
78
using System.Threading;
@@ -35,6 +36,18 @@ public sealed record class AIJsonSchemaCreateOptions
3536
/// </remarks>
3637
public Func<ParameterInfo, bool>? IncludeParameter { get; init; }
3738

39+
/// <summary>
40+
/// Gets a callback that is invoked for each parameter in the <see cref="MethodBase"/> provided to
41+
/// <see cref="AIJsonUtilities.CreateFunctionJsonSchema"/> to obtain a description for the parameter.
42+
/// </summary>
43+
/// <remarks>
44+
/// The delegate receives a <see cref="ParameterInfo"/> instance and returns a string describing
45+
/// the parameter. If <see langword="null"/>, or if the delegate returns <see langword="null"/>,
46+
/// the description will be sourced from the <see cref="MethodBase"/> metadata (like <see cref="DescriptionAttribute"/>),
47+
/// if available.
48+
/// </remarks>
49+
public Func<ParameterInfo, string?>? ParameterDescriptionProvider { get; init; }
50+
3851
/// <summary>
3952
/// Gets a <see cref="AIJsonSchemaTransformOptions"/> governing transformations on the JSON schema after it has been generated.
4053
/// </summary>

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,16 @@ public static JsonElement CreateFunctionJsonSchema(
109109
}
110110

111111
bool hasDefaultValue = TryGetEffectiveDefaultValue(parameter, out object? defaultValue);
112+
113+
// Use a description from the description provider, if available. Otherwise, fall back to the DescriptionAttribute.
114+
string? parameterDescription =
115+
inferenceOptions.ParameterDescriptionProvider?.Invoke(parameter) ??
116+
parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description;
117+
112118
JsonNode parameterSchema = CreateJsonSchemaCore(
113119
type: parameter.ParameterType,
114120
parameter: parameter,
115-
description: parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
121+
description: parameterDescription,
116122
hasDefaultValue: hasDefaultValue,
117123
defaultValue: defaultValue,
118124
serializerOptions,

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ public static void AIJsonSchemaCreateOptions_UsesStructuralEquality()
109109
property.SetValue(options2, includeParameter);
110110
break;
111111
112+
case null when property.PropertyType == typeof(Func<ParameterInfo, string?>):
113+
Func<ParameterInfo, string?> parameterDescriptionProvider = static (parameter) => "description";
114+
property.SetValue(options1, parameterDescriptionProvider);
115+
property.SetValue(options2, parameterDescriptionProvider);
116+
break;
117+
112118
case null when property.PropertyType == typeof(AIJsonSchemaTransformOptions):
113119
AIJsonSchemaTransformOptions transformOptions = new AIJsonSchemaTransformOptions { RequireAllProperties = true };
114120
property.SetValue(options1, transformOptions);
@@ -1164,6 +1170,108 @@ public static void CreateFunctionJsonSchema_InvokesIncludeParameterCallbackForEv
11641170
Assert.Contains("fifth", schemaString);
11651171
}
11661172

1173+
[Fact]
1174+
public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_OverridesDescriptionAttribute()
1175+
{
1176+
Delegate method = (
1177+
[Description("Original description for first")] int first,
1178+
[Description("Original description for second")] string second) =>
1179+
{
1180+
};
1181+
1182+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new()
1183+
{
1184+
ParameterDescriptionProvider = p => p.Name == "first" ? "Overridden description for first" : null
1185+
});
1186+
1187+
JsonElement properties = schema.GetProperty("properties");
1188+
Assert.Equal("Overridden description for first", properties.GetProperty("first").GetProperty("description").GetString());
1189+
Assert.Equal("Original description for second", properties.GetProperty("second").GetProperty("description").GetString());
1190+
}
1191+
1192+
[Fact]
1193+
public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_AddsDescriptionWhenAttributeMissing()
1194+
{
1195+
Delegate method = (int first, string second) =>
1196+
{
1197+
};
1198+
1199+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new()
1200+
{
1201+
ParameterDescriptionProvider = p => p.Name switch
1202+
{
1203+
"first" => "Added description for first",
1204+
"second" => "Added description for second",
1205+
_ => null
1206+
}
1207+
});
1208+
1209+
JsonElement properties = schema.GetProperty("properties");
1210+
Assert.Equal("Added description for first", properties.GetProperty("first").GetProperty("description").GetString());
1211+
Assert.Equal("Added description for second", properties.GetProperty("second").GetProperty("description").GetString());
1212+
}
1213+
1214+
[Fact]
1215+
public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_ReturnsNull_UsesAttributeDescriptions()
1216+
{
1217+
Delegate method = (
1218+
[Description("Description from attribute")] int first,
1219+
string second) =>
1220+
{
1221+
};
1222+
1223+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new()
1224+
{
1225+
ParameterDescriptionProvider = _ => null
1226+
});
1227+
1228+
JsonElement properties = schema.GetProperty("properties");
1229+
Assert.Equal("Description from attribute", properties.GetProperty("first").GetProperty("description").GetString());
1230+
Assert.False(properties.GetProperty("second").TryGetProperty("description", out _));
1231+
}
1232+
1233+
[Fact]
1234+
public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_NullValue_UsesAttributeDescriptions()
1235+
{
1236+
Delegate method = (
1237+
[Description("Description from attribute")] int first,
1238+
string second) =>
1239+
{
1240+
};
1241+
1242+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new()
1243+
{
1244+
ParameterDescriptionProvider = null
1245+
});
1246+
1247+
JsonElement properties = schema.GetProperty("properties");
1248+
Assert.Equal("Description from attribute", properties.GetProperty("first").GetProperty("description").GetString());
1249+
Assert.False(properties.GetProperty("second").TryGetProperty("description", out _));
1250+
}
1251+
1252+
[Fact]
1253+
public static void CreateFunctionJsonSchema_ParameterDescriptionProvider_OnlyCalledForActualParameters()
1254+
{
1255+
Delegate method = (int first, string second) =>
1256+
{
1257+
};
1258+
1259+
List<string?> calledParameterNames = [];
1260+
JsonElement schema = AIJsonUtilities.CreateFunctionJsonSchema(method.Method, inferenceOptions: new()
1261+
{
1262+
ParameterDescriptionProvider = p =>
1263+
{
1264+
calledParameterNames.Add(p.Name);
1265+
return p.Name == "first" ? "Description for first" : null;
1266+
}
1267+
});
1268+
1269+
JsonElement properties = schema.GetProperty("properties");
1270+
Assert.Equal(2, properties.EnumerateObject().Count());
1271+
Assert.Equal("Description for first", properties.GetProperty("first").GetProperty("description").GetString());
1272+
Assert.Equal(["first", "second"], calledParameterNames);
1273+
}
1274+
11671275
[Fact]
11681276
public static void TransformJsonSchema_ConvertBooleanSchemas()
11691277
{

0 commit comments

Comments
 (0)