From 090d7a2113330dbf093496284569078a8600b417 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 25 Oct 2024 23:10:16 -0400 Subject: [PATCH] Add NativeAOT testapp project for M.E.AI (#5573) * Add NativeAOT testapp project for M.E.AI * Address PR feedback --- .../JsonContext.cs | 4 +- .../OpenAIChatClient.cs | 56 +++++++++++++++++-- .../Utilities/AIJsonUtilities.Defaults.cs | 4 +- ...ensions.AI.AotCompatibility.TestApp.csproj | 26 +++++++++ .../Program.cs | 22 ++++++++ 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Microsoft.Extensions.AI.AotCompatibility.TestApp.csproj create mode 100644 test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Program.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs index 5576cbf134a..1e1dabffab7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs @@ -48,11 +48,11 @@ private static JsonSerializerOptions CreateDefaultToolJsonOptions() { // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, // and we want to be flexible in terms of what can be put into the various collections in the object model. - // Otherwise, use the source-generated options to enable Native AOT. + // Otherwise, use the source-generated options to enable trimming and Native AOT. if (JsonSerializer.IsReflectionEnabledByDefault) { - // Keep in sync with the JsonSourceGenerationOptions on JsonContext below. + // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext above. JsonSerializerOptions options = new(JsonSerializerDefaults.Web) { TypeInfoResolver = new DefaultJsonTypeInfoResolver(), diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 935bb88f812..42851cdf62f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -587,10 +589,9 @@ private sealed class OpenAIChatToolJson string? result = resultContent.Result as string; if (result is null && resultContent.Result is not null) { - JsonSerializerOptions options = ToolCallJsonSerializerOptions ?? JsonContext.Default.Options; try { - result = JsonSerializer.Serialize(resultContent.Result, options.GetTypeInfo(typeof(object))); + result = JsonSerializer.Serialize(resultContent.Result, JsonContext.GetTypeInfo(typeof(object), ToolCallJsonSerializerOptions)); } catch (NotSupportedException) { @@ -617,7 +618,9 @@ private sealed class OpenAIChatToolJson ChatToolCall.CreateFunctionToolCall( callRequest.CallId, callRequest.Name, - BinaryData.FromObjectAsJson(callRequest.Arguments, ToolCallJsonSerializerOptions))); + new(JsonSerializer.SerializeToUtf8Bytes( + callRequest.Arguments, + JsonContext.GetTypeInfo(typeof(IDictionary), ToolCallJsonSerializerOptions))))); } } @@ -670,8 +673,53 @@ private static FunctionCallContent ParseCallContentFromBinaryData(BinaryData ut8 argumentParser: static json => JsonSerializer.Deserialize(json, JsonContext.Default.IDictionaryStringObject)!); /// Source-generated JSON type information. + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] [JsonSerializable(typeof(OpenAIChatToolJson))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(JsonElement))] - private sealed partial class JsonContext : JsonSerializerContext; + private sealed partial class JsonContext : JsonSerializerContext + { + /// Gets the singleton used as the default in JSON serialization operations. + private static readonly JsonSerializerOptions _defaultToolJsonOptions = CreateDefaultToolJsonOptions(); + + /// Gets JSON type information for the specified type. + /// + /// This first tries to get the type information from , + /// falling back to if it can't. + /// + public static JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? firstOptions) => + firstOptions?.TryGetTypeInfo(type, out JsonTypeInfo? info) is true ? + info : + _defaultToolJsonOptions.GetTypeInfo(type); + + /// Creates the default to use for serialization-related operations. + [UnconditionalSuppressMessage("AotAnalysis", "IL3050", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")] + private static JsonSerializerOptions CreateDefaultToolJsonOptions() + { + // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, + // and we want to be flexible in terms of what can be put into the various collections in the object model. + // Otherwise, use the source-generated options to enable trimming and Native AOT. + + if (JsonSerializer.IsReflectionEnabledByDefault) + { + // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext above. + JsonSerializerOptions options = new(JsonSerializerDefaults.Web) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, + }; + + options.MakeReadOnly(); + return options; + } + + return Default.Options; + } + } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI/Utilities/AIJsonUtilities.Defaults.cs index 94340160cb1..de2c2a695b6 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Utilities/AIJsonUtilities.Defaults.cs @@ -23,11 +23,11 @@ private static JsonSerializerOptions CreateDefaultOptions() { // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, // and we want to be flexible in terms of what can be put into the various collections in the object model. - // Otherwise, use the source-generated options to enable Native AOT. + // Otherwise, use the source-generated options to enable trimming and Native AOT. if (JsonSerializer.IsReflectionEnabledByDefault) { - // Keep in sync with the JsonSourceGenerationOptions on JsonContext below. + // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below. JsonSerializerOptions options = new(JsonSerializerDefaults.Web) { TypeInfoResolver = new DefaultJsonTypeInfoResolver(), diff --git a/test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Microsoft.Extensions.AI.AotCompatibility.TestApp.csproj b/test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Microsoft.Extensions.AI.AotCompatibility.TestApp.csproj new file mode 100644 index 00000000000..183cd150937 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Microsoft.Extensions.AI.AotCompatibility.TestApp.csproj @@ -0,0 +1,26 @@ + + + + Exe + $(LatestTargetFramework) + true + false + true + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Program.cs b/test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Program.cs new file mode 100644 index 00000000000..b518dfa7739 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.AotCompatibility.TestApp/Program.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable S125 // Remove this commented out code + +using Microsoft.Extensions.AI; + +// Use types from each library. + +// Microsoft.Extensions.AI.Ollama +using var b = new OllamaChatClient("http://localhost:11434", "llama3.2"); + +// Microsoft.Extensions.AI.AzureAIInference +// using var a = new Azure.AI.Inference.ChatCompletionClient(new Uri("http://localhost"), new("apikey")); // uncomment once warnings in Azure.AI.Inference are addressed + +// Microsoft.Extensions.AI.OpenAI +// using var c = new OpenAI.OpenAIClient("apikey").AsChatClient("gpt-4o-mini"); // uncomment once warnings in OpenAI are addressed + +// Microsoft.Extensions.AI +AIFunctionFactory.Create(() => { }); + +System.Console.WriteLine("Success!");