diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs index 84effb1737b..a320600bee2 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs @@ -1,16 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Schema; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -21,13 +17,8 @@ namespace Microsoft.Extensions.AI; /// /// Provides extension methods on that simplify working with structured output. /// -public static partial class ChatClientStructuredOutputExtensions +public static class ChatClientStructuredOutputExtensions { - private const string UsesReflectionJsonSerializerMessage = - "This method uses the reflection-based JsonSerializer which can break in trimmed or AOT applications."; - - private static JsonSerializerOptions? _defaultJsonSerializerOptions; - /// Sends chat messages to the model, requesting a response matching the type . /// The . /// The chat content to send. @@ -44,8 +35,6 @@ public static partial class ChatClientStructuredOutputExtensions /// by the client, including any messages for roundtrips to the model as part of the implementation of this request, will be included. /// /// The type of structured output to request. - [RequiresUnreferencedCode(UsesReflectionJsonSerializerMessage)] - [RequiresDynamicCode(UsesReflectionJsonSerializerMessage)] public static Task> CompleteAsync( this IChatClient chatClient, IList chatMessages, @@ -53,7 +42,7 @@ public static Task> CompleteAsync( bool? useNativeJsonSchema = null, CancellationToken cancellationToken = default) where T : class => - CompleteAsync(chatClient, chatMessages, DefaultJsonSerializerOptions, options, useNativeJsonSchema, cancellationToken); + CompleteAsync(chatClient, chatMessages, JsonDefaults.Options, options, useNativeJsonSchema, cancellationToken); /// Sends a user chat text message to the model, requesting a response matching the type . /// The . @@ -67,10 +56,6 @@ public static Task> CompleteAsync( /// The to monitor for cancellation requests. The default is . /// The response messages generated by the client. /// The type of structured output to request. - [RequiresDynamicCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. " - + "Use System.Text.Json source generation for native AOT applications.")] - [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. " - + "Use System.Text.Json source generation for native AOT applications.")] public static Task> CompleteAsync( this IChatClient chatClient, string chatMessage, @@ -154,7 +139,7 @@ public static async Task> CompleteAsync( }); schemaNode.Insert(0, "$schema", "https://json-schema.org/draft/2020-12/schema"); schemaNode.Add("additionalProperties", false); - var schema = JsonSerializer.Serialize(schemaNode, JsonNodeContext.Default.JsonNode); + var schema = JsonSerializer.Serialize(schemaNode, JsonDefaults.Options.GetTypeInfo(typeof(JsonNode))); ChatMessage? promptAugmentation = null; options = (options ?? new()).Clone(); @@ -201,28 +186,4 @@ public static async Task> CompleteAsync( } } } - - private static JsonSerializerOptions DefaultJsonSerializerOptions - { - [RequiresUnreferencedCode(UsesReflectionJsonSerializerMessage)] - [RequiresDynamicCode(UsesReflectionJsonSerializerMessage)] - get => _defaultJsonSerializerOptions ?? GetOrCreateDefaultJsonSerializerOptions(); - } - - [RequiresUnreferencedCode(UsesReflectionJsonSerializerMessage)] - [RequiresDynamicCode(UsesReflectionJsonSerializerMessage)] - private static JsonSerializerOptions GetOrCreateDefaultJsonSerializerOptions() - { - var options = new JsonSerializerOptions(JsonSerializerDefaults.General) - { - Converters = { new JsonStringEnumConverter() }, - TypeInfoResolver = new DefaultJsonTypeInfoResolver(), - WriteIndented = true, - }; - return Interlocked.CompareExchange(ref _defaultJsonSerializerOptions, options, null) ?? options; - } - - [JsonSerializable(typeof(JsonNode))] - [JsonSourceGenerationOptions(WriteIndented = true)] - private sealed partial class JsonNodeContext : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs index 84ce1fa15a0..70928e63e82 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs @@ -23,17 +23,12 @@ namespace Microsoft.Extensions.AI; /// Provides factory methods for creating commonly-used implementations of . public static class AIFunctionFactory { - internal const string UsesReflectionJsonSerializerMessage = - "This method uses the reflection-based JsonSerializer which can break in trimmed or AOT applications."; - /// Lazily-initialized default options instance. private static AIFunctionFactoryCreateOptions? _defaultOptions; /// Creates an instance for a method, specified via a delegate. /// The method to be represented via the created . /// The created for invoking . - [RequiresUnreferencedCode(UsesReflectionJsonSerializerMessage)] - [RequiresDynamicCode(UsesReflectionJsonSerializerMessage)] public static AIFunction Create(Delegate method) => Create(method, _defaultOptions ??= new()); /// Creates an instance for a method, specified via a delegate. @@ -52,8 +47,6 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryCreateOptions /// The name to use for the . /// The description to use for the . /// The created for invoking . - [RequiresUnreferencedCode("Reflection is used to access types from the supplied Delegate.")] - [RequiresDynamicCode("Reflection is used to access types from the supplied Delegate.")] public static AIFunction Create(Delegate method, string? name, string? description = null) => Create(method, (_defaultOptions ??= new()).SerializerOptions, name, description); @@ -80,8 +73,6 @@ public static AIFunction Create(Delegate method, JsonSerializerOptions options, /// This should be if and only if is a static method. /// /// The created for invoking . - [RequiresUnreferencedCode("Reflection is used to access types from the supplied MethodInfo.")] - [RequiresDynamicCode("Reflection is used to access types from the supplied MethodInfo.")] public static AIFunction Create(MethodInfo method, object? target = null) => Create(method, target, _defaultOptions ??= new()); @@ -107,8 +98,8 @@ private sealed class ReflectionAIFunction : AIFunction { private readonly MethodInfo _method; private readonly object? _target; - private readonly Func, AIFunctionContext?, object?>[] _parameterMarshalers; - private readonly Func> _returnMarshaler; + private readonly Func, AIFunctionContext?, object?>[] _parameterMarshallers; + private readonly Func> _returnMarshaller; private readonly JsonTypeInfo? _returnTypeInfo; private readonly bool _needsAIFunctionContext; @@ -185,11 +176,11 @@ static bool IsAsyncMethod(MethodInfo method) // Get marshaling delegates for parameters and build up the parameter metadata. var parameters = method.GetParameters(); - _parameterMarshalers = new Func, AIFunctionContext?, object?>[parameters.Length]; + _parameterMarshallers = new Func, AIFunctionContext?, object?>[parameters.Length]; bool sawAIContextParameter = false; for (int i = 0; i < parameters.Length; i++) { - if (GetParameterMarshaler(options.SerializerOptions, parameters[i], ref sawAIContextParameter, out _parameterMarshalers[i]) is AIFunctionParameterMetadata parameterView) + if (GetParameterMarshaller(options.SerializerOptions, parameters[i], ref sawAIContextParameter, out _parameterMarshallers[i]) is AIFunctionParameterMetadata parameterView) { parameterMetadata?.Add(parameterView); } @@ -198,7 +189,7 @@ static bool IsAsyncMethod(MethodInfo method) _needsAIFunctionContext = sawAIContextParameter; // Get the return type and a marshaling func for the return value. - Type returnType = GetReturnMarshaler(method, out _returnMarshaler); + Type returnType = GetReturnMarshaller(method, out _returnMarshaller); _returnTypeInfo = returnType != typeof(void) ? options.SerializerOptions.GetTypeInfo(returnType) : null; Metadata = new AIFunctionMetadata(functionName) @@ -224,8 +215,8 @@ static bool IsAsyncMethod(MethodInfo method) IEnumerable>? arguments, CancellationToken cancellationToken) { - var paramMarshalers = _parameterMarshalers; - object?[] args = paramMarshalers.Length != 0 ? new object?[paramMarshalers.Length] : []; + var paramMarshallers = _parameterMarshallers; + object?[] args = paramMarshallers.Length != 0 ? new object?[paramMarshallers.Length] : []; IReadOnlyDictionary argDict = arguments is null || args.Length == 0 ? EmptyReadOnlyDictionary.Instance : @@ -242,10 +233,10 @@ static bool IsAsyncMethod(MethodInfo method) for (int i = 0; i < args.Length; i++) { - args[i] = paramMarshalers[i](argDict, context); + args[i] = paramMarshallers[i](argDict, context); } - object? result = await _returnMarshaler(ReflectionInvoke(_method, _target, args)).ConfigureAwait(false); + object? result = await _returnMarshaller(ReflectionInvoke(_method, _target, args)).ConfigureAwait(false); switch (_returnTypeInfo) { @@ -271,11 +262,11 @@ static bool IsAsyncMethod(MethodInfo method) /// /// Gets a delegate for handling the marshaling of a parameter. /// - private static AIFunctionParameterMetadata? GetParameterMarshaler( + private static AIFunctionParameterMetadata? GetParameterMarshaller( JsonSerializerOptions options, ParameterInfo parameter, ref bool sawAIFunctionContext, - out Func, AIFunctionContext?, object?> marshaler) + out Func, AIFunctionContext?, object?> marshaller) { if (string.IsNullOrWhiteSpace(parameter.Name)) { @@ -292,7 +283,7 @@ static bool IsAsyncMethod(MethodInfo method) sawAIFunctionContext = true; - marshaler = static (_, ctx) => + marshaller = static (_, ctx) => { Debug.Assert(ctx is not null, "Expected a non-null context object."); return ctx; @@ -300,12 +291,12 @@ static bool IsAsyncMethod(MethodInfo method) return null; } - // Resolve the contract used to marshall the value from JSON -- can throw if not supported or not found. + // Resolve the contract used to marshal the value from JSON -- can throw if not supported or not found. Type parameterType = parameter.ParameterType; JsonTypeInfo typeInfo = options.GetTypeInfo(parameterType); - // Create a marshaler that simply looks up the parameter by name in the arguments dictionary. - marshaler = (IReadOnlyDictionary arguments, AIFunctionContext? _) => + // Create a marshaller that simply looks up the parameter by name in the arguments dictionary. + marshaller = (IReadOnlyDictionary arguments, AIFunctionContext? _) => { // If the parameter has an argument specified in the dictionary, return that argument. if (arguments.TryGetValue(parameter.Name, out object? value)) @@ -368,7 +359,7 @@ static bool IsAsyncMethod(MethodInfo method) /// /// Gets a delegate for handling the result value of a method, converting it into the to return from the invocation. /// - private static Type GetReturnMarshaler(MethodInfo method, out Func> marshaler) + private static Type GetReturnMarshaller(MethodInfo method, out Func> marshaller) { // Handle each known return type for the method Type returnType = method.ReturnType; @@ -376,7 +367,7 @@ private static Type GetReturnMarshaler(MethodInfo method, out Func + marshaller = async static result => { await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false); return null; @@ -387,7 +378,7 @@ private static Type GetReturnMarshaler(MethodInfo method, out Func + marshaller = async static result => { await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(false); return null; @@ -401,7 +392,7 @@ private static Type GetReturnMarshaler(MethodInfo method, out Func)) { MethodInfo taskResultGetter = GetMethodFromGenericMethodDefinition(returnType, _taskGetResult); - marshaler = async result => + marshaller = async result => { await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false); return ReflectionInvoke(taskResultGetter, result, null); @@ -414,7 +405,7 @@ private static Type GetReturnMarshaler(MethodInfo method, out Func + marshaller = async result => { var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(result), null)!; await task.ConfigureAwait(false); @@ -425,7 +416,7 @@ private static Type GetReturnMarshaler(MethodInfo method, out Func new ValueTask(result); + marshaller = result => new ValueTask(result); return returnType; // Throws an exception if a result is found to be null unexpectedly diff --git a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs index 8e0db9b4813..4a843d2bbde 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryCreateOptions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using Microsoft.Shared.Diagnostics; @@ -19,10 +18,8 @@ public sealed class AIFunctionFactoryCreateOptions /// /// Initializes a new instance of the class with default serializer options. /// - [RequiresUnreferencedCode(AIFunctionFactory.UsesReflectionJsonSerializerMessage)] - [RequiresDynamicCode(AIFunctionFactory.UsesReflectionJsonSerializerMessage)] public AIFunctionFactoryCreateOptions() - : this(JsonSerializerOptions.Default) + : this(JsonDefaults.Options) { } diff --git a/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs b/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs index f7aabcff6fd..7da71aa7fa0 100644 --- a/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -30,9 +31,10 @@ private static JsonSerializerOptions CreateDefaultOptions() // Keep in sync with the JsonSourceGenerationOptions on JsonContext below. var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, }; options.MakeReadOnly(); @@ -45,7 +47,10 @@ private static JsonSerializerOptions CreateDefaultOptions() } // Keep in sync with CreateDefaultOptions above. - [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(ChatOptions))] [JsonSerializable(typeof(EmbeddingGenerationOptions))] @@ -57,7 +62,9 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(JsonDocument))] [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(JsonNode))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(string))] [JsonSerializable(typeof(int))]