Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@
"Member": "bool Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.IncludeSchemaKeyword { get; init; }",
"Stage": "Stable"
},
{
"Member": "System.Func<System.Reflection.ParameterInfo, string?>? Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.ParameterDescriptionProvider { get; init; }",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.AIJsonSchemaTransformOptions? Microsoft.Extensions.AI.AIJsonSchemaCreateOptions.TransformOptions { get; init; }",
"Stage": "Stable"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,6 +36,18 @@ public sealed record class AIJsonSchemaCreateOptions
/// </remarks>
public Func<ParameterInfo, bool>? IncludeParameter { get; init; }

/// <summary>
/// Gets a callback that is invoked for each parameter in the <see cref="MethodBase"/> provided to
/// <see cref="AIJsonUtilities.CreateFunctionJsonSchema"/> to obtain a description for the parameter.
/// </summary>
/// <remarks>
/// The delegate receives a <see cref="ParameterInfo"/> instance and returns a string describing
/// the parameter. If <see langword="null"/>, or if the delegate returns <see langword="null"/>,
/// the description will be sourced from the <see cref="MethodBase"/> metadata (like <see cref="DescriptionAttribute"/>),
/// if available.
/// </remarks>
public Func<ParameterInfo, string?>? ParameterDescriptionProvider { get; init; }

/// <summary>
/// Gets a <see cref="AIJsonSchemaTransformOptions"/> governing transformations on the JSON schema after it has been generated.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DescriptionAttribute>(inherit: true)?.Description;

JsonNode parameterSchema = CreateJsonSchemaCore(
type: parameter.ParameterType,
parameter: parameter,
description: parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
description: parameterDescription,
hasDefaultValue: hasDefaultValue,
defaultValue: defaultValue,
serializerOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ public static void AIJsonSchemaCreateOptions_UsesStructuralEquality()
property.SetValue(options2, includeParameter);
break;

case null when property.PropertyType == typeof(Func<ParameterInfo, string?>):
Func<ParameterInfo, string?> 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);
Expand Down Expand Up @@ -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<string?> 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()
{
Expand Down
Loading