diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index af8b19c8d84..1bfc61e0a38 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -32,6 +32,8 @@ namespace Microsoft.Extensions.AI; // [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")] // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] +// [JsonDerivedType(typeof(AdditionalDetailsRequestContent), typeDiscriminator: "additionalDetailsRequestContent")] +// [JsonDerivedType(typeof(AdditionalDetailsResponseContent), typeDiscriminator: "additionalDetailsResponseContent")] public class AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AdditionalDetailsRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AdditionalDetailsRequestContent.cs new file mode 100644 index 00000000000..704d4cba0c2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AdditionalDetailsRequestContent.cs @@ -0,0 +1,43 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a request for additional details from the user. +/// +[Experimental("MEAI001")] +public sealed class AdditionalDetailsRequestContent : UserInputRequestContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID that uniquely identifies the additional details request/response pair. + /// The additional details request. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public AdditionalDetailsRequestContent(string id, AIContent request) + : base(id) + { + Request = Throw.IfNull(request); + } + + /// + /// Gets the additional details request. + /// + public AIContent Request { get; } + + /// + /// Creates a to provide the requested additional details. + /// + /// The containing the requested additional details. + /// The representing the response. + /// is . + public AdditionalDetailsResponseContent CreateResponse(AIContent response) => new(Id, response); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AdditionalDetailsResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AdditionalDetailsResponseContent.cs new file mode 100644 index 00000000000..a20980c2aeb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AdditionalDetailsResponseContent.cs @@ -0,0 +1,35 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a response for additional details request from the user. +/// +[Experimental("MEAI001")] +public sealed class AdditionalDetailsResponseContent : UserInputResponseContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID that uniquely identifies the additional details request/response pair. + /// The additional details response. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public AdditionalDetailsResponseContent(string id, AIContent response) + : base(id) + { + Response = Throw.IfNull(response); + } + + /// + /// Gets the additional details response. + /// + public AIContent Response { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs index b2a2e0e6e95..267bfe3c4dc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs @@ -15,6 +15,7 @@ namespace Microsoft.Extensions.AI; [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), "mcpServerToolApprovalRequest")] +[JsonDerivedType(typeof(AdditionalDetailsRequestContent), "additionalDetailsRequestContent")] public class UserInputRequestContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs index 6902f047282..980f84dcb61 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs @@ -15,6 +15,7 @@ namespace Microsoft.Extensions.AI; [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), "mcpServerToolApprovalResponse")] +[JsonDerivedType(typeof(AdditionalDetailsResponseContent), "additionalDetailsResponseContent")] public class UserInputResponseContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index d01294836bc..8b1c1fb1996 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -59,6 +59,8 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(McpServerToolApprovalResponseContent), typeDiscriminatorId: "mcpServerToolApprovalResponse", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); + AddAIContentType(options, typeof(AdditionalDetailsRequestContent), typeDiscriminatorId: "additionalDetailsRequestContent", checkBuiltIn: false); + AddAIContentType(options, typeof(AdditionalDetailsResponseContent), typeDiscriminatorId: "additionalDetailsResponseContent", checkBuiltIn: false); if (JsonSerializer.IsReflectionEnabledByDefault) { @@ -133,6 +135,8 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(McpServerToolApprovalResponseContent))] [JsonSerializable(typeof(CodeInterpreterToolCallContent))] [JsonSerializable(typeof(CodeInterpreterToolResultContent))] + [JsonSerializable(typeof(AdditionalDetailsRequestContent))] + [JsonSerializable(typeof(AdditionalDetailsResponseContent))] [JsonSerializable(typeof(ResponseContinuationToken))] // IEmbeddingGenerator diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs index e5734ccd7cf..152eea7dcf1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -75,7 +75,9 @@ public void Serialization_DerivedTypes_Roundtrips() new McpServerToolCallContent("call123", "myTool", "myServer"), new McpServerToolResultContent("call123"), new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new McpServerToolApprovalResponseContent("request123", approved: true) + new McpServerToolApprovalResponseContent("request123", approved: true), + new AdditionalDetailsRequestContent("request123", new TextContent("Please provide the image you mentioned but did not provide.")), + new AdditionalDetailsResponseContent("response123", new DataContent(new byte[]{ 1, 2, 3 }, "image/jpeg")) ]); var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AdditionalDetailsRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AdditionalDetailsRequestContentTests.cs new file mode 100644 index 00000000000..247137dd288 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AdditionalDetailsRequestContentTests.cs @@ -0,0 +1,66 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.AI.Contents; + +public class AdditionalDetailsRequestContentTests +{ + [Fact] + public void Constructor_InvalidArguments_Throws() + { + Assert.Throws("id", () => new AdditionalDetailsRequestContent(null!, new TextContent("request"))); + Assert.Throws("id", () => new AdditionalDetailsRequestContent("", new TextContent("request"))); + Assert.Throws("id", () => new AdditionalDetailsRequestContent("\r\t\n ", new TextContent("request"))); + + Assert.Throws("request", () => new AdditionalDetailsRequestContent("id", null!)); + } + + [Fact] + public void Constructor_Roundtrips() + { + string id = "abc"; + TextContent request = new("What is your name?"); + AdditionalDetailsRequestContent content = new(id, request); + + Assert.Same(id, content.Id); + Assert.Same(request, content.Request); + } + + [Fact] + public void CreateResponse_ReturnsExpectedResponse() + { + string id = "req-1"; + string request = "What is your name?"; + TextContent response = new TextContent("My name is John"); + + AdditionalDetailsRequestContent content = new(id, new TextContent(request)); + + var textResponse = content.CreateResponse(response); + + Assert.NotNull(textResponse); + Assert.Same(id, textResponse.Id); + Assert.Same(response, textResponse.Response); + Assert.Throws("response", () => content.CreateResponse(null!)); + } + + [Fact] + public void Serialization_Roundtrips() + { + var content = new AdditionalDetailsRequestContent("request123", new TextContent("What is your name?")); + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedContent); + Assert.Equal(content.Id, deserializedContent.Id); + + TextContent originalRequest = Assert.IsType(content.Request); + TextContent deserializedRequest = Assert.IsType(deserializedContent.Request); + + Assert.Equal(originalRequest.Text, deserializedRequest.Text); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AdditionalDetailsResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AdditionalDetailsResponseContentTests.cs new file mode 100644 index 00000000000..34f847a3b51 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AdditionalDetailsResponseContentTests.cs @@ -0,0 +1,49 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.AI.Contents; + +public class AdditionalDetailsResponseContentTests +{ + [Fact] + public void Constructor_InvalidArguments_Throws() + { + Assert.Throws("id", () => new AdditionalDetailsResponseContent(null!, new TextContent("response"))); + Assert.Throws("id", () => new AdditionalDetailsResponseContent("", new TextContent("response"))); + Assert.Throws("id", () => new AdditionalDetailsResponseContent("\r\t\n ", new TextContent("response"))); + + Assert.Throws("response", () => new AdditionalDetailsResponseContent("id", null!)); + } + + [Fact] + public void Constructor_Roundtrips() + { + string id = "test-id"; + TextContent response = new("test-response"); + AdditionalDetailsResponseContent content = new(id, response); + + Assert.Same(id, content.Id); + Assert.Same(response, content.Response); + } + + [Fact] + public void Serialization_Roundtrips() + { + var content = new AdditionalDetailsResponseContent("response123", new TextContent("This is my answer")); + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedContent); + Assert.Equal(content.Id, deserializedContent.Id); + + TextContent originalResponse = Assert.IsType(content.Response); + TextContent deserializedResponse = Assert.IsType(deserializedContent.Response); + + Assert.Equal(originalResponse.Text, deserializedResponse.Text); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs index fc4dac9cabb..8d0b5e62a1b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs @@ -43,6 +43,7 @@ public void Serialization_DerivedTypes_Roundtrips() [ new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), + new AdditionalDetailsRequestContent("request123", new TextContent("I need more details. Where would you like to fly from and to?")), ]; var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputRequestContentArray); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs index 2442e57272d..3916546e437 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs @@ -41,6 +41,7 @@ public void Serialization_DerivedTypes_Roundtrips() [ new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), new McpServerToolApprovalResponseContent("request123", true), + new AdditionalDetailsResponseContent("response123", new TextContent("I would like to fly from New York to San Francisco.")) ]; var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputResponseContentArray);