diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index 7eefdd90a09..b50fc531179 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -49,11 +49,11 @@ public FunctionCallContent(string callId, string name, IDictionary /// - /// When an instance of is serialized using , any exception - /// stored in this property will be serialized as a string. When deserialized, the string will be converted back to an instance - /// of the base type. As such, consumers shouldn't rely on the exact type of the exception stored in this property. + /// This property is for information purposes only. The is not serialized as part of serializing + /// instances of this class with ; as such, upon deserialization, this property will be . + /// Consumers should not rely on indicating success. /// - [JsonConverter(typeof(FunctionCallExceptionConverter))] + [JsonIgnore] public Exception? Exception { get; set; } /// Gets a string representing this instance to display in the debugger. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallExceptionConverter.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallExceptionConverter.cs deleted file mode 100644 index 0c36f11ca40..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallExceptionConverter.cs +++ /dev/null @@ -1,96 +0,0 @@ -// 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.ComponentModel; -#if NET -using System.Runtime.ExceptionServices; -#endif -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// Serializes an exception as a string and deserializes it back as a base containing that contents as a message. -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class FunctionCallExceptionConverter : JsonConverter -{ - private const string ClassNamePropertyName = "className"; - private const string MessagePropertyName = "message"; - private const string InnerExceptionPropertyName = "innerException"; - private const string StackTracePropertyName = "stackTraceString"; - - /// - public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) - { - _ = Throw.IfNull(writer); - _ = Throw.IfNull(value); - - // Schema and property order taken from Exception.GetObjectData() implementation. - - writer.WriteStartObject(); - writer.WriteString(ClassNamePropertyName, value.GetType().ToString()); - writer.WriteString(MessagePropertyName, value.Message); - writer.WritePropertyName(InnerExceptionPropertyName); - if (value.InnerException is Exception innerEx) - { - Write(writer, innerEx, options); - } - else - { - writer.WriteNullValue(); - } - - writer.WriteString(StackTracePropertyName, value.StackTrace); - writer.WriteEndObject(); - } - - /// - public override Exception? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException(); - } - - using var doc = JsonDocument.ParseValue(ref reader); - return ParseExceptionCore(doc.RootElement); - - static Exception ParseExceptionCore(JsonElement element) - { - string? message = null; - string? stackTrace = null; - Exception? innerEx = null; - - foreach (JsonProperty property in element.EnumerateObject()) - { - switch (property.Name) - { - case MessagePropertyName: - message = property.Value.GetString(); - break; - - case StackTracePropertyName: - stackTrace = property.Value.GetString(); - break; - - case InnerExceptionPropertyName when property.Value.ValueKind is not JsonValueKind.Null: - innerEx = ParseExceptionCore(property.Value); - break; - } - } - -#pragma warning disable CA2201 // Do not raise reserved exception types - Exception result = new(message, innerEx); -#pragma warning restore CA2201 -#if NET - if (stackTrace != null) - { - ExceptionDispatchInfo.SetRemoteStackTrace(result, stackTrace); - } -#endif - return result; - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs index 0a416d64f5f..f793e2ceceb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -20,10 +20,31 @@ public sealed class FunctionResultContent : AIContent /// /// The function call ID for which this is the result. /// The function name that produced the result. - /// The function call result. - /// Any exception that occurred when invoking the function. + /// + /// This may be if the function returned , if the function was void-returning + /// and thus had no result, or if the function call failed. Typically, however, in order to provide meaningfully representative + /// information to an AI service, a human-readable representation of those conditions should be supplied. + /// [JsonConstructor] - public FunctionResultContent(string callId, string name, object? result = null, Exception? exception = null) + public FunctionResultContent(string callId, string name, object? result) + { + CallId = Throw.IfNull(callId); + Name = Throw.IfNull(name); + Result = result; + } + + /// + /// Initializes a new instance of the class. + /// + /// The function call ID for which this is the result. + /// The function name that produced the result. + /// + /// This may be if the function returned , if the function was void-returning + /// and thus had no result, or if the function call failed. Typically, however, in order to provide meaningfully representative + /// information to an AI service, a human-readable representation of those conditions should be supplied. + /// + /// Any exception that occurred when invoking the function. + public FunctionResultContent(string callId, string name, object? result, Exception? exception) { CallId = Throw.IfNull(callId); Name = Throw.IfNull(name); @@ -35,9 +56,13 @@ public FunctionResultContent(string callId, string name, object? result = null, /// Initializes a new instance of the class. /// /// The function call for which this is the result. - /// The function call result. + /// + /// This may be if the function returned , if the function was void-returning + /// and thus had no result, or if the function call failed. Typically, however, in order to provide meaningfully representative + /// information to an AI service, a human-readable representation of those conditions should be supplied. + /// /// Any exception that occurred when invoking the function. - public FunctionResultContent(FunctionCallContent functionCall, object? result = null, Exception? exception = null) + public FunctionResultContent(FunctionCallContent functionCall, object? result, Exception? exception = null) : this(Throw.IfNull(functionCall).CallId, functionCall.Name, result, exception) { } @@ -59,17 +84,22 @@ public FunctionResultContent(FunctionCallContent functionCall, object? result = /// /// Gets or sets the result of the function call, or a generic error message if the function call failed. /// + /// + /// This may be if the function returned , if the function was void-returning + /// and thus had no result, or if the function call failed. Typically, however, in order to provide meaningfully representative + /// information to an AI service, a human-readable representation of those conditions should be supplied. + /// public object? Result { get; set; } /// /// Gets or sets an exception that occurred if the function call failed. /// /// - /// When an instance of is serialized using , any exception - /// stored in this property will be serialized as a string. When deserialized, the string will be converted back to an instance - /// of the base type. As such, consumers shouldn't rely on the exact type of the exception stored in this property. + /// This property is for information purposes only. The is not serialized as part of serializing + /// instances of this class with ; as such, upon deserialization, this property will be . + /// Consumers should not rely on indicating success. /// - [JsonConverter(typeof(FunctionCallExceptionConverter))] + [JsonIgnore] public Exception? Exception { get; set; } /// Gets a string representing this instance to display in the debugger. diff --git a/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs b/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs index 06317f570a2..467d6eb3feb 100644 --- a/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI/JsonDefaults.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -16,6 +17,8 @@ internal static partial class JsonDefaults public static JsonSerializerOptions Options { get; } = CreateDefaultOptions(); /// 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 CreateDefaultOptions() { // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, @@ -28,9 +31,7 @@ private static JsonSerializerOptions CreateDefaultOptions() var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, -#pragma warning disable IL3050, IL2026 // only used when reflection-based serialization is enabled TypeInfoResolver = new DefaultJsonTypeInfoResolver(), -#pragma warning restore IL3050, IL2026 }; options.MakeReadOnly(); diff --git a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj index 39b33458d0c..8e389b61652 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj +++ b/src/Libraries/Microsoft.Extensions.AI/Microsoft.Extensions.AI.csproj @@ -18,11 +18,6 @@ true - - - $(NoWarn);IL2026 - - true true diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs index 791bb4cc0e7..054b0eeefec 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs @@ -5,9 +5,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -#if NET -using System.Runtime.ExceptionServices; -#endif using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; @@ -89,41 +86,19 @@ public void ItShouldBeSerializableAndDeserializableWithException() { // Arrange var ex = new InvalidOperationException("hello", new NullReferenceException("bye")); -#if NET - ExceptionDispatchInfo.SetRemoteStackTrace(ex, "stack trace"); -#endif - var sut = new FunctionCallContent("callId1", "functionName") { Exception = ex }; + var sut = new FunctionCallContent("callId1", "functionName", new Dictionary { ["key"] = "value" }) { Exception = ex }; // Act var json = JsonSerializer.SerializeToNode(sut, TestJsonSerializerContext.Default.Options); var deserializedSut = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.Options); // Assert - JsonObject jsonEx = Assert.IsType(json!["exception"]); - Assert.Equal(4, jsonEx.Count); - Assert.Equal("System.InvalidOperationException", (string?)jsonEx["className"]); - Assert.Equal("hello", (string?)jsonEx["message"]); -#if NET - Assert.StartsWith("stack trace", (string?)jsonEx["stackTraceString"]); -#endif - JsonObject jsonExInner = Assert.IsType(jsonEx["innerException"]); - Assert.Equal(4, jsonExInner.Count); - Assert.Equal("System.NullReferenceException", (string?)jsonExInner["className"]); - Assert.Equal("bye", (string?)jsonExInner["message"]); - Assert.Null(jsonExInner["innerException"]); - Assert.Null(jsonExInner["stackTraceString"]); - Assert.NotNull(deserializedSut); - Assert.IsType(deserializedSut.Exception); - Assert.Equal("hello", deserializedSut.Exception.Message); -#if NET - Assert.StartsWith("stack trace", deserializedSut.Exception.StackTrace); -#endif - - Assert.IsType(deserializedSut.Exception.InnerException); - Assert.Equal("bye", deserializedSut.Exception.InnerException.Message); - Assert.Null(deserializedSut.Exception.InnerException.StackTrace); - Assert.Null(deserializedSut.Exception.InnerException.InnerException); + Assert.Equal("callId1", deserializedSut.CallId); + Assert.Equal("functionName", deserializedSut.Name); + Assert.NotNull(deserializedSut.Arguments); + Assert.Single(deserializedSut.Arguments); + Assert.Null(deserializedSut.Exception); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs index a24120ca9a9..a70386e42c6 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -12,7 +12,7 @@ public class FunctionResultContentTests [Fact] public void Constructor_PropsDefault() { - FunctionResultContent c = new("callId1", "functionName"); + FunctionResultContent c = new("callId1", "functionName", null); Assert.Equal("callId1", c.CallId); Assert.Equal("functionName", c.Name); Assert.Null(c.RawRepresentation); @@ -54,7 +54,7 @@ public void Constructor_FunctionCallContent_PropsRoundtrip() [Fact] public void Constructor_PropsRoundtrip() { - FunctionResultContent c = new("callId1", "functionName"); + FunctionResultContent c = new("callId1", "functionName", null); Assert.Null(c.RawRepresentation); object raw = new(); @@ -106,7 +106,7 @@ public void ItShouldBeSerializableAndDeserializable() public void ItShouldBeSerializableAndDeserializableWithException() { // Arrange - var sut = new FunctionResultContent("callId1", "functionName") { Exception = new InvalidOperationException("hello") }; + var sut = new FunctionResultContent("callId1", "functionName", null, new InvalidOperationException("hello")); // Act var json = JsonSerializer.Serialize(sut, TestJsonSerializerContext.Default.Options); @@ -114,7 +114,9 @@ public void ItShouldBeSerializableAndDeserializableWithException() // Assert Assert.NotNull(deserializedSut); - Assert.IsType(deserializedSut.Exception); - Assert.Contains("hello", deserializedSut.Exception.Message); + Assert.Equal(sut.Name, deserializedSut.Name); + Assert.Equal(sut.CallId, deserializedSut.CallId); + Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); + Assert.Null(deserializedSut.Exception); } }