From 33322d8f752d3db9870d36ca142f09be53d2ba5b Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 10 Jan 2025 16:38:00 +0000 Subject: [PATCH 1/2] Add an extension method for registering custom AIContent types --- .../Utilities/AIJsonUtilities.cs | 72 +++++++++++++++++++ .../Utilities/AIJsonUtilitiesTests.cs | 64 +++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs new file mode 100644 index 00000000000..83234dd00c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -0,0 +1,72 @@ +// 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.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable S1121 // Assignments should not be made from within sub-expressions + +namespace Microsoft.Extensions.AI; + +public static partial class AIJsonUtilities +{ + /// + /// Adds a custom content type to the polymorphic configuration for . + /// + /// The custom content type to configure. + /// The options instance to configure. + /// The type discriminator id for the content type. + /// or is . + /// is a built-in content type. + /// is a read-only instance. + public static void AddAIContentType(this JsonSerializerOptions options, string typeDiscriminatorId) + where TContent : AIContent + { + _ = Throw.IfNull(options); + _ = Throw.IfNull(typeDiscriminatorId); + + AddAIContentType(options, typeof(TContent), typeDiscriminatorId); + } + + /// + /// Adds a custom content type to the polymorphic configuration for . + /// + /// The options instance to configure. + /// The custom content type to configure. + /// The type discriminator id for the content type. + /// , , or is . + /// is a built-in content type or does not derived from . + /// is a read-only instance. + public static void AddAIContentType(this JsonSerializerOptions options, Type contentType, string typeDiscriminatorId) + { + _ = Throw.IfNull(options); + _ = Throw.IfNull(contentType); + _ = Throw.IfNull(typeDiscriminatorId); + + if (!typeof(AIContent).IsAssignableFrom(contentType)) + { + Throw.ArgumentException(nameof(contentType), "The content type must derive from AIContent."); + } + + AddAIContentTypeCore(options, contentType, typeDiscriminatorId); + } + + private static void AddAIContentTypeCore(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId) + { + if (contentType.Assembly == typeof(AIContent).Assembly) + { + Throw.ArgumentException(nameof(contentType), "Cannot register built-in AI content types."); + } + + IJsonTypeInfoResolver resolver = options.TypeInfoResolver ?? DefaultOptions.TypeInfoResolver!; + options.TypeInfoResolver = resolver.WithAddedModifier(typeInfo => + { + if (typeInfo.Type == typeof(AIContent)) + { + (typeInfo.PolymorphismOptions ??= new()).DerivedTypes.Add(new(contentType, typeDiscriminatorId)); + } + }); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index b156de3f18e..e79d2c9034e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -296,4 +296,68 @@ public static void CreateJsonSchema_ValidateWithTestData(ITestData testData) JsonNode? serializedValue = JsonSerializer.SerializeToNode(testData.Value, testData.Type, options); SchemaTestHelpers.AssertDocumentMatchesSchema(schemaAsNode, serializedValue); } + + [Fact] + public static void AddAIContentType_DerivedAIContent() + { + JsonSerializerOptions options = new(); + options.AddAIContentType("derivativeContent"); + + AIContent c = new DerivedAIContent { DerivedValue = 42 }; + string json = JsonSerializer.Serialize(c, options); + Assert.Equal("""{"$type":"derivativeContent","DerivedValue":42,"AdditionalProperties":null}""", json); + + AIContent? deserialized = JsonSerializer.Deserialize(json, options); + Assert.IsType(deserialized); + } + + [Fact] + public static void AddAIContentType_ReadOnlyJsonSerializerOptions_ThrowsInvalidOperationException() + { + Assert.Throws(() => AIJsonUtilities.DefaultOptions.AddAIContentType("derivativeContent")); + } + + [Fact] + public static void AddAIContentType_NonAIContent_ThrowsArgumentException() + { + JsonSerializerOptions options = new(); + Assert.Throws(() => options.AddAIContentType(typeof(int), "discriminator")); + Assert.Throws(() => options.AddAIContentType(typeof(object), "discriminator")); + Assert.Throws(() => options.AddAIContentType(typeof(ChatMessage), "discriminator")); + } + + [Fact] + public static void AddAIContentType_BuiltInAIContent_ThrowsArgumentException() + { + JsonSerializerOptions options = new(); + Assert.Throws(() => options.AddAIContentType("discriminator")); + Assert.Throws(() => options.AddAIContentType("discriminator")); + } + + [Fact] + public static void AddAIContentType_ConflictingIdentifier_ThrowsInvalidOperationException() + { + JsonSerializerOptions options = new(); + options.AddAIContentType("text"); + options.AddAIContentType("audio"); + + AIContent c = new DerivedAIContent(); + Assert.Throws(() => JsonSerializer.Serialize(c, options)); + } + + [Fact] + public static void AddAIContentType_NullArguments_ThrowsArgumentNullException() + { + JsonSerializerOptions options = new(); + Assert.Throws(() => ((JsonSerializerOptions)null!).AddAIContentType("discriminator")); + Assert.Throws(() => ((JsonSerializerOptions)null!).AddAIContentType(typeof(DerivedAIContent), "discriminator")); + Assert.Throws(() => options.AddAIContentType(null!)); + Assert.Throws(() => options.AddAIContentType(typeof(DerivedAIContent), null!)); + Assert.Throws(() => options.AddAIContentType(null!, "discriminator")); + } + + private class DerivedAIContent : AIContent + { + public int DerivedValue { get; set; } + } } From 795975af235be3239c54ea43e78b148cc8dbef43 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 10 Jan 2025 16:42:30 +0000 Subject: [PATCH 2/2] Fix method chaining. --- .../Utilities/AIJsonUtilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index 83234dd00c3..2596b86cd48 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -27,7 +27,7 @@ public static void AddAIContentType(this JsonSerializerOptions options _ = Throw.IfNull(options); _ = Throw.IfNull(typeDiscriminatorId); - AddAIContentType(options, typeof(TContent), typeDiscriminatorId); + AddAIContentTypeCore(options, typeof(TContent), typeDiscriminatorId); } ///