From 04ceb384dae44fb97048ec1889e46170af4a997e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 12 Feb 2026 16:18:34 -0600 Subject: [PATCH 01/11] Squashed approach from https://github.com/dotnet/extensions/pull/7245 --- .../CompatibilitySuppressions.xml | 429 ++++++++++++++++++ .../Contents/AIContent.cs | 10 +- .../FunctionApprovalRequestContent.cs | 27 +- .../FunctionApprovalResponseContent.cs | 23 +- .../Contents/FunctionCallContent.cs | 1 + .../Contents/FunctionResultContent.cs | 1 + .../Contents/InputRequestContent.cs | 31 ++ .../Contents/InputResponseContent.cs | 31 ++ .../McpServerToolApprovalRequestContent.cs | 42 -- .../McpServerToolApprovalResponseContent.cs | 33 -- .../Contents/McpServerToolCallContent.cs | 42 +- .../Contents/McpServerToolResultContent.cs | 18 +- .../Contents/UserInputRequestContent.cs | 36 -- .../Contents/UserInputResponseContent.cs | 36 -- .../Functions/ApprovalRequiredAIFunction.cs | 3 - ...dMcpServerToolAlwaysRequireApprovalMode.cs | 3 - .../HostedMcpServerToolApprovalMode.cs | 4 - ...edMcpServerToolNeverRequireApprovalMode.cs | 3 - ...cpServerToolRequireSpecificApprovalMode.cs | 3 - .../Microsoft.Extensions.AI.Abstractions.json | 232 ++++------ .../Tools/HostedMcpServerTool.cs | 79 +--- .../Utilities/AIJsonUtilities.Defaults.cs | 21 +- .../OpenAIJsonContext.cs | 1 - .../OpenAIResponsesChatClient.cs | 121 +++-- .../FunctionInvokingChatClient.cs | 15 +- .../ChatCompletion/OpenTelemetryChatClient.cs | 68 +-- .../ChatReduction/SummarizingChatReducer.cs | 4 +- .../AssertExtensions.cs | 17 + .../Contents/AIContentTests.cs | 13 +- .../FunctionApprovalRequestContentTests.cs | 37 +- .../FunctionApprovalResponseContentTests.cs | 41 +- .../Contents/FunctionCallContentTests.cs | 56 +++ .../Contents/FunctionResultContentTests.cs | 51 +++ .../Contents/InputRequestContentTests.cs | 69 +++ .../Contents/InputResponseContentTests.cs | 68 +++ .../Contents/McpServerToolCallContentTests.cs | 44 +- .../McpServerToolResultContentTests.cs | 32 +- .../Contents/UserInputRequestContentTests.cs | 67 --- .../Contents/UserInputResponseContentTests.cs | 65 --- .../TestJsonSerializerContext.cs | 6 +- .../Tools/HostedMcpServerToolTests.cs | 90 +--- .../OpenAIConversionTests.cs | 299 +++++++++++- .../OpenAIResponseClientIntegrationTests.cs | 23 +- .../OpenAIResponseClientTests.cs | 322 ++++++++++++- ...unctionInvokingChatClientApprovalsTests.cs | 345 +++++++++++--- .../FunctionInvokingChatClientTests.cs | 10 +- .../OpenTelemetryChatClientTests.cs | 18 +- .../SummarizingChatReducerTests.cs | 32 +- 48 files changed, 2104 insertions(+), 918 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs delete mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..d266b9d2d4e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,429 @@ + + + + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0007 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index af8b19c8d84..55d01b2d310 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -18,18 +18,16 @@ namespace Microsoft.Extensions.AI; [JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")] [JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")] [JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")] +[JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")] +[JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")] +[JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] +[JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")] // These should be added in once they're no longer [Experimental]. If they're included while still // experimental, any JsonSerializerContext that includes AIContent will incur errors about using // experimental types in its source generated files. When [Experimental] is removed from these types, // these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions // as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. -// [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")] -// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")] -// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] -// [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")] -// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")] -// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")] // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs index f5a394cd63d..5e9cbcc3d04 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs @@ -2,42 +2,39 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// -/// Represents a request for user approval of a function call. +/// Represents a request for approval before invoking a function call. /// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class FunctionApprovalRequestContent : UserInputRequestContent +public sealed class FunctionApprovalRequestContent : InputRequestContent { /// /// Initializes a new instance of the class. /// - /// The ID that uniquely identifies the function approval request/response pair. - /// The function call that requires user approval. - /// is . - /// is empty or composed entirely of whitespace. + /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified . + /// The function call that requires approval before execution. + /// is . + /// is empty or composed entirely of whitespace. /// is . - public FunctionApprovalRequestContent(string id, FunctionCallContent functionCall) - : base(id) + public FunctionApprovalRequestContent(string requestId, FunctionCallContent functionCall) + : base(requestId) { FunctionCall = Throw.IfNull(functionCall); } /// - /// Gets the function call that pre-invoke approval is required for. + /// Gets the function call that requires approval before execution. /// public FunctionCallContent FunctionCall { get; } /// - /// Creates a to indicate whether the function call is approved or rejected based on the value of . + /// Creates a indicating whether the function call is approved or rejected. /// /// if the function call is approved; otherwise, . /// An optional reason for the approval or rejection. - /// The representing the approval response. - public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(Id, approved, FunctionCall) { Reason = reason }; + /// The correlated with this request. + public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(RequestId, approved, FunctionCall) { Reason = reason }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs index 5cc04c61442..04f12309b37 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs @@ -2,41 +2,38 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// -/// Represents a response to a function approval request. +/// Represents a response to a , indicating whether the function call was approved. /// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class FunctionApprovalResponseContent : UserInputResponseContent +public sealed class FunctionApprovalResponseContent : InputResponseContent { /// /// Initializes a new instance of the class. /// - /// The ID that uniquely identifies the function approval request/response pair. + /// The unique identifier of the associated with this response. /// if the function call is approved; otherwise, . - /// The function call that requires user approval. - /// is . - /// is empty or composed entirely of whitespace. + /// The function call that was subject to approval. + /// is . + /// is empty or composed entirely of whitespace. /// is . - public FunctionApprovalResponseContent(string id, bool approved, FunctionCallContent functionCall) - : base(id) + public FunctionApprovalResponseContent(string requestId, bool approved, FunctionCallContent functionCall) + : base(requestId) { Approved = approved; FunctionCall = Throw.IfNull(functionCall); } /// - /// Gets a value indicating whether the user approved the request. + /// Gets a value indicating whether the function call was approved for execution. /// public bool Approved { get; } /// - /// Gets the function call for which approval was requested. + /// Gets the function call that was subject to approval. /// public FunctionCallContent FunctionCall { get; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index 6ec7febc486..042e862f986 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,6 +14,7 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] +[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] public class FunctionCallContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs index d5eb4884709..9ffdb11472d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -13,6 +13,7 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] +[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] public class FunctionResultContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs new file mode 100644 index 00000000000..80fdf292d50 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -0,0 +1,31 @@ +// 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.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a request for input from the user or application. +/// +[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] +public class InputRequestContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier that correlates this request with its corresponding response. + /// is . + /// is empty or composed entirely of whitespace. + protected InputRequestContent(string requestId) + { + RequestId = Throw.IfNullOrWhitespace(requestId); + } + + /// + /// Gets the unique identifier that correlates this request with its corresponding . + /// + public string RequestId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs new file mode 100644 index 00000000000..086427dbab5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -0,0 +1,31 @@ +// 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.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents the response to an . +/// +[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] +public class InputResponseContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier that correlates this response with its corresponding request. + /// is . + /// is empty or composed entirely of whitespace. + protected InputResponseContent(string requestId) + { + RequestId = Throw.IfNullOrWhitespace(requestId); + } + + /// + /// Gets the unique identifier that correlates this response with its corresponding . + /// + public string RequestId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs deleted file mode 100644 index 8a611b1fc82..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs +++ /dev/null @@ -1,42 +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.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents a request for user approval of an MCP server tool call. -/// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class McpServerToolApprovalRequestContent : UserInputRequestContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID that uniquely identifies the MCP server tool approval request/response pair. - /// The tool call that requires user approval. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public McpServerToolApprovalRequestContent(string id, McpServerToolCallContent toolCall) - : base(id) - { - ToolCall = Throw.IfNull(toolCall); - } - - /// - /// Gets the tool call that pre-invoke approval is required for. - /// - public McpServerToolCallContent ToolCall { get; } - - /// - /// Creates a to indicate whether the function call is approved or rejected based on the value of . - /// - /// if the function call is approved; otherwise, . - /// The representing the approval response. - public McpServerToolApprovalResponseContent CreateResponse(bool approved) => new(Id, approved); -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs deleted file mode 100644 index 4eaab83e0db..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs +++ /dev/null @@ -1,33 +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.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents a response to an MCP server tool approval request. -/// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class McpServerToolApprovalResponseContent : UserInputResponseContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID that uniquely identifies the MCP server tool approval request/response pair. - /// if the MCP server tool call is approved; otherwise, . - /// is . - /// is empty or composed entirely of whitespace. - public McpServerToolApprovalResponseContent(string id, bool approved) - : base(id) - { - Approved = approved; - } - - /// - /// Gets a value indicating whether the user approved the request. - /// - public bool Approved { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index ef050f69ea2..62b7b25f065 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -2,9 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -13,44 +10,35 @@ namespace Microsoft.Extensions.AI; /// Represents a tool call request to a MCP server. /// /// +/// /// This content type is used to represent an invocation of an MCP server tool by a hosted service. -/// It is informational only. +/// It is informational only and may appear as part of an approval request +/// to convey what is being approved, or as a record of which MCP server tool was invoked. +/// /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class McpServerToolCallContent : AIContent +public sealed class McpServerToolCallContent : FunctionCallContent { /// /// Initializes a new instance of the class. /// /// The tool call ID. - /// The tool name. + /// The tool name. /// The MCP server name that hosts the tool. - /// or is . - /// or is empty or composed entirely of whitespace. - public McpServerToolCallContent(string callId, string toolName, string? serverName) + /// or is . + /// or is empty or composed entirely of whitespace. + /// + /// This content is informational only and may appear as part of an approval request + /// to convey what is being approved, or as a record of which MCP server tool was invoked. + /// + public McpServerToolCallContent(string callId, string name, string? serverName) + : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name)) { - CallId = Throw.IfNullOrWhitespace(callId); - ToolName = Throw.IfNullOrWhitespace(toolName); ServerName = serverName; + InformationalOnly = true; } - /// - /// Gets the tool call ID. - /// - public string CallId { get; } - - /// - /// Gets the name of the tool called. - /// - public string ToolName { get; } - /// /// Gets the name of the MCP server that hosts the tool. /// public string? ServerName { get; } - - /// - /// Gets or sets the arguments used for the tool call. - /// - public IReadOnlyDictionary? Arguments { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index a8798328019..694b86d4e43 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -2,9 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -16,8 +13,7 @@ namespace Microsoft.Extensions.AI; /// This content type is used to represent the result of an invocation of an MCP server tool by a hosted service. /// It is informational only. /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class McpServerToolResultContent : AIContent +public sealed class McpServerToolResultContent : FunctionResultContent { /// /// Initializes a new instance of the class. @@ -26,17 +22,7 @@ public sealed class McpServerToolResultContent : AIContent /// is . /// is empty or composed entirely of whitespace. public McpServerToolResultContent(string callId) + : base(Throw.IfNullOrWhitespace(callId), result: null) { - CallId = Throw.IfNullOrWhitespace(callId); } - - /// - /// Gets the tool call ID. - /// - public string CallId { get; } - - /// - /// Gets or sets the output of the tool call. - /// - public IList? Output { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs deleted file mode 100644 index 9f40b33253c..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs +++ /dev/null @@ -1,36 +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.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents a request for user input. -/// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] -[JsonDerivedType(typeof(McpServerToolApprovalRequestContent), "mcpServerToolApprovalRequest")] -public class UserInputRequestContent : AIContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID that uniquely identifies the user input request/response pair. - /// is . - /// is empty or composed entirely of whitespace. - protected UserInputRequestContent(string id) - { - Id = Throw.IfNullOrWhitespace(id); - } - - /// - /// Gets the ID that uniquely identifies the user input request/response pair. - /// - public string Id { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs deleted file mode 100644 index eaddd46f920..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs +++ /dev/null @@ -1,36 +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.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents the response to a request for user input. -/// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] -[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] -[JsonDerivedType(typeof(McpServerToolApprovalResponseContent), "mcpServerToolApprovalResponse")] -public class UserInputResponseContent : AIContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID that uniquely identifies the user input request/response pair. - /// is . - /// is empty or composed entirely of whitespace. - protected UserInputResponseContent(string id) - { - Id = Throw.IfNullOrWhitespace(id); - } - - /// - /// Gets the ID that uniquely identifies the user input request/response pair. - /// - public string Id { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs index 77a93342784..1f51cd16c9d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/ApprovalRequiredAIFunction.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -15,7 +13,6 @@ namespace Microsoft.Extensions.AI; /// This class simply augments an with an indication that approval is required before invocation. /// It does not enforce the requirement for user approval; it is the responsibility of the invoker to obtain that approval before invoking the function. /// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class ApprovalRequiredAIFunction : DelegatingAIFunction { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs index 608839b116b..dfb48583360 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -13,7 +11,6 @@ namespace Microsoft.Extensions.AI; /// /// Use to get an instance of . /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] [DebuggerDisplay(nameof(AlwaysRequire))] public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs index 2d81f4c924f..9c4ca705238 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -15,8 +13,6 @@ namespace Microsoft.Extensions.AI; /// The predefined values , and are provided to specify handling for all tools. /// To specify approval behavior for individual tool names, use . /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] -[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")] [JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")] [JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs index b21e3a61352..ede9f0f1309 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -13,7 +11,6 @@ namespace Microsoft.Extensions.AI; /// /// Use to get an instance of . /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] [DebuggerDisplay(nameof(NeverRequire))] public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs index 84d1233a357..526c49bc027 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs @@ -3,16 +3,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; /// /// Represents a mode where approval behavior is specified for individual tool names. /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index d7bfcc15bcf..95cfc015c86 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -729,11 +729,11 @@ }, { "Type": "sealed class Microsoft.Extensions.AI.ApprovalRequiredAIFunction : Microsoft.Extensions.AI.DelegatingAIFunction", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.ApprovalRequiredAIFunction.ApprovalRequiredAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -1930,46 +1930,46 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent", - "Stage": "Experimental", + "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.InputRequestContent", + "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string id, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Experimental" + "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string requestId, Microsoft.Extensions.AI.FunctionCallContent functionCall);", + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.CreateResponse(bool approved, string? reason = null);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionCall { get; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent", - "Stage": "Experimental", + "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.InputResponseContent", + "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string id, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Experimental" + "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", + "Stage": "Stable" } ], "Properties": [ { "Member": "bool Microsoft.Extensions.AI.FunctionApprovalResponseContent.Approved { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionCall { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "string? Microsoft.Extensions.AI.FunctionApprovalResponseContent.Reason { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -2227,145 +2227,141 @@ }, { "Type": "class Microsoft.Extensions.AI.HostedMcpServerTool : Microsoft.Extensions.AI.AITool", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string serverAddress);", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, string serverAddress, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", - "Stage": "Experimental" + "Stage": "Stable" }, { - "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverUrl);", - "Stage": "Experimental" + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverAddress);", + "Stage": "Stable" }, { - "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverUrl, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", - "Stage": "Experimental" + "Member": "Microsoft.Extensions.AI.HostedMcpServerTool.HostedMcpServerTool(string serverName, System.Uri serverAddress, System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", + "Stage": "Stable" } ], "Properties": [ { "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.HostedMcpServerTool.AdditionalProperties { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerTool.AllowedTools { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode? Microsoft.Extensions.AI.HostedMcpServerTool.ApprovalMode { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.HostedMcpServerTool.AuthorizationToken { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { - "Member": "System.Collections.Generic.IDictionary Microsoft.Extensions.AI.HostedMcpServerTool.Headers { get; }", - "Stage": "Experimental" + "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.HostedMcpServerTool.Headers { get; set; }", + "Stage": "Stable" }, { "Member": "override string Microsoft.Extensions.AI.HostedMcpServerTool.Name { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerAddress { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "string? Microsoft.Extensions.AI.HostedMcpServerTool.ServerDescription { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "string Microsoft.Extensions.AI.HostedMcpServerTool.ServerName { get; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.HostedMcpServerToolAlwaysRequireApprovalMode();", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.Equals(object? obj);", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode.GetHashCode();", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "class Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.RequireSpecific(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolAlwaysRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.AlwaysRequire { get; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "static Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode.NeverRequire { get; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.HostedMcpServerToolNeverRequireApprovalMode();", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.Equals(object? obj);", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolNeverRequireApprovalMode.GetHashCode();", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { "Type": "sealed class Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode : Microsoft.Extensions.AI.HostedMcpServerToolApprovalMode", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.HostedMcpServerToolRequireSpecificApprovalMode(System.Collections.Generic.IList? alwaysRequireApprovalToolNames, System.Collections.Generic.IList? neverRequireApprovalToolNames);", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override bool Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.Equals(object? obj);", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "override int Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.GetHashCode();", - "Stage": "Experimental" + "Stage": "Stable" } ], "Properties": [ { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.AlwaysRequireApprovalToolNames { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedMcpServerToolRequireSpecificApprovalMode.NeverRequireApprovalToolNames { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -2429,11 +2425,11 @@ }, { "Type": "interface Microsoft.Extensions.AI.IChatReducer", - "Stage": "Stable", + "Stage": "Experimental", "Methods": [ { "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IChatReducer.ReduceAsync(System.Collections.Generic.IEnumerable messages, System.Threading.CancellationToken cancellationToken);", - "Stage": "Stable" + "Stage": "Experimental" } ] }, @@ -2701,114 +2697,88 @@ ] }, { - "Type": "interface Microsoft.Extensions.AI.ISpeechToTextClient : System.IDisposable", - "Stage": "Experimental", + "Type": "class Microsoft.Extensions.AI.InputRequestContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", "Methods": [ { - "Member": "object? Microsoft.Extensions.AI.ISpeechToTextClient.GetService(System.Type serviceType, object? serviceKey = null);", - "Stage": "Experimental" - }, - { - "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ISpeechToTextClient.GetStreamingTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" - }, + "Member": "Microsoft.Extensions.AI.InputRequestContent.InputRequestContent(string requestId);", + "Stage": "Stable" + } + ], + "Properties": [ { - "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.ISpeechToTextClient.GetTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" + "Member": "string Microsoft.Extensions.AI.InputRequestContent.RequestId { get; }", + "Stage": "Stable" } ] }, { - "Type": "interface Microsoft.Extensions.AI.IToolReductionStrategy", - "Stage": "Experimental", + "Type": "class Microsoft.Extensions.AI.InputResponseContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", "Methods": [ { - "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IToolReductionStrategy.SelectToolsForRequestAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", - "Stage": "Experimental" + "Member": "Microsoft.Extensions.AI.InputResponseContent.InputResponseContent(string requestId);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.InputResponseContent.RequestId { get; }", + "Stage": "Stable" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.McpServerToolApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent", + "Type": "interface Microsoft.Extensions.AI.ISpeechToTextClient : System.IDisposable", "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.McpServerToolApprovalRequestContent(string id, Microsoft.Extensions.AI.McpServerToolCallContent toolCall);", + "Member": "object? Microsoft.Extensions.AI.ISpeechToTextClient.GetService(System.Type serviceType, object? serviceKey = null);", "Stage": "Experimental" }, { - "Member": "Microsoft.Extensions.AI.McpServerToolApprovalResponseContent Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.CreateResponse(bool approved);", + "Member": "System.Collections.Generic.IAsyncEnumerable Microsoft.Extensions.AI.ISpeechToTextClient.GetStreamingTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Experimental" - } - ], - "Properties": [ + }, { - "Member": "Microsoft.Extensions.AI.McpServerToolCallContent Microsoft.Extensions.AI.McpServerToolApprovalRequestContent.ToolCall { get; }", + "Member": "System.Threading.Tasks.Task Microsoft.Extensions.AI.ISpeechToTextClient.GetTextAsync(System.IO.Stream audioSpeechStream, Microsoft.Extensions.AI.SpeechToTextOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Experimental" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.McpServerToolApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent", + "Type": "interface Microsoft.Extensions.AI.IToolReductionStrategy", "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolApprovalResponseContent.McpServerToolApprovalResponseContent(string id, bool approved);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "bool Microsoft.Extensions.AI.McpServerToolApprovalResponseContent.Approved { get; }", + "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IToolReductionStrategy.SelectToolsForRequestAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));", "Stage": "Experimental" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.AIContent", - "Stage": "Experimental", + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.FunctionCallContent", + "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string toolName, string? serverName);", - "Stage": "Experimental" + "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName);", + "Stage": "Stable" } ], "Properties": [ - { - "Member": "System.Collections.Generic.IReadOnlyDictionary? Microsoft.Extensions.AI.McpServerToolCallContent.Arguments { get; set; }", - "Stage": "Experimental" - }, - { - "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.CallId { get; }", - "Stage": "Experimental" - }, { "Member": "string? Microsoft.Extensions.AI.McpServerToolCallContent.ServerName { get; }", - "Stage": "Experimental" - }, - { - "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.ToolName { get; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.AIContent", - "Stage": "Experimental", + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.FunctionResultContent", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "string Microsoft.Extensions.AI.McpServerToolResultContent.CallId { get; }", - "Stage": "Experimental" - }, - { - "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.McpServerToolResultContent.Output { get; set; }", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -3435,38 +3405,6 @@ "Stage": "Stable" } ] - }, - { - "Type": "class Microsoft.Extensions.AI.UserInputRequestContent : Microsoft.Extensions.AI.AIContent", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.UserInputRequestContent.UserInputRequestContent(string id);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "string Microsoft.Extensions.AI.UserInputRequestContent.Id { get; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.UserInputResponseContent : Microsoft.Extensions.AI.AIContent", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.UserInputResponseContent.UserInputResponseContent(string id);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "string Microsoft.Extensions.AI.UserInputResponseContent.Id { get; }", - "Stage": "Experimental" - } - ] } ] } \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index ef95d68031c..682933df7f7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -12,18 +10,11 @@ namespace Microsoft.Extensions.AI; /// /// Represents a hosted MCP server tool that can be specified to an AI service. /// -[Experimental(DiagnosticIds.Experiments.AIMcpServers, UrlFormat = DiagnosticIds.UrlFormat)] public class HostedMcpServerTool : AITool { - /// The name of the Authorization header. - private const string AuthorizationHeaderName = "Authorization"; - /// Any additional properties associated with the tool. private IReadOnlyDictionary? _additionalProperties; - /// Lazily-initialized collection of headers to include when calling the remote MCP server. - private Dictionary? _headers; - /// /// Initializes a new instance of the class. /// @@ -55,12 +46,12 @@ public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDic /// Initializes a new instance of the class. /// /// The name of the remote MCP server. - /// The URL of the remote MCP server. - /// or is . + /// The URL of the remote MCP server. + /// or is . /// is empty or composed entirely of whitespace. - /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl) - : this(serverName, ValidateUrl(serverUrl)) + /// is not an absolute URL. + public HostedMcpServerTool(string serverName, Uri serverAddress) + : this(serverName, ValidateUrl(serverAddress)) { } @@ -68,27 +59,27 @@ public HostedMcpServerTool(string serverName, Uri serverUrl) /// Initializes a new instance of the class. /// /// The name of the remote MCP server. - /// The URL of the remote MCP server. + /// The URL of the remote MCP server. /// Any additional properties associated with the tool. - /// or is . + /// or is . /// is empty or composed entirely of whitespace. - /// is not an absolute URL. - public HostedMcpServerTool(string serverName, Uri serverUrl, IReadOnlyDictionary? additionalProperties) - : this(serverName, ValidateUrl(serverUrl)) + /// is not an absolute URL. + public HostedMcpServerTool(string serverName, Uri serverAddress, IReadOnlyDictionary? additionalProperties) + : this(serverName, ValidateUrl(serverAddress)) { _additionalProperties = additionalProperties; } - private static string ValidateUrl(Uri serverUrl) + private static string ValidateUrl(Uri serverAddress) { - _ = Throw.IfNull(serverUrl); + _ = Throw.IfNull(serverAddress); - if (!serverUrl.IsAbsoluteUri) + if (!serverAddress.IsAbsoluteUri) { - Throw.ArgumentException(nameof(serverUrl), "The provided URL is not absolute."); + Throw.ArgumentException(nameof(serverAddress), "The provided URL is not absolute."); } - return serverUrl.AbsoluteUri; + return serverAddress.AbsoluteUri; } /// @@ -107,39 +98,6 @@ private static string ValidateUrl(Uri serverUrl) /// public string ServerAddress { get; } - /// - /// Gets or sets the OAuth authorization token that the AI service should use when calling the remote MCP server. - /// - /// - /// When set, this value is automatically added to the dictionary with the key "Authorization" - /// and the value "Bearer {token}". Setting this property will overwrite any existing "Authorization" header in . - /// Setting this property to will remove the "Authorization" header from . - /// - public string? AuthorizationToken - { - get - { - if (_headers?.TryGetValue(AuthorizationHeaderName, out string? value) is true && - value?.StartsWith("Bearer ", StringComparison.Ordinal) is true) - { - return value.Substring("Bearer ".Length); - } - - return null; - } - set - { - if (value is not null) - { - Headers[AuthorizationHeaderName] = $"Bearer {value}"; - } - else if (_headers is not null) - { - _ = _headers.Remove(AuthorizationHeaderName); - } - } - } - /// /// Gets or sets the description of the remote MCP server, used to provide more context to the AI service. /// @@ -171,12 +129,15 @@ public string? AuthorizationToken public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } /// - /// Gets a mutable dictionary of HTTP headers to include when calling the remote MCP server. + /// Gets or sets a mutable dictionary of HTTP headers to include when calling the remote MCP server. /// /// /// /// The underlying provider is not guaranteed to support or honor the headers. /// + /// + /// This property is useful for specifying the authentication header or other headers required by the MCP server. + /// /// - public IDictionary Headers => _headers ??= new Dictionary(); + public IDictionary? Headers { get; set; } } 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 0f2e4340358..8c9ffeaa54a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -51,12 +51,6 @@ private static JsonSerializerOptions CreateDefaultOptions() // Temporary workaround: these types are [Experimental] and can't be added as [JsonDerivedType] on AIContent yet, // or else consuming assemblies that used source generation with AIContent would implicitly reference them. // Once they're no longer [Experimental] and added as [JsonDerivedType] on AIContent, these lines should be removed. - AddAIContentType(options, typeof(FunctionApprovalRequestContent), typeDiscriminatorId: "functionApprovalRequest", checkBuiltIn: false); - AddAIContentType(options, typeof(FunctionApprovalResponseContent), typeDiscriminatorId: "functionApprovalResponse", checkBuiltIn: false); - AddAIContentType(options, typeof(McpServerToolCallContent), typeDiscriminatorId: "mcpServerToolCall", checkBuiltIn: false); - AddAIContentType(options, typeof(McpServerToolResultContent), typeDiscriminatorId: "mcpServerToolResult", checkBuiltIn: false); - AddAIContentType(options, typeof(McpServerToolApprovalRequestContent), typeDiscriminatorId: "mcpServerToolApprovalRequest", checkBuiltIn: false); - 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(ImageGenerationToolCallContent), typeDiscriminatorId: "imageGenerationToolCall", checkBuiltIn: false); @@ -123,16 +117,15 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(AIContent))] [JsonSerializable(typeof(IEnumerable))] + // InputRequestContent and InputResponseContent are polymorphic base types that may be + // serialized as root types (not just as AIContent). They have protected constructors so + // can't be instantiated directly, but we still need metadata when serializing derived + // types (e.g., FunctionApprovalRequestContent) as InputRequestContent. + [JsonSerializable(typeof(InputRequestContent))] + [JsonSerializable(typeof(InputResponseContent))] + // Temporary workaround: These should be implicitly added in once they're no longer [Experimental] // and are included via [JsonDerivedType] on AIContent. - [JsonSerializable(typeof(UserInputRequestContent))] - [JsonSerializable(typeof(UserInputResponseContent))] - [JsonSerializable(typeof(FunctionApprovalRequestContent))] - [JsonSerializable(typeof(FunctionApprovalResponseContent))] - [JsonSerializable(typeof(McpServerToolCallContent))] - [JsonSerializable(typeof(McpServerToolResultContent))] - [JsonSerializable(typeof(McpServerToolApprovalRequestContent))] - [JsonSerializable(typeof(McpServerToolApprovalResponseContent))] [JsonSerializable(typeof(CodeInterpreterToolCallContent))] [JsonSerializable(typeof(CodeInterpreterToolResultContent))] [JsonSerializable(typeof(ImageGenerationToolCallContent))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs index 8f42edb24d6..9a040864613 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs @@ -17,7 +17,6 @@ namespace Microsoft.Extensions.AI; WriteIndented = true)] [JsonSerializable(typeof(OpenAIClientExtensions.ToolJson))] [JsonSerializable(typeof(IDictionary))] -[JsonSerializable(typeof(IReadOnlyDictionary))] [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(JsonElement))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index af13a52328d..eedcf3b79df 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -167,6 +167,7 @@ internal static ChatResponse FromOpenAIResponse(ResponseResult responseResult, C internal static IEnumerable ToChatMessages(IEnumerable items, CreateResponseOptions? options = null) { ChatMessage? message = null; + Dictionary? mcpApprovalRequests = null; foreach (ResponseItem outputItem in items) { @@ -210,9 +211,13 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable FromOpenAIStreamingRe ChatRole? lastRole = null; bool anyFunctions = false; ResponseStatus? latestResponseStatus = null; + Dictionary? mcpApprovalRequests = null; UpdateConversationId(resumeResponseId); @@ -447,9 +467,13 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; case McpToolCallApprovalRequestItem mtcari: - yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) + // Store for correlation with responses. + (mcpApprovalRequests ??= new())[mtcari.Id] = mtcari; + + // We are reusing the mtcari.Id as the McpServerToolCallContent.CallId since we don't have one yet. + yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) { - Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!, + Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), RawRepresentation = mtcari, }) { @@ -457,6 +481,24 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => }); break; + case McpToolCallApprovalResponseItem mtcari + when mcpApprovalRequests?.TryGetValue(mtcari.ApprovalRequestId, out McpToolCallApprovalRequestItem? request) is true: + _ = mcpApprovalRequests.Remove(mtcari.ApprovalRequestId); + + // Correlate with the original request to get tool details. + // McpToolCallApprovalResponseItem without a correlated request falls through to default. + yield return CreateUpdate(new FunctionApprovalResponseContent( + mtcari.ApprovalRequestId, + mtcari.Approved, + new McpServerToolCallContent(mtcari.ApprovalRequestId, request.ToolName, request.ServerLabel) + { + Arguments = JsonSerializer.Deserialize(request.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), + }) + { + RawRepresentation = mtcari, + }); + break; + case CodeInterpreterCallResponseItem cicri: // The CodeInterpreterToolCallContent has already been yielded as part of delta updates. // Only yield the CodeInterpreterToolResultContent here for the outputs. @@ -635,16 +677,22 @@ void IDisposable.Dispose() if (isUrl) { - // For http: favor headers over authorization token. - if (mcpTool.Headers.Count > 0) + if (mcpTool.Headers is { Count: > 0 }) { responsesMcpTool.Headers = mcpTool.Headers; } } else { - // For connectors: Only set AuthorizationToken, do not include headers. - responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken; + // For connectors: extract Bearer token from Headers and set as AuthorizationToken. + // Use case-insensitive comparison since auth scheme is case-insensitive per RFC 7235. + // Allow flexible whitespace in the header value. + if (mcpTool.Headers?.TryGetValue("Authorization", out string? authHeader) is true && + authHeader.AsSpan().Trim() is { Length: > 0 } trimmedAuthHeader && + trimmedAuthHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + responsesMcpTool.AuthorizationToken = trimmedAuthHeader.Slice("Bearer ".Length).TrimStart().ToString(); + } } if (mcpTool.AllowedTools is not null) @@ -886,7 +934,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null; + Dictionary? idToContentMapping = null; foreach (ChatMessage input in inputs) { @@ -919,7 +967,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep, - McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved), + FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.RequestId, funcResp.Approved), _ => null }; @@ -1001,6 +1049,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable contents) { @@ -1115,10 +1167,6 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera break; } break; - - case McpServerToolApprovalResponseContent mcpApprovalResponseContent: - yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved); - break; } } @@ -1146,6 +1194,10 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera }; break; + case McpServerToolCallContent mstcc: + (idToContentMapping ??= [])[mstcc.CallId] = mstcc; + break; + case FunctionCallContent callContent: yield return ResponseItem.CreateFunctionCallItem( callContent.CallId, @@ -1155,34 +1207,33 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; - case McpServerToolApprovalRequestContent mcpApprovalRequestContent: + case FunctionApprovalRequestContent funcReq when funcReq.FunctionCall is McpServerToolCallContent mcpToolCall: yield return ResponseItem.CreateMcpApprovalRequestItem( - mcpApprovalRequestContent.Id, - mcpApprovalRequestContent.ToolCall.ServerName, - mcpApprovalRequestContent.ToolCall.ToolName, - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(mcpApprovalRequestContent.ToolCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject))); - break; - - case McpServerToolCallContent mstcc: - (idToContentMapping ??= [])[mstcc.CallId] = mstcc; + funcReq.RequestId, + mcpToolCall.ServerName, + mcpToolCall.Name, + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes( + mcpToolCall.Arguments!, + AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; case McpServerToolResultContent mstrc: - if (idToContentMapping?.TryGetValue(mstrc.CallId, out AIContent? callContentFromMapping) is true && - callContentFromMapping is McpServerToolCallContent associatedCall) + if (idToContentMapping?.TryGetValue(mstrc.CallId, out McpServerToolCallContent? associatedCall) is true) { _ = idToContentMapping.Remove(mstrc.CallId); McpToolCallItem mtci = ResponseItem.CreateMcpToolCallItem( associatedCall.ServerName, - associatedCall.ToolName, - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(associatedCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject))); - if (mstrc.Output?.OfType().FirstOrDefault() is ErrorContent errorContent) + associatedCall.Name, + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes( + associatedCall.Arguments!, + AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); + if (mstrc.Result is ErrorContent errorContent) { mtci.Error = BinaryData.FromString(errorContent.Message); } - else + else if (mstrc.Result is TextContent textContent) { - mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []); + mtci.ToolOutput = textContent.Text; } yield return mtci; @@ -1367,7 +1418,7 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList { contents.Add(new McpServerToolCallContent(mtci.Id, mtci.ToolName, mtci.ServerLabel) { - Arguments = JsonSerializer.Deserialize(mtci.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!, + Arguments = JsonSerializer.Deserialize(mtci.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), // We purposefully do not set the RawRepresentation on the McpServerToolCallContent, only on the McpServerToolResultContent, to avoid // the same McpToolCallItem being included on two different AIContent instances. When these are roundtripped, we want only one @@ -1377,9 +1428,9 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents.Add(new McpServerToolResultContent(mtci.Id) { RawRepresentation = mtci, - Output = [mtci.Error is not null ? + Result = mtci.Error is not null ? new ErrorContent(mtci.Error.ToString()) : - new TextContent(mtci.ToolOutput)], + new TextContent(mtci.ToolOutput), }); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 9fd95df0424..7fcf5b770de 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1464,13 +1464,13 @@ private static bool CurrentActivityIsInvokeAgent var content = message.Contents[j]; switch (content) { - case FunctionApprovalRequestContent farc: + case FunctionApprovalRequestContent farc when !farc.FunctionCall.InformationalOnly: // Validation: Capture each call id for each approval request to ensure later we have a matching response. _ = (approvalRequestCallIds ??= []).Add(farc.FunctionCall.CallId); - (allApprovalRequestsMessages ??= []).Add(farc.Id, message); + (allApprovalRequestsMessages ??= []).Add(farc.RequestId, message); break; - case FunctionApprovalResponseContent farc: + case FunctionApprovalResponseContent farc when !farc.FunctionCall.InformationalOnly: // Validation: Remove the call id for each approval response, to check it off the list of requests we need responses for. _ = approvalRequestCallIds?.Remove(farc.FunctionCall.CallId); (allApprovalResponses ??= []).Add(farc); @@ -1542,7 +1542,7 @@ private static bool CurrentActivityIsInvokeAgent ref List? targetList = ref approvalResponse.Approved ? ref approvedFunctionCalls : ref rejectedFunctionCalls; ChatMessage? requestMessage = null; - _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.Id, out requestMessage); + _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.RequestId, out requestMessage); (targetList ??= []).Add(new() { Response = approvalResponse, RequestMessage = requestMessage }); } @@ -1711,7 +1711,7 @@ private static bool TryReplaceFunctionCallsWithApprovalRequests(IList if (content[i] is FunctionCallContent fcc && !fcc.InformationalOnly) { updatedContent ??= [.. content]; // Clone the list if we haven't already - updatedContent[i] = new FunctionApprovalRequestContent(fcc.CallId, fcc); + updatedContent[i] = new FunctionApprovalRequestContent(ComposeApprovalRequestId(fcc.CallId), fcc); } } } @@ -1766,7 +1766,7 @@ private IList ReplaceFunctionCallsWithApprovalRequests( var functionCall = (FunctionCallContent)message.Contents[contentIndex]; LogFunctionRequiresApproval(functionCall.Name); - message.Contents[contentIndex] = new FunctionApprovalRequestContent(functionCall.CallId, functionCall); + message.Contents[contentIndex] = new FunctionApprovalRequestContent(ComposeApprovalRequestId(functionCall.CallId), functionCall); outputMessages[messageIndex] = message; lastMessageIndex = messageIndex; @@ -1783,6 +1783,9 @@ private static TimeSpan GetElapsedTime(long startingTimestamp) => new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency))); #endif + /// Composes an approval request ID from a function call ID. + private static string ComposeApprovalRequestId(string callId) => $"ficc_{callId}"; + /// /// Execute the provided and return the resulting /// wrapped in objects. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index bcb4fb2a241..ad3cf4feae0 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -258,6 +258,30 @@ internal static string SerializeChatMessages( m.Parts.Add(new OtelGenericPart { Type = "reasoning", Content = trc.Text }); break; + case McpServerToolCallContent mstcc: + m.Parts.Add(new OtelServerToolCallPart + { + Id = mstcc.CallId, + Name = mstcc.Name, + ServerToolCall = new OtelMcpToolCall + { + Arguments = mstcc.Arguments, + ServerName = mstcc.ServerName, + }, + }); + break; + + case McpServerToolResultContent mstrc: + m.Parts.Add(new OtelServerToolCallResponsePart + { + Id = mstrc.CallId, + ServerToolCallResponse = new OtelMcpToolCallResponse + { + Output = mstrc.Result, + }, + }); + break; + case FunctionCallContent fcc: m.Parts.Add(new OtelToolCallRequestPart { @@ -357,50 +381,26 @@ internal static string SerializeChatMessages( }); break; - case McpServerToolCallContent mstcc: - m.Parts.Add(new OtelServerToolCallPart - { - Id = mstcc.CallId, - Name = mstcc.ToolName, - ServerToolCall = new OtelMcpToolCall - { - Arguments = mstcc.Arguments, - ServerName = mstcc.ServerName, - }, - }); - break; - - case McpServerToolResultContent mstrc: - m.Parts.Add(new OtelServerToolCallResponsePart - { - Id = mstrc.CallId, - ServerToolCallResponse = new OtelMcpToolCallResponse - { - Output = mstrc.Output, - }, - }); - break; - - case McpServerToolApprovalRequestContent mstarc: + case FunctionApprovalRequestContent fareqc when fareqc.FunctionCall is McpServerToolCallContent mcpToolCall: m.Parts.Add(new OtelServerToolCallPart { - Id = mstarc.Id, - Name = mstarc.ToolCall.ToolName, + Id = fareqc.RequestId, + Name = fareqc.FunctionCall.Name, ServerToolCall = new OtelMcpApprovalRequest { - Arguments = mstarc.ToolCall.Arguments, - ServerName = mstarc.ToolCall.ServerName, + Arguments = mcpToolCall.Arguments, + ServerName = mcpToolCall.ServerName, }, }); break; - case McpServerToolApprovalResponseContent mstaresp: + case FunctionApprovalResponseContent farespc when farespc.FunctionCall is McpServerToolCallContent: m.Parts.Add(new OtelServerToolCallResponsePart { - Id = mstaresp.Id, + Id = farespc.RequestId, ServerToolCallResponse = new OtelMcpApprovalResponse { - Approved = mstaresp.Approved, + Approved = farespc.Approved, }, }); break; @@ -858,7 +858,7 @@ private sealed class OtelMcpToolCall { public string Type { get; set; } = "mcp"; public string? ServerName { get; set; } - public IReadOnlyDictionary? Arguments { get; set; } + public IDictionary? Arguments { get; set; } } private sealed class OtelMcpToolCallResponse @@ -871,7 +871,7 @@ private sealed class OtelMcpApprovalRequest { public string Type { get; set; } = "mcp_approval_request"; public string? ServerName { get; set; } - public IReadOnlyDictionary? Arguments { get; set; } + public IDictionary? Arguments { get; set; } } private sealed class OtelMcpApprovalResponse diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs index a3b38ca14cd..7121f61cd8f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatReduction/SummarizingChatReducer.cs @@ -208,8 +208,8 @@ public IEnumerable ToChatMessages() private static bool IsToolRelatedContent(AIContent content) => content is FunctionCallContent or FunctionResultContent - or UserInputRequestContent - or UserInputResponseContent; + or InputRequestContent + or InputResponseContent; /// Builds the list of messages to send to the chat client for summarization. private IEnumerable ToSummarizerChatMessages(int indexOfFirstMessageToKeep, string summarizationPrompt) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs index 546b93a7f20..975ccb26230 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs @@ -46,6 +46,23 @@ public static void EqualMessageLists(List expectedMessages, List { { "param1", 123 } })), new McpServerToolCallContent("call123", "myTool", "myServer"), new McpServerToolResultContent("call123"), - new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new McpServerToolApprovalResponseContent("request123", approved: true), + new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), + new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), new ImageGenerationToolCallContent { ImageId = "img123" }, new ImageGenerationToolResultContent { ImageId = "img456", Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] } ]); + // Verify each element roundtrips individually + foreach (AIContent content in message.Contents) + { + var serializedElement = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedElement = JsonSerializer.Deserialize(serializedElement, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserializedElement); + Assert.Equal(content.GetType(), deserializedElement.GetType()); + } + var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); ChatMessage? deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserialized); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs index cc5cc1dd8d9..8028167d8ad 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs @@ -15,9 +15,9 @@ public void Constructor_InvalidArguments_Throws() { FunctionCallContent functionCall = new("FCC1", "TestFunction"); - Assert.Throws("id", () => new FunctionApprovalRequestContent(null!, functionCall)); - Assert.Throws("id", () => new FunctionApprovalRequestContent("", functionCall)); - Assert.Throws("id", () => new FunctionApprovalRequestContent("\r\t\n ", functionCall)); + Assert.Throws("requestId", () => new FunctionApprovalRequestContent(null!, functionCall)); + Assert.Throws("requestId", () => new FunctionApprovalRequestContent("", functionCall)); + Assert.Throws("requestId", () => new FunctionApprovalRequestContent("\r\t\n ", functionCall)); Assert.Throws("functionCall", () => new FunctionApprovalRequestContent("id", null!)); } @@ -32,7 +32,7 @@ public void Constructor_Roundtrips(string id) FunctionApprovalRequestContent content = new(id, functionCall); - Assert.Same(id, content.Id); + Assert.Same(id, content.RequestId); Assert.Same(functionCall, content.FunctionCall); } @@ -49,7 +49,7 @@ public void CreateResponse_ReturnsExpectedResponse(bool approved) var response = content.CreateResponse(approved); Assert.NotNull(response); - Assert.Same(id, response.Id); + Assert.Same(id, response.RequestId); Assert.Equal(approved, response.Approved); Assert.Same(functionCall, response.FunctionCall); Assert.Null(response.Reason); @@ -70,7 +70,7 @@ public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, str var response = content.CreateResponse(approved, reason); Assert.NotNull(response); - Assert.Same(id, response.Id); + Assert.Same(id, response.RequestId); Assert.Equal(approved, response.Approved); Assert.Same(functionCall, response.FunctionCall); Assert.Equal(reason, response.Reason); @@ -81,13 +81,22 @@ public void Serialization_Roundtrips() { var content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })); - var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); - - Assert.NotNull(deserializedContent); - Assert.Equal(content.Id, deserializedContent.Id); - Assert.NotNull(deserializedContent.FunctionCall); - Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId); - Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + + static void AssertSerializationRoundtrips(FunctionApprovalRequestContent content) + where T : AIContent + { + T contentAsT = (T)(object)content; + string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); + T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + var deserializedContent = Assert.IsType(deserialized); + Assert.Equal(content.RequestId, deserializedContent.RequestId); + Assert.NotNull(deserializedContent.FunctionCall); + Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId); + Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name); + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs index 405955463a1..036ef83b65a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs @@ -14,9 +14,9 @@ public void Constructor_InvalidArguments_Throws() { FunctionCallContent functionCall = new("FCC1", "TestFunction"); - Assert.Throws("id", () => new FunctionApprovalResponseContent(null!, true, functionCall)); - Assert.Throws("id", () => new FunctionApprovalResponseContent("", true, functionCall)); - Assert.Throws("id", () => new FunctionApprovalResponseContent("\r\t\n ", true, functionCall)); + Assert.Throws("requestId", () => new FunctionApprovalResponseContent(null!, true, functionCall)); + Assert.Throws("requestId", () => new FunctionApprovalResponseContent("", true, functionCall)); + Assert.Throws("requestId", () => new FunctionApprovalResponseContent("\r\t\n ", true, functionCall)); Assert.Throws("functionCall", () => new FunctionApprovalResponseContent("id", true, null!)); } @@ -30,15 +30,44 @@ public void Constructor_Roundtrips(string id, bool approved) FunctionCallContent functionCall = new("FCC1", "TestFunction"); FunctionApprovalResponseContent content = new(id, approved, functionCall); - Assert.Same(id, content.Id); + Assert.Same(id, content.RequestId); Assert.Equal(approved, content.Approved); Assert.Same(functionCall, content.FunctionCall); } + [Fact] + public void Serialization_Roundtrips() + { + var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) + { + Reason = "Approved for testing" + }; + + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + + static void AssertSerializationRoundtrips(FunctionApprovalResponseContent content) + where T : AIContent + { + T contentAsT = (T)(object)content; + string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); + T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + var deserializedContent = Assert.IsType(deserialized); + Assert.Equal(content.RequestId, deserializedContent.RequestId); + Assert.Equal(content.Approved, deserializedContent.Approved); + Assert.Equal(content.Reason, deserializedContent.Reason); + Assert.NotNull(deserializedContent.FunctionCall); + Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId); + Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name); + } + } + [Theory] [InlineData(null)] [InlineData("Custom rejection reason")] - public void Serialization_Roundtrips(string? reason) + public void Serialization_WithReason_Roundtrips(string? reason) { var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) { @@ -49,7 +78,7 @@ public void Serialization_Roundtrips(string? reason) var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedContent); - Assert.Equal(content.Id, deserializedContent.Id); + Assert.Equal(content.RequestId, deserializedContent.RequestId); Assert.Equal(content.Approved, deserializedContent.Approved); Assert.Equal(content.Reason, deserializedContent.Reason); Assert.NotNull(deserializedContent.FunctionCall); 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 9fde659d65f..0ca4db7a123 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs @@ -405,4 +405,60 @@ public static void CreateFromParsedArguments_NullInput_ThrowsArgumentNullExcepti Assert.Throws("name", () => FunctionCallContent.CreateFromParsedArguments("{}", "callId", null!, _ => null)); Assert.Throws("argumentParser", () => FunctionCallContent.CreateFromParsedArguments("{}", "callId", "functionName", null!)); } + + [Fact] + public void Serialization_Roundtrips() + { + var content = new FunctionCallContent("call123", "myFunction") + { + Arguments = new Dictionary { { "arg1", "value1" } } + }; + + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + + static void AssertSerializationRoundtrips(FunctionCallContent content) + where T : AIContent + { + T contentAsT = (T)(object)content; + string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); + T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + var deserializedContent = Assert.IsType(deserialized); + Assert.Equal(content.CallId, deserializedContent.CallId); + Assert.Equal(content.Name, deserializedContent.Name); + Assert.NotNull(deserializedContent.Arguments); + Assert.Equal("value1", deserializedContent.Arguments["arg1"]?.ToString()); + } + } + + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + FunctionCallContent[] contents = + [ + new FunctionCallContent("call1", "function1", new Dictionary { { "param1", 123 } }), + new McpServerToolCallContent("call2", "myTool", "myServer"), + ]; + + // Verify each element roundtrips individually + foreach (var content in contents) + { + var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + Assert.Equal(content.GetType(), deserialized.GetType()); + } + + // Verify the array roundtrips + var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionCallContentArray); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionCallContentArray); + Assert.NotNull(deserializedContents); + Assert.Equal(contents.Length, deserializedContents.Length); + for (int i = 0; i < deserializedContents.Length; i++) + { + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + } + } } 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 1542a4b823a..7bf2cdf9d6f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -91,4 +91,55 @@ public void ItShouldBeSerializableAndDeserializableWithException() Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); Assert.Null(deserializedSut.Exception); } + + [Fact] + public void Serialization_Roundtrips() + { + var content = new FunctionResultContent("call123", "result"); + + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + + static void AssertSerializationRoundtrips(FunctionResultContent content) + where T : AIContent + { + T contentAsT = (T)(object)content; + string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); + T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + var deserializedContent = Assert.IsType(deserialized); + Assert.Equal(content.CallId, deserializedContent.CallId); + Assert.Equal("result", deserializedContent.Result?.ToString()); + } + } + + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + FunctionResultContent[] contents = + [ + new FunctionResultContent("call1", "result1"), + new McpServerToolResultContent("call2"), + ]; + + // Verify each element roundtrips individually + foreach (var content in contents) + { + var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + Assert.Equal(content.GetType(), deserialized.GetType()); + } + + // Verify the array roundtrips + var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionResultContentArray); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionResultContentArray); + Assert.NotNull(deserializedContents); + Assert.Equal(contents.Length, deserializedContents.Length); + for (int i = 0; i < deserializedContents.Length; i++) + { + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs new file mode 100644 index 00000000000..76636a5e879 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs @@ -0,0 +1,69 @@ +// 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.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class InputRequestContentTests +{ + [Fact] + public void Constructor_InvalidArguments_Throws() + { + Assert.Throws("requestId", () => new TestInputRequestContent(null!)); + Assert.Throws("requestId", () => new TestInputRequestContent("")); + Assert.Throws("requestId", () => new TestInputRequestContent("\r\t\n ")); + } + + [Theory] + [InlineData("abc")] + [InlineData("123")] + [InlineData("!@#")] + public void Constructor_Roundtrips(string id) + { + TestInputRequestContent content = new(id); + + Assert.Equal(id, content.RequestId); + } + + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + InputRequestContent[] contents = + [ + new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new FunctionApprovalRequestContent("request456", new McpServerToolCallContent("call456", "myTool", "myServer")), + ]; + + // Verify each element roundtrips individually + foreach (var content in contents) + { + var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + Assert.Equal(content.GetType(), deserialized.GetType()); + } + + // Verify the array roundtrips + var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.InputRequestContentArray); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.InputRequestContentArray); + Assert.NotNull(deserializedContents); + Assert.Equal(contents.Length, deserializedContents.Length); + for (int i = 0; i < deserializedContents.Length; i++) + { + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + } + } + + private sealed class TestInputRequestContent : InputRequestContent + { + public TestInputRequestContent(string requestId) + : base(requestId) + { + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs new file mode 100644 index 00000000000..3a2b6411538 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs @@ -0,0 +1,68 @@ +// 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; + +public class InputResponseContentTests +{ + [Fact] + public void Constructor_InvalidArguments_Throws() + { + Assert.Throws("requestId", () => new TestInputResponseContent(null!)); + Assert.Throws("requestId", () => new TestInputResponseContent("")); + Assert.Throws("requestId", () => new TestInputResponseContent("\r\t\n ")); + } + + [Theory] + [InlineData("abc")] + [InlineData("123")] + [InlineData("!@#")] + public void Constructor_Roundtrips(string id) + { + TestInputResponseContent content = new(id); + + Assert.Equal(id, content.RequestId); + } + + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + InputResponseContent[] contents = + [ + new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), + new FunctionApprovalResponseContent("request456", true, new McpServerToolCallContent("call456", "myTool", "myServer")), + ]; + + // Verify each element roundtrips individually + foreach (var content in contents) + { + var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + Assert.Equal(content.GetType(), deserialized.GetType()); + } + + // Verify the array roundtrips + var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.InputResponseContentArray); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.InputResponseContentArray); + Assert.NotNull(deserializedContents); + Assert.Equal(contents.Length, deserializedContents.Length); + for (int i = 0; i < deserializedContents.Length; i++) + { + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + } + } + + private class TestInputResponseContent : InputResponseContent + { + public TestInputResponseContent(string requestId) + : base(requestId) + { + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs index d5c5b43ed0a..cc696618257 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using Xunit; namespace Microsoft.Extensions.AI; @@ -18,9 +19,10 @@ public void Constructor_PropsDefault() Assert.Null(c.AdditionalProperties); Assert.Equal("callId1", c.CallId); - Assert.Equal("toolName", c.ToolName); + Assert.Equal("toolName", c.Name); Assert.Null(c.ServerName); Assert.Null(c.Arguments); + Assert.True(c.InformationalOnly); } [Fact] @@ -39,12 +41,16 @@ public void Constructor_PropsRoundtrip() Assert.Same(props, c.AdditionalProperties); Assert.Null(c.Arguments); - IReadOnlyDictionary args = new Dictionary(); + IDictionary args = new Dictionary(); c.Arguments = args; Assert.Same(args, c.Arguments); + Assert.True(c.InformationalOnly); + c.InformationalOnly = false; + Assert.False(c.InformationalOnly); + Assert.Equal("callId1", c.CallId); - Assert.Equal("toolName", c.ToolName); + Assert.Equal("toolName", c.Name); Assert.Equal("serverName", c.ServerName); } @@ -52,9 +58,37 @@ public void Constructor_PropsRoundtrip() public void Constructor_Throws() { Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", null)); - Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", string.Empty, null)); + Assert.Throws("name", () => new McpServerToolCallContent("callId1", string.Empty, null)); Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", null)); - Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, null)); + Assert.Throws("name", () => new McpServerToolCallContent("callId1", null!, null)); + } + + [Fact] + public void Serialization_Roundtrips() + { + var content = new McpServerToolCallContent("call123", "myTool", "myServer") + { + Arguments = new Dictionary { { "arg1", "value1" } } + }; + + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + + static void AssertSerializationRoundtrips(McpServerToolCallContent content) + where T : AIContent + { + T contentAsT = (T)(object)content; + string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); + T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + var deserializedContent = Assert.IsType(deserialized); + Assert.Equal(content.CallId, deserializedContent.CallId); + Assert.Equal(content.Name, deserializedContent.Name); + Assert.Equal(content.ServerName, deserializedContent.ServerName); + Assert.NotNull(deserializedContent.Arguments); + Assert.Equal("value1", deserializedContent.Arguments["arg1"]?.ToString()); + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs index 8fa6cc8a381..d71a1a6243f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Text.Json; using Xunit; @@ -17,7 +16,7 @@ public void Constructor_PropsDefault() Assert.Equal("callId", c.CallId); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.Output); + Assert.Null(c.Result); } [Fact] @@ -37,10 +36,10 @@ public void Constructor_PropsRoundtrip() Assert.Equal("callId", c.CallId); - Assert.Null(c.Output); - IList output = []; - c.Output = output; - Assert.Same(output, c.Output); + Assert.Null(c.Result); + object result = "test result"; + c.Result = result; + Assert.Same(result, c.Result); } [Fact] @@ -55,14 +54,23 @@ public void Serialization_Roundtrips() { var content = new McpServerToolResultContent("call123") { - Output = new List { new TextContent("result") } + Result = "result" }; - var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); - Assert.NotNull(deserializedContent); - Assert.Equal(content.CallId, deserializedContent.CallId); - Assert.NotNull(deserializedContent.Output); + static void AssertSerializationRoundtrips(McpServerToolResultContent content) + where T : AIContent + { + T contentAsT = (T)(object)content; + string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); + T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + var deserializedContent = Assert.IsType(deserialized); + Assert.Equal(content.CallId, deserializedContent.CallId); + Assert.Equal("result", ((JsonElement)deserializedContent.Result!).GetString()); + } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs deleted file mode 100644 index fc4dac9cabb..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs +++ /dev/null @@ -1,67 +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.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Xunit; - -namespace Microsoft.Extensions.AI.Contents; - -public class UserInputRequestContentTests -{ - [Fact] - public void Constructor_InvalidArguments_Throws() - { - Assert.Throws("id", () => new TestUserInputRequestContent(null!)); - Assert.Throws("id", () => new TestUserInputRequestContent("")); - Assert.Throws("id", () => new TestUserInputRequestContent("\r\t\n ")); - } - - [Theory] - [InlineData("abc")] - [InlineData("123")] - [InlineData("!@#")] - public void Constructor_Roundtrips(string id) - { - TestUserInputRequestContent content = new(id); - - Assert.Equal(id, content.Id); - } - - [Fact] - public void Serialization_DerivedTypes_Roundtrips() - { - UserInputRequestContent content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })); - var serializedContent = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(serializedContent, AIJsonUtilities.DefaultOptions); - Assert.NotNull(deserializedContent); - Assert.Equal(content.GetType(), deserializedContent.GetType()); - - UserInputRequestContent[] contents = - [ - new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), - new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - ]; - - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputRequestContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputRequestContentArray); - Assert.NotNull(deserializedContents); - - Assert.Equal(contents.Count(), deserializedContents.Length); - for (int i = 0; i < deserializedContents.Length; i++) - { - Assert.NotNull(contents.ElementAt(i)); - Assert.Equal(contents.ElementAt(i).GetType(), deserializedContents[i].GetType()); - } - } - - private sealed class TestUserInputRequestContent : UserInputRequestContent - { - public TestUserInputRequestContent(string id) - : base(id) - { - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs deleted file mode 100644 index 2442e57272d..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs +++ /dev/null @@ -1,65 +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.Text.Json; -using Xunit; - -namespace Microsoft.Extensions.AI.Contents; - -public class UserInputResponseContentTests -{ - [Fact] - public void Constructor_InvalidArguments_Throws() - { - Assert.Throws("id", () => new TestUserInputResponseContent(null!)); - Assert.Throws("id", () => new TestUserInputResponseContent("")); - Assert.Throws("id", () => new TestUserInputResponseContent("\r\t\n ")); - } - - [Theory] - [InlineData("abc")] - [InlineData("123")] - [InlineData("!@#")] - public void Constructor_Roundtrips(string id) - { - TestUserInputResponseContent content = new(id); - - Assert.Equal(id, content.Id); - } - - [Fact] - public void Serialization_DerivedTypes_Roundtrips() - { - UserInputResponseContent content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")); - var serializedContent = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(serializedContent, AIJsonUtilities.DefaultOptions); - Assert.NotNull(deserializedContent); - Assert.Equal(content.GetType(), deserializedContent.GetType()); - - UserInputResponseContent[] contents = - [ - new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), - new McpServerToolApprovalResponseContent("request123", true), - ]; - - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputResponseContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.UserInputResponseContentArray); - Assert.NotNull(deserializedContents); - - Assert.Equal(contents.Length, deserializedContents.Length); - for (int i = 0; i < deserializedContents.Length; i++) - { - Assert.NotNull(contents[i]); - Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); - } - } - - private class TestUserInputResponseContent : UserInputResponseContent - { - public TestUserInputResponseContent(string id) - : base(id) - { - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index 6c448d0efb1..0437c47eb1c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -39,8 +39,10 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(ChatResponseFormatTests.SomeType))] [JsonSerializable(typeof(ChatResponseFormatTests.TypeWithDisplayName))] [JsonSerializable(typeof(ResponseContinuationToken))] -[JsonSerializable(typeof(UserInputRequestContent[]))] -[JsonSerializable(typeof(UserInputResponseContent[]))] +[JsonSerializable(typeof(InputRequestContent[]))] +[JsonSerializable(typeof(InputResponseContent[]))] +[JsonSerializable(typeof(FunctionCallContent[]))] +[JsonSerializable(typeof(FunctionResultContent[]))] [JsonSerializable(typeof(ReasoningOptions))] [JsonSerializable(typeof(ReasoningEffort))] [JsonSerializable(typeof(ReasoningOutput))] diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs index 56c04ce1dfa..454fd74a731 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -20,12 +20,10 @@ public void Constructor_PropsDefault() Assert.Equal("https://localhost/", tool.ServerAddress); Assert.Empty(tool.Description); - Assert.Null(tool.AuthorizationToken); Assert.Null(tool.ServerDescription); Assert.Null(tool.AllowedTools); Assert.Null(tool.ApprovalMode); - Assert.NotNull(tool.Headers); - Assert.Empty(tool.Headers); + Assert.Null(tool.Headers); } [Fact] @@ -72,11 +70,6 @@ public void Constructor_Roundtrips() Assert.Equal("connector_id", tool.ServerAddress); Assert.Empty(tool.Description); - Assert.Null(tool.AuthorizationToken); - string authToken = "Bearer token123"; - tool.AuthorizationToken = authToken; - Assert.Equal(authToken, tool.AuthorizationToken); - Assert.Null(tool.ServerDescription); string serverDescription = "This is a test server"; tool.ServerDescription = serverDescription; @@ -98,92 +91,33 @@ public void Constructor_Roundtrips() tool.ApprovalMode = customApprovalMode; Assert.Same(customApprovalMode, tool.ApprovalMode); + Assert.Null(tool.Headers); + tool.Headers = new Dictionary { ["X-Custom-Header"] = "value1" }; Assert.NotNull(tool.Headers); Assert.Single(tool.Headers); - tool.Headers["X-Custom-Header"] = "value1"; - Assert.True(tool.Headers.Count == 2); Assert.Equal("value1", tool.Headers["X-Custom-Header"]); } [Fact] public void Constructor_WithHeaders_Uri_Roundtrips() { - var headers = new Dictionary + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")) { - ["Authorization"] = "Bearer token456", - ["X-Custom"] = "value2" + Headers = new Dictionary + { + ["Authorization"] = "Bearer token456", + ["X-Custom"] = "value2" + } }; - HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/")); - foreach (KeyValuePair keyValuePair in headers) - { - tool.Headers[keyValuePair.Key] = keyValuePair.Value; - } Assert.Equal("serverName", tool.ServerName); Assert.Equal("https://localhost/", tool.ServerAddress); + Assert.NotNull(tool.Headers); Assert.Equal(2, tool.Headers.Count); Assert.Equal("Bearer token456", tool.Headers["Authorization"]); - Assert.Equal("token456", tool.AuthorizationToken); Assert.Equal("value2", tool.Headers["X-Custom"]); } - [Fact] - public void Constructor_WithNullHeaders_CreatesEmptyDictionary() - { - HostedMcpServerTool tool1 = new("serverName", "connector_id"); - Assert.NotNull(tool1.Headers); - Assert.Empty(tool1.Headers); - - HostedMcpServerTool tool2 = new("serverName", new Uri("https://localhost/")); - Assert.NotNull(tool2.Headers); - Assert.Empty(tool2.Headers); - } - - [Fact] - public void AuthorizationToken_And_Headers_NoOrderingIssues() - { - // Verify that setting AuthorizationToken followed by adding to Headers works - var tool1 = new HostedMcpServerTool("server", "https://localhost/") - { - AuthorizationToken = "token123" - }; - tool1.Headers["X-Custom"] = "value1"; - - Assert.Equal(2, tool1.Headers.Count); - Assert.Equal("Bearer token123", tool1.Headers["Authorization"]); - Assert.Equal("token123", tool1.AuthorizationToken); - Assert.Equal("value1", tool1.Headers["X-Custom"]); - - // Verify that adding to Headers followed by setting AuthorizationToken works the same - var tool2 = new HostedMcpServerTool("server", "https://localhost/"); - tool2.Headers["X-Custom"] = "value1"; - tool2.AuthorizationToken = "token123"; - - Assert.Equal(2, tool2.Headers.Count); - Assert.Equal("Bearer token123", tool2.Headers["Authorization"]); - Assert.Equal("token123", tool2.AuthorizationToken); - Assert.Equal("value1", tool2.Headers["X-Custom"]); - - // Verify setting AuthorizationToken to null removes only Authorization header - tool2.AuthorizationToken = null; - Assert.Single(tool2.Headers); - Assert.False(tool2.Headers.ContainsKey("Authorization")); - Assert.Null(tool2.AuthorizationToken); - Assert.Equal("value1", tool2.Headers["X-Custom"]); - } - - [Fact] - public void Headers_WithNullAuthorization() - { - var tool = new HostedMcpServerTool("server", "https://localhost/"); - tool.Headers["Authorization"] = null!; - tool.Headers["X-Custom"] = "value1"; - Assert.Equal(2, tool.Headers.Count); - Assert.Null(tool.Headers["Authorization"]); - Assert.Null(tool.AuthorizationToken); - Assert.Equal("value1", tool.Headers["X-Custom"]); - } - [Fact] public void Constructor_Throws() { @@ -193,9 +127,9 @@ public void Constructor_Throws() Assert.Throws("serverName", () => new HostedMcpServerTool(null!, new Uri("https://localhost/"))); Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", string.Empty)); - Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", new Uri("/api/mcp", UriKind.Relative))); + Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", new Uri("/api/mcp", UriKind.Relative))); Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", (string)null!)); - Assert.Throws("serverUrl", () => new HostedMcpServerTool("name", (Uri)null!)); + Assert.Throws("serverAddress", () => new HostedMcpServerTool("name", (Uri)null!)); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index d2e4dd39867..3daf4796bad 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -431,7 +431,7 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthToken_ProducesVa { var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") { - AuthorizationToken = "test-token" + Headers = new Dictionary { ["Authorization"] = "Bearer test-token" } }; var result = mcpTool.AsOpenAIResponseTool(); @@ -449,9 +449,12 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithAuthTokenAndCustomHe { var mcpTool = new HostedMcpServerTool("test-server", "http://localhost:8000") { - AuthorizationToken = "test-token" + Headers = new Dictionary + { + ["Authorization"] = "Bearer test-token", + ["X-Custom-Header"] = "custom-value" + } }; - mcpTool.Headers["X-Custom-Header"] = "custom-value"; var result = mcpTool.AsOpenAIResponseTool(); @@ -559,11 +562,11 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolWithRequireSpecificAppro } [Fact] - public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_OnlySetsAuthToken() + public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_ExtractsAuthToken() { var mcpTool = new HostedMcpServerTool("calendar", "connector_googlecalendar") { - AuthorizationToken = "connector-token" + Headers = new Dictionary { ["Authorization"] = "Bearer connector-token" } }; var result = mcpTool.AsOpenAIResponseTool(); @@ -572,7 +575,7 @@ public void AsOpenAIResponseTool_WithHostedMcpServerToolConnector_OnlySetsAuthTo var tool = Assert.IsType(result); Assert.Equal("connector-token", tool.AuthorizationToken); - // For connectors, headers should not be set even though AuthorizationToken adds to Headers internally + // For connectors, headers should not be set - only AuthorizationToken Assert.Empty(tool.Headers); } @@ -880,13 +883,126 @@ public void AsChatResponse_ConvertsOpenAIResponse() // as all constructors/factory methods currently are internal. Update this test when such functionality is available. } + /// + /// Derived type to allow creating StreamingResponseOutputItemDoneUpdate instances for testing. + /// The base class has internal constructors, but we can derive and set the Item property. + /// + private sealed class TestableStreamingResponseOutputItemDoneUpdate : StreamingResponseOutputItemDoneUpdate + { + } + [Fact] - public void AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpdates() + public async Task AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpdates() { Assert.Throws("responseUpdates", () => ((IAsyncEnumerable)null!).AsChatResponseUpdatesAsync()); - // The OpenAI library currently doesn't provide any way to create a StreamingResponseUpdate instance, - // as all constructors/factory methods currently are internal. Update this test when such functionality is available. + // Create streaming updates with various ResponseItem types + FunctionCallResponseItem functionCall = ResponseItem.CreateFunctionCallItem("call_abc", "MyFunction", BinaryData.FromString("""{"arg":"value"}""")); + McpToolCallItem mcpToolCall = ResponseItem.CreateMcpToolCallItem("deepwiki", "ask_question", BinaryData.FromString("""{"query":"hello"}""")); + mcpToolCall.Id = "mcp_call_123"; + mcpToolCall.ToolOutput = "The answer is 42"; + McpToolCallApprovalRequestItem mcpApprovalRequest = ResponseItem.CreateMcpApprovalRequestItem( + "mcpr_123", + "deepwiki", + "ask_question", + BinaryData.FromString("""{"repo":"dotnet/extensions"}""")); + McpToolCallApprovalResponseItem mcpApprovalResponse = ResponseItem.CreateMcpApprovalResponseItem("mcpr_123", approved: true); + + List updates = []; + await foreach (ChatResponseUpdate update in CreateStreamingUpdates().AsChatResponseUpdatesAsync()) + { + updates.Add(update); + } + + // Verify we got the expected updates + Assert.Equal(4, updates.Count); + + // First update should be FunctionCallContent + FunctionCallContent? fcc = updates[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(fcc); + Assert.Equal("call_abc", fcc.CallId); + Assert.Equal("MyFunction", fcc.Name); + + // Second update should be McpServerToolCallContent + McpServerToolResultContent + McpServerToolCallContent? mcpToolCallContent = updates[1].Contents.OfType().FirstOrDefault(); + Assert.NotNull(mcpToolCallContent); + Assert.Equal("mcp_call_123", mcpToolCallContent.CallId); + Assert.Equal("ask_question", mcpToolCallContent.Name); + Assert.Equal("deepwiki", mcpToolCallContent.ServerName); + Assert.Null(mcpToolCallContent.RawRepresentation); // Intentionally null to avoid duplication during roundtrip + + McpServerToolResultContent? mcpToolResultContent = updates[1].Contents.OfType().FirstOrDefault(); + Assert.NotNull(mcpToolResultContent); + Assert.Equal("mcp_call_123", mcpToolResultContent.CallId); + Assert.NotNull(mcpToolResultContent.RawRepresentation); + Assert.Same(mcpToolCall, mcpToolResultContent.RawRepresentation); + + // Third update should be FunctionApprovalRequestContent with McpServerToolCallContent + FunctionApprovalRequestContent? approvalRequest = updates[2].Contents.OfType().FirstOrDefault(); + Assert.NotNull(approvalRequest); + Assert.Equal("mcpr_123", approvalRequest.RequestId); + Assert.NotNull(approvalRequest.RawRepresentation); + Assert.Same(mcpApprovalRequest, approvalRequest.RawRepresentation); + + McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequest.FunctionCall); + Assert.Equal("ask_question", nestedMcpCall.Name); + Assert.Equal("deepwiki", nestedMcpCall.ServerName); + + // Fourth update should be FunctionApprovalResponseContent correlated with request + FunctionApprovalResponseContent? approvalResponse = updates[3].Contents.OfType().FirstOrDefault(); + Assert.NotNull(approvalResponse); + Assert.Equal("mcpr_123", approvalResponse.RequestId); + Assert.True(approvalResponse.Approved); + Assert.NotNull(approvalResponse.RawRepresentation); + Assert.Same(mcpApprovalResponse, approvalResponse.RawRepresentation); + + // The correlated FunctionCall should be McpServerToolCallContent with tool details from the request + McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponse.FunctionCall); + Assert.Equal("mcpr_123", correlatedMcpCall.CallId); + Assert.Equal("ask_question", correlatedMcpCall.Name); + Assert.Equal("deepwiki", correlatedMcpCall.ServerName); + Assert.NotNull(correlatedMcpCall.Arguments); + Assert.Equal("dotnet/extensions", correlatedMcpCall.Arguments["repo"]?.ToString()); + + async IAsyncEnumerable CreateStreamingUpdates() + { + await Task.Yield(); + + yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = functionCall }; + yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpToolCall }; + yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpApprovalRequest }; + yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpApprovalResponse }; + } + } + + [Fact] + public async Task AsChatResponseUpdatesAsync_McpToolCallApprovalResponseItem_WithoutCorrelatedRequest_FallsBackToAIContent() + { + // Create an approval response without a matching request in the stream. + McpToolCallApprovalResponseItem mcpApprovalResponse = ResponseItem.CreateMcpApprovalResponseItem("unknown_request_id", approved: true); + + List updates = []; + await foreach (ChatResponseUpdate update in CreateStreamingUpdates().AsChatResponseUpdatesAsync()) + { + updates.Add(update); + } + + Assert.Single(updates); + + // Should NOT have a FunctionApprovalResponseContent since there was no correlated request + Assert.Empty(updates[0].Contents.OfType()); + + // Should have a generic AIContent with RawRepresentation set to the response item + AIContent? genericContent = updates[0].Contents.FirstOrDefault(c => c.RawRepresentation == mcpApprovalResponse); + Assert.NotNull(genericContent); + Assert.IsNotType(genericContent); + Assert.Same(mcpApprovalResponse, genericContent.RawRepresentation); + + async IAsyncEnumerable CreateStreamingUpdates() + { + await Task.Yield(); + yield return new TestableStreamingResponseOutputItemDoneUpdate { Item = mcpApprovalResponse }; + } } [Fact] @@ -976,6 +1092,171 @@ public void AsChatMessages_FromResponseItems_WithFunctionCall_HandlesCorrectly() Assert.Equal("value", functionCall.Arguments!["param"]?.ToString()); } + [Fact] + public void AsChatMessages_FromResponseItems_AllContentTypes_SetsRawRepresentation() + { + // Create ResponseItems of various types that ToChatMessages handles. + // Each type should roundtrip with RawRepresentation set. + MessageResponseItem assistantItem = ResponseItem.CreateAssistantMessageItem("Hello from the assistant!"); + ReasoningResponseItem reasoningItem = ResponseItem.CreateReasoningItem("This is reasoning text"); + FunctionCallResponseItem functionCallItem = ResponseItem.CreateFunctionCallItem("call_abc", "MyFunction", BinaryData.FromString("""{"arg": "value"}""")); + FunctionCallOutputResponseItem functionOutputItem = ResponseItem.CreateFunctionCallOutputItem("call_abc", "function result output"); + McpToolCallItem mcpToolCallItem = ResponseItem.CreateMcpToolCallItem("deepwiki", "ask_question", BinaryData.FromString("""{"query":"hello"}""")); + mcpToolCallItem.Id = "mcp_call_123"; + mcpToolCallItem.ToolOutput = "The answer is 42"; + McpToolCallApprovalRequestItem mcpApprovalRequestItem = ResponseItem.CreateMcpApprovalRequestItem( + "mcpr_123", + "deepwiki", + "ask_question", + BinaryData.FromString("""{"repoName":"dotnet/extensions"}""")); + + // Use matching ID so response can correlate with the request + McpToolCallApprovalResponseItem mcpApprovalResponseItem = ResponseItem.CreateMcpApprovalResponseItem("mcpr_123", approved: true); + + ResponseItem[] items = [assistantItem, reasoningItem, functionCallItem, functionOutputItem, mcpToolCallItem, mcpApprovalRequestItem, mcpApprovalResponseItem]; + + // Convert to ChatMessages + ChatMessage[] messages = items.AsChatMessages().ToArray(); + + // All items should be grouped into a single assistant message + Assert.Single(messages); + ChatMessage message = messages[0]; + Assert.Equal(ChatRole.Assistant, message.Role); + + // The message itself should have RawRepresentation from MessageResponseItem + Assert.NotNull(message.RawRepresentation); + Assert.Same(assistantItem, message.RawRepresentation); + + // Verify each content type has RawRepresentation set + + // 1. MessageResponseItem -> TextContent with ResponseContentPart as RawRepresentation + TextContent? textContent = message.Contents.OfType().FirstOrDefault(); + Assert.NotNull(textContent); + Assert.Equal("Hello from the assistant!", textContent.Text); + Assert.NotNull(textContent.RawRepresentation); + Assert.IsAssignableFrom(textContent.RawRepresentation); + + // 2. ReasoningResponseItem -> TextReasoningContent + TextReasoningContent? reasoningContent = message.Contents.OfType().FirstOrDefault(); + Assert.NotNull(reasoningContent); + Assert.Equal("This is reasoning text", reasoningContent.Text); + Assert.NotNull(reasoningContent.RawRepresentation); + Assert.Same(reasoningItem, reasoningContent.RawRepresentation); + + // 3. FunctionCallResponseItem -> FunctionCallContent + FunctionCallContent? functionCallContent = message.Contents.OfType().FirstOrDefault(); + Assert.NotNull(functionCallContent); + Assert.Equal("call_abc", functionCallContent.CallId); + Assert.Equal("MyFunction", functionCallContent.Name); + Assert.NotNull(functionCallContent.RawRepresentation); + Assert.Same(functionCallItem, functionCallContent.RawRepresentation); + + // 4. FunctionCallOutputResponseItem -> FunctionResultContent + FunctionResultContent? functionResultContent = message.Contents.OfType().FirstOrDefault(); + Assert.NotNull(functionResultContent); + Assert.Equal("call_abc", functionResultContent.CallId); + Assert.Equal("function result output", functionResultContent.Result); + Assert.NotNull(functionResultContent.RawRepresentation); + Assert.Same(functionOutputItem, functionResultContent.RawRepresentation); + + // 5. McpToolCallItem -> McpServerToolCallContent + McpServerToolResultContent + // Note: AddMcpToolCallContent creates both contents; RawRepresentation is only on the result, not the call + McpServerToolCallContent? mcpToolCall = message.Contents.OfType().FirstOrDefault(c => c.CallId == "mcp_call_123"); + Assert.NotNull(mcpToolCall); + Assert.Equal("mcp_call_123", mcpToolCall.CallId); + Assert.Equal("ask_question", mcpToolCall.Name); + Assert.Equal("deepwiki", mcpToolCall.ServerName); + Assert.Null(mcpToolCall.RawRepresentation); // Intentionally null to avoid duplication during roundtrip + + McpServerToolResultContent? mcpToolResult = message.Contents.OfType().FirstOrDefault(c => c.CallId == "mcp_call_123"); + Assert.NotNull(mcpToolResult); + Assert.Equal("mcp_call_123", mcpToolResult.CallId); + Assert.NotNull(mcpToolResult.RawRepresentation); + Assert.Same(mcpToolCallItem, mcpToolResult.RawRepresentation); + + // 6. McpToolCallApprovalRequestItem -> FunctionApprovalRequestContent + FunctionApprovalRequestContent? approvalRequestContent = message.Contents.OfType().FirstOrDefault(); + Assert.NotNull(approvalRequestContent); + Assert.Equal("mcpr_123", approvalRequestContent.RequestId); + Assert.NotNull(approvalRequestContent.RawRepresentation); + Assert.Same(mcpApprovalRequestItem, approvalRequestContent.RawRepresentation); + + // The nested FunctionCall should be McpServerToolCallContent + McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequestContent.FunctionCall); + Assert.Equal("ask_question", nestedMcpCall.Name); + Assert.Equal("deepwiki", nestedMcpCall.ServerName); + Assert.NotNull(nestedMcpCall.RawRepresentation); + Assert.Same(mcpApprovalRequestItem, nestedMcpCall.RawRepresentation); + + // 7. McpToolCallApprovalResponseItem -> FunctionApprovalResponseContent (correlated with request) + FunctionApprovalResponseContent? approvalResponseContent = message.Contents.OfType().FirstOrDefault(); + Assert.NotNull(approvalResponseContent); + Assert.Equal("mcpr_123", approvalResponseContent.RequestId); + Assert.True(approvalResponseContent.Approved); + Assert.NotNull(approvalResponseContent.RawRepresentation); + Assert.Same(mcpApprovalResponseItem, approvalResponseContent.RawRepresentation); + + // The correlated FunctionCall should be McpServerToolCallContent with tool details from the request + McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponseContent.FunctionCall); + Assert.Equal("mcpr_123", correlatedMcpCall.CallId); + Assert.Equal("ask_question", correlatedMcpCall.Name); + Assert.Equal("deepwiki", correlatedMcpCall.ServerName); + Assert.NotNull(correlatedMcpCall.Arguments); + Assert.Equal("dotnet/extensions", correlatedMcpCall.Arguments["repoName"]?.ToString()); + } + + [Fact] + public void AsChatMessages_McpToolCallApprovalResponseItem_WithoutCorrelatedRequest_FallsBackToAIContent() + { + // Create an approval response without a matching request in the batch. + // This simulates receiving a response item when we don't have the original request. + MessageResponseItem assistantItem = ResponseItem.CreateAssistantMessageItem("Hello"); + McpToolCallApprovalResponseItem mcpApprovalResponseItem = ResponseItem.CreateMcpApprovalResponseItem("unknown_request_id", approved: true); + + ResponseItem[] items = [assistantItem, mcpApprovalResponseItem]; + + // Convert to ChatMessages + ChatMessage[] messages = items.AsChatMessages().ToArray(); + + Assert.Single(messages); + ChatMessage message = messages[0]; + + // Should NOT have a FunctionApprovalResponseContent since there was no correlated request + Assert.Empty(message.Contents.OfType()); + + // Should have a generic AIContent with RawRepresentation set to the response item + AIContent? genericContent = message.Contents.FirstOrDefault(c => c.RawRepresentation == mcpApprovalResponseItem); + Assert.NotNull(genericContent); + Assert.IsNotType(genericContent); + Assert.Same(mcpApprovalResponseItem, genericContent.RawRepresentation); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AsOpenAIResponseItems_McpServerToolContents_RoundtripsToolOutputAndError(bool isError) + { + var mcpToolCall = new McpServerToolCallContent("call_123", "get_weather", "weather_server") { Arguments = new Dictionary { ["city"] = "Seattle" } }; + var mcpToolResult = new McpServerToolResultContent("call_123") { Result = isError ? new ErrorContent("error") : new TextContent("sunny") }; + + var items = new ChatMessage[] { new(ChatRole.Assistant, [mcpToolCall, mcpToolResult]) }.AsOpenAIResponseItems().ToArray(); + + McpToolCallItem mtci = Assert.IsType(Assert.Single(items)); + Assert.Equal("get_weather", mtci.ToolName); + Assert.Equal("weather_server", mtci.ServerLabel); + + if (isError) + { + Assert.Contains("error", mtci.Error?.ToString()); + Assert.Null(mtci.ToolOutput); + } + else + { + Assert.Equal("sunny", mtci.ToolOutput); + Assert.Null(mtci.Error); + } + } + [Fact] public void AsOpenAIChatCompletion_WithNullArgument_ThrowsArgumentNullException() { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 0bf3f7fcf0f..e1965468cde 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -149,7 +149,7 @@ await client.GetStreamingResponseAsync(Prompt, chatOptions).ToChatResponseAsync( Assert.NotNull(response); Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType()); Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType()); - Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType()); + Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType()); Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text); } @@ -203,8 +203,8 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() var approvalResponse = new ChatMessage(ChatRole.Tool, response.Messages .SelectMany(m => m.Contents) - .OfType() - .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.CallId, true)) + .OfType() + .Select(c => c.CreateResponse(true)) .ToArray()); if (approvalResponse.Contents.Count == 0) { @@ -377,7 +377,7 @@ public async Task RemoteMCP_Connector() { SkipIfNotEnabled(); - if (TestRunnerConfiguration.Instance["RemoteMCP:ConnectorAccessToken"] is not string accessToken) + if (TestRunnerConfiguration.Instance["RemoteMCP:ConnectorAccessToken"] is not string { Length: > 0 } accessToken) { throw new SkipTestException( "To run this test, set a value for RemoteMCP:ConnectorAccessToken. " + @@ -394,9 +394,9 @@ async Task RunAsync(bool streaming, bool approval) Tools = [new HostedMcpServerTool("calendar", "connector_googlecalendar") { ApprovalMode = approval ? - HostedMcpServerToolApprovalMode.AlwaysRequire : - HostedMcpServerToolApprovalMode.NeverRequire, - AuthorizationToken = accessToken + HostedMcpServerToolApprovalMode.AlwaysRequire : + HostedMcpServerToolApprovalMode.NeverRequire, + Headers = new Dictionary { ["Authorization"] = $"Bearer {accessToken}" }, } ], }; @@ -412,8 +412,9 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() if (approval) { input.AddRange(response.Messages); - var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - Assert.Equal("search_events", approvalRequest.ToolCall.ToolName); + var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); + var mcpCallContent = Assert.IsType(approvalRequest.FunctionCall); + Assert.Equal("search_events", mcpCallContent.Name); input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)])); response = streaming ? @@ -423,10 +424,10 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() Assert.NotNull(response); var toolCall = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - Assert.Equal("search_events", toolCall.ToolName); + Assert.Equal("search_events", toolCall.Name); var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - var content = Assert.IsType(Assert.Single(toolResult.Output!)); + var content = Assert.IsType(toolResult.Result); Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index abd1414d85f..6193d58b291 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1396,7 +1396,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) { Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))] }; - McpServerToolApprovalRequestContent approvalRequest; + FunctionApprovalRequestContent approvalRequest; using (VerbatimHttpHandler handler = new(input, output)) using (HttpClient httpClient = new(handler)) @@ -1406,7 +1406,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions); - approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); + approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); chatOptions.ConversationId = response.ConversationId; } @@ -1543,7 +1543,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) var call = Assert.IsType(message.Contents[0]); Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", call.CallId); Assert.Equal("deepwiki", call.ServerName); - Assert.Equal("ask_question", call.ToolName); + Assert.Equal("ask_question", call.Name); Assert.NotNull(call.Arguments); Assert.Equal(2, call.Arguments.Count); Assert.Equal("dotnet/extensions", ((JsonElement)call.Arguments["repoName"]!).GetString()); @@ -1551,8 +1551,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) var result = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId); - Assert.NotNull(result.Output); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(result.Output)).Text); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(result.Result).Text); Assert.NotNull(response.Usage); Assert.Equal(542, response.Usage.InputTokenCount); @@ -1798,28 +1797,26 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var firstCall = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstCall.CallId); Assert.Equal("deepwiki", firstCall.ServerName); - Assert.Equal("read_wiki_structure", firstCall.ToolName); + Assert.Equal("read_wiki_structure", firstCall.Name); Assert.NotNull(firstCall.Arguments); Assert.Single(firstCall.Arguments); Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString()); var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId); - Assert.NotNull(firstResult.Output); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text); + Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result).Text); var secondCall = Assert.IsType(message.Contents[3]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.CallId); Assert.Equal("deepwiki", secondCall.ServerName); - Assert.Equal("ask_question", secondCall.ToolName); + Assert.Equal("ask_question", secondCall.Name); Assert.NotNull(secondCall.Arguments); Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString()); Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString()); var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId); - Assert.NotNull(secondResult.Output); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Output)).Text); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(secondResult.Result).Text); Assert.NotNull(response.Usage); Assert.Equal(1329, response.Usage.InputTokenCount); @@ -2211,28 +2208,26 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var firstCall = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstCall.CallId); Assert.Equal("deepwiki", firstCall.ServerName); - Assert.Equal("read_wiki_structure", firstCall.ToolName); + Assert.Equal("read_wiki_structure", firstCall.Name); Assert.NotNull(firstCall.Arguments); Assert.Single(firstCall.Arguments); Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString()); var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId); - Assert.NotNull(firstResult.Output); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text); + Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result).Text); var secondCall = Assert.IsType(message.Contents[3]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.CallId); Assert.Equal("deepwiki", secondCall.ServerName); - Assert.Equal("ask_question", secondCall.ToolName); + Assert.Equal("ask_question", secondCall.Name); Assert.NotNull(secondCall.Arguments); Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString()); Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString()); var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId); - Assert.NotNull(secondResult.Output); - Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Output)).Text); + Assert.StartsWith("The path to the `README.md` file", Assert.IsType(secondResult.Result).Text); Assert.NotNull(response.Usage); Assert.Equal(1420, response.Usage.InputTokenCount); @@ -2240,6 +2235,203 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() Assert.Equal(1569, response.Usage.TotalTokenCount); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task McpToolCall_ErrorResponse_NonStreaming(bool rawTool) + { + const string Input = """ + { + "model": "gpt-4o-mini", + "tools": [ + { + "type": "mcp", + "server_label": "mymcp", + "server_url": "https://mcp.example.com/mcp", + "require_approval": "never" + } + ], + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Test error handling" + } + ] + } + ] + } + """; + + const string Output = """ + { + "id": "resp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", + "object": "response", + "created_at": 1757299100, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "mcpl_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", + "type": "mcp_list_tools", + "server_label": "mymcp", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "A tool that always errors", + "input_schema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "name": "test_error" + } + ] + }, + { + "id": "mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", + "type": "mcp_call", + "approval_request_id": null, + "arguments": "{}", + "error": { + "type": "mcp_tool_execution_error", + "content": [ + { + "type": "text", + "text": "An error occurred invoking 'test_error'.", + "annotations": null, + "meta": null + } + ] + }, + "name": "test_error", + "output": null, + "server_label": "mymcp" + }, + { + "id": "msg_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The tool encountered an error during execution." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "never", + "server_description": null, + "server_label": "mymcp", + "server_url": "https://mcp.example.com/" + } + ], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 500, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 50, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 550 + }, + "user": null, + "metadata": {} + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + AITool mcpTool = rawTool ? + ResponseTool.CreateMcpTool("mymcp", serverUri: new("https://mcp.example.com/mcp"), toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)).AsAITool() : + new HostedMcpServerTool("mymcp", new Uri("https://mcp.example.com/mcp")) + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, + }; + + ChatOptions chatOptions = new() + { + Tools = [mcpTool], + }; + + var response = await client.GetResponseAsync("Test error handling", chatOptions); + Assert.NotNull(response); + + Assert.Equal("resp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", response.ResponseId); + Assert.Equal("resp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", response.ConversationId); + Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_757_299_100), response.CreatedAt); + Assert.Null(response.FinishReason); + + var message = Assert.Single(response.Messages); + Assert.Equal(ChatRole.Assistant, response.Messages[0].Role); + Assert.Equal("The tool encountered an error during execution.", response.Messages[0].Text); + + Assert.Equal(4, message.Contents.Count); + + var toolCall = Assert.IsType(message.Contents[1]); + Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolCall.CallId); + Assert.Equal("mymcp", toolCall.ServerName); + Assert.Equal("test_error", toolCall.Name); + Assert.NotNull(toolCall.Arguments); + Assert.Empty(toolCall.Arguments); + + var toolResult = Assert.IsType(message.Contents[2]); + Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolResult.CallId); + var errorContent = Assert.IsType(toolResult.Result); + Assert.Contains("An error occurred invoking 'test_error'.", errorContent.Message); + + Assert.NotNull(response.Usage); + Assert.Equal(500, response.Usage.InputTokenCount); + Assert.Equal(50, response.Usage.OutputTokenCount); + Assert.Equal(550, response.Usage.TotalTokenCount); + } + [Fact] public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInRequest() { @@ -2309,11 +2501,13 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR var mcpTool = new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp")) { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, - AuthorizationToken = "test-auth-token-12345" + Headers = new Dictionary + { + ["Authorization"] = "Bearer test-auth-token-12345", + ["X-Custom-Header"] = "custom-value" + } }; - mcpTool.Headers!["X-Custom-Header"] = "custom-value"; - var response = await client.GetResponseAsync("hello", new ChatOptions { Tools = [mcpTool] }); Assert.NotNull(response); @@ -2322,6 +2516,94 @@ public async Task McpToolCall_WithAuthorizationTokenAndCustomHeaders_IncludesInR Assert.Equal("Hi!", message.Text); } + [Theory] + [InlineData("bearer test-auth-token-12345")] + [InlineData("BEARER test-auth-token-12345")] + [InlineData("BeArEr test-auth-token-12345")] + [InlineData(" Bearer test-auth-token-12345")] + [InlineData("Bearer test-auth-token-12345 ")] + [InlineData(" Bearer test-auth-token-12345 ")] + [InlineData("Bearer test-auth-token-12345")] + public async Task McpToolCall_WithCaseInsensitiveBearerToken_ExtractsToken(string authHeaderValue) + { + // Use a connector ID (non-URL) to trigger the Bearer token extraction code path + string expectedToken = "test-auth-token-12345"; + string expectedInput = $$""" + { + "model": "gpt-4o-mini", + "tools": [ + { + "type": "mcp", + "server_label": "my-connector", + "connector_id": "connector-id-123", + "authorization": "{{expectedToken}}", + "require_approval": "never" + } + ], + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello" + } + ] + } + ] + } + """; + + const string Output = """ + { + "id": "resp_bearer01", + "object": "response", + "created_at": 1757299043, + "status": "completed", + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_bearer01", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hi!" + } + ] + } + ], + "usage": { + "input_tokens": 10, + "output_tokens": 2, + "total_tokens": 12 + } + } + """; + + using VerbatimHttpHandler handler = new(expectedInput, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + // Use string constructor with non-URL to trigger connector ID path + var mcpTool = new HostedMcpServerTool("my-connector", "connector-id-123") + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, + Headers = new Dictionary + { + ["Authorization"] = authHeaderValue, + } + }; + + var response = await client.GetResponseAsync("hello", new ChatOptions { Tools = [mcpTool] }); + + Assert.NotNull(response); + Assert.Equal("resp_bearer01", response.ResponseId); + } + [Fact] public async Task GetResponseAsync_BackgroundResponses_FirstCall() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index 0d2fd351c30..120646134c7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.Extensions.AI.ChatCompletion; +namespace Microsoft.Extensions.AI; public class FunctionInvokingChatClientApprovalsTests { @@ -45,8 +45,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAllRequireApprovalAsy [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) ]; @@ -81,8 +81,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAnyRequireApprovalAsy [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) ]; @@ -125,8 +125,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAnyRequestOrAdditiona [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) ]; @@ -152,13 +152,13 @@ public async Task ApprovedApprovalResponsesAreExecutedAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -204,13 +204,13 @@ public async Task ApprovedApprovalResponsesAreGroupedWhenMessageIdIsNullAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), // Note: No MessageId set - this is the bug trigger new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -256,19 +256,19 @@ public async Task ApprovedApprovalResponsesFromSeparateFCCMessagesAreExecutedAsy new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp2" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -315,13 +315,13 @@ public async Task RejectedApprovalResponsesAreFailedAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -374,13 +374,13 @@ public async Task MixedApprovedAndRejectedApprovalResponsesAreExecutedAndFailedA new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -438,16 +438,16 @@ public async Task RejectedApprovalResponsesWithCustomReasonAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied permission for this operation" }, - new FunctionApprovalResponseContent("callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) { Reason = "Function Func2 is not allowed at this time" } @@ -504,15 +504,15 @@ public async Task MixedApprovalResponsesWithCustomAndDefaultReasonsAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), - new FunctionApprovalRequestContent("callId3", new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), + new FunctionApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "Custom rejection for Func1" }, - new FunctionApprovalResponseContent("callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), - new FunctionApprovalResponseContent("callId3", true, new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "Custom rejection for Func1" }, + new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), + new FunctionApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) ]), ]; @@ -596,11 +596,11 @@ public async Task RejectedApprovalResponsesWithEmptyOrWhitespaceReasonUsesDefaul new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = reason }, @@ -654,13 +654,13 @@ public async Task ApprovedInputsAreExecutedAndFunctionResultsAreConvertedAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -682,7 +682,7 @@ public async Task ApprovedInputsAreExecutedAndFunctionResultsAreConvertedAsync() new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1"), new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 3 } })) + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 3 } })) ]), ]; @@ -708,23 +708,23 @@ public async Task AlreadyExecutedApprovalsAreIgnoredAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1"), new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId3", new FunctionCallContent("callId3", "Func1")), + new FunctionApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func1")), ]) { MessageId = "resp2" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId3", true, new FunctionCallContent("callId3", "Func1")), + new FunctionApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func1")), ]), ]; @@ -834,7 +834,7 @@ public async Task ApprovalRequestWithoutApprovalResponseThrowsAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, ]; @@ -864,8 +864,8 @@ public async Task ApprovedApprovalResponsesWithoutApprovalRequestAreExecutedAsyn new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -910,8 +910,8 @@ public async Task FunctionCallContentIsNotPassedToDownstreamServiceWithServiceTh [ new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -1156,6 +1156,219 @@ public async Task FunctionCallsWithInformationalOnlyTrueAreNotReplacedWithApprov Assert.Equal(0, functionInvokedCount); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(bool useAdditionalTools) + { + AITool[] tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")), + new HostedMcpServerTool("myServer", "https://localhost/mcp") + ]; + + var options = new ChatOptions + { + Tools = useAdditionalTools ? null : tools + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func"), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) + ]; + + List expectedOutput = + [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null); + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(bool useAdditionalTools) + { + AITool[] tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")), + new HostedMcpServerTool("myServer", "https://localhost/mcp") + ]; + + var options = new ChatOptions + { + Tools = useAdditionalTools ? null : tools + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func")), + new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + ]; + + List expectedDownstreamClientInput = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func") + ]), + new ChatMessage(ChatRole.Tool, + [ + new FunctionResultContent("callId1", result: "Result 1") + ]), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, [ + new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }, + new TextContent("world") + ]) + ]; + + List output = + [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func") + ]), + new ChatMessage(ChatRole.Tool, + [ + new FunctionResultContent("callId1", result: "Result 1") + ]), + new ChatMessage(ChatRole.Assistant, [ + new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }, + new TextContent("world") + ]) + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + } + + [Theory] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, true)] + public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAdditionalTools, bool approveFuncCall, bool approveMcpCall) + { + Assert.NotEqual(approveFuncCall, approveMcpCall); + + AITool[] tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")), + new HostedMcpServerTool("myServer", "https://localhost/mcp") + ]; + + var options = new ChatOptions + { + Tools = useAdditionalTools ? null : tools + }; + + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("ficc_callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), + new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + ]; + + List expectedDownstreamClientInput = [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func") + ]), + new ChatMessage(ChatRole.Tool, + [ + approveFuncCall ? + new FunctionResultContent("callId1", result: "Result 1") : + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") + ]), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, [ + new TextContent("world"), + .. approveMcpCall ? + [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] : + Array.Empty() + ]) + ]; + + List output = [ + new ChatMessage(ChatRole.Assistant, + [ + new FunctionCallContent("callId1", "Func"), + ]), + new ChatMessage(ChatRole.Tool, + [ + approveFuncCall ? + new FunctionResultContent("callId1", result: "Result 1") : + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") + ]), + new ChatMessage(ChatRole.Assistant, [ + new TextContent("world"), + .. approveMcpCall ? + [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] : + Array.Empty() + ]) + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); + } + [Fact] public async Task ApprovalResponsePreservesOriginalRequestMessageMetadata() { @@ -1261,7 +1474,7 @@ private static async Task> InvokeAndAssertMultiRoundAsync( IChatClient service = configurePipeline(innerClient.AsBuilder()).Build(); - var result = await service.GetResponseAsync(new EnumeratedOnceEnumerable(input), options, cts.Token); + var result = await service.GetResponseAsync(new EnumeratedOnceEnumerable(CloneInput(input)), options, cts.Token); Assert.NotNull(result); var actualOutput = result.Messages as List ?? result.Messages.ToList(); @@ -1343,7 +1556,7 @@ private static async Task> InvokeAndAssertStreamingMultiRoundA IChatClient service = configurePipeline(innerClient.AsBuilder()).Build(); - var result = await service.GetStreamingResponseAsync(new EnumeratedOnceEnumerable(input), options, cts.Token).ToChatResponseAsync(); + var result = await service.GetStreamingResponseAsync(new EnumeratedOnceEnumerable(CloneInput(input)), options, cts.Token).ToChatResponseAsync(); Assert.NotNull(result); var actualOutput = result.Messages as List ?? result.Messages.ToList(); @@ -1362,4 +1575,20 @@ private static async IAsyncEnumerable YieldAsync(params T[] items) yield return item; } } + + private static List CloneInput(List input) => + input.Select(m => new ChatMessage(m.Role, m.Contents.Select(CloneFcc).ToList()) { MessageId = m.MessageId }).ToList(); + + private static AIContent CloneFcc(AIContent c) => c switch + { + McpServerToolCallContent mstcc => new McpServerToolCallContent(mstcc.CallId, mstcc.Name, mstcc.ServerName) + { + Arguments = mstcc.Arguments, + InformationalOnly = mstcc.InformationalOnly + }, + FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InformationalOnly = fcc.InformationalOnly }, + FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.RequestId, (FunctionCallContent)CloneFcc(farc.FunctionCall)), + FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.RequestId, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, + _ => c + }; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 2ddf757d185..05ab5f1cfc9 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -3291,7 +3291,7 @@ public async Task LogsFunctionRequiresApproval() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]) ]; @@ -3327,11 +3327,11 @@ public async Task LogsProcessingApprovalResponse() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")) ]) }; @@ -3364,11 +3364,11 @@ public async Task LogsFunctionRejected() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied" } + new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied" } ]) }; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index 3f1c9f59bce..fbe78145f91 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -615,7 +615,7 @@ public async Task ServerToolCallContentTypes_SerializedCorrectly(bool streaming) new ImageGenerationToolCallContent { ImageId = "img-123" }, new ImageGenerationToolResultContent { ImageId = "img-123", Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }, new McpServerToolCallContent("mcp-call-1", "myTool", "myServer") { Arguments = new Dictionary { ["param1"] = "value1" } }, - new McpServerToolResultContent("mcp-call-1") { Output = [new TextContent("Tool result")] }, + new McpServerToolResultContent("mcp-call-1") { Result = new TextContent("Tool result") }, ])); }, GetStreamingResponseAsyncCallback = CallbackAsync, @@ -631,7 +631,7 @@ async static IAsyncEnumerable CallbackAsync( yield return new() { Contents = [new ImageGenerationToolCallContent { ImageId = "img-123" }] }; yield return new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img-123", Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }] }; yield return new() { Contents = [new McpServerToolCallContent("mcp-call-1", "myTool", "myServer") { Arguments = new Dictionary { ["param1"] = "value1" } }] }; - yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Output = [new TextContent("Tool result")] }] }; + yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Result = new TextContent("Tool result") }] }; } using var chatClient = innerClient @@ -734,12 +734,10 @@ async static IAsyncEnumerable CallbackAsync( "id": "mcp-call-1", "server_tool_call_response": { "type": "mcp", - "output": [ - { - "$type": "text", - "text": "Tool result" - } - ] + "output": { + "$type": "text", + "text": "Tool result" + } } } ] @@ -785,11 +783,11 @@ public async Task McpServerToolApprovalContentTypes_SerializedCorrectly() [ new(ChatRole.Assistant, [ - new McpServerToolApprovalRequestContent("approval-1", toolCall), + new FunctionApprovalRequestContent("approval-1", toolCall), ]), new(ChatRole.User, [ - new McpServerToolApprovalResponseContent("approval-1", true), + new FunctionApprovalResponseContent("approval-1", true, toolCall), ]), ]; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs index 8fa801c4811..5c92abf8a9a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatReduction/SummarizingChatReducerTests.cs @@ -96,15 +96,15 @@ public async Task ReduceAsync_PreservesCompleteToolCallSequence() new ChatMessage(ChatRole.User, "What's the time?"), new ChatMessage(ChatRole.Assistant, "Let me check"), new ChatMessage(ChatRole.User, "What's the weather?"), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather"), new TestUserInputRequestContent("uir1")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather"), new TestInputRequestContent("uir1")]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny")]), - new ChatMessage(ChatRole.User, [new TestUserInputResponseContent("uir1")]), + new ChatMessage(ChatRole.User, [new TestInputResponseContent("uir1")]), new ChatMessage(ChatRole.Assistant, "It's sunny"), ]; chatClient.GetResponseAsyncCallback = (msgs, _, _) => { - Assert.DoesNotContain(msgs, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent)); + Assert.DoesNotContain(msgs, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestInputRequestContent or TestInputResponseContent)); return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Asked about time"))); }; @@ -120,10 +120,10 @@ public async Task ReduceAsync_PreservesCompleteToolCallSequence() m => { Assert.Contains(m.Contents, c => c is FunctionCallContent); - Assert.Contains(m.Contents, c => c is TestUserInputRequestContent); + Assert.Contains(m.Contents, c => c is TestInputRequestContent); }, m => Assert.Contains(m.Contents, c => c is FunctionResultContent), - m => Assert.Contains(m.Contents, c => c is TestUserInputResponseContent), + m => Assert.Contains(m.Contents, c => c is TestInputResponseContent), m => Assert.Contains("sunny", m.Text)); } @@ -185,9 +185,9 @@ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() List messages = [ new ChatMessage(ChatRole.User, "What's the weather in Seattle?"), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" }), new TestUserInputRequestContent("uir2")]), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather", new Dictionary { ["location"] = "Seattle" }), new TestInputRequestContent("uir2")]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call1", "Sunny, 72°F")]), - new ChatMessage(ChatRole.User, [new TestUserInputResponseContent("uir2")]), + new ChatMessage(ChatRole.User, [new TestInputResponseContent("uir2")]), new ChatMessage(ChatRole.Assistant, "It's sunny and 72°F in Seattle."), new ChatMessage(ChatRole.User, "What about New York?"), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call2", "get_weather", new Dictionary { ["location"] = "New York" })]), @@ -200,7 +200,7 @@ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() var msgList = msgs.ToList(); Assert.Equal(4, msgList.Count); // 3 non-function messages + system prompt - Assert.DoesNotContain(msgList, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestUserInputRequestContent or TestUserInputResponseContent)); + Assert.DoesNotContain(msgList, m => m.Contents.Any(c => c is FunctionCallContent or FunctionResultContent or TestInputRequestContent or TestInputResponseContent)); Assert.Contains(msgList, m => m.Text.Contains("What's the weather in Seattle?")); Assert.Contains(msgList, m => m.Text.Contains("sunny and 72°F in Seattle")); Assert.Contains(msgList, m => m.Text.Contains("What about New York?")); @@ -220,8 +220,8 @@ public async Task ReduceAsync_ExcludesToolCallsFromSummarizedPortion() Assert.Contains(resultList, m => m.Contents.Any(c => c is FunctionResultContent fr && fr.CallId == "call2")); Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionCallContent fc && fc.CallId == "call1")); Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is FunctionResultContent fr && fr.CallId == "call1")); - Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestUserInputRequestContent)); - Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestUserInputResponseContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestInputRequestContent)); + Assert.DoesNotContain(resultList, m => m.Contents.Any(c => c is TestInputResponseContent)); Assert.DoesNotContain(resultList, m => m.Text.Contains("sunny and 72°F in Seattle")); } @@ -385,18 +385,18 @@ need frequent exercise. The user then asked about whether they're good around ki m => Assert.StartsWith("Do they make good lap dogs", m.Text, StringComparison.Ordinal)); } - private sealed class TestUserInputRequestContent : UserInputRequestContent + private sealed class TestInputRequestContent : InputRequestContent { - public TestUserInputRequestContent(string id) - : base(id) + public TestInputRequestContent(string requestId) + : base(requestId) { } } - private sealed class TestUserInputResponseContent : UserInputResponseContent + private sealed class TestInputResponseContent : InputResponseContent { - public TestUserInputResponseContent(string id) - : base(id) + public TestInputResponseContent(string requestId) + : base(requestId) { } } From e29e812af1f5c22383884c22cdba5f48f797eb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 10 Feb 2026 12:09:23 -0600 Subject: [PATCH 02/11] Add Tool Call and Result Content --- .../CompatibilitySuppressions.xml | 197 ++++++++++++ .../Contents/AIContent.cs | 4 +- .../FunctionApprovalRequestContent.cs | 40 --- .../FunctionApprovalResponseContent.cs | 44 --- .../Contents/FunctionCallContent.cs | 12 +- .../Contents/FunctionResultContent.cs | 15 +- .../Contents/InputRequestContent.cs | 2 +- .../Contents/InputResponseContent.cs | 2 +- .../Contents/McpServerToolCallContent.cs | 18 +- .../Contents/McpServerToolResultContent.cs | 11 +- .../Contents/ToolApprovalRequestContent.cs | 83 +++++ .../Contents/ToolApprovalResponseContent.cs | 84 +++++ .../Contents/ToolCallContent.cs | 31 ++ .../Contents/ToolResultContent.cs | 35 +++ ...icrosoft.Extensions.AI.Abstractions.csproj | 2 +- .../Microsoft.Extensions.AI.Abstractions.json | 196 ++++++++---- .../Utilities/AIJsonUtilities.Defaults.cs | 6 +- .../OpenAIResponsesChatClient.cs | 26 +- .../FunctionInvokingChatClient.cs | 63 ++-- .../ImageGeneratingChatClient.cs | 8 +- .../ChatCompletion/OpenTelemetryChatClient.cs | 15 +- .../AssertExtensions.cs | 47 ++- .../ChatResponseUpdateExtensionsTests.cs | 28 +- .../Contents/AIContentTests.cs | 10 +- .../CodeInterpreterToolCallContentTests.cs | 14 +- .../CodeInterpreterToolResultContentTests.cs | 14 +- .../Contents/FunctionCallContentTests.cs | 29 -- .../Contents/FunctionResultContentTests.cs | 29 -- .../Contents/InputRequestContentTests.cs | 7 +- .../Contents/InputResponseContentTests.cs | 7 +- .../Contents/McpServerToolCallContentTests.cs | 7 +- .../McpServerToolResultContentTests.cs | 21 +- ....cs => ToolApprovalRequestContentTests.cs} | 39 +-- ...cs => ToolApprovalResponseContentTests.cs} | 42 +-- .../Contents/ToolCallContentTests.cs | 41 +++ .../Contents/ToolResultContentTests.cs | 40 +++ .../TestJsonSerializerContext.cs | 2 + .../OpenAIConversionTests.cs | 38 +-- .../OpenAIResponseClientIntegrationTests.cs | 12 +- .../OpenAIResponseClientTests.cs | 25 +- ...unctionInvokingChatClientApprovalsTests.cs | 291 +++++++++--------- .../FunctionInvokingChatClientTests.cs | 27 +- .../ImageGeneratingChatClientTests.cs | 4 +- .../OpenTelemetryChatClientTests.cs | 46 ++- 44 files changed, 1108 insertions(+), 606 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs delete mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs create mode 100644 src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs rename test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/{FunctionApprovalRequestContentTests.cs => ToolApprovalRequestContentTests.cs} (64%) rename test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/{FunctionApprovalResponseContentTests.cs => ToolApprovalResponseContentTests.cs} (56%) create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index d266b9d2d4e..ea52aefa5dc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -426,4 +426,201 @@ lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true + + + CP0002 + M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 55d01b2d310..2c5a3b9f895 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -18,8 +18,8 @@ namespace Microsoft.Extensions.AI; [JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")] [JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")] [JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")] -[JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")] -[JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")] +[JsonDerivedType(typeof(ToolApprovalRequestContent), typeDiscriminator: "toolApprovalRequest")] +[JsonDerivedType(typeof(ToolApprovalResponseContent), typeDiscriminator: "toolApprovalResponse")] [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")] diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs deleted file mode 100644 index 5e9cbcc3d04..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ /dev/null @@ -1,40 +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 Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents a request for approval before invoking a function call. -/// -public sealed class FunctionApprovalRequestContent : InputRequestContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified . - /// The function call that requires approval before execution. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public FunctionApprovalRequestContent(string requestId, FunctionCallContent functionCall) - : base(requestId) - { - FunctionCall = Throw.IfNull(functionCall); - } - - /// - /// Gets the function call that requires approval before execution. - /// - public FunctionCallContent FunctionCall { get; } - - /// - /// Creates a indicating whether the function call is approved or rejected. - /// - /// if the function call is approved; otherwise, . - /// An optional reason for the approval or rejection. - /// The correlated with this request. - public FunctionApprovalResponseContent CreateResponse(bool approved, string? reason = null) => new(RequestId, approved, FunctionCall) { Reason = reason }; -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs deleted file mode 100644 index 04f12309b37..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs +++ /dev/null @@ -1,44 +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 Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents a response to a , indicating whether the function call was approved. -/// -public sealed class FunctionApprovalResponseContent : InputResponseContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the associated with this response. - /// if the function call is approved; otherwise, . - /// The function call that was subject to approval. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public FunctionApprovalResponseContent(string requestId, bool approved, FunctionCallContent functionCall) - : base(requestId) - { - Approved = approved; - FunctionCall = Throw.IfNull(functionCall); - } - - /// - /// Gets a value indicating whether the function call was approved for execution. - /// - public bool Approved { get; } - - /// - /// Gets the function call that was subject to approval. - /// - public FunctionCallContent FunctionCall { get; } - - /// - /// Gets or sets the optional reason for the approval or rejection. - /// - public string? Reason { get; set; } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs index 042e862f986..f83e3c94f4b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,8 +14,7 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] -public class FunctionCallContent : AIContent +public class FunctionCallContent : ToolCallContent { /// /// Initializes a new instance of the class. @@ -25,17 +24,12 @@ public class FunctionCallContent : AIContent /// The function original arguments. [JsonConstructor] public FunctionCallContent(string callId, string name, IDictionary? arguments = null) + : base(callId) { - CallId = Throw.IfNull(callId); Name = Throw.IfNull(name); Arguments = arguments; } - /// - /// Gets the function call ID. - /// - public string CallId { get; } - /// /// Gets the name of the function requested. /// @@ -52,7 +46,7 @@ public FunctionCallContent(string callId, string name, IDictionary /// 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. + /// Consumers should not rely on indicating success. /// [JsonIgnore] public Exception? Exception { get; set; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs index 9ffdb11472d..000ea1243be 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionResultContent.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -13,8 +12,7 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] -public class FunctionResultContent : AIContent +public class FunctionResultContent : ToolResultContent { /// /// Initializes a new instance of the class. @@ -27,20 +25,11 @@ public class FunctionResultContent : AIContent /// [JsonConstructor] public FunctionResultContent(string callId, object? result) + : base(callId) { - CallId = Throw.IfNull(callId); Result = result; } - /// - /// Gets the ID of the function call for which this is the result. - /// - /// - /// If this is the result for a , this property should contain the same - /// value. - /// - public string CallId { get; } - /// /// Gets or sets the result of the function call, or a generic error message if the function call failed. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs index 80fdf292d50..8272191f648 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI; /// /// Represents a request for input from the user or application. /// -[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")] +[JsonDerivedType(typeof(ToolApprovalRequestContent), "toolApprovalRequest")] public class InputRequestContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs index 086427dbab5..54f9642d9e6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputResponseContent.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI; /// /// Represents the response to an . /// -[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")] +[JsonDerivedType(typeof(ToolApprovalResponseContent), "toolApprovalResponse")] public class InputResponseContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 62b7b25f065..62cf9dec941 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -16,7 +18,7 @@ namespace Microsoft.Extensions.AI; /// to convey what is being approved, or as a record of which MCP server tool was invoked. /// /// -public sealed class McpServerToolCallContent : FunctionCallContent +public sealed class McpServerToolCallContent : ToolCallContent { /// /// Initializes a new instance of the class. @@ -31,14 +33,24 @@ public sealed class McpServerToolCallContent : FunctionCallContent /// to convey what is being approved, or as a record of which MCP server tool was invoked. /// public McpServerToolCallContent(string callId, string name, string? serverName) - : base(Throw.IfNullOrWhitespace(callId), Throw.IfNullOrWhitespace(name)) + : base(Throw.IfNullOrWhitespace(callId)) { + Name = Throw.IfNullOrWhitespace(name); ServerName = serverName; - InformationalOnly = true; } + /// + /// Gets the name of the tool requested. + /// + public string Name { get; } + /// /// Gets the name of the MCP server that hosts the tool. /// public string? ServerName { get; } + + /// + /// Gets or sets the arguments requested to be provided to the tool. + /// + public IDictionary? Arguments { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index 694b86d4e43..df2753e2688 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -13,7 +15,7 @@ namespace Microsoft.Extensions.AI; /// This content type is used to represent the result of an invocation of an MCP server tool by a hosted service. /// It is informational only. /// -public sealed class McpServerToolResultContent : FunctionResultContent +public sealed class McpServerToolResultContent : ToolResultContent { /// /// Initializes a new instance of the class. @@ -22,7 +24,12 @@ public sealed class McpServerToolResultContent : FunctionResultContent /// is . /// is empty or composed entirely of whitespace. public McpServerToolResultContent(string callId) - : base(Throw.IfNullOrWhitespace(callId), result: null) + : base(Throw.IfNullOrWhitespace(callId)) { } + + /// + /// Gets or sets the output contents of the tool call. + /// + public IList? Outputs { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs new file mode 100644 index 00000000000..cc8c7db410d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs @@ -0,0 +1,83 @@ +// 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; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a request for approval before invoking a tool call. +/// +public sealed class ToolApprovalRequestContent : InputRequestContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified . + /// The function call that requires approval before execution. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public ToolApprovalRequestContent(string requestId, FunctionCallContent functionCall) + : base(requestId) + { + ToolCall = Throw.IfNull(functionCall); + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified . + /// The MCP server tool call that requires approval before execution. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public ToolApprovalRequestContent(string requestId, McpServerToolCallContent mcpServerToolCall) + : base(requestId) + { + ToolCall = Throw.IfNull(mcpServerToolCall); + } + + /// + /// Initializes a new instance of the class for JSON deserialization. + /// + /// The unique identifier that correlates this request with its corresponding response. + /// The tool call that requires approval before execution. + [JsonConstructor] + [EditorBrowsable(EditorBrowsableState.Never)] + public ToolApprovalRequestContent(string requestId, ToolCallContent toolCall) + : base(requestId) + { + _ = Throw.IfNull(toolCall); + + if (toolCall is not FunctionCallContent and not McpServerToolCallContent) + { + Throw.ArgumentException(nameof(toolCall), $"Unsupported type '{toolCall.GetType().Name}'."); + } + + ToolCall = toolCall; + } + + /// + /// Gets the tool call that requires approval before execution. + /// + public ToolCallContent ToolCall { get; } + + /// + /// Creates a indicating whether the tool call is approved or rejected. + /// + /// if the tool call is approved; otherwise, . + /// An optional reason for the approval or rejection. + /// The correlated with this request. + public ToolApprovalResponseContent CreateResponse(bool approved, string? reason = null) => ToolCall switch + { + FunctionCallContent fcc => new ToolApprovalResponseContent(RequestId, approved, fcc) { Reason = reason }, + McpServerToolCallContent mcp => new ToolApprovalResponseContent(RequestId, approved, mcp) { Reason = reason }, + + // This should never occur since the constructor enforces the allowed types. + _ => throw new InvalidOperationException($"Unsupported ToolCallContent type '{ToolCall.GetType().Name}'."), + }; +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs new file mode 100644 index 00000000000..2fd9bf7c29e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs @@ -0,0 +1,84 @@ +// 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; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a response to a , indicating whether the tool call was approved. +/// +public sealed class ToolApprovalResponseContent : InputResponseContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the associated with this response. + /// if the tool call is approved; otherwise, . + /// The function call that was subject to approval. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public ToolApprovalResponseContent(string requestId, bool approved, FunctionCallContent functionCall) + : base(requestId) + { + Approved = approved; + ToolCall = Throw.IfNull(functionCall); + } + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the associated with this response. + /// if the tool call is approved; otherwise, . + /// The MCP server tool call that was subject to approval. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public ToolApprovalResponseContent(string requestId, bool approved, McpServerToolCallContent mcpServerToolCall) + : base(requestId) + { + Approved = approved; + ToolCall = Throw.IfNull(mcpServerToolCall); + } + + /// + /// Initializes a new instance of the class for JSON deserialization. + /// + /// The unique identifier of the associated with this response. + /// if the tool call is approved; otherwise, . + /// The tool call that was subject to approval. + [JsonConstructor] + [EditorBrowsable(EditorBrowsableState.Never)] + public ToolApprovalResponseContent(string requestId, bool approved, ToolCallContent toolCall) + : base(requestId) + { + _ = Throw.IfNull(toolCall); + + if (toolCall is not FunctionCallContent and not McpServerToolCallContent) + { + Throw.ArgumentException(nameof(toolCall), $"Unsupported type '{toolCall.GetType().Name}'."); + } + + Approved = approved; + ToolCall = toolCall; + } + + /// + /// Gets a value indicating whether the tool call was approved for execution. + /// + public bool Approved { get; } + + /// + /// Gets the tool call that was subject to approval. + /// + public ToolCallContent ToolCall { get; } + + /// + /// Gets or sets the optional reason for the approval or rejection. + /// + public string? Reason { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs new file mode 100644 index 00000000000..8370431658a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs @@ -0,0 +1,31 @@ +// 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.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a tool call request. +/// +[JsonDerivedType(typeof(FunctionCallContent), "functionCall")] +[JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] +public class ToolCallContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The tool call ID. + /// is . + protected ToolCallContent(string callId) + { + CallId = Throw.IfNull(callId); + } + + /// + /// Gets the tool call ID. + /// + public string CallId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs new file mode 100644 index 00000000000..9e5aacd8b87 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.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.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents the result of a tool call. +/// +[JsonDerivedType(typeof(FunctionResultContent), "functionResult")] +[JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] +public class ToolResultContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The tool call ID for which this is the result. + /// is . + protected ToolResultContent(string callId) + { + CallId = Throw.IfNull(callId); + } + + /// + /// Gets the ID of the tool call for which this is the result. + /// + /// + /// If this is the result for a , this property should contain the same + /// value. + /// + public string CallId { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index c11dbd0b2dc..7ef083fb495 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -15,7 +15,7 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);MEAI001 + $(NoWarn);MEAI001;LA0003;LA0006;S1128 true true diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 95cfc015c86..8c6c37b2c5f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1930,51 +1930,7 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.InputRequestContent", - "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string requestId, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Stable" - }, - { - "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.CreateResponse(bool approved, string? reason = null);", - "Stage": "Stable" - } - ], - "Properties": [ - { - "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionCall { get; }", - "Stage": "Stable" - } - ] - }, - { - "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.InputResponseContent", - "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Stable" - } - ], - "Properties": [ - { - "Member": "bool Microsoft.Extensions.AI.FunctionApprovalResponseContent.Approved { get; }", - "Stage": "Stable" - }, - { - "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionCall { get; }", - "Stage": "Stable" - }, - { - "Member": "string? Microsoft.Extensions.AI.FunctionApprovalResponseContent.Reason { get; set; }", - "Stage": "Stable" - } - ] - }, - { - "Type": "class Microsoft.Extensions.AI.FunctionCallContent : Microsoft.Extensions.AI.AIContent", + "Type": "class Microsoft.Extensions.AI.FunctionCallContent : Microsoft.Extensions.AI.ToolCallContent", "Stage": "Stable", "Methods": [ { @@ -1991,10 +1947,6 @@ "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.FunctionCallContent.Arguments { get; set; }", "Stage": "Stable" }, - { - "Member": "string Microsoft.Extensions.AI.FunctionCallContent.CallId { get; }", - "Stage": "Stable" - }, { "Member": "System.Exception? Microsoft.Extensions.AI.FunctionCallContent.Exception { get; set; }", "Stage": "Stable" @@ -2004,13 +1956,13 @@ "Stage": "Stable" }, { - "Member": "string Microsoft.Extensions.AI.FunctionCallContent.Name { get; }", + "Member": "string Microsoft.Extensions.AI.FunctionCallContent.Name { get; set; }", "Stage": "Stable" } ] }, { - "Type": "class Microsoft.Extensions.AI.FunctionResultContent : Microsoft.Extensions.AI.AIContent", + "Type": "class Microsoft.Extensions.AI.FunctionResultContent : Microsoft.Extensions.AI.ToolResultContent", "Stage": "Stable", "Methods": [ { @@ -2019,10 +1971,6 @@ } ], "Properties": [ - { - "Member": "string Microsoft.Extensions.AI.FunctionResultContent.CallId { get; }", - "Stage": "Stable" - }, { "Member": "System.Exception? Microsoft.Extensions.AI.FunctionResultContent.Exception { get; set; }", "Stage": "Stable" @@ -2757,15 +2705,31 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.FunctionCallContent", + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolCallContent : Microsoft.Extensions.AI.ToolCallContent", "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName);", + "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName, System.Collections.Generic.IDictionary? arguments = null);", "Stage": "Stable" } ], "Properties": [ + { + "Member": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.McpServerToolCallContent.Arguments { get; set; }", + "Stage": "Stable" + }, + { + "Member": "System.Exception? Microsoft.Extensions.AI.McpServerToolCallContent.Exception { get; set; }", + "Stage": "Stable" + }, + { + "Member": "bool Microsoft.Extensions.AI.McpServerToolCallContent.InformationalOnly { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.Name { get; set; }", + "Stage": "Stable" + }, { "Member": "string? Microsoft.Extensions.AI.McpServerToolCallContent.ServerName { get; }", "Stage": "Stable" @@ -2773,11 +2737,25 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.FunctionResultContent", + "Type": "sealed class Microsoft.Extensions.AI.McpServerToolResultContent : Microsoft.Extensions.AI.ToolResultContent", "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);", + "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId, string? serverName, object? result = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "System.Exception? Microsoft.Extensions.AI.McpServerToolResultContent.Exception { get; set; }", + "Stage": "Stable" + }, + { + "Member": "object? Microsoft.Extensions.AI.McpServerToolResultContent.Result { get; set; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.McpServerToolResultContent.ServerName { get; }", "Stage": "Stable" } ] @@ -3318,6 +3296,106 @@ } ] }, + { + "Type": "sealed class Microsoft.Extensions.AI.ToolApprovalRequestContent : Microsoft.Extensions.AI.InputRequestContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolApprovalRequestContent(string requestId, Microsoft.Extensions.AI.FunctionCallContent functionCall);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolApprovalRequestContent(string requestId, Microsoft.Extensions.AI.McpServerToolCallContent mcpServerToolCall);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolApprovalRequestContent(string requestId, Microsoft.Extensions.AI.ToolCallContent toolCall);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent Microsoft.Extensions.AI.ToolApprovalRequestContent.CreateResponse(bool approved, string? reason = null);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "Microsoft.Extensions.AI.ToolCallContent Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolCall { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "sealed class Microsoft.Extensions.AI.ToolApprovalResponseContent : Microsoft.Extensions.AI.InputResponseContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.McpServerToolCallContent mcpServerToolCall);", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.ToolCallContent toolCall);", + "Stage": "Stable" + } + ], + "Properties": [ + { + "Member": "bool Microsoft.Extensions.AI.ToolApprovalResponseContent.Approved { get; }", + "Stage": "Stable" + }, + { + "Member": "string? Microsoft.Extensions.AI.ToolApprovalResponseContent.Reason { get; set; }", + "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.ToolCallContent Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolCall { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "abstract class Microsoft.Extensions.AI.ToolCallContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ToolCallContent.ToolCallContent(string callId);", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ToolCallContent.ToolCallContent();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.ToolCallContent.CallId { get; set; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "abstract class Microsoft.Extensions.AI.ToolResultContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Methods": [ + { + "Member": "Microsoft.Extensions.AI.ToolResultContent.ToolResultContent(string callId);", + "Stage": "Experimental" + }, + { + "Member": "Microsoft.Extensions.AI.ToolResultContent.ToolResultContent();", + "Stage": "Experimental" + } + ], + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.ToolResultContent.CallId { get; set; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.UriContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", 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 8c9ffeaa54a..7e9bda6043a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -120,10 +120,14 @@ private static JsonSerializerOptions CreateDefaultOptions() // InputRequestContent and InputResponseContent are polymorphic base types that may be // serialized as root types (not just as AIContent). They have protected constructors so // can't be instantiated directly, but we still need metadata when serializing derived - // types (e.g., FunctionApprovalRequestContent) as InputRequestContent. + // types (e.g., ToolApprovalRequestContent) as InputRequestContent. [JsonSerializable(typeof(InputRequestContent))] [JsonSerializable(typeof(InputResponseContent))] + // ToolCallContent and ToolResultContent are polymorphic base types for tool calls/results. + [JsonSerializable(typeof(ToolCallContent))] + [JsonSerializable(typeof(ToolResultContent))] + // Temporary workaround: These should be implicitly added in once they're no longer [Experimental] // and are included via [JsonDerivedType] on AIContent. [JsonSerializable(typeof(CodeInterpreterToolCallContent))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index eedcf3b79df..831862c5941 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -215,7 +215,7 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable (mcpApprovalRequests ??= new())[mtcari.Id] = mtcari; // We are reusing the mtcari.Id as the McpServerToolCallContent.CallId since we don't have one yet. - yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) + yield return CreateUpdate(new ToolApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) { Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments, OpenAIJsonContext.Default.IDictionaryStringObject), RawRepresentation = mtcari, @@ -487,7 +487,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => // Correlate with the original request to get tool details. // McpToolCallApprovalResponseItem without a correlated request falls through to default. - yield return CreateUpdate(new FunctionApprovalResponseContent( + yield return CreateUpdate(new ToolApprovalResponseContent( mtcari.ApprovalRequestId, mtcari.Approved, new McpServerToolCallContent(mtcari.ApprovalRequestId, request.ToolName, request.ServerLabel) @@ -967,7 +967,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep, - FunctionApprovalResponseContent { FunctionCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.RequestId, funcResp.Approved), + ToolApprovalResponseContent { ToolCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.RequestId, funcResp.Approved), _ => null }; @@ -1049,7 +1049,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable))))); break; - case FunctionApprovalRequestContent funcReq when funcReq.FunctionCall is McpServerToolCallContent mcpToolCall: + case ToolApprovalRequestContent funcReq when funcReq.ToolCall is McpServerToolCallContent mcpToolCall: yield return ResponseItem.CreateMcpApprovalRequestItem( funcReq.RequestId, mcpToolCall.ServerName, @@ -1227,13 +1227,13 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes( associatedCall.Arguments!, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); - if (mstrc.Result is ErrorContent errorContent) + if (mstrc.Outputs?.OfType().FirstOrDefault() is ErrorContent errorContent) { mtci.Error = BinaryData.FromString(errorContent.Message); } - else if (mstrc.Result is TextContent textContent) + else { - mtci.ToolOutput = textContent.Text; + mtci.ToolOutput = string.Concat(mstrc.Outputs?.OfType() ?? []); } yield return mtci; @@ -1428,9 +1428,9 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents.Add(new McpServerToolResultContent(mtci.Id) { RawRepresentation = mtci, - Result = mtci.Error is not null ? - new ErrorContent(mtci.Error.ToString()) : - new TextContent(mtci.ToolOutput), + Outputs = mtci.Error is not null ? + [new ErrorContent(mtci.Error.ToString())] : + [new TextContent(mtci.ToolOutput)], }); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 7fcf5b770de..fd847eb56e0 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -41,9 +41,9 @@ namespace Microsoft.Extensions.AI; /// /// /// Further, if a requested function is an , the will not -/// attempt to invoke it directly. Instead, it will replace that with a +/// attempt to invoke it directly. Instead, it will replace that with a /// that wraps the and indicates that the function requires approval before it can be invoked. The caller is then -/// responsible for responding to that approval request by sending a corresponding in a subsequent +/// responsible for responding to that approval request by sending a corresponding in a subsequent /// request. The will then process that approval response and invoke the function as appropriate. /// /// @@ -801,10 +801,10 @@ private static bool HasAnyTools(params ReadOnlySpan?> toolLists) } /// - /// Gets whether contains any or instances. + /// Gets whether contains any or instances. /// private static bool HasAnyApprovalContent(List messages) => - messages.Exists(static m => m.Contents.Any(static c => c is FunctionApprovalRequestContent or FunctionApprovalResponseContent)); + messages.Exists(static m => m.Contents.Any(static c => c is ToolApprovalRequestContent or ToolApprovalResponseContent)); /// Copies any from to . private static bool CopyFunctionCalls( @@ -1381,9 +1381,9 @@ private static bool CurrentActivityIsInvokeAgent } /// - /// 1. Remove all and from the . - /// 2. Recreate for any that haven't been executed yet. - /// 3. Generate failed for any rejected . + /// 1. Remove all and from the . + /// 2. Recreate for any that haven't been executed yet. + /// 3. Generate failed for any rejected . /// 4. add all the new content items to and return them as the pre-invocation history. /// private (List? preDownstreamCallHistory, List? approvals) ProcessFunctionApprovalResponses( @@ -1441,7 +1441,7 @@ private static bool CurrentActivityIsInvokeAgent List messages) { Dictionary? allApprovalRequestsMessages = null; - List? allApprovalResponses = null; + List? allApprovalResponses = null; HashSet? approvalRequestCallIds = null; HashSet? functionResultCallIds = null; @@ -1464,15 +1464,15 @@ private static bool CurrentActivityIsInvokeAgent var content = message.Contents[j]; switch (content) { - case FunctionApprovalRequestContent farc when !farc.FunctionCall.InformationalOnly: + case ToolApprovalRequestContent farc when farc.ToolCall is FunctionCallContent { InformationalOnly: false }: // Validation: Capture each call id for each approval request to ensure later we have a matching response. - _ = (approvalRequestCallIds ??= []).Add(farc.FunctionCall.CallId); + _ = (approvalRequestCallIds ??= []).Add(farc.ToolCall.CallId); (allApprovalRequestsMessages ??= []).Add(farc.RequestId, message); break; - case FunctionApprovalResponseContent farc when !farc.FunctionCall.InformationalOnly: + case ToolApprovalResponseContent farc when farc.ToolCall is FunctionCallContent { InformationalOnly: false }: // Validation: Remove the call id for each approval response, to check it off the list of requests we need responses for. - _ = approvalRequestCallIds?.Remove(farc.FunctionCall.CallId); + _ = approvalRequestCallIds?.Remove(farc.ToolCall.CallId); (allApprovalResponses ??= []).Add(farc); break; @@ -1518,7 +1518,7 @@ private static bool CurrentActivityIsInvokeAgent if (approvalRequestCallIds is { Count: > 0 }) { Throw.InvalidOperationException( - $"FunctionApprovalRequestContent found with FunctionCall.CallId(s) '{string.Join(", ", approvalRequestCallIds)}' that have no matching FunctionApprovalResponseContent."); + $"ToolApprovalRequestContent found with FunctionCall.CallId(s) '{string.Join(", ", approvalRequestCallIds)}' that have no matching ToolApprovalResponseContent."); } // 2nd iteration, over all approval responses: @@ -1531,12 +1531,12 @@ private static bool CurrentActivityIsInvokeAgent foreach (var approvalResponse in allApprovalResponses) { // Skip any approval responses that have already been processed. - if (functionResultCallIds?.Contains(approvalResponse.FunctionCall.CallId) is true) + if (approvalResponse.ToolCall is not FunctionCallContent fcc || functionResultCallIds?.Contains(fcc.CallId) is true) { continue; } - LogProcessingApprovalResponse(approvalResponse.FunctionCall.Name, approvalResponse.Approved); + LogProcessingApprovalResponse(fcc.Name, approvalResponse.Approved); // Split the responses into approved and rejected. ref List? targetList = ref approvalResponse.Approved ? ref approvedFunctionCalls : ref rejectedFunctionCalls; @@ -1560,7 +1560,7 @@ private static bool CurrentActivityIsInvokeAgent rejections is { Count: > 0 } ? rejections.ConvertAll(m => { - LogFunctionRejected(m.Response.FunctionCall.Name, m.Response.Reason); + LogFunctionRejected(m.FunctionCallContent.Name, m.Response.Reason); string result = "Tool call invocation rejected."; if (!string.IsNullOrWhiteSpace(m.Response.Reason)) @@ -1569,13 +1569,13 @@ private static bool CurrentActivityIsInvokeAgent } // Mark the function call as purely informational since we're handling it (by rejecting it) - m.Response.FunctionCall.InformationalOnly = true; - return (AIContent)new FunctionResultContent(m.Response.FunctionCall.CallId, result); + m.FunctionCallContent.InformationalOnly = true; + return (AIContent)new FunctionResultContent(m.FunctionCallContent.CallId, result); }) : null; /// - /// Extracts the from the provided to recreate the original function call messages. + /// Extracts the from the provided to recreate the original function call messages. /// The output messages tries to mimic the original messages that contained the , e.g. if the /// had been split into separate messages, this method will recreate similarly split messages, each with their own . /// @@ -1625,7 +1625,7 @@ private static bool CurrentActivityIsInvokeAgent } else { - currentMessage.Contents.Add(resultWithRequestMessage.Response.FunctionCall); + currentMessage.Contents.Add(resultWithRequestMessage.Response.ToolCall); } #pragma warning disable IDE0058 // Temporary workaround for Roslyn analyzer issue (see https://github.com/dotnet/roslyn/issues/80499) @@ -1654,7 +1654,7 @@ private static bool CurrentActivityIsInvokeAgent private static ChatMessage ConvertToFunctionCallContentMessage(ApprovalResultWithRequestMessage resultWithRequestMessage, string? fallbackMessageId) { ChatMessage functionCallMessage = resultWithRequestMessage.RequestMessage?.Clone() ?? new() { Role = ChatRole.Assistant }; - functionCallMessage.Contents = [resultWithRequestMessage.Response.FunctionCall]; + functionCallMessage.Contents = [resultWithRequestMessage.Response.ToolCall]; functionCallMessage.MessageId ??= fallbackMessageId; return functionCallMessage; } @@ -1697,7 +1697,7 @@ private static (bool hasApprovalRequiringFcc, int lastApprovalCheckedFCCIndex) C } /// - /// Replaces all with and ouputs a new list if any of them were replaced. + /// Replaces all with and ouputs a new list if any of them were replaced. /// /// true if any was replaced, false otherwise. private static bool TryReplaceFunctionCallsWithApprovalRequests(IList content, out List? updatedContent) @@ -1711,7 +1711,7 @@ private static bool TryReplaceFunctionCallsWithApprovalRequests(IList if (content[i] is FunctionCallContent fcc && !fcc.InformationalOnly) { updatedContent ??= [.. content]; // Clone the list if we haven't already - updatedContent[i] = new FunctionApprovalRequestContent(ComposeApprovalRequestId(fcc.CallId), fcc); + updatedContent[i] = new ToolApprovalRequestContent(ComposeApprovalRequestId(fcc.CallId), fcc); } } } @@ -1720,7 +1720,7 @@ private static bool TryReplaceFunctionCallsWithApprovalRequests(IList } /// - /// Replaces all from with + /// Replaces all from with /// if any one of them requires approval. /// private IList ReplaceFunctionCallsWithApprovalRequests( @@ -1766,7 +1766,7 @@ private IList ReplaceFunctionCallsWithApprovalRequests( var functionCall = (FunctionCallContent)message.Contents[contentIndex]; LogFunctionRequiresApproval(functionCall.Name); - message.Contents[contentIndex] = new FunctionApprovalRequestContent(ComposeApprovalRequestId(functionCall.CallId), functionCall); + message.Contents[contentIndex] = new ToolApprovalRequestContent(ComposeApprovalRequestId(functionCall.CallId), functionCall); outputMessages[messageIndex] = message; lastMessageIndex = messageIndex; @@ -1787,7 +1787,7 @@ private static TimeSpan GetElapsedTime(long startingTimestamp) => private static string ComposeApprovalRequestId(string callId) => $"ficc_{callId}"; /// - /// Execute the provided and return the resulting + /// Execute the provided and return the resulting /// wrapped in objects. /// private async Task<(IList? FunctionResultContentMessages, bool ShouldTerminate, int ConsecutiveErrorCount)> InvokeApprovedFunctionApprovalResponsesAsync( @@ -1803,7 +1803,7 @@ private static TimeSpan GetElapsedTime(long startingTimestamp) => { // The FRC that is generated here is already added to originalMessages by ProcessFunctionCallsAsync. var modeAndMessages = await ProcessFunctionCallsAsync( - originalMessages, options, notInvokedApprovals.Select(x => x.Response.FunctionCall).ToList(), 0, consecutiveErrorCount, isStreaming, cancellationToken); + originalMessages, options, notInvokedApprovals.Select(x => x.Response.ToolCall).OfType().ToList(), 0, consecutiveErrorCount, isStreaming, cancellationToken); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; return (modeAndMessages.MessagesAdded, modeAndMessages.ShouldTerminate, consecutiveErrorCount); @@ -1903,9 +1903,10 @@ public enum FunctionInvocationStatus Exception, } - private struct ApprovalResultWithRequestMessage + private readonly struct ApprovalResultWithRequestMessage { - public FunctionApprovalResponseContent Response { get; set; } - public ChatMessage? RequestMessage { get; set; } + public ToolApprovalResponseContent Response { get; init; } + public ChatMessage? RequestMessage { get; init; } + public FunctionCallContent FunctionCallContent => (FunctionCallContent)Response.ToolCall; } } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ImageGeneratingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ImageGeneratingChatClient.cs index 739ad2c797e..f57fc966c01 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ImageGeneratingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ImageGeneratingChatClient.cs @@ -333,10 +333,7 @@ public IList ReplaceImageGenerationFunctionResults(IList c if (functionCall.Name != nameof(GetImagesForEdit)) { - newContents.Add(new ImageGenerationToolCallContent - { - ImageId = functionCall.CallId, - }); + newContents.Add(new ImageGenerationToolCallContent(functionCall.CallId)); } } else if (content is FunctionResultContent functionResult && @@ -347,9 +344,8 @@ public IList ReplaceImageGenerationFunctionResults(IList c if (imageContents.Any()) { // Insert ImageGenerationToolResultContent in its place, do not preserve the FunctionResultContent - newContents.Add(new ImageGenerationToolResultContent + newContents.Add(new ImageGenerationToolResultContent(functionResult.CallId) { - ImageId = functionResult.CallId, Outputs = imageContents }); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index ad3cf4feae0..55e3cd4a0a1 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -277,7 +277,7 @@ internal static string SerializeChatMessages( Id = mstrc.CallId, ServerToolCallResponse = new OtelMcpToolCallResponse { - Output = mstrc.Result, + Output = mstrc.Outputs, }, }); break; @@ -364,7 +364,7 @@ internal static string SerializeChatMessages( case ImageGenerationToolCallContent igtcc: m.Parts.Add(new OtelServerToolCallPart { - Id = igtcc.ImageId, + Id = igtcc.CallId, Name = "image_generation", ServerToolCall = new OtelImageGenerationToolCall(), }); @@ -373,7 +373,7 @@ internal static string SerializeChatMessages( case ImageGenerationToolResultContent igtrc: m.Parts.Add(new OtelServerToolCallResponsePart { - Id = igtrc.ImageId, + Id = igtrc.CallId, ServerToolCallResponse = new OtelImageGenerationToolCallResponse { Output = igtrc.Outputs, @@ -381,11 +381,11 @@ internal static string SerializeChatMessages( }); break; - case FunctionApprovalRequestContent fareqc when fareqc.FunctionCall is McpServerToolCallContent mcpToolCall: + case ToolApprovalRequestContent fareqc when fareqc.ToolCall is McpServerToolCallContent mcpToolCall: m.Parts.Add(new OtelServerToolCallPart { Id = fareqc.RequestId, - Name = fareqc.FunctionCall.Name, + Name = mcpToolCall.Name, ServerToolCall = new OtelMcpApprovalRequest { Arguments = mcpToolCall.Arguments, @@ -394,7 +394,7 @@ internal static string SerializeChatMessages( }); break; - case FunctionApprovalResponseContent farespc when farespc.FunctionCall is McpServerToolCallContent: + case ToolApprovalResponseContent farespc when farespc.ToolCall is McpServerToolCallContent: m.Parts.Add(new OtelServerToolCallResponsePart { Id = farespc.RequestId, @@ -927,3 +927,4 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(IEnumerable))] private sealed partial class OtelContext : JsonSerializerContext; } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs index 975ccb26230..8111bf80e94 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AssertExtensions.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; @@ -46,27 +46,51 @@ public static void EqualMessageLists(List expectedMessages, List + /// Asserts that two ToolCallContent instances have the same Name and Arguments, + /// regardless of whether they are FunctionCallContent or McpServerToolCallContent. + /// + private static void AssertToolCallNameAndArguments(ToolCallContent expected, ToolCallContent actual) + { + (string? expectedName, IDictionary? expectedArgs) = expected switch + { + FunctionCallContent fcc => (fcc.Name, fcc.Arguments), + McpServerToolCallContent mcp => (mcp.Name, mcp.Arguments), + _ => throw new XunitException($"Unexpected ToolCallContent type: {expected.GetType()}") + }; + + (string? actualName, IDictionary? actualArgs) = actual switch + { + FunctionCallContent fcc => (fcc.Name, fcc.Arguments), + McpServerToolCallContent mcp => (mcp.Name, mcp.Arguments), + _ => throw new XunitException($"Unexpected ToolCallContent type: {actual.GetType()}") + }; + + Assert.Equal(expectedName, actualName); + EqualFunctionCallParameters(expectedArgs, actualArgs); + } + /// /// Asserts that the two function call parameters are equal, up to JSON equivalence. /// @@ -136,3 +160,4 @@ static JsonElement NormalizeToElement(object? value, JsonSerializerOptions optio => value is JsonElement e ? e : JsonSerializer.SerializeToElement(value, options); } } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs index 08fbfec24be..c6d5ed90644 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatResponseUpdateExtensionsTests.cs @@ -931,16 +931,16 @@ public async Task ToChatResponse_CoalescesImageGenerationToolResultContent(bool new(null, " some images"), // Initial ImageGenerationToolResultContent with ID "img1" - new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img1", Outputs = [image1] }] }, + new() { Contents = [new ImageGenerationToolResultContent("img1") { Outputs = [image1] }] }, // Another ImageGenerationToolResultContent with different ID "img2" - new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img2", Outputs = [image2] }] }, + new() { Contents = [new ImageGenerationToolResultContent("img2") { Outputs = [image2] }] }, // Another ImageGenerationToolResultContent with same ID "img1" - should replace the first one - new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img1", Outputs = [image3] }] }, + new() { Contents = [new ImageGenerationToolResultContent("img1") { Outputs = [image3] }] }, // ImageGenerationToolResultContent with same ID "img2" - should replace the second one - new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img2", Outputs = [image4] }] }, + new() { Contents = [new ImageGenerationToolResultContent("img2") { Outputs = [image4] }] }, // Final text new(null, "Here are those generated images"), @@ -961,13 +961,13 @@ public async Task ToChatResponse_CoalescesImageGenerationToolResultContent(bool Assert.Equal(2, imageResults.Length); // Verify the first image result (ID "img1") has the latest content (image3) - var firstImageResult = imageResults.First(ir => ir.ImageId == "img1"); + var firstImageResult = imageResults.First(ir => ir.CallId == "img1"); Assert.NotNull(firstImageResult.Outputs); var firstOutput = Assert.Single(firstImageResult.Outputs); Assert.Same(image3, firstOutput); // Should be the later image, not image1 // Verify the second image result (ID "img2") has the latest content (image4) - var secondImageResult = imageResults.First(ir => ir.ImageId == "img2"); + var secondImageResult = imageResults.First(ir => ir.CallId == "img2"); Assert.NotNull(secondImageResult.Outputs); var secondOutput = Assert.Single(secondImageResult.Outputs); Assert.Same(image4, secondOutput); // Should be the later image, not image2 @@ -976,7 +976,7 @@ public async Task ToChatResponse_CoalescesImageGenerationToolResultContent(bool [Theory] [InlineData(false)] [InlineData(true)] - public async Task ToChatResponse_ImageGenerationToolResultContentWithNullOrEmptyImageId_DoesNotCoalesce(bool useAsync) + public async Task ToChatResponse_ImageGenerationToolResultContentWithDistinctCallIds_DoesNotCoalesce(bool useAsync) { var image1 = new DataContent((byte[])[1, 2, 3, 4], "image/png") { Name = "image1.png" }; var image2 = new DataContent((byte[])[5, 6, 7, 8], "image/jpeg") { Name = "image2.jpg" }; @@ -984,20 +984,20 @@ public async Task ToChatResponse_ImageGenerationToolResultContentWithNullOrEmpty ChatResponseUpdate[] updates = { - // ImageGenerationToolResultContent with null ImageId - should not coalesce - new() { Contents = [new ImageGenerationToolResultContent { ImageId = null, Outputs = [image1] }] }, + // ImageGenerationToolResultContent with unique CallId - should not coalesce + new() { Contents = [new ImageGenerationToolResultContent("id-1") { Outputs = [image1] }] }, - // ImageGenerationToolResultContent with empty ImageId - should not coalesce - new() { Contents = [new ImageGenerationToolResultContent { ImageId = "", Outputs = [image2] }] }, + // ImageGenerationToolResultContent with different CallId - should not coalesce + new() { Contents = [new ImageGenerationToolResultContent("id-2") { Outputs = [image2] }] }, - // Another with null ImageId - should not coalesce with the first - new() { Contents = [new ImageGenerationToolResultContent { ImageId = null, Outputs = [image3] }] }, + // Another with unique CallId - should not coalesce with the others + new() { Contents = [new ImageGenerationToolResultContent("id-3") { Outputs = [image3] }] }, }; ChatResponse response = useAsync ? await YieldAsync(updates).ToChatResponseAsync() : updates.ToChatResponse(); ChatMessage message = Assert.Single(response.Messages); - // Should have all 3 image result contents since they can't be coalesced + // Should have all 3 image result contents since they have distinct CallIds var imageResults = message.Contents.OfType().ToArray(); Assert.Equal(3, imageResults.Length); 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 d89fce3bf4e..0b7c4c3ec75 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; @@ -70,12 +70,12 @@ public void Serialization_DerivedTypes_Roundtrips() new HostedFileContent("file123"), new HostedVectorStoreContent("vectorStore123"), new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 20, TotalTokenCount = 30 }), - new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), - new FunctionApprovalResponseContent("request123", approved: true, new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new ToolApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new ToolApprovalResponseContent("request123", approved: true, new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), new McpServerToolCallContent("call123", "myTool", "myServer"), new McpServerToolResultContent("call123"), - new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), + new ToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), + new ToolApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), new ImageGenerationToolCallContent { ImageId = "img123" }, new ImageGenerationToolResultContent { ImageId = "img456", Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] } ]); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs index 1807f4a169a..8a79ab0c35e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs @@ -12,20 +12,18 @@ public class CodeInterpreterToolCallContentTests [Fact] public void Constructor_PropsDefault() { - CodeInterpreterToolCallContent c = new(); + CodeInterpreterToolCallContent c = new("callId1"); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.CallId); + Assert.Equal("callId1", c.CallId); Assert.Null(c.Inputs); } [Fact] public void Properties_Roundtrip() { - CodeInterpreterToolCallContent c = new(); + CodeInterpreterToolCallContent c = new("call123"); - Assert.Null(c.CallId); - c.CallId = "call123"; Assert.Equal("call123", c.CallId); Assert.Null(c.Inputs); @@ -47,9 +45,8 @@ public void Properties_Roundtrip() [Fact] public void Inputs_SupportsMultipleContentTypes() { - CodeInterpreterToolCallContent c = new() + CodeInterpreterToolCallContent c = new("call456") { - CallId = "call456", Inputs = [ new TextContent("import numpy as np"), @@ -68,9 +65,8 @@ public void Inputs_SupportsMultipleContentTypes() [Fact] public void Serialization_Roundtrips() { - CodeInterpreterToolCallContent content = new() + CodeInterpreterToolCallContent content = new("call123") { - CallId = "call123", Inputs = [ new TextContent("print('hello')"), diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs index 6fb1303be53..01f22a05e1f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs @@ -12,20 +12,18 @@ public class CodeInterpreterToolResultContentTests [Fact] public void Constructor_PropsDefault() { - CodeInterpreterToolResultContent c = new(); + CodeInterpreterToolResultContent c = new("callId1"); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.CallId); + Assert.Equal("callId1", c.CallId); Assert.Null(c.Outputs); } [Fact] public void Properties_Roundtrip() { - CodeInterpreterToolResultContent c = new(); + CodeInterpreterToolResultContent c = new("call123"); - Assert.Null(c.CallId); - c.CallId = "call123"; Assert.Equal("call123", c.CallId); Assert.Null(c.Outputs); @@ -47,9 +45,8 @@ public void Properties_Roundtrip() [Fact] public void Output_SupportsMultipleContentTypes() { - CodeInterpreterToolResultContent c = new() + CodeInterpreterToolResultContent c = new("call789") { - CallId = "call789", Outputs = [ new TextContent("Execution completed"), @@ -70,9 +67,8 @@ public void Output_SupportsMultipleContentTypes() [Fact] public void Serialization_Roundtrips() { - CodeInterpreterToolResultContent content = new() + CodeInterpreterToolResultContent content = new("call123") { - CallId = "call123", Outputs = [ new TextContent("Hello, World!"), 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 0ca4db7a123..47f61ee674c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs @@ -432,33 +432,4 @@ static void AssertSerializationRoundtrips(FunctionCallContent content) } } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() - { - FunctionCallContent[] contents = - [ - new FunctionCallContent("call1", "function1", new Dictionary { { "param1", 123 } }), - new McpServerToolCallContent("call2", "myTool", "myServer"), - ]; - - // Verify each element roundtrips individually - foreach (var content in contents) - { - var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); - Assert.NotNull(deserialized); - Assert.Equal(content.GetType(), deserialized.GetType()); - } - - // Verify the array roundtrips - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionCallContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionCallContentArray); - Assert.NotNull(deserializedContents); - Assert.Equal(contents.Length, deserializedContents.Length); - for (int i = 0; i < deserializedContents.Length; i++) - { - Assert.NotNull(deserializedContents[i]); - Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); - } - } } 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 7bf2cdf9d6f..0709be7f9fd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -113,33 +113,4 @@ static void AssertSerializationRoundtrips(FunctionResultContent content) } } - [Fact] - public void Serialization_DerivedTypes_Roundtrips() - { - FunctionResultContent[] contents = - [ - new FunctionResultContent("call1", "result1"), - new McpServerToolResultContent("call2"), - ]; - - // Verify each element roundtrips individually - foreach (var content in contents) - { - var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); - Assert.NotNull(deserialized); - Assert.Equal(content.GetType(), deserialized.GetType()); - } - - // Verify the array roundtrips - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.FunctionResultContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.FunctionResultContentArray); - Assert.NotNull(deserializedContents); - Assert.Equal(contents.Length, deserializedContents.Length); - for (int i = 0; i < deserializedContents.Length; i++) - { - Assert.NotNull(deserializedContents[i]); - Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); - } - } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs index 76636a5e879..63b58a34456 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -34,8 +34,8 @@ public void Serialization_DerivedTypes_Roundtrips() { InputRequestContent[] contents = [ - new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), - new FunctionApprovalRequestContent("request456", new McpServerToolCallContent("call456", "myTool", "myServer")), + new ToolApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new ToolApprovalRequestContent("request456", new McpServerToolCallContent("call456", "myTool", "myServer")), ]; // Verify each element roundtrips individually @@ -67,3 +67,4 @@ public TestInputRequestContent(string requestId) } } } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs index 3a2b6411538..1ab1d2387ba 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -33,8 +33,8 @@ public void Serialization_DerivedTypes_Roundtrips() { InputResponseContent[] contents = [ - new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), - new FunctionApprovalResponseContent("request456", true, new McpServerToolCallContent("call456", "myTool", "myServer")), + new ToolApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), + new ToolApprovalResponseContent("request456", true, new McpServerToolCallContent("call456", "myTool", "myServer")), ]; // Verify each element roundtrips individually @@ -66,3 +66,4 @@ public TestInputResponseContent(string requestId) } } } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs index cc696618257..eb17c95cd43 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -22,7 +22,6 @@ public void Constructor_PropsDefault() Assert.Equal("toolName", c.Name); Assert.Null(c.ServerName); Assert.Null(c.Arguments); - Assert.True(c.InformationalOnly); } [Fact] @@ -45,10 +44,6 @@ public void Constructor_PropsRoundtrip() c.Arguments = args; Assert.Same(args, c.Arguments); - Assert.True(c.InformationalOnly); - c.InformationalOnly = false; - Assert.False(c.InformationalOnly); - Assert.Equal("callId1", c.CallId); Assert.Equal("toolName", c.Name); Assert.Equal("serverName", c.ServerName); @@ -73,7 +68,7 @@ public void Serialization_Roundtrips() }; AssertSerializationRoundtrips(content); - AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); static void AssertSerializationRoundtrips(McpServerToolCallContent content) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs index d71a1a6243f..12016b0f56f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -1,7 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Text.Json; using Xunit; @@ -16,7 +17,7 @@ public void Constructor_PropsDefault() Assert.Equal("callId", c.CallId); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.Result); + Assert.Null(c.Outputs); } [Fact] @@ -36,10 +37,10 @@ public void Constructor_PropsRoundtrip() Assert.Equal("callId", c.CallId); - Assert.Null(c.Result); - object result = "test result"; - c.Result = result; - Assert.Same(result, c.Result); + Assert.Null(c.Outputs); + IList outputs = [new TextContent("test result")]; + c.Outputs = outputs; + Assert.Same(outputs, c.Outputs); } [Fact] @@ -54,11 +55,11 @@ public void Serialization_Roundtrips() { var content = new McpServerToolResultContent("call123") { - Result = "result" + Outputs = [new TextContent("result")] }; AssertSerializationRoundtrips(content); - AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); static void AssertSerializationRoundtrips(McpServerToolResultContent content) @@ -70,7 +71,9 @@ static void AssertSerializationRoundtrips(McpServerToolResultContent content) Assert.NotNull(deserialized); var deserializedContent = Assert.IsType(deserialized); Assert.Equal(content.CallId, deserializedContent.CallId); - Assert.Equal("result", ((JsonElement)deserializedContent.Result!).GetString()); + Assert.NotNull(deserializedContent.Outputs); + Assert.Equal("result", Assert.IsType(Assert.Single(deserializedContent.Outputs)).Text); } } } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs similarity index 64% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs index 8028167d8ad..996ac404a79 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -8,18 +8,18 @@ namespace Microsoft.Extensions.AI.Contents; -public class FunctionApprovalRequestContentTests +public class ToolApprovalRequestContentTests { [Fact] public void Constructor_InvalidArguments_Throws() { FunctionCallContent functionCall = new("FCC1", "TestFunction"); - Assert.Throws("requestId", () => new FunctionApprovalRequestContent(null!, functionCall)); - Assert.Throws("requestId", () => new FunctionApprovalRequestContent("", functionCall)); - Assert.Throws("requestId", () => new FunctionApprovalRequestContent("\r\t\n ", functionCall)); + Assert.Throws("requestId", () => new ToolApprovalRequestContent(null!, functionCall)); + Assert.Throws("requestId", () => new ToolApprovalRequestContent("", functionCall)); + Assert.Throws("requestId", () => new ToolApprovalRequestContent("\r\t\n ", functionCall)); - Assert.Throws("functionCall", () => new FunctionApprovalRequestContent("id", null!)); + Assert.Throws("functionCall", () => new ToolApprovalRequestContent("id", (FunctionCallContent)null!)); } [Theory] @@ -30,10 +30,10 @@ public void Constructor_Roundtrips(string id) { FunctionCallContent functionCall = new("FCC1", "TestFunction"); - FunctionApprovalRequestContent content = new(id, functionCall); + ToolApprovalRequestContent content = new(id, functionCall); Assert.Same(id, content.RequestId); - Assert.Same(functionCall, content.FunctionCall); + Assert.Same(functionCall, content.ToolCall); } [Theory] @@ -44,14 +44,14 @@ public void CreateResponse_ReturnsExpectedResponse(bool approved) string id = "req-1"; FunctionCallContent functionCall = new("FCC1", "TestFunction"); - FunctionApprovalRequestContent content = new(id, functionCall); + ToolApprovalRequestContent content = new(id, functionCall); var response = content.CreateResponse(approved); Assert.NotNull(response); Assert.Same(id, response.RequestId); Assert.Equal(approved, response.Approved); - Assert.Same(functionCall, response.FunctionCall); + Assert.Same(functionCall, response.ToolCall); Assert.Null(response.Reason); } @@ -65,38 +65,39 @@ public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, str string id = "req-1"; FunctionCallContent functionCall = new("FCC1", "TestFunction"); - FunctionApprovalRequestContent content = new(id, functionCall); + ToolApprovalRequestContent content = new(id, functionCall); var response = content.CreateResponse(approved, reason); Assert.NotNull(response); Assert.Same(id, response.RequestId); Assert.Equal(approved, response.Approved); - Assert.Same(functionCall, response.FunctionCall); + Assert.Same(functionCall, response.ToolCall); Assert.Equal(reason, response.Reason); } [Fact] public void Serialization_Roundtrips() { - var content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })); + var content = new ToolApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })); - AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); - static void AssertSerializationRoundtrips(FunctionApprovalRequestContent content) + static void AssertSerializationRoundtrips(ToolApprovalRequestContent content) where T : AIContent { T contentAsT = (T)(object)content; string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserialized); - var deserializedContent = Assert.IsType(deserialized); + var deserializedContent = Assert.IsType(deserialized); Assert.Equal(content.RequestId, deserializedContent.RequestId); - Assert.NotNull(deserializedContent.FunctionCall); - Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId); - Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name); + Assert.NotNull(deserializedContent.ToolCall); + var functionCall = Assert.IsType(deserializedContent.ToolCall); + Assert.Equal(content.ToolCall.CallId, functionCall.CallId); + Assert.Equal(((FunctionCallContent)content.ToolCall).Name, functionCall.Name); } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs similarity index 56% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs index 036ef83b65a..b536030135d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -7,18 +7,18 @@ namespace Microsoft.Extensions.AI.Contents; -public class FunctionApprovalResponseContentTests +public class ToolApprovalResponseContentTests { [Fact] public void Constructor_InvalidArguments_Throws() { FunctionCallContent functionCall = new("FCC1", "TestFunction"); - Assert.Throws("requestId", () => new FunctionApprovalResponseContent(null!, true, functionCall)); - Assert.Throws("requestId", () => new FunctionApprovalResponseContent("", true, functionCall)); - Assert.Throws("requestId", () => new FunctionApprovalResponseContent("\r\t\n ", true, functionCall)); + Assert.Throws("requestId", () => new ToolApprovalResponseContent(null!, true, functionCall)); + Assert.Throws("requestId", () => new ToolApprovalResponseContent("", true, functionCall)); + Assert.Throws("requestId", () => new ToolApprovalResponseContent("\r\t\n ", true, functionCall)); - Assert.Throws("functionCall", () => new FunctionApprovalResponseContent("id", true, null!)); + Assert.Throws("functionCall", () => new ToolApprovalResponseContent("id", true, (FunctionCallContent)null!)); } [Theory] @@ -28,39 +28,40 @@ public void Constructor_InvalidArguments_Throws() public void Constructor_Roundtrips(string id, bool approved) { FunctionCallContent functionCall = new("FCC1", "TestFunction"); - FunctionApprovalResponseContent content = new(id, approved, functionCall); + ToolApprovalResponseContent content = new(id, approved, functionCall); Assert.Same(id, content.RequestId); Assert.Equal(approved, content.Approved); - Assert.Same(functionCall, content.FunctionCall); + Assert.Same(functionCall, content.ToolCall); } [Fact] public void Serialization_Roundtrips() { - var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) + var content = new ToolApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) { Reason = "Approved for testing" }; - AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); - static void AssertSerializationRoundtrips(FunctionApprovalResponseContent content) + static void AssertSerializationRoundtrips(ToolApprovalResponseContent content) where T : AIContent { T contentAsT = (T)(object)content; string json = JsonSerializer.Serialize(contentAsT, AIJsonUtilities.DefaultOptions); T? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserialized); - var deserializedContent = Assert.IsType(deserialized); + var deserializedContent = Assert.IsType(deserialized); Assert.Equal(content.RequestId, deserializedContent.RequestId); Assert.Equal(content.Approved, deserializedContent.Approved); Assert.Equal(content.Reason, deserializedContent.Reason); - Assert.NotNull(deserializedContent.FunctionCall); - Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId); - Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name); + Assert.NotNull(deserializedContent.ToolCall); + var functionCall = Assert.IsType(deserializedContent.ToolCall); + Assert.Equal(content.ToolCall.CallId, functionCall.CallId); + Assert.Equal(((FunctionCallContent)content.ToolCall).Name, functionCall.Name); } } @@ -69,20 +70,21 @@ static void AssertSerializationRoundtrips(FunctionApprovalResponseContent con [InlineData("Custom rejection reason")] public void Serialization_WithReason_Roundtrips(string? reason) { - var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) + var content = new ToolApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) { Reason = reason }; var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedContent); Assert.Equal(content.RequestId, deserializedContent.RequestId); Assert.Equal(content.Approved, deserializedContent.Approved); Assert.Equal(content.Reason, deserializedContent.Reason); - Assert.NotNull(deserializedContent.FunctionCall); - Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId); - Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name); + Assert.NotNull(deserializedContent.ToolCall); + var functionCall = Assert.IsType(deserializedContent.ToolCall); + Assert.Equal(content.ToolCall.CallId, functionCall.CallId); + Assert.Equal(((FunctionCallContent)content.ToolCall).Name, functionCall.Name); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs new file mode 100644 index 00000000000..8f8e065216a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ToolCallContentTests +{ + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + ToolCallContent[] contents = + [ + new FunctionCallContent("call1", "function1", new Dictionary { { "param1", 123 } }), + new McpServerToolCallContent("call2", "myTool", "myServer"), + ]; + + // Verify each element roundtrips individually + foreach (var content in contents) + { + var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + Assert.Equal(content.GetType(), deserialized.GetType()); + } + + // Verify the array roundtrips + var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.ToolCallContentArray); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.ToolCallContentArray); + Assert.NotNull(deserializedContents); + Assert.Equal(contents.Length, deserializedContents.Length); + for (int i = 0; i < deserializedContents.Length; i++) + { + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs new file mode 100644 index 00000000000..6cc75bf00b3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ToolResultContentTests +{ + [Fact] + public void Serialization_DerivedTypes_Roundtrips() + { + ToolResultContent[] contents = + [ + new FunctionResultContent("call1", "result1"), + new McpServerToolResultContent("call2"), + ]; + + // Verify each element roundtrips individually + foreach (var content in contents) + { + var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized); + Assert.Equal(content.GetType(), deserialized.GetType()); + } + + // Verify the array roundtrips + var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.ToolResultContentArray); + var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.ToolResultContentArray); + Assert.NotNull(deserializedContents); + Assert.Equal(contents.Length, deserializedContents.Length); + for (int i = 0; i < deserializedContents.Length; i++) + { + Assert.NotNull(deserializedContents[i]); + Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index 0437c47eb1c..65e175ceb90 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -43,6 +43,8 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(InputResponseContent[]))] [JsonSerializable(typeof(FunctionCallContent[]))] [JsonSerializable(typeof(FunctionResultContent[]))] +[JsonSerializable(typeof(ToolCallContent[]))] +[JsonSerializable(typeof(ToolResultContent[]))] [JsonSerializable(typeof(ReasoningOptions))] [JsonSerializable(typeof(ReasoningEffort))] [JsonSerializable(typeof(ReasoningOutput))] diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 3daf4796bad..0afdd6936a1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -937,19 +937,19 @@ public async Task AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpda Assert.NotNull(mcpToolResultContent.RawRepresentation); Assert.Same(mcpToolCall, mcpToolResultContent.RawRepresentation); - // Third update should be FunctionApprovalRequestContent with McpServerToolCallContent - FunctionApprovalRequestContent? approvalRequest = updates[2].Contents.OfType().FirstOrDefault(); + // Third update should be ToolApprovalRequestContent with McpServerToolCallContent + ToolApprovalRequestContent? approvalRequest = updates[2].Contents.OfType().FirstOrDefault(); Assert.NotNull(approvalRequest); Assert.Equal("mcpr_123", approvalRequest.RequestId); Assert.NotNull(approvalRequest.RawRepresentation); Assert.Same(mcpApprovalRequest, approvalRequest.RawRepresentation); - McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequest.FunctionCall); + McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequest.ToolCall); Assert.Equal("ask_question", nestedMcpCall.Name); Assert.Equal("deepwiki", nestedMcpCall.ServerName); - // Fourth update should be FunctionApprovalResponseContent correlated with request - FunctionApprovalResponseContent? approvalResponse = updates[3].Contents.OfType().FirstOrDefault(); + // Fourth update should be ToolApprovalResponseContent correlated with request + ToolApprovalResponseContent? approvalResponse = updates[3].Contents.OfType().FirstOrDefault(); Assert.NotNull(approvalResponse); Assert.Equal("mcpr_123", approvalResponse.RequestId); Assert.True(approvalResponse.Approved); @@ -957,7 +957,7 @@ public async Task AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpda Assert.Same(mcpApprovalResponse, approvalResponse.RawRepresentation); // The correlated FunctionCall should be McpServerToolCallContent with tool details from the request - McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponse.FunctionCall); + McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponse.ToolCall); Assert.Equal("mcpr_123", correlatedMcpCall.CallId); Assert.Equal("ask_question", correlatedMcpCall.Name); Assert.Equal("deepwiki", correlatedMcpCall.ServerName); @@ -989,13 +989,13 @@ public async Task AsChatResponseUpdatesAsync_McpToolCallApprovalResponseItem_Wit Assert.Single(updates); - // Should NOT have a FunctionApprovalResponseContent since there was no correlated request - Assert.Empty(updates[0].Contents.OfType()); + // Should NOT have a ToolApprovalResponseContent since there was no correlated request + Assert.Empty(updates[0].Contents.OfType()); // Should have a generic AIContent with RawRepresentation set to the response item AIContent? genericContent = updates[0].Contents.FirstOrDefault(c => c.RawRepresentation == mcpApprovalResponse); Assert.NotNull(genericContent); - Assert.IsNotType(genericContent); + Assert.IsNotType(genericContent); Assert.Same(mcpApprovalResponse, genericContent.RawRepresentation); async IAsyncEnumerable CreateStreamingUpdates() @@ -1174,22 +1174,22 @@ public void AsChatMessages_FromResponseItems_AllContentTypes_SetsRawRepresentati Assert.NotNull(mcpToolResult.RawRepresentation); Assert.Same(mcpToolCallItem, mcpToolResult.RawRepresentation); - // 6. McpToolCallApprovalRequestItem -> FunctionApprovalRequestContent - FunctionApprovalRequestContent? approvalRequestContent = message.Contents.OfType().FirstOrDefault(); + // 6. McpToolCallApprovalRequestItem -> ToolApprovalRequestContent + ToolApprovalRequestContent? approvalRequestContent = message.Contents.OfType().FirstOrDefault(); Assert.NotNull(approvalRequestContent); Assert.Equal("mcpr_123", approvalRequestContent.RequestId); Assert.NotNull(approvalRequestContent.RawRepresentation); Assert.Same(mcpApprovalRequestItem, approvalRequestContent.RawRepresentation); // The nested FunctionCall should be McpServerToolCallContent - McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequestContent.FunctionCall); + McpServerToolCallContent nestedMcpCall = Assert.IsType(approvalRequestContent.ToolCall); Assert.Equal("ask_question", nestedMcpCall.Name); Assert.Equal("deepwiki", nestedMcpCall.ServerName); Assert.NotNull(nestedMcpCall.RawRepresentation); Assert.Same(mcpApprovalRequestItem, nestedMcpCall.RawRepresentation); - // 7. McpToolCallApprovalResponseItem -> FunctionApprovalResponseContent (correlated with request) - FunctionApprovalResponseContent? approvalResponseContent = message.Contents.OfType().FirstOrDefault(); + // 7. McpToolCallApprovalResponseItem -> ToolApprovalResponseContent (correlated with request) + ToolApprovalResponseContent? approvalResponseContent = message.Contents.OfType().FirstOrDefault(); Assert.NotNull(approvalResponseContent); Assert.Equal("mcpr_123", approvalResponseContent.RequestId); Assert.True(approvalResponseContent.Approved); @@ -1197,7 +1197,7 @@ public void AsChatMessages_FromResponseItems_AllContentTypes_SetsRawRepresentati Assert.Same(mcpApprovalResponseItem, approvalResponseContent.RawRepresentation); // The correlated FunctionCall should be McpServerToolCallContent with tool details from the request - McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponseContent.FunctionCall); + McpServerToolCallContent correlatedMcpCall = Assert.IsType(approvalResponseContent.ToolCall); Assert.Equal("mcpr_123", correlatedMcpCall.CallId); Assert.Equal("ask_question", correlatedMcpCall.Name); Assert.Equal("deepwiki", correlatedMcpCall.ServerName); @@ -1221,13 +1221,13 @@ public void AsChatMessages_McpToolCallApprovalResponseItem_WithoutCorrelatedRequ Assert.Single(messages); ChatMessage message = messages[0]; - // Should NOT have a FunctionApprovalResponseContent since there was no correlated request - Assert.Empty(message.Contents.OfType()); + // Should NOT have a ToolApprovalResponseContent since there was no correlated request + Assert.Empty(message.Contents.OfType()); // Should have a generic AIContent with RawRepresentation set to the response item AIContent? genericContent = message.Contents.FirstOrDefault(c => c.RawRepresentation == mcpApprovalResponseItem); Assert.NotNull(genericContent); - Assert.IsNotType(genericContent); + Assert.IsNotType(genericContent); Assert.Same(mcpApprovalResponseItem, genericContent.RawRepresentation); } @@ -1237,7 +1237,7 @@ public void AsChatMessages_McpToolCallApprovalResponseItem_WithoutCorrelatedRequ public void AsOpenAIResponseItems_McpServerToolContents_RoundtripsToolOutputAndError(bool isError) { var mcpToolCall = new McpServerToolCallContent("call_123", "get_weather", "weather_server") { Arguments = new Dictionary { ["city"] = "Seattle" } }; - var mcpToolResult = new McpServerToolResultContent("call_123") { Result = isError ? new ErrorContent("error") : new TextContent("sunny") }; + var mcpToolResult = new McpServerToolResultContent("call_123") { Outputs = isError ? [new ErrorContent("error")] : [new TextContent("sunny")] }; var items = new ChatMessage[] { new(ChatRole.Assistant, [mcpToolCall, mcpToolResult]) }.AsOpenAIResponseItems().ToArray(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index e1965468cde..c9faeaebd52 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -149,7 +149,7 @@ await client.GetStreamingResponseAsync(Prompt, chatOptions).ToChatResponseAsync( Assert.NotNull(response); Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType()); Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType()); - Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType()); + Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType()); Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text); } @@ -203,7 +203,7 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() var approvalResponse = new ChatMessage(ChatRole.Tool, response.Messages .SelectMany(m => m.Contents) - .OfType() + .OfType() .Select(c => c.CreateResponse(true)) .ToArray()); if (approvalResponse.Contents.Count == 0) @@ -412,8 +412,8 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() if (approval) { input.AddRange(response.Messages); - var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - var mcpCallContent = Assert.IsType(approvalRequest.FunctionCall); + var approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); + var mcpCallContent = Assert.IsType(approvalRequest.ToolCall); Assert.Equal("search_events", mcpCallContent.Name); input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)])); @@ -427,7 +427,7 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() Assert.Equal("search_events", toolCall.Name); var toolResult = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); - var content = Assert.IsType(toolResult.Result); + var content = Assert.IsType(Assert.Single(toolResult.Outputs!)); Assert.Equal(@"{""events"": [], ""next_page_token"": null}", content.Text); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 6193d58b291..52823c80a5e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1396,7 +1396,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) { Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))] }; - FunctionApprovalRequestContent approvalRequest; + ToolApprovalRequestContent approvalRequest; using (VerbatimHttpHandler handler = new(input, output)) using (HttpClient httpClient = new(handler)) @@ -1406,7 +1406,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions); - approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); + approvalRequest = Assert.Single(response.Messages.SelectMany(m => m.Contents).OfType()); chatOptions.ConversationId = response.ConversationId; } @@ -1551,7 +1551,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) var result = Assert.IsType(message.Contents[1]); Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(result.Result).Text); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(result.Outputs!)).Text); Assert.NotNull(response.Usage); Assert.Equal(542, response.Usage.InputTokenCount); @@ -1804,7 +1804,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result).Text); + Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Outputs!)).Text); var secondCall = Assert.IsType(message.Contents[3]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.CallId); @@ -1816,7 +1816,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId); - Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(secondResult.Result).Text); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Outputs!)).Text); Assert.NotNull(response.Usage); Assert.Equal(1329, response.Usage.InputTokenCount); @@ -2215,7 +2215,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var firstResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId); - Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(firstResult.Result).Text); + Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Outputs!)).Text); var secondCall = Assert.IsType(message.Contents[3]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.CallId); @@ -2227,7 +2227,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() var secondResult = Assert.IsType(message.Contents[4]); Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId); - Assert.StartsWith("The path to the `README.md` file", Assert.IsType(secondResult.Result).Text); + Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Outputs!)).Text); Assert.NotNull(response.Usage); Assert.Equal(1420, response.Usage.InputTokenCount); @@ -2423,7 +2423,7 @@ public async Task McpToolCall_ErrorResponse_NonStreaming(bool rawTool) var toolResult = Assert.IsType(message.Contents[2]); Assert.Equal("mcp_689023b0fa88819f99f48aff343d5ad50475557f6fefb5f0", toolResult.CallId); - var errorContent = Assert.IsType(toolResult.Result); + var errorContent = Assert.IsType(Assert.Single(toolResult.Outputs!)); Assert.Contains("An error occurred invoking 'test_error'.", errorContent.Message); Assert.NotNull(response.Usage); @@ -5915,12 +5915,12 @@ public async Task HostedImageGenerationTool_NonStreaming() // First content should be the tool call var toolCall = contents[0] as ImageGenerationToolCallContent; Assert.NotNull(toolCall); - Assert.Equal("img_call_abc123", toolCall.ImageId); + Assert.Equal("img_call_abc123", toolCall.CallId); // Second content should be the result with image data var toolResult = contents[1] as ImageGenerationToolResultContent; Assert.NotNull(toolResult); - Assert.Equal("img_call_abc123", toolResult.ImageId); + Assert.Equal("img_call_abc123", toolResult.CallId); Assert.Single(toolResult.Outputs!); var imageData = toolResult.Outputs![0] as DataContent; @@ -6021,7 +6021,7 @@ public async Task HostedImageGenerationTool_Streaming() u.Contents != null && u.Contents.Any(c => c is ImageGenerationToolCallContent)); Assert.NotNull(toolCallUpdate); var toolCall = toolCallUpdate.Contents.OfType().First(); - Assert.Equal("img_call_def456", toolCall.ImageId); + Assert.Equal("img_call_def456", toolCall.CallId); // Should have partial image content var partialImageUpdate = updates.FirstOrDefault(u => @@ -6180,7 +6180,7 @@ static bool HasCorrectImageData(AIContent o, int index) u.Contents != null && u.Contents.Any(c => c is ImageGenerationToolCallContent)); Assert.NotNull(toolCallUpdate); var toolCall = toolCallUpdate.Contents.OfType().First(); - Assert.Equal("img_call_ghi789", toolCall.ImageId); + Assert.Equal("img_call_ghi789", toolCall.CallId); } [Theory] @@ -6391,3 +6391,4 @@ public override ReadOnlyMemory ToBytes() } } } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index 120646134c7..ec5c80c9234 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -45,8 +45,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAllRequireApprovalAsy [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) ]; @@ -81,8 +81,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAnyRequireApprovalAsy [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) ]; @@ -125,8 +125,8 @@ public async Task AllFunctionCallsReplacedWithApprovalsWhenAnyRequestOrAdditiona [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) ]; @@ -152,13 +152,13 @@ public async Task ApprovedApprovalResponsesAreExecutedAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -204,13 +204,13 @@ public async Task ApprovedApprovalResponsesAreGroupedWhenMessageIdIsNullAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), // Note: No MessageId set - this is the bug trigger new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -256,19 +256,19 @@ public async Task ApprovedApprovalResponsesFromSeparateFCCMessagesAreExecutedAsy new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp2" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -315,13 +315,13 @@ public async Task RejectedApprovalResponsesAreFailedAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -374,13 +374,13 @@ public async Task MixedApprovedAndRejectedApprovalResponsesAreExecutedAndFailedA new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -438,16 +438,16 @@ public async Task RejectedApprovalResponsesWithCustomReasonAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) + new ToolApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied permission for this operation" }, - new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) { Reason = "Function Func2 is not allowed at this time" } @@ -504,15 +504,15 @@ public async Task MixedApprovalResponsesWithCustomAndDefaultReasonsAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), - new FunctionApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), + new ToolApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "Custom rejection for Func1" }, - new FunctionApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), - new FunctionApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) + new ToolApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "Custom rejection for Func1" }, + new ToolApprovalResponseContent("ficc_callId2", false, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })), + new ToolApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func3", arguments: new Dictionary { { "s", "test" } })) ]), ]; @@ -596,11 +596,11 @@ public async Task RejectedApprovalResponsesWithEmptyOrWhitespaceReasonUsesDefaul new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) + new ToolApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = reason }, @@ -654,13 +654,13 @@ public async Task ApprovedInputsAreExecutedAndFunctionResultsAreConvertedAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -682,7 +682,7 @@ public async Task ApprovedInputsAreExecutedAndFunctionResultsAreConvertedAsync() new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1"), new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 3 } })) + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 3 } })) ]), ]; @@ -708,23 +708,23 @@ public async Task AlreadyExecutedApprovalsAreIgnoredAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId2", new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1"), new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func1")), + new ToolApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func1")), ]) { MessageId = "resp2" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func1")), + new ToolApprovalResponseContent("ficc_callId3", true, new FunctionCallContent("callId3", "Func1")), ]), ]; @@ -834,17 +834,17 @@ public async Task ApprovalRequestWithoutApprovalResponseThrowsAsync() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, ]; var invokeException = await Assert.ThrowsAsync( async () => await InvokeAndAssertAsync(options, input, [], [], [])); - Assert.Equal("FunctionApprovalRequestContent found with FunctionCall.CallId(s) 'callId1' that have no matching FunctionApprovalResponseContent.", invokeException.Message); + Assert.Equal("ToolApprovalRequestContent found with FunctionCall.CallId(s) 'callId1' that have no matching ToolApprovalResponseContent.", invokeException.Message); var invokeStreamingException = await Assert.ThrowsAsync( async () => await InvokeAndAssertStreamingAsync(options, input, [], [], [])); - Assert.Equal("FunctionApprovalRequestContent found with FunctionCall.CallId(s) 'callId1' that have no matching FunctionApprovalResponseContent.", invokeStreamingException.Message); + Assert.Equal("ToolApprovalRequestContent found with FunctionCall.CallId(s) 'callId1' that have no matching ToolApprovalResponseContent.", invokeStreamingException.Message); } [Fact] @@ -864,8 +864,8 @@ public async Task ApprovedApprovalResponsesWithoutApprovalRequestAreExecutedAsyn new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -910,8 +910,8 @@ public async Task FunctionCallContentIsNotPassedToDownstreamServiceWithServiceTh [ new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")), + new ToolApprovalResponseContent("ficc_callId2", true, new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })) ]), ]; @@ -1084,25 +1084,25 @@ async IAsyncEnumerable YieldInnerClientUpdates( Assert.Equal(2, updateYieldCount); break; case 2: - var approvalRequest1 = update.Contents.OfType().First(); - Assert.Equal("callId1", approvalRequest1.FunctionCall.CallId); - Assert.Equal("Func1", approvalRequest1.FunctionCall.Name); + var approvalRequest1 = update.Contents.OfType().First(); + Assert.Equal("callId1", approvalRequest1.ToolCall.CallId); + Assert.Equal("Func1", ((FunctionCallContent)approvalRequest1.ToolCall).Name); // Third content should have been buffered, since we have not yet encountered a function call that requires approval. Assert.Equal(4, updateYieldCount); break; case 3: - var approvalRequest2 = update.Contents.OfType().First(); - Assert.Equal("callId2", approvalRequest2.FunctionCall.CallId); - Assert.Equal("Func2", approvalRequest2.FunctionCall.Name); + var approvalRequest2 = update.Contents.OfType().First(); + Assert.Equal("callId2", approvalRequest2.ToolCall.CallId); + Assert.Equal("Func2", ((FunctionCallContent)approvalRequest2.ToolCall).Name); // Fourth content can be yielded immediately, since it is the first function call that requires approval. Assert.Equal(4, updateYieldCount); break; case 4: - var approvalRequest3 = update.Contents.OfType().First(); - Assert.Equal("callId1", approvalRequest3.FunctionCall.CallId); - Assert.Equal("Func3", approvalRequest3.FunctionCall.Name); + var approvalRequest3 = update.Contents.OfType().First(); + Assert.Equal("callId1", approvalRequest3.ToolCall.CallId); + Assert.Equal("Func3", ((FunctionCallContent)approvalRequest3.ToolCall).Name); // Fifth content can be yielded immediately, since we previously encountered a function call that requires approval. Assert.Equal(5, updateYieldCount); @@ -1137,7 +1137,7 @@ public async Task FunctionCallsWithInformationalOnlyTrueAreNotReplacedWithApprov new ChatMessage(ChatRole.Assistant, [alreadyProcessedFunctionCall]), ]; - // Expected output should contain the same FunctionCallContent, not a FunctionApprovalRequestContent + // Expected output should contain the same FunctionCallContent, not a ToolApprovalRequestContent List expectedOutput = [ new ChatMessage(ChatRole.Assistant, [alreadyProcessedFunctionCall]), @@ -1156,6 +1156,55 @@ public async Task FunctionCallsWithInformationalOnlyTrueAreNotReplacedWithApprov Assert.Equal(0, functionInvokedCount); } + [Fact] + public async Task ApprovalResponsePreservesOriginalRequestMessageMetadata() + { + var options = new ChatOptions + { + Tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func1")), + ] + }; + + const string OriginalMessageId = "original-message-id"; + + // Create input with approval request containing a known MessageId on the containing message + List input = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new ToolApprovalRequestContent("approval-request-id", new FunctionCallContent("function-call-id", "Func1")) + ]) { MessageId = OriginalMessageId }, // This MessageId should be preserved + new ChatMessage(ChatRole.User, + [ + new ToolApprovalResponseContent("approval-request-id", true, new FunctionCallContent("function-call-id", "Func1")) + ]), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, "world"), + ]; + + // The reconstructed function call message should preserve the original MessageId + List expectedOutput = + [ + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("function-call-id", "Func1")]) { MessageId = OriginalMessageId }, + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("function-call-id", result: "Result 1")]), + new ChatMessage(ChatRole.Assistant, "world"), + ]; + + var actualOutput = await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput); + + // Verify that the reconstructed function call message has the original MessageId, not a synthetic one + Assert.Equal(OriginalMessageId, actualOutput[0].MessageId); + + actualOutput = await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, expectedOutput); + Assert.Equal(OriginalMessageId, actualOutput[0].MessageId); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -1182,7 +1231,7 @@ public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(boo new ChatMessage(ChatRole.Assistant, [ new FunctionCallContent("callId1", "Func"), - new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) ]; @@ -1190,8 +1239,8 @@ public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(boo [ new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), - new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) ]; @@ -1220,13 +1269,13 @@ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(b new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), - new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func")), - new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func")), + new ToolApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), ]; @@ -1235,11 +1284,11 @@ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(b new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), new ChatMessage(ChatRole.Assistant, [ @@ -1254,7 +1303,7 @@ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(b List downstreamClientOutput = [ new ChatMessage(ChatRole.Assistant, [ - new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }, + new McpServerToolResultContent("callId2") { Outputs = [new TextContent("Result 2")] }, new TextContent("world") ]) ]; @@ -1270,7 +1319,7 @@ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(b new FunctionResultContent("callId1", result: "Result 1") ]), new ChatMessage(ChatRole.Assistant, [ - new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }, + new McpServerToolResultContent("callId2") { Outputs = [new TextContent("Result 2")] }, new TextContent("world") ]) ]; @@ -1304,13 +1353,13 @@ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAd new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), - new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), - new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalResponseContent("ficc_callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), + new ToolApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), ]; @@ -1318,11 +1367,11 @@ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAd new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + new ToolApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) ]), new ChatMessage(ChatRole.Assistant, [ @@ -1341,7 +1390,7 @@ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAd new ChatMessage(ChatRole.Assistant, [ new TextContent("world"), .. approveMcpCall ? - [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] : + [new McpServerToolResultContent("callId2") { Outputs = [new TextContent("Result 2")] }] : Array.Empty() ]) ]; @@ -1360,7 +1409,7 @@ .. approveMcpCall ? new ChatMessage(ChatRole.Assistant, [ new TextContent("world"), .. approveMcpCall ? - [new McpServerToolResultContent("callId2") { Result = new List { new TextContent("Result 2") } }] : + [new McpServerToolResultContent("callId2") { Outputs = [new TextContent("Result 2")] }] : Array.Empty() ]) ]; @@ -1369,55 +1418,6 @@ .. approveMcpCall ? await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput, additionalTools: useAdditionalTools ? tools : null); } - [Fact] - public async Task ApprovalResponsePreservesOriginalRequestMessageMetadata() - { - var options = new ChatOptions - { - Tools = - [ - new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func1")), - ] - }; - - const string OriginalMessageId = "original-message-id"; - - // Create input with approval request containing a known MessageId on the containing message - List input = - [ - new ChatMessage(ChatRole.User, "hello"), - new ChatMessage(ChatRole.Assistant, - [ - new FunctionApprovalRequestContent("approval-request-id", new FunctionCallContent("function-call-id", "Func1")) - ]) { MessageId = OriginalMessageId }, // This MessageId should be preserved - new ChatMessage(ChatRole.User, - [ - new FunctionApprovalResponseContent("approval-request-id", true, new FunctionCallContent("function-call-id", "Func1")) - ]), - ]; - - List downstreamClientOutput = - [ - new ChatMessage(ChatRole.Assistant, "world"), - ]; - - // The reconstructed function call message should preserve the original MessageId - List expectedOutput = - [ - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("function-call-id", "Func1")]) { MessageId = OriginalMessageId }, - new ChatMessage(ChatRole.Tool, [new FunctionResultContent("function-call-id", result: "Result 1")]), - new ChatMessage(ChatRole.Assistant, "world"), - ]; - - var actualOutput = await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput); - - // Verify that the reconstructed function call message has the original MessageId, not a synthetic one - Assert.Equal(OriginalMessageId, actualOutput[0].MessageId); - - actualOutput = await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, expectedOutput); - Assert.Equal(OriginalMessageId, actualOutput[0].MessageId); - } - private static Task> InvokeAndAssertAsync( ChatOptions? options, List input, @@ -1584,11 +1584,26 @@ private static List CloneInput(List input) => McpServerToolCallContent mstcc => new McpServerToolCallContent(mstcc.CallId, mstcc.Name, mstcc.ServerName) { Arguments = mstcc.Arguments, - InformationalOnly = mstcc.InformationalOnly }, - FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InformationalOnly = fcc.InformationalOnly }, - FunctionApprovalRequestContent farc => new FunctionApprovalRequestContent(farc.RequestId, (FunctionCallContent)CloneFcc(farc.FunctionCall)), - FunctionApprovalResponseContent farc => new FunctionApprovalResponseContent(farc.RequestId, farc.Approved, (FunctionCallContent)CloneFcc(farc.FunctionCall)) { Reason = farc.Reason }, + FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) + { + InformationalOnly = fcc.InformationalOnly + }, + ToolApprovalRequestContent farc when farc.ToolCall is McpServerToolCallContent => + new ToolApprovalRequestContent(farc.RequestId, (McpServerToolCallContent)CloneFcc(farc.ToolCall)), + ToolApprovalRequestContent farc => + new ToolApprovalRequestContent(farc.RequestId, (FunctionCallContent)CloneFcc(farc.ToolCall)), + ToolApprovalResponseContent farc when farc.ToolCall is McpServerToolCallContent => + new ToolApprovalResponseContent(farc.RequestId, farc.Approved, (McpServerToolCallContent)CloneFcc(farc.ToolCall)) + { + Reason = farc.Reason + }, + ToolApprovalResponseContent farc => + new ToolApprovalResponseContent(farc.RequestId, farc.Approved, (FunctionCallContent)CloneFcc(farc.ToolCall)) + { + Reason = farc.Reason + }, _ => c }; } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 05ab5f1cfc9..03f00e8c13c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -403,7 +403,7 @@ public async Task FunctionReturningFunctionResultContentWithMatchingCallId_UsesI { FunctionInvoker = (ctx, cancellationToken) => { - returnedFrc = new FunctionResultContent(ctx.CallContent.CallId, "Custom result from function") + returnedFrc = new FunctionResultContent(ctx.CallContent.CallId!, "Custom result from function") { RawRepresentation = "CustomRaw" }; @@ -566,7 +566,7 @@ public async Task FunctionReturningDerivedFunctionResultContent_PropagatesInstan FunctionInvoker = (ctx, cancellationToken) => { // Return a derived FunctionResultContent - returnedFrc = new DerivedFunctionResultContent(ctx.CallContent.CallId, "Derived result") + returnedFrc = new DerivedFunctionResultContent(ctx.CallContent.CallId!, "Derived result") { CustomProperty = "CustomValue" }; @@ -2258,7 +2258,7 @@ private static List CloneContents(IList contents) // Clone FunctionCallContent to avoid sharing InformationalOnly state if (content is FunctionCallContent fcc) { - cloned.Add(new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) + cloned.Add(new FunctionCallContent(fcc.CallId!, fcc.Name, fcc.Arguments) { InformationalOnly = fcc.InformationalOnly, Exception = fcc.Exception, @@ -3045,7 +3045,7 @@ public async Task RespectsChatOptionsToolsModificationsByFunctionTool_AddApprova var result = await client.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "test")], options).ToChatResponseAsync(); // FunctionB should have been converted to an approval request (not executed) - Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => frc.FunctionCall.Name == "FunctionB")); + Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => ((FunctionCallContent)frc.ToolCall).Name == "FunctionB")); // And FunctionA should have been executed Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => frc.Result?.ToString() == "FunctionA result")); @@ -3055,7 +3055,7 @@ public async Task RespectsChatOptionsToolsModificationsByFunctionTool_AddApprova var result = await client.GetResponseAsync([new ChatMessage(ChatRole.User, "test")], options); // FunctionB should have been converted to an approval request (not executed) - Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => frc.FunctionCall.Name == "FunctionB")); + Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => ((FunctionCallContent)frc.ToolCall).Name == "FunctionB")); // And FunctionA should have been executed Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => frc.Result?.ToString() == "FunctionA result")); @@ -3151,7 +3151,7 @@ public async Task RespectsChatOptionsToolsModificationsByFunctionTool_ReplaceWit var result = await client.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "test")], options).ToChatResponseAsync(); // FunctionB should have been converted to an approval request (not executed) - Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => frc.FunctionCall.Name == "FunctionB")); + Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => ((FunctionCallContent)frc.ToolCall).Name == "FunctionB")); // Original FunctionB result should NOT be present Assert.DoesNotContain(result.Messages, m => m.Contents.OfType() @@ -3162,7 +3162,7 @@ public async Task RespectsChatOptionsToolsModificationsByFunctionTool_ReplaceWit var result = await client.GetResponseAsync([new ChatMessage(ChatRole.User, "test")], options); // FunctionB should have been converted to an approval request (not executed) - Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => frc.FunctionCall.Name == "FunctionB")); + Assert.Contains(result.Messages, m => m.Contents.OfType().Any(frc => ((FunctionCallContent)frc.ToolCall).Name == "FunctionB")); // Original FunctionB result should NOT be present Assert.DoesNotContain(result.Messages, m => m.Contents.OfType() @@ -3291,7 +3291,7 @@ public async Task LogsFunctionRequiresApproval() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]) ]; @@ -3327,11 +3327,11 @@ public async Task LogsProcessingApprovalResponse() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")) + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func1")) ]) }; @@ -3364,11 +3364,11 @@ public async Task LogsFunctionRejected() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied" } + new ToolApprovalResponseContent("ficc_callId1", false, new FunctionCallContent("callId1", "Func1")) { Reason = "User denied" } ]) }; @@ -3383,3 +3383,4 @@ public async Task LogsFunctionRejected() // the threshold condition. The logging call is at line 1078 and will execute // when MaximumConsecutiveErrorsPerRequest is exceeded. } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ImageGeneratingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ImageGeneratingChatClientTests.cs index a70d19abffc..0571b06d9fc 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ImageGeneratingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/ImageGeneratingChatClientTests.cs @@ -337,7 +337,7 @@ public async Task GetResponseAsync_WithFunctionCallContent_ReplacesWithImageGene Assert.Single(message.Contents); var imageToolCallContent = Assert.IsType(message.Contents[0]); - Assert.Equal(callId, imageToolCallContent.ImageId); + Assert.Equal(callId, imageToolCallContent.CallId); } [Fact] @@ -381,6 +381,6 @@ async IAsyncEnumerable GetUpdatesAsync() Assert.Single(update.Contents); var imageToolCallContent = Assert.IsType(update.Contents[0]); - Assert.Equal(callId, imageToolCallContent.ImageId); + Assert.Equal(callId, imageToolCallContent.CallId); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs index fbe78145f91..8d4fe24f235 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -610,12 +610,12 @@ public async Task ServerToolCallContentTypes_SerializedCorrectly(bool streaming) return new ChatResponse(new ChatMessage(ChatRole.Assistant, [ new TextContent("Processing with tools..."), - new CodeInterpreterToolCallContent { CallId = "ci-call-1", Inputs = [new TextContent("print('hello')")] }, - new CodeInterpreterToolResultContent { CallId = "ci-call-1", Outputs = [new TextContent("hello")] }, - new ImageGenerationToolCallContent { ImageId = "img-123" }, - new ImageGenerationToolResultContent { ImageId = "img-123", Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }, + new CodeInterpreterToolCallContent("ci-call-1") { Inputs = [new TextContent("print('hello')")] }, + new CodeInterpreterToolResultContent("ci-call-1") { Outputs = [new TextContent("hello")] }, + new ImageGenerationToolCallContent("img-123"), + new ImageGenerationToolResultContent("img-123") { Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }, new McpServerToolCallContent("mcp-call-1", "myTool", "myServer") { Arguments = new Dictionary { ["param1"] = "value1" } }, - new McpServerToolResultContent("mcp-call-1") { Result = new TextContent("Tool result") }, + new McpServerToolResultContent("mcp-call-1") { Outputs = [new TextContent("Tool result")] }, ])); }, GetStreamingResponseAsyncCallback = CallbackAsync, @@ -626,12 +626,21 @@ async static IAsyncEnumerable CallbackAsync( { await Task.Yield(); yield return new(ChatRole.Assistant, "Processing with tools..."); - yield return new() { Contents = [new CodeInterpreterToolCallContent { CallId = "ci-call-1", Inputs = [new TextContent("print('hello')")] }] }; - yield return new() { Contents = [new CodeInterpreterToolResultContent { CallId = "ci-call-1", Outputs = [new TextContent("hello")] }] }; - yield return new() { Contents = [new ImageGenerationToolCallContent { ImageId = "img-123" }] }; - yield return new() { Contents = [new ImageGenerationToolResultContent { ImageId = "img-123", Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] }] }; + yield return new() { Contents = [new CodeInterpreterToolCallContent("ci-call-1") { Inputs = [new TextContent("print('hello')")] }] }; + yield return new() { Contents = [new CodeInterpreterToolResultContent("ci-call-1") { Outputs = [new TextContent("hello")] }] }; + yield return new() { Contents = [new ImageGenerationToolCallContent("img-123")] }; + yield return new() + { + Contents = + [ + new ImageGenerationToolResultContent("img-123") + { + Outputs = [new UriContent(new Uri("https://example.com/image.png"), "image/png")] + } + ] + }; yield return new() { Contents = [new McpServerToolCallContent("mcp-call-1", "myTool", "myServer") { Arguments = new Dictionary { ["param1"] = "value1" } }] }; - yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Result = new TextContent("Tool result") }] }; + yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Outputs = [new TextContent("Tool result")] }] }; } using var chatClient = innerClient @@ -734,10 +743,12 @@ async static IAsyncEnumerable CallbackAsync( "id": "mcp-call-1", "server_tool_call_response": { "type": "mcp", - "output": { - "$type": "text", - "text": "Tool result" - } + "output": [ + { + "$type": "text", + "text": "Tool result" + } + ] } } ] @@ -783,11 +794,11 @@ public async Task McpServerToolApprovalContentTypes_SerializedCorrectly() [ new(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("approval-1", toolCall), + new ToolApprovalRequestContent("approval-1", toolCall), ]), new(ChatRole.User, [ - new FunctionApprovalResponseContent("approval-1", true, toolCall), + new ToolApprovalResponseContent("approval-1", true, toolCall), ]), ]; @@ -837,3 +848,4 @@ private sealed class NonSerializableAIContent : AIContent; private static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim(); } + From d06120c950f0b1ee1c9362fc1dbdcc31ebbee1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 12 Feb 2026 18:34:56 -0600 Subject: [PATCH 03/11] Make ImageGeneration/CodeInterpreter extend Tool Contents --- .../ChatCompletion/ChatResponseExtensions.cs | 16 ++++----- .../Contents/AIContent.cs | 2 ++ .../CodeInterpreterToolCallContent.cs | 13 ++++--- .../CodeInterpreterToolResultContent.cs | 13 ++++--- .../ImageGenerationToolCallContent.cs | 13 ++++--- .../ImageGenerationToolResultContent.cs | 13 ++++--- .../Contents/ToolCallContent.cs | 9 +++++ .../Contents/ToolResultContent.cs | 9 +++++ .../Microsoft.Extensions.AI.Abstractions.json | 34 +++++-------------- .../Utilities/AIJsonUtilities.Defaults.cs | 6 ++++ .../Utilities/AIJsonUtilities.cs | 13 +++++++ .../OpenAIAssistantsChatClient.cs | 6 ++-- .../OpenAIResponsesChatClient.cs | 23 ++++--------- .../Contents/AIContentTests.cs | 4 +-- .../ImageGenerationToolCallContentTests.cs | 24 +++++-------- .../ImageGenerationToolResultContentTests.cs | 23 +++++-------- .../Contents/ToolCallContentTests.cs | 28 +++++++++------ .../Contents/ToolResultContentTests.cs | 28 +++++++++------ 18 files changed, 140 insertions(+), 137 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index 62bab814d13..bc96315eaf3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -196,15 +196,15 @@ private static void CoalesceImageResultContent(IList contents) for (int i = 0; i < contents.Count; i++) { - if (contents[i] is ImageGenerationToolResultContent imageResult && !string.IsNullOrEmpty(imageResult.ImageId)) + if (contents[i] is ImageGenerationToolResultContent imageResult) { - // Check if there's an existing ImageGenerationToolResultContent with the same ImageId to replace + // Check if there's an existing ImageGenerationToolResultContent with the same CallId to replace if (imageResultIndexById is null) { imageResultIndexById = new(StringComparer.Ordinal); } - if (imageResultIndexById.TryGetValue(imageResult.ImageId!, out int existingIndex)) + if (imageResultIndexById.TryGetValue(imageResult.CallId, out int existingIndex)) { // Replace the existing imageResult with the new one contents[existingIndex] = imageResult; @@ -213,7 +213,7 @@ private static void CoalesceImageResultContent(IList contents) } else { - imageResultIndexById[imageResult.ImageId!] = i; + imageResultIndexById[imageResult.CallId] = i; } } } @@ -320,9 +320,8 @@ internal static void CoalesceContent(IList contents) CoalesceContent(inputs); } - return new() + return new(firstContent.CallId) { - CallId = firstContent.CallId, Inputs = inputs, AdditionalProperties = firstContent.AdditionalProperties?.Clone(), }; @@ -331,7 +330,7 @@ internal static void CoalesceContent(IList contents) Coalesce( contents, mergeSingle: true, - canMerge: static (r1, r2) => r1.CallId is not null && r2.CallId is not null && r1.CallId == r2.CallId, + canMerge: static (r1, r2) => r1.CallId == r2.CallId, static (contents, start, end) => { var firstContent = (CodeInterpreterToolResultContent)contents[start]; @@ -358,9 +357,8 @@ internal static void CoalesceContent(IList contents) CoalesceContent(output); } - return new() + return new(firstContent.CallId) { - CallId = firstContent.CallId, Outputs = output, AdditionalProperties = firstContent.AdditionalProperties?.Clone(), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index 2c5a3b9f895..61f159afc3f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -30,6 +30,8 @@ namespace Microsoft.Extensions.AI; // as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] +// [JsonDerivedType(typeof(ImageGenerationToolCallContent), typeDiscriminator: "imageGenerationToolCall")] +// [JsonDerivedType(typeof(ImageGenerationToolResultContent), typeDiscriminator: "imageGenerationToolResult")] public class AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs index d70ad0911e0..5e0ff465824 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -15,20 +16,18 @@ namespace Microsoft.Extensions.AI; /// It is informational only and represents the call itself, not the result. /// [Experimental(DiagnosticIds.Experiments.AICodeInterpreter, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class CodeInterpreterToolCallContent : AIContent +public sealed class CodeInterpreterToolCallContent : ToolCallContent { /// /// Initializes a new instance of the class. /// - public CodeInterpreterToolCallContent() + /// The tool call ID. + [JsonConstructor] + public CodeInterpreterToolCallContent(string callId) + : base(callId) { } - /// - /// Gets or sets the tool call ID. - /// - public string? CallId { get; set; } - /// /// Gets or sets the inputs to the code interpreter tool. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs index 012a7fc7be2..8d82fe1ff2d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -11,20 +12,18 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a code interpreter tool invocation by a hosted service. /// [Experimental(DiagnosticIds.Experiments.AICodeInterpreter, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class CodeInterpreterToolResultContent : AIContent +public sealed class CodeInterpreterToolResultContent : ToolResultContent { /// /// Initializes a new instance of the class. /// - public CodeInterpreterToolResultContent() + /// The tool call ID. + [JsonConstructor] + public CodeInterpreterToolResultContent(string callId) + : base(callId) { } - /// - /// Gets or sets the tool call ID that this result corresponds to. - /// - public string? CallId { get; set; } - /// /// Gets or sets the output of code interpreter tool. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs index 9e22207cf00..ed4fba0d66d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -10,17 +11,15 @@ namespace Microsoft.Extensions.AI; /// Represents the invocation of an image generation tool call by a hosted service. /// [Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class ImageGenerationToolCallContent : AIContent +public sealed class ImageGenerationToolCallContent : ToolCallContent { /// /// Initializes a new instance of the class. /// - public ImageGenerationToolCallContent() + /// The tool call ID. + [JsonConstructor] + public ImageGenerationToolCallContent(string callId) + : base(callId) { } - - /// - /// Gets or sets the unique identifier of the image generation item. - /// - public string? ImageId { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs index 17ccb6bc0e0..c260c2ddb8e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -15,20 +16,18 @@ namespace Microsoft.Extensions.AI; /// It is informational only and represents the call itself, not the result. /// [Experimental(DiagnosticIds.Experiments.AIImageGeneration, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class ImageGenerationToolResultContent : AIContent +public sealed class ImageGenerationToolResultContent : ToolResultContent { /// /// Initializes a new instance of the class. /// - public ImageGenerationToolResultContent() + /// The tool call ID. + [JsonConstructor] + public ImageGenerationToolResultContent(string callId) + : base(callId) { } - /// - /// Gets or sets the unique identifier of the image generation item. - /// - public string? ImageId { get; set; } - /// /// Gets or sets the generated content items. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs index 8370431658a..b0d80aac27a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs @@ -12,6 +12,15 @@ namespace Microsoft.Extensions.AI; /// [JsonDerivedType(typeof(FunctionCallContent), "functionCall")] [JsonDerivedType(typeof(McpServerToolCallContent), "mcpServerToolCall")] + +// Same as in AIContent. +// These should be added in once they're no longer [Experimental]. If they're included while still +// experimental, any JsonSerializerContext that includes ToolCallContent will incur errors about using +// experimental types in its source generated files. When [Experimental] is removed from these types, +// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions +// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. +// [JsonDerivedType(typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall")] +// [JsonDerivedType(typeof(ImageGenerationToolCallContent), "imageGenerationToolCall")] public class ToolCallContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs index 9e5aacd8b87..07a2220ffef 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs @@ -12,6 +12,15 @@ namespace Microsoft.Extensions.AI; /// [JsonDerivedType(typeof(FunctionResultContent), "functionResult")] [JsonDerivedType(typeof(McpServerToolResultContent), "mcpServerToolResult")] + +// Same as in AIContent. +// These should be added in once they're no longer [Experimental]. If they're included while still +// experimental, any JsonSerializerContext that includes ToolResultContent will incur errors about using +// experimental types in its source generated files. When [Experimental] is removed from these types, +// these lines should be uncommented and the corresponding lines in AIJsonUtilities.CreateDefaultOptions +// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed. +// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult")] +// [JsonDerivedType(typeof(ImageGenerationToolResultContent), "imageGenerationToolResult")] public class ToolResultContent : AIContent { /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 8c6c37b2c5f..9844509d08b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1476,19 +1476,15 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.CodeInterpreterToolCallContent : Microsoft.Extensions.AI.AIContent", + "Type": "sealed class Microsoft.Extensions.AI.CodeInterpreterToolCallContent : Microsoft.Extensions.AI.ToolCallContent", "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.CodeInterpreterToolCallContent.CodeInterpreterToolCallContent();", + "Member": "Microsoft.Extensions.AI.CodeInterpreterToolCallContent.CodeInterpreterToolCallContent(string callId);", "Stage": "Experimental" } ], "Properties": [ - { - "Member": "string? Microsoft.Extensions.AI.CodeInterpreterToolCallContent.CallId { get; set; }", - "Stage": "Experimental" - }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.CodeInterpreterToolCallContent.Inputs { get; set; }", "Stage": "Experimental" @@ -1496,19 +1492,15 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.CodeInterpreterToolResultContent : Microsoft.Extensions.AI.AIContent", + "Type": "sealed class Microsoft.Extensions.AI.CodeInterpreterToolResultContent : Microsoft.Extensions.AI.ToolResultContent", "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.CodeInterpreterToolResultContent.CodeInterpreterToolResultContent();", + "Member": "Microsoft.Extensions.AI.CodeInterpreterToolResultContent.CodeInterpreterToolResultContent(string callId);", "Stage": "Experimental" } ], "Properties": [ - { - "Member": "string? Microsoft.Extensions.AI.CodeInterpreterToolResultContent.CallId { get; set; }", - "Stage": "Experimental" - }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.CodeInterpreterToolResultContent.Outputs { get; set; }", "Stage": "Experimental" @@ -2551,35 +2543,25 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.ImageGenerationToolCallContent : Microsoft.Extensions.AI.AIContent", + "Type": "sealed class Microsoft.Extensions.AI.ImageGenerationToolCallContent : Microsoft.Extensions.AI.ToolCallContent", "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.ImageGenerationToolCallContent.ImageGenerationToolCallContent();", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "string? Microsoft.Extensions.AI.ImageGenerationToolCallContent.ImageId { get; set; }", + "Member": "Microsoft.Extensions.AI.ImageGenerationToolCallContent.ImageGenerationToolCallContent(string callId);", "Stage": "Experimental" } ] }, { - "Type": "sealed class Microsoft.Extensions.AI.ImageGenerationToolResultContent : Microsoft.Extensions.AI.AIContent", + "Type": "sealed class Microsoft.Extensions.AI.ImageGenerationToolResultContent : Microsoft.Extensions.AI.ToolResultContent", "Stage": "Experimental", "Methods": [ { - "Member": "Microsoft.Extensions.AI.ImageGenerationToolResultContent.ImageGenerationToolResultContent();", + "Member": "Microsoft.Extensions.AI.ImageGenerationToolResultContent.ImageGenerationToolResultContent(string callId);", "Stage": "Experimental" } ], "Properties": [ - { - "Member": "string? Microsoft.Extensions.AI.ImageGenerationToolResultContent.ImageId { get; set; }", - "Stage": "Experimental" - }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.ImageGenerationToolResultContent.Outputs { get; set; }", "Stage": "Experimental" 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 7e9bda6043a..845769658d1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -56,6 +56,12 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(ImageGenerationToolCallContent), typeDiscriminatorId: "imageGenerationToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(ImageGenerationToolResultContent), typeDiscriminatorId: "imageGenerationToolResult", checkBuiltIn: false); + // Also register the experimental types as derived types of ToolCallContent/ToolResultContent. + AddDerivedContentType(options, typeof(ToolCallContent), typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall"); + AddDerivedContentType(options, typeof(ToolCallContent), typeof(ImageGenerationToolCallContent), "imageGenerationToolCall"); + AddDerivedContentType(options, typeof(ToolResultContent), typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult"); + AddDerivedContentType(options, typeof(ToolResultContent), typeof(ImageGenerationToolResultContent), "imageGenerationToolResult"); + if (JsonSerializer.IsReflectionEnabledByDefault) { // If reflection-based serialization is enabled by default, use it as a fallback for all other types. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index b69d0fb2aab..be3aa232ad5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -203,6 +203,19 @@ private static void AddAIContentType(JsonSerializerOptions options, Type content }); } + /// Adds a derived type to the polymorphism options of the specified base type. + private static void AddDerivedContentType(JsonSerializerOptions options, Type baseType, Type derivedType, string typeDiscriminatorId) + { + IJsonTypeInfoResolver resolver = options.TypeInfoResolver ?? DefaultOptions.TypeInfoResolver!; + options.TypeInfoResolver = resolver.WithAddedModifier(typeInfo => + { + if (typeInfo.Type == baseType) + { + (typeInfo.PolymorphismOptions ??= new()).DerivedTypes.Add(new(derivedType, typeDiscriminatorId)); + } + }); + } + #if NET /// Provides a stream that writes to an . private sealed class IncrementalHashStream : Stream diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 35b85741de6..2355e9fb1d8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -202,9 +202,8 @@ public async IAsyncEnumerable GetStreamingResponseAsync( case RunStepDetailsUpdate details: if (!string.IsNullOrEmpty(details.CodeInterpreterInput)) { - CodeInterpreterToolCallContent hcitcc = new() + CodeInterpreterToolCallContent hcitcc = new(details.ToolCallId) { - CallId = details.ToolCallId, Inputs = [new DataContent(Encoding.UTF8.GetBytes(details.CodeInterpreterInput), OpenAIClientExtensions.PythonMediaType)], RawRepresentation = details, }; @@ -221,9 +220,8 @@ public async IAsyncEnumerable GetStreamingResponseAsync( if (details.CodeInterpreterOutputs is { Count: > 0 }) { - CodeInterpreterToolResultContent hcitrc = new() + CodeInterpreterToolResultContent hcitrc = new(details.ToolCallId) { - CallId = details.ToolCallId, RawRepresentation = details, }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 831862c5941..b9c17a0f826 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -244,9 +244,8 @@ internal static IEnumerable ToChatMessages(IEnumerable break; case StreamingResponseImageGenerationCallInProgressUpdate imageGenInProgress: - yield return CreateUpdate(new ImageGenerationToolCallContent + yield return CreateUpdate(new ImageGenerationToolCallContent(imageGenInProgress.ItemId) { - ImageId = imageGenInProgress.ItemId, RawRepresentation = imageGenInProgress, }); break; @@ -444,9 +442,8 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; case StreamingResponseCodeInterpreterCallCodeDeltaUpdate codeInterpreterDeltaUpdate: - yield return CreateUpdate(new CodeInterpreterToolCallContent + yield return CreateUpdate(new CodeInterpreterToolCallContent(codeInterpreterDeltaUpdate.ItemId) { - CallId = codeInterpreterDeltaUpdate.ItemId, Inputs = [new DataContent(Encoding.UTF8.GetBytes(codeInterpreterDeltaUpdate.Delta), OpenAIClientExtensions.PythonMediaType)], RawRepresentation = codeInterpreterDeltaUpdate, }); @@ -1445,9 +1442,8 @@ private static void AddAllMcpFilters(IList toolNames, McpToolFilter filt /// Creates a for the specified . private static CodeInterpreterToolResultContent CreateCodeInterpreterResultContent(CodeInterpreterCallResponseItem cicri) => - new() + new(cicri.Id) { - CallId = cicri.Id, Outputs = cicri.Outputs is { Count: > 0 } outputs ? outputs.Select(o => o switch { @@ -1463,14 +1459,10 @@ private static void AddImageGenerationContents(ImageGenerationCallResponseItem o var imageGenTool = options?.Tools.OfType().FirstOrDefault(); string outputFormat = imageGenTool?.OutputFileFormat?.ToString() ?? "png"; - contents.Add(new ImageGenerationToolCallContent - { - ImageId = outputItem.Id, - }); + contents.Add(new ImageGenerationToolCallContent(outputItem.Id)); - contents.Add(new ImageGenerationToolResultContent + contents.Add(new ImageGenerationToolResultContent(outputItem.Id) { - ImageId = outputItem.Id, RawRepresentation = outputItem, Outputs = [new DataContent(outputItem.ImageResultBytes, $"image/{outputFormat}")] }); @@ -1481,9 +1473,8 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami var imageGenTool = options?.Tools.OfType().FirstOrDefault(); var outputType = imageGenTool?.OutputFileFormat?.ToString() ?? "png"; - return new ImageGenerationToolResultContent + return new ImageGenerationToolResultContent(update.ItemId) { - ImageId = update.ItemId, RawRepresentation = update, Outputs = [ 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 0b7c4c3ec75..cd0a171d798 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -76,8 +76,8 @@ public void Serialization_DerivedTypes_Roundtrips() new McpServerToolResultContent("call123"), new ToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), new ToolApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), - new ImageGenerationToolCallContent { ImageId = "img123" }, - new ImageGenerationToolResultContent { ImageId = "img456", Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] } + new ImageGenerationToolCallContent("img123"), + new ImageGenerationToolResultContent("img456") { Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] } ]); // Verify each element roundtrips individually diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolCallContentTests.cs index 33c5e19090f..c506126f989 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolCallContentTests.cs @@ -11,20 +11,18 @@ public class ImageGenerationToolCallContentTests [Fact] public void Constructor_PropsDefault() { - ImageGenerationToolCallContent c = new(); + ImageGenerationToolCallContent c = new("call123"); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.ImageId); + Assert.Equal("call123", c.CallId); } [Fact] public void Properties_Roundtrip() { - ImageGenerationToolCallContent c = new(); + ImageGenerationToolCallContent c = new("img123"); - Assert.Null(c.ImageId); - c.ImageId = "img123"; - Assert.Equal("img123", c.ImageId); + Assert.Equal("img123", c.CallId); Assert.Null(c.RawRepresentation); object raw = new(); @@ -40,25 +38,19 @@ public void Properties_Roundtrip() [Fact] public void Serialization_Roundtrips() { - ImageGenerationToolCallContent content = new() - { - ImageId = "img123" - }; + ImageGenerationToolCallContent content = new("img123"); var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedSut); - Assert.Equal("img123", deserializedSut.ImageId); + Assert.Equal("img123", deserializedSut.CallId); } [Fact] public void Serialization_PolymorphicAsAIContent_Roundtrips() { - AIContent content = new ImageGenerationToolCallContent - { - ImageId = "img456" - }; + AIContent content = new ImageGenerationToolCallContent("img456"); var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); Assert.Contains("\"$type\"", json); @@ -68,6 +60,6 @@ public void Serialization_PolymorphicAsAIContent_Roundtrips() Assert.NotNull(deserialized); Assert.IsType(deserialized); - Assert.Equal("img456", ((ImageGenerationToolCallContent)deserialized).ImageId); + Assert.Equal("img456", ((ImageGenerationToolCallContent)deserialized).CallId); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolResultContentTests.cs index d30c2513351..b3bb47f9169 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ImageGenerationToolResultContentTests.cs @@ -12,21 +12,19 @@ public class ImageGenerationToolResultContentTests [Fact] public void Constructor_PropsDefault() { - ImageGenerationToolResultContent c = new(); + ImageGenerationToolResultContent c = new("call123"); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.ImageId); + Assert.Equal("call123", c.CallId); Assert.Null(c.Outputs); } [Fact] public void Properties_Roundtrip() { - ImageGenerationToolResultContent c = new(); + ImageGenerationToolResultContent c = new("img123"); - Assert.Null(c.ImageId); - c.ImageId = "img123"; - Assert.Equal("img123", c.ImageId); + Assert.Equal("img123", c.CallId); Assert.Null(c.Outputs); IList outputs = [new DataContent(new byte[] { 1, 2, 3 }, "image/png")]; @@ -47,9 +45,8 @@ public void Properties_Roundtrip() [Fact] public void Outputs_SupportsMultipleContentTypes() { - ImageGenerationToolResultContent c = new() + ImageGenerationToolResultContent c = new("img456") { - ImageId = "img456", Outputs = [ new DataContent(new byte[] { 1, 2, 3 }, "image/png"), @@ -68,9 +65,8 @@ public void Outputs_SupportsMultipleContentTypes() [Fact] public void Serialization_Roundtrips() { - ImageGenerationToolResultContent content = new() + ImageGenerationToolResultContent content = new("img123") { - ImageId = "img123", Outputs = [ new DataContent(new byte[] { 1, 2, 3 }, "image/png"), @@ -82,7 +78,7 @@ public void Serialization_Roundtrips() var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedSut); - Assert.Equal("img123", deserializedSut.ImageId); + Assert.Equal("img123", deserializedSut.CallId); Assert.NotNull(deserializedSut.Outputs); Assert.Equal(2, deserializedSut.Outputs.Count); Assert.IsType(deserializedSut.Outputs[0]); @@ -94,9 +90,8 @@ public void Serialization_Roundtrips() [Fact] public void Serialization_PolymorphicAsAIContent_Roundtrips() { - AIContent content = new ImageGenerationToolResultContent + AIContent content = new ImageGenerationToolResultContent("img789") { - ImageId = "img789", Outputs = [ new DataContent(new byte[] { 7, 8, 9 }, "image/png"), @@ -114,7 +109,7 @@ public void Serialization_PolymorphicAsAIContent_Roundtrips() Assert.IsType(deserialized); var imageResult = (ImageGenerationToolResultContent)deserialized; - Assert.Equal("img789", imageResult.ImageId); + Assert.Equal("img789", imageResult.CallId); Assert.NotNull(imageResult.Outputs); Assert.Equal(2, imageResult.Outputs.Count); Assert.IsType(imageResult.Outputs[0]); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs index 8f8e065216a..572c6c1a5d7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs @@ -12,14 +12,16 @@ public class ToolCallContentTests [Fact] public void Serialization_DerivedTypes_Roundtrips() { - ToolCallContent[] contents = + ChatMessage message = new(ChatRole.Assistant, [ new FunctionCallContent("call1", "function1", new Dictionary { { "param1", 123 } }), new McpServerToolCallContent("call2", "myTool", "myServer"), - ]; + new CodeInterpreterToolCallContent("call3"), + new ImageGenerationToolCallContent("call4"), + ]); // Verify each element roundtrips individually - foreach (var content in contents) + foreach (var content in message.Contents) { var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); @@ -27,15 +29,19 @@ public void Serialization_DerivedTypes_Roundtrips() Assert.Equal(content.GetType(), deserialized.GetType()); } - // Verify the array roundtrips - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.ToolCallContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.ToolCallContentArray); - Assert.NotNull(deserializedContents); - Assert.Equal(contents.Length, deserializedContents.Length); - for (int i = 0; i < deserializedContents.Length; i++) + // Verify the message roundtrips - can't use Array because that's not included as + // JsonSerializable in AIJsonUtilities and we can't use TestJsonSerializerContext here + // because it doesn't include the experimental types. + var serializedMessage = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); + ChatMessage? deserialized2 = JsonSerializer.Deserialize(serializedMessage, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + + Assert.Equal(message.Role, deserialized2.Role); + Assert.Equal(message.Contents.Count, deserialized2.Contents.Count); + for (int i = 0; i < message.Contents.Count; i++) { - Assert.NotNull(deserializedContents[i]); - Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + Assert.NotNull(deserialized2.Contents[i]); + Assert.Equal(message.Contents[i].GetType(), deserialized2.Contents[i].GetType()); } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs index 6cc75bf00b3..f0c8d376f53 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs @@ -11,14 +11,16 @@ public class ToolResultContentTests [Fact] public void Serialization_DerivedTypes_Roundtrips() { - ToolResultContent[] contents = + ChatMessage message = new(ChatRole.Tool, [ new FunctionResultContent("call1", "result1"), new McpServerToolResultContent("call2"), - ]; + new CodeInterpreterToolResultContent("call3"), + new ImageGenerationToolResultContent("call4"), + ]); // Verify each element roundtrips individually - foreach (var content in contents) + foreach (var content in message.Contents) { var serialized = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); var deserialized = JsonSerializer.Deserialize(serialized, AIJsonUtilities.DefaultOptions); @@ -26,15 +28,19 @@ public void Serialization_DerivedTypes_Roundtrips() Assert.Equal(content.GetType(), deserialized.GetType()); } - // Verify the array roundtrips - var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.ToolResultContentArray); - var deserializedContents = JsonSerializer.Deserialize(serializedContents, TestJsonSerializerContext.Default.ToolResultContentArray); - Assert.NotNull(deserializedContents); - Assert.Equal(contents.Length, deserializedContents.Length); - for (int i = 0; i < deserializedContents.Length; i++) + // Verify the message roundtrips - can't use Array because that's not included as + // JsonSerializable in AIJsonUtilities and we can't use TestJsonSerializerContext here + // because it doesn't include the experimental types. + var serializedMessage = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); + ChatMessage? deserialized2 = JsonSerializer.Deserialize(serializedMessage, AIJsonUtilities.DefaultOptions); + Assert.NotNull(deserialized2); + + Assert.Equal(message.Role, deserialized2.Role); + Assert.Equal(message.Contents.Count, deserialized2.Contents.Count); + for (int i = 0; i < message.Contents.Count; i++) { - Assert.NotNull(deserializedContents[i]); - Assert.Equal(contents[i].GetType(), deserializedContents[i].GetType()); + Assert.NotNull(deserialized2.Contents[i]); + Assert.Equal(message.Contents[i].GetType(), deserialized2.Contents[i].GetType()); } } } From e27169c62564e7effd6bc1488a0503c4295d75a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 12 Feb 2026 22:02:35 -0600 Subject: [PATCH 04/11] Make TCC and TRC effectively sealed --- .../Contents/ToolCallContent.cs | 2 +- .../Contents/ToolResultContent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs index b0d80aac27a..8cc504dc540 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.cs @@ -28,7 +28,7 @@ public class ToolCallContent : AIContent /// /// The tool call ID. /// is . - protected ToolCallContent(string callId) + internal ToolCallContent(string callId) { CallId = Throw.IfNull(callId); } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs index 07a2220ffef..64e1b2c5510 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs @@ -28,7 +28,7 @@ public class ToolResultContent : AIContent /// /// The tool call ID for which this is the result. /// is . - protected ToolResultContent(string callId) + internal ToolResultContent(string callId) { CallId = Throw.IfNull(callId); } From 06b7f455d45c4b53ff1e714675eaa16585eaeff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 12 Feb 2026 22:16:20 -0600 Subject: [PATCH 05/11] Update compat suppressions --- .../CompatibilitySuppressions.xml | 706 +++++++++++------- 1 file changed, 427 insertions(+), 279 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml index ea52aefa5dc..b2eebebb7c0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -1,92 +1,188 @@  - + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0001 T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0001 T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0001 T:Microsoft.Extensions.AI.UserInputRequestContent - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0001 T:Microsoft.Extensions.AI.UserInputResponseContent - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent lib/net462/Microsoft.Extensions.AI.Abstractions.dll lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent lib/net462/Microsoft.Extensions.AI.Abstractions.dll lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent lib/net462/Microsoft.Extensions.AI.Abstractions.dll lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent lib/net462/Microsoft.Extensions.AI.Abstractions.dll lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent lib/net462/Microsoft.Extensions.AI.Abstractions.dll lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent lib/net462/Microsoft.Extensions.AI.Abstractions.dll lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 + CP0001 T:Microsoft.Extensions.AI.FunctionApprovalRequestContent - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 + CP0001 T:Microsoft.Extensions.AI.FunctionApprovalResponseContent - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputRequestContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.UserInputResponseContent + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent @@ -115,172 +211,366 @@ lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.set_CallId(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.#ctor + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.set_CallId(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.#ctor + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.get_ImageId + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.set_ImageId(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.#ctor + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.get_ImageId + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.set_ImageId(System.String) + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalRequestContent - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalResponseContent - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll - lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.set_CallId(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.#ctor + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.set_CallId(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.#ctor + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.get_ImageId + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.set_ImageId(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.#ctor + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.get_ImageId + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.set_ImageId(System.String) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.set_CallId(System.String) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.#ctor + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.set_CallId(System.String) + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0001 - T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 - T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + CP0002 + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 - T:Microsoft.Extensions.AI.UserInputRequestContent + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.#ctor lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 - T:Microsoft.Extensions.AI.UserInputResponseContent + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.get_ImageId lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.set_ImageId(System.String) lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.#ctor lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.get_ImageId lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.set_ImageId(System.String) lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + CP0002 + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll true - - CP0001 - T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 - T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.set_CallId(System.String) lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 - T:Microsoft.Extensions.AI.UserInputRequestContent + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.#ctor lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0001 - T:Microsoft.Extensions.AI.UserInputResponseContent + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.set_CallId(System.String) lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true @@ -301,326 +591,184 @@ CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.#ctor lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.get_ImageId lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.set_ImageId(System.String) lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.#ctor lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.get_ImageId lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalResponseContent + CP0002 + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.set_ImageId(System.String) lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true - - - CP0001 - T:Microsoft.Extensions.AI.McpServerToolApprovalRequestContent - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0001 - T:Microsoft.Extensions.AI.McpServerToolApprovalResponseContent - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0001 - T:Microsoft.Extensions.AI.UserInputRequestContent - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0001 - T:Microsoft.Extensions.AI.UserInputResponseContent - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - CP0002 M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalRequestContent - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0007 - T:Microsoft.Extensions.AI.FunctionApprovalResponseContent - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - - CP0002 - M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.FunctionCallContent.set_Name(System.String) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.#ctor(System.String,System.String,System.String,System.Collections.Generic.IDictionary{System.String,System.Object}) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolCallContent.set_Name(System.String) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - true - - - CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.set_CallId(System.String) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Exception - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.set_CallId(System.String) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.HostedMcpServerTool.get_AuthorizationToken + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.HostedMcpServerTool.set_AuthorizationToken(System.String) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Exception(System.Exception) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.get_ImageId + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.ImageGenerationToolCallContent.set_ImageId(System.String) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.get_ImageId + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Outputs - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.ImageGenerationToolResultContent.set_ImageId(System.String) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) - lib/net462/Microsoft.Extensions.AI.Abstractions.dll - lib/net462/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_Arguments + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.McpServerToolCallContent.get_ToolName + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.McpServerToolResultContent.get_Output + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true CP0002 - M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Outputs(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll - lib/net10.0/Microsoft.Extensions.AI.Abstractions.dll + M:Microsoft.Extensions.AI.McpServerToolResultContent.set_Output(System.Collections.Generic.IList{Microsoft.Extensions.AI.AIContent}) + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll true - + \ No newline at end of file From d0ee50a78635b628f3a8d8097627c4e8a0799e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 12 Feb 2026 22:30:02 -0600 Subject: [PATCH 06/11] Remove not needed JsonConstructors --- .../Contents/CodeInterpreterToolCallContent.cs | 1 - .../Contents/CodeInterpreterToolResultContent.cs | 1 - .../Contents/ImageGenerationToolCallContent.cs | 1 - .../Contents/ImageGenerationToolResultContent.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs index 5e0ff465824..6ffff1bf301 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs @@ -22,7 +22,6 @@ public sealed class CodeInterpreterToolCallContent : ToolCallContent /// Initializes a new instance of the class. /// /// The tool call ID. - [JsonConstructor] public CodeInterpreterToolCallContent(string callId) : base(callId) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs index 8d82fe1ff2d..64e56266ebb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs @@ -18,7 +18,6 @@ public sealed class CodeInterpreterToolResultContent : ToolResultContent /// Initializes a new instance of the class. /// /// The tool call ID. - [JsonConstructor] public CodeInterpreterToolResultContent(string callId) : base(callId) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs index ed4fba0d66d..d1a07111020 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs @@ -17,7 +17,6 @@ public sealed class ImageGenerationToolCallContent : ToolCallContent /// Initializes a new instance of the class. /// /// The tool call ID. - [JsonConstructor] public ImageGenerationToolCallContent(string callId) : base(callId) { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs index c260c2ddb8e..20ca4c7d75f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs @@ -22,7 +22,6 @@ public sealed class ImageGenerationToolResultContent : ToolResultContent /// Initializes a new instance of the class. /// /// The tool call ID. - [JsonConstructor] public ImageGenerationToolResultContent(string callId) : base(callId) { From b58fbd22b227afb5ca1f6cf1525ba8c06ee5c53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 13 Feb 2026 10:45:29 -0600 Subject: [PATCH 07/11] Improve ToolApproval tests --- .../ToolApprovalRequestContentTests.cs | 60 +++++++++++-------- .../ToolApprovalResponseContentTests.cs | 54 ++++++++++++----- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs index 996ac404a79..6a031c75b09 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs @@ -14,44 +14,56 @@ public class ToolApprovalRequestContentTests public void Constructor_InvalidArguments_Throws() { FunctionCallContent functionCall = new("FCC1", "TestFunction"); + McpServerToolCallContent mcpCall = new("MCC1", "TestTool", "TestServer"); + // FunctionCallContent overload Assert.Throws("requestId", () => new ToolApprovalRequestContent(null!, functionCall)); Assert.Throws("requestId", () => new ToolApprovalRequestContent("", functionCall)); Assert.Throws("requestId", () => new ToolApprovalRequestContent("\r\t\n ", functionCall)); - Assert.Throws("functionCall", () => new ToolApprovalRequestContent("id", (FunctionCallContent)null!)); + + // McpServerToolCallContent overload + Assert.Throws("requestId", () => new ToolApprovalRequestContent(null!, mcpCall)); + Assert.Throws("requestId", () => new ToolApprovalRequestContent("", mcpCall)); + Assert.Throws("requestId", () => new ToolApprovalRequestContent("\r\t\n ", mcpCall)); + Assert.Throws("mcpServerToolCall", () => new ToolApprovalRequestContent("id", (McpServerToolCallContent)null!)); + + // ToolCallContent (JsonConstructor) overload + Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", (ToolCallContent)null!)); + Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", new CodeInterpreterToolCallContent("call1"))); + Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", new ImageGenerationToolCallContent("call1"))); } - [Theory] - [InlineData("abc")] - [InlineData("123")] - [InlineData("!@#")] - public void Constructor_Roundtrips(string id) + public static TheoryData ToolCallContentInstances => new() { - FunctionCallContent functionCall = new("FCC1", "TestFunction"); + new FunctionCallContent("FCC1", "TestFunction", new Dictionary { { "param1", 123 } }), + new McpServerToolCallContent("MCC1", "TestTool", "TestServer") { Arguments = new Dictionary { { "arg1", "value1" } } }, + }; - ToolApprovalRequestContent content = new(id, functionCall); + [Theory] + [MemberData(nameof(ToolCallContentInstances))] + public void Constructor_Roundtrips(ToolCallContent toolCall) + { + string id = "req-1"; + ToolApprovalRequestContent content = new(id, toolCall); Assert.Same(id, content.RequestId); - Assert.Same(functionCall, content.ToolCall); + Assert.Same(toolCall, content.ToolCall); } [Theory] - [InlineData(true)] - [InlineData(false)] - public void CreateResponse_ReturnsExpectedResponse(bool approved) + [MemberData(nameof(ToolCallContentInstances))] + public void CreateResponse_ReturnsExpectedResponse(ToolCallContent toolCall) { string id = "req-1"; - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - - ToolApprovalRequestContent content = new(id, functionCall); + ToolApprovalRequestContent content = new(id, toolCall); - var response = content.CreateResponse(approved); + var response = content.CreateResponse(approved: true); Assert.NotNull(response); Assert.Same(id, response.RequestId); - Assert.Equal(approved, response.Approved); - Assert.Same(functionCall, response.ToolCall); + Assert.True(response.Approved); + Assert.Same(toolCall, response.ToolCall); Assert.Null(response.Reason); } @@ -76,10 +88,11 @@ public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, str Assert.Equal(reason, response.Reason); } - [Fact] - public void Serialization_Roundtrips() + [Theory] + [MemberData(nameof(ToolCallContentInstances))] + public void Serialization_Roundtrips(ToolCallContent toolCall) { - var content = new ToolApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })); + var content = new ToolApprovalRequestContent("request123", toolCall); AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); @@ -95,9 +108,8 @@ static void AssertSerializationRoundtrips(ToolApprovalRequestContent content) var deserializedContent = Assert.IsType(deserialized); Assert.Equal(content.RequestId, deserializedContent.RequestId); Assert.NotNull(deserializedContent.ToolCall); - var functionCall = Assert.IsType(deserializedContent.ToolCall); - Assert.Equal(content.ToolCall.CallId, functionCall.CallId); - Assert.Equal(((FunctionCallContent)content.ToolCall).Name, functionCall.Name); + Assert.IsType(content.ToolCall.GetType(), deserializedContent.ToolCall); + Assert.Equal(content.ToolCall.CallId, deserializedContent.ToolCall.CallId); } } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs index b536030135d..209280d3ab5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Text.Json; using Xunit; @@ -13,32 +14,54 @@ public class ToolApprovalResponseContentTests public void Constructor_InvalidArguments_Throws() { FunctionCallContent functionCall = new("FCC1", "TestFunction"); + McpServerToolCallContent mcpCall = new("MCC1", "TestTool", "TestServer"); + // FunctionCallContent overload Assert.Throws("requestId", () => new ToolApprovalResponseContent(null!, true, functionCall)); Assert.Throws("requestId", () => new ToolApprovalResponseContent("", true, functionCall)); Assert.Throws("requestId", () => new ToolApprovalResponseContent("\r\t\n ", true, functionCall)); - Assert.Throws("functionCall", () => new ToolApprovalResponseContent("id", true, (FunctionCallContent)null!)); + + // McpServerToolCallContent overload + Assert.Throws("requestId", () => new ToolApprovalResponseContent(null!, true, mcpCall)); + Assert.Throws("requestId", () => new ToolApprovalResponseContent("", true, mcpCall)); + Assert.Throws("requestId", () => new ToolApprovalResponseContent("\r\t\n ", true, mcpCall)); + Assert.Throws("mcpServerToolCall", () => new ToolApprovalResponseContent("id", true, (McpServerToolCallContent)null!)); + + // ToolCallContent (JsonConstructor) overload + Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, (ToolCallContent)null!)); + Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, new CodeInterpreterToolCallContent("call1"))); + Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, new ImageGenerationToolCallContent("call1"))); } + public static TheoryData ToolCallContentInstances => new() + { + new FunctionCallContent("FCC1", "TestFunction", new Dictionary { { "param1", 123 } }), + new McpServerToolCallContent("MCC1", "TestTool", "TestServer") { Arguments = new Dictionary { { "arg1", "value1" } } }, + }; + [Theory] - [InlineData("abc", true)] - [InlineData("123", false)] - [InlineData("!@#", true)] - public void Constructor_Roundtrips(string id, bool approved) + [MemberData(nameof(ToolCallContentInstances))] + public void Constructor_Roundtrips(ToolCallContent toolCall) { - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - ToolApprovalResponseContent content = new(id, approved, functionCall); + ToolApprovalResponseContent content = new("req-1", true, toolCall); - Assert.Same(id, content.RequestId); - Assert.Equal(approved, content.Approved); - Assert.Same(functionCall, content.ToolCall); + Assert.Equal("req-1", content.RequestId); + Assert.True(content.Approved); + Assert.Same(toolCall, content.ToolCall); + + content = new("req-2", false, toolCall); + + Assert.Equal("req-2", content.RequestId); + Assert.False(content.Approved); + Assert.Same(toolCall, content.ToolCall); } - [Fact] - public void Serialization_Roundtrips() + [Theory] + [MemberData(nameof(ToolCallContentInstances))] + public void Serialization_Roundtrips(ToolCallContent toolCall) { - var content = new ToolApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) + var content = new ToolApprovalResponseContent("request123", true, toolCall) { Reason = "Approved for testing" }; @@ -59,9 +82,8 @@ static void AssertSerializationRoundtrips(ToolApprovalResponseContent content Assert.Equal(content.Approved, deserializedContent.Approved); Assert.Equal(content.Reason, deserializedContent.Reason); Assert.NotNull(deserializedContent.ToolCall); - var functionCall = Assert.IsType(deserializedContent.ToolCall); - Assert.Equal(content.ToolCall.CallId, functionCall.CallId); - Assert.Equal(((FunctionCallContent)content.ToolCall).Name, functionCall.Name); + Assert.IsType(content.ToolCall.GetType(), deserializedContent.ToolCall); + Assert.Equal(content.ToolCall.CallId, deserializedContent.ToolCall.CallId); } } From 87509a0134b04a4b5ef7a330cc72a9cd54d9d13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 13 Feb 2026 11:25:02 -0600 Subject: [PATCH 08/11] Remove incorrect usings --- .../Contents/CodeInterpreterToolCallContent.cs | 1 - .../Contents/CodeInterpreterToolResultContent.cs | 1 - .../Contents/ImageGenerationToolCallContent.cs | 1 - .../Contents/ImageGenerationToolResultContent.cs | 1 - .../Contents/McpServerToolCallContent.cs | 1 - .../Contents/McpServerToolResultContent.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs index 6ffff1bf301..c155b0228b8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs index 64e56266ebb..9384c927cb7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs index d1a07111020..45522e3d599 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs index 20ca4c7d75f..796583130cf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 62cf9dec941..5f0bf047781 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index df2753e2688..684cbf688c3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; From 1b70bee3f244527ed0115abd04ff1b2c398f198d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 13 Feb 2026 11:32:01 -0600 Subject: [PATCH 09/11] Fix api baseline --- ...icrosoft.Extensions.AI.Abstractions.csproj | 2 +- .../Microsoft.Extensions.AI.Abstractions.json | 58 ++++--------------- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index 7ef083fb495..c11dbd0b2dc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -15,7 +15,7 @@ $(TargetFrameworks);netstandard2.0 - $(NoWarn);MEAI001;LA0003;LA0006;S1128 + $(NoWarn);MEAI001 true true diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index 9844509d08b..c8519971429 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1948,7 +1948,7 @@ "Stage": "Stable" }, { - "Member": "string Microsoft.Extensions.AI.FunctionCallContent.Name { get; set; }", + "Member": "string Microsoft.Extensions.AI.FunctionCallContent.Name { get; }", "Stage": "Stable" } ] @@ -2365,11 +2365,11 @@ }, { "Type": "interface Microsoft.Extensions.AI.IChatReducer", - "Stage": "Experimental", + "Stage": "Stable", "Methods": [ { "Member": "System.Threading.Tasks.Task> Microsoft.Extensions.AI.IChatReducer.ReduceAsync(System.Collections.Generic.IEnumerable messages, System.Threading.CancellationToken cancellationToken);", - "Stage": "Experimental" + "Stage": "Stable" } ] }, @@ -2691,7 +2691,7 @@ "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName, System.Collections.Generic.IDictionary? arguments = null);", + "Member": "Microsoft.Extensions.AI.McpServerToolCallContent.McpServerToolCallContent(string callId, string name, string? serverName);", "Stage": "Stable" } ], @@ -2701,15 +2701,7 @@ "Stage": "Stable" }, { - "Member": "System.Exception? Microsoft.Extensions.AI.McpServerToolCallContent.Exception { get; set; }", - "Stage": "Stable" - }, - { - "Member": "bool Microsoft.Extensions.AI.McpServerToolCallContent.InformationalOnly { get; set; }", - "Stage": "Stable" - }, - { - "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.Name { get; set; }", + "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.Name { get; }", "Stage": "Stable" }, { @@ -2723,21 +2715,13 @@ "Stage": "Stable", "Methods": [ { - "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId, string? serverName, object? result = null);", + "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);", "Stage": "Stable" } ], "Properties": [ { - "Member": "System.Exception? Microsoft.Extensions.AI.McpServerToolResultContent.Exception { get; set; }", - "Stage": "Stable" - }, - { - "Member": "object? Microsoft.Extensions.AI.McpServerToolResultContent.Result { get; set; }", - "Stage": "Stable" - }, - { - "Member": "string? Microsoft.Extensions.AI.McpServerToolResultContent.ServerName { get; }", + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.McpServerToolResultContent.Outputs { get; set; }", "Stage": "Stable" } ] @@ -3339,41 +3323,21 @@ ] }, { - "Type": "abstract class Microsoft.Extensions.AI.ToolCallContent : Microsoft.Extensions.AI.AIContent", + "Type": "class Microsoft.Extensions.AI.ToolCallContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.ToolCallContent.ToolCallContent(string callId);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.ToolCallContent.ToolCallContent();", - "Stage": "Experimental" - } - ], "Properties": [ { - "Member": "string Microsoft.Extensions.AI.ToolCallContent.CallId { get; set; }", + "Member": "string Microsoft.Extensions.AI.ToolCallContent.CallId { get; }", "Stage": "Stable" } ] }, { - "Type": "abstract class Microsoft.Extensions.AI.ToolResultContent : Microsoft.Extensions.AI.AIContent", + "Type": "class Microsoft.Extensions.AI.ToolResultContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.ToolResultContent.ToolResultContent(string callId);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.ToolResultContent.ToolResultContent();", - "Stage": "Experimental" - } - ], "Properties": [ { - "Member": "string Microsoft.Extensions.AI.ToolResultContent.CallId { get; set; }", + "Member": "string Microsoft.Extensions.AI.ToolResultContent.CallId { get; }", "Stage": "Stable" } ] From 422720bfd821e8d5f8a9201419e610577fd19570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 13 Feb 2026 12:06:49 -0600 Subject: [PATCH 10/11] Cleanup --- .../OpenAIResponsesChatClient.cs | 10 ++-- .../FunctionInvokingChatClient.cs | 12 ++--- .../ChatCompletion/OpenTelemetryChatClient.cs | 48 +++++++++---------- .../Contents/FunctionCallContentTests.cs | 2 +- .../Contents/FunctionResultContentTests.cs | 2 +- .../Contents/InputRequestContentTests.cs | 1 - .../Contents/InputResponseContentTests.cs | 1 - ...unctionInvokingChatClientApprovalsTests.cs | 17 ++----- .../FunctionInvokingChatClientTests.cs | 6 +-- 9 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index b9c17a0f826..d5b571308eb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -964,7 +964,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep, - ToolApprovalResponseContent { ToolCall: McpServerToolCallContent } funcResp => ResponseItem.CreateMcpApprovalResponseItem(funcResp.RequestId, funcResp.Approved), + ToolApprovalResponseContent { ToolCall: McpServerToolCallContent } toolResp => ResponseItem.CreateMcpApprovalResponseItem(toolResp.RequestId, toolResp.Approved), _ => null }; @@ -1046,8 +1046,8 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable))))); break; - case ToolApprovalRequestContent funcReq when funcReq.ToolCall is McpServerToolCallContent mcpToolCall: + case ToolApprovalRequestContent toolReq when toolReq.ToolCall is McpServerToolCallContent mcpToolCall: yield return ResponseItem.CreateMcpApprovalRequestItem( - funcReq.RequestId, + toolReq.RequestId, mcpToolCall.ServerName, mcpToolCall.Name, BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes( diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index fd847eb56e0..188b6f3fa64 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1464,16 +1464,16 @@ private static bool CurrentActivityIsInvokeAgent var content = message.Contents[j]; switch (content) { - case ToolApprovalRequestContent farc when farc.ToolCall is FunctionCallContent { InformationalOnly: false }: + case ToolApprovalRequestContent tarc when tarc.ToolCall is FunctionCallContent { InformationalOnly: false }: // Validation: Capture each call id for each approval request to ensure later we have a matching response. - _ = (approvalRequestCallIds ??= []).Add(farc.ToolCall.CallId); - (allApprovalRequestsMessages ??= []).Add(farc.RequestId, message); + _ = (approvalRequestCallIds ??= []).Add(tarc.ToolCall.CallId); + (allApprovalRequestsMessages ??= []).Add(tarc.RequestId, message); break; - case ToolApprovalResponseContent farc when farc.ToolCall is FunctionCallContent { InformationalOnly: false }: + case ToolApprovalResponseContent tarc when tarc.ToolCall is FunctionCallContent { InformationalOnly: false }: // Validation: Remove the call id for each approval response, to check it off the list of requests we need responses for. - _ = approvalRequestCallIds?.Remove(farc.ToolCall.CallId); - (allApprovalResponses ??= []).Add(farc); + _ = approvalRequestCallIds?.Remove(tarc.ToolCall.CallId); + (allApprovalResponses ??= []).Add(tarc); break; case FunctionResultContent frc: diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 55e3cd4a0a1..6a1c6c6b1b2 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -258,30 +258,6 @@ internal static string SerializeChatMessages( m.Parts.Add(new OtelGenericPart { Type = "reasoning", Content = trc.Text }); break; - case McpServerToolCallContent mstcc: - m.Parts.Add(new OtelServerToolCallPart - { - Id = mstcc.CallId, - Name = mstcc.Name, - ServerToolCall = new OtelMcpToolCall - { - Arguments = mstcc.Arguments, - ServerName = mstcc.ServerName, - }, - }); - break; - - case McpServerToolResultContent mstrc: - m.Parts.Add(new OtelServerToolCallResponsePart - { - Id = mstrc.CallId, - ServerToolCallResponse = new OtelMcpToolCallResponse - { - Output = mstrc.Outputs, - }, - }); - break; - case FunctionCallContent fcc: m.Parts.Add(new OtelToolCallRequestPart { @@ -381,6 +357,30 @@ internal static string SerializeChatMessages( }); break; + case McpServerToolCallContent mstcc: + m.Parts.Add(new OtelServerToolCallPart + { + Id = mstcc.CallId, + Name = mstcc.Name, + ServerToolCall = new OtelMcpToolCall + { + Arguments = mstcc.Arguments, + ServerName = mstcc.ServerName, + }, + }); + break; + + case McpServerToolResultContent mstrc: + m.Parts.Add(new OtelServerToolCallResponsePart + { + Id = mstrc.CallId, + ServerToolCallResponse = new OtelMcpToolCallResponse + { + Output = mstrc.Outputs, + }, + }); + break; + case ToolApprovalRequestContent fareqc when fareqc.ToolCall is McpServerToolCallContent mcpToolCall: m.Parts.Add(new OtelServerToolCallPart { 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 47f61ee674c..2cf79cbcbc2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs @@ -415,6 +415,7 @@ public void Serialization_Roundtrips() }; AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); static void AssertSerializationRoundtrips(FunctionCallContent content) @@ -431,5 +432,4 @@ static void AssertSerializationRoundtrips(FunctionCallContent content) Assert.Equal("value1", deserializedContent.Arguments["arg1"]?.ToString()); } } - } 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 0709be7f9fd..bcb50b36c85 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionResultContentTests.cs @@ -98,6 +98,7 @@ public void Serialization_Roundtrips() var content = new FunctionResultContent("call123", "result"); AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); AssertSerializationRoundtrips(content); static void AssertSerializationRoundtrips(FunctionResultContent content) @@ -112,5 +113,4 @@ static void AssertSerializationRoundtrips(FunctionResultContent content) Assert.Equal("result", deserializedContent.Result?.ToString()); } } - } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs index 63b58a34456..5930e4a6c4f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs @@ -25,7 +25,6 @@ public void Constructor_InvalidArguments_Throws() public void Constructor_Roundtrips(string id) { TestInputRequestContent content = new(id); - Assert.Equal(id, content.RequestId); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs index 1ab1d2387ba..e5cae07ea58 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputResponseContentTests.cs @@ -24,7 +24,6 @@ public void Constructor_InvalidArguments_Throws() public void Constructor_Roundtrips(string id) { TestInputResponseContent content = new(id); - Assert.Equal(id, content.RequestId); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index ec5c80c9234..4e6f03278ac 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -1589,19 +1589,12 @@ private static List CloneInput(List input) => { InformationalOnly = fcc.InformationalOnly }, - ToolApprovalRequestContent farc when farc.ToolCall is McpServerToolCallContent => - new ToolApprovalRequestContent(farc.RequestId, (McpServerToolCallContent)CloneFcc(farc.ToolCall)), - ToolApprovalRequestContent farc => - new ToolApprovalRequestContent(farc.RequestId, (FunctionCallContent)CloneFcc(farc.ToolCall)), - ToolApprovalResponseContent farc when farc.ToolCall is McpServerToolCallContent => - new ToolApprovalResponseContent(farc.RequestId, farc.Approved, (McpServerToolCallContent)CloneFcc(farc.ToolCall)) + ToolApprovalRequestContent tarc => + new ToolApprovalRequestContent(tarc.RequestId, (ToolCallContent)CloneFcc(tarc.ToolCall)), + ToolApprovalResponseContent tarc => + new ToolApprovalResponseContent(tarc.RequestId, tarc.Approved, (ToolCallContent)CloneFcc(tarc.ToolCall)) { - Reason = farc.Reason - }, - ToolApprovalResponseContent farc => - new ToolApprovalResponseContent(farc.RequestId, farc.Approved, (FunctionCallContent)CloneFcc(farc.ToolCall)) - { - Reason = farc.Reason + Reason = tarc.Reason }, _ => c }; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 03f00e8c13c..ffcbcdc05ec 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -403,7 +403,7 @@ public async Task FunctionReturningFunctionResultContentWithMatchingCallId_UsesI { FunctionInvoker = (ctx, cancellationToken) => { - returnedFrc = new FunctionResultContent(ctx.CallContent.CallId!, "Custom result from function") + returnedFrc = new FunctionResultContent(ctx.CallContent.CallId, "Custom result from function") { RawRepresentation = "CustomRaw" }; @@ -566,7 +566,7 @@ public async Task FunctionReturningDerivedFunctionResultContent_PropagatesInstan FunctionInvoker = (ctx, cancellationToken) => { // Return a derived FunctionResultContent - returnedFrc = new DerivedFunctionResultContent(ctx.CallContent.CallId!, "Derived result") + returnedFrc = new DerivedFunctionResultContent(ctx.CallContent.CallId, "Derived result") { CustomProperty = "CustomValue" }; @@ -2258,7 +2258,7 @@ private static List CloneContents(IList contents) // Clone FunctionCallContent to avoid sharing InformationalOnly state if (content is FunctionCallContent fcc) { - cloned.Add(new FunctionCallContent(fcc.CallId!, fcc.Name, fcc.Arguments) + cloned.Add(new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) { InformationalOnly = fcc.InformationalOnly, Exception = fcc.Exception, From 5343bb03380eaf85a8915ed00a531ae994222930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Tue, 17 Feb 2026 17:46:53 -0600 Subject: [PATCH 11/11] ToolApproval: remove specialized ctors --- .../Contents/ToolApprovalRequestContent.cs | 52 +------- .../Contents/ToolApprovalResponseContent.cs | 44 +------ .../Microsoft.Extensions.AI.Abstractions.json | 16 --- .../ToolApprovalRequestContentTests.cs | 25 +--- .../ToolApprovalResponseContentTests.cs | 25 +--- .../OpenAIResponseClientTests.cs | 123 ++++++++++++++++++ 6 files changed, 144 insertions(+), 141 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs index cc8c7db410d..da5e75d49a8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.ComponentModel; using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; @@ -16,49 +15,16 @@ public sealed class ToolApprovalRequestContent : InputRequestContent /// /// Initializes a new instance of the class. /// - /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified . - /// The function call that requires approval before execution. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public ToolApprovalRequestContent(string requestId, FunctionCallContent functionCall) - : base(requestId) - { - ToolCall = Throw.IfNull(functionCall); - } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier that correlates this request with its corresponding response. This may differ from the of the specified . - /// The MCP server tool call that requires approval before execution. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public ToolApprovalRequestContent(string requestId, McpServerToolCallContent mcpServerToolCall) - : base(requestId) - { - ToolCall = Throw.IfNull(mcpServerToolCall); - } - - /// - /// Initializes a new instance of the class for JSON deserialization. - /// /// The unique identifier that correlates this request with its corresponding response. /// The tool call that requires approval before execution. + /// is . + /// is empty or composed entirely of whitespace. + /// is . [JsonConstructor] - [EditorBrowsable(EditorBrowsableState.Never)] public ToolApprovalRequestContent(string requestId, ToolCallContent toolCall) : base(requestId) { - _ = Throw.IfNull(toolCall); - - if (toolCall is not FunctionCallContent and not McpServerToolCallContent) - { - Throw.ArgumentException(nameof(toolCall), $"Unsupported type '{toolCall.GetType().Name}'."); - } - - ToolCall = toolCall; + ToolCall = Throw.IfNull(toolCall); } /// @@ -72,12 +38,6 @@ public ToolApprovalRequestContent(string requestId, ToolCallContent toolCall) /// if the tool call is approved; otherwise, . /// An optional reason for the approval or rejection. /// The correlated with this request. - public ToolApprovalResponseContent CreateResponse(bool approved, string? reason = null) => ToolCall switch - { - FunctionCallContent fcc => new ToolApprovalResponseContent(RequestId, approved, fcc) { Reason = reason }, - McpServerToolCallContent mcp => new ToolApprovalResponseContent(RequestId, approved, mcp) { Reason = reason }, - - // This should never occur since the constructor enforces the allowed types. - _ => throw new InvalidOperationException($"Unsupported ToolCallContent type '{ToolCall.GetType().Name}'."), - }; + public ToolApprovalResponseContent CreateResponse(bool approved, string? reason = null) => + new ToolApprovalResponseContent(RequestId, approved, ToolCall) { Reason = reason }; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs index 2fd9bf7c29e..ab073f36b2d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.ComponentModel; using System.Text.Json.Serialization; using Microsoft.Shared.Diagnostics; @@ -18,53 +17,16 @@ public sealed class ToolApprovalResponseContent : InputResponseContent /// /// The unique identifier of the associated with this response. /// if the tool call is approved; otherwise, . - /// The function call that was subject to approval. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public ToolApprovalResponseContent(string requestId, bool approved, FunctionCallContent functionCall) - : base(requestId) - { - Approved = approved; - ToolCall = Throw.IfNull(functionCall); - } - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the associated with this response. - /// if the tool call is approved; otherwise, . - /// The MCP server tool call that was subject to approval. + /// The tool call that was subject to approval. /// is . /// is empty or composed entirely of whitespace. - /// is . - public ToolApprovalResponseContent(string requestId, bool approved, McpServerToolCallContent mcpServerToolCall) - : base(requestId) - { - Approved = approved; - ToolCall = Throw.IfNull(mcpServerToolCall); - } - - /// - /// Initializes a new instance of the class for JSON deserialization. - /// - /// The unique identifier of the associated with this response. - /// if the tool call is approved; otherwise, . - /// The tool call that was subject to approval. + /// is . [JsonConstructor] - [EditorBrowsable(EditorBrowsableState.Never)] public ToolApprovalResponseContent(string requestId, bool approved, ToolCallContent toolCall) : base(requestId) { - _ = Throw.IfNull(toolCall); - - if (toolCall is not FunctionCallContent and not McpServerToolCallContent) - { - Throw.ArgumentException(nameof(toolCall), $"Unsupported type '{toolCall.GetType().Name}'."); - } - Approved = approved; - ToolCall = toolCall; + ToolCall = Throw.IfNull(toolCall); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index c8519971429..ec91cd3876c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -3266,14 +3266,6 @@ "Type": "sealed class Microsoft.Extensions.AI.ToolApprovalRequestContent : Microsoft.Extensions.AI.InputRequestContent", "Stage": "Stable", "Methods": [ - { - "Member": "Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolApprovalRequestContent(string requestId, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Stable" - }, - { - "Member": "Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolApprovalRequestContent(string requestId, Microsoft.Extensions.AI.McpServerToolCallContent mcpServerToolCall);", - "Stage": "Stable" - }, { "Member": "Microsoft.Extensions.AI.ToolApprovalRequestContent.ToolApprovalRequestContent(string requestId, Microsoft.Extensions.AI.ToolCallContent toolCall);", "Stage": "Stable" @@ -3294,14 +3286,6 @@ "Type": "sealed class Microsoft.Extensions.AI.ToolApprovalResponseContent : Microsoft.Extensions.AI.InputResponseContent", "Stage": "Stable", "Methods": [ - { - "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Stable" - }, - { - "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.McpServerToolCallContent mcpServerToolCall);", - "Stage": "Stable" - }, { "Member": "Microsoft.Extensions.AI.ToolApprovalResponseContent.ToolApprovalResponseContent(string requestId, bool approved, Microsoft.Extensions.AI.ToolCallContent toolCall);", "Stage": "Stable" diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs index 6a031c75b09..f70edfa3e7b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs @@ -13,31 +13,18 @@ public class ToolApprovalRequestContentTests [Fact] public void Constructor_InvalidArguments_Throws() { - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - McpServerToolCallContent mcpCall = new("MCC1", "TestTool", "TestServer"); - - // FunctionCallContent overload - Assert.Throws("requestId", () => new ToolApprovalRequestContent(null!, functionCall)); - Assert.Throws("requestId", () => new ToolApprovalRequestContent("", functionCall)); - Assert.Throws("requestId", () => new ToolApprovalRequestContent("\r\t\n ", functionCall)); - Assert.Throws("functionCall", () => new ToolApprovalRequestContent("id", (FunctionCallContent)null!)); - - // McpServerToolCallContent overload - Assert.Throws("requestId", () => new ToolApprovalRequestContent(null!, mcpCall)); - Assert.Throws("requestId", () => new ToolApprovalRequestContent("", mcpCall)); - Assert.Throws("requestId", () => new ToolApprovalRequestContent("\r\t\n ", mcpCall)); - Assert.Throws("mcpServerToolCall", () => new ToolApprovalRequestContent("id", (McpServerToolCallContent)null!)); - - // ToolCallContent (JsonConstructor) overload - Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", (ToolCallContent)null!)); - Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", new CodeInterpreterToolCallContent("call1"))); - Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", new ImageGenerationToolCallContent("call1"))); + Assert.Throws("requestId", () => new ToolApprovalRequestContent(null!, new FunctionCallContent("FCC1", "TestFunction"))); + Assert.Throws("requestId", () => new ToolApprovalRequestContent("", new FunctionCallContent("FCC1", "TestFunction"))); + Assert.Throws("requestId", () => new ToolApprovalRequestContent("\r\t\n ", new FunctionCallContent("FCC1", "TestFunction"))); + Assert.Throws("toolCall", () => new ToolApprovalRequestContent("id", null!)); } public static TheoryData ToolCallContentInstances => new() { new FunctionCallContent("FCC1", "TestFunction", new Dictionary { { "param1", 123 } }), new McpServerToolCallContent("MCC1", "TestTool", "TestServer") { Arguments = new Dictionary { { "arg1", "value1" } } }, + new CodeInterpreterToolCallContent("CI1") { Inputs = [new DataContent("print('hello')"u8.ToArray(), "text/x-python")] }, + new ImageGenerationToolCallContent("IG1"), }; [Theory] diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs index 209280d3ab5..37a83712233 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs @@ -13,31 +13,18 @@ public class ToolApprovalResponseContentTests [Fact] public void Constructor_InvalidArguments_Throws() { - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - McpServerToolCallContent mcpCall = new("MCC1", "TestTool", "TestServer"); - - // FunctionCallContent overload - Assert.Throws("requestId", () => new ToolApprovalResponseContent(null!, true, functionCall)); - Assert.Throws("requestId", () => new ToolApprovalResponseContent("", true, functionCall)); - Assert.Throws("requestId", () => new ToolApprovalResponseContent("\r\t\n ", true, functionCall)); - Assert.Throws("functionCall", () => new ToolApprovalResponseContent("id", true, (FunctionCallContent)null!)); - - // McpServerToolCallContent overload - Assert.Throws("requestId", () => new ToolApprovalResponseContent(null!, true, mcpCall)); - Assert.Throws("requestId", () => new ToolApprovalResponseContent("", true, mcpCall)); - Assert.Throws("requestId", () => new ToolApprovalResponseContent("\r\t\n ", true, mcpCall)); - Assert.Throws("mcpServerToolCall", () => new ToolApprovalResponseContent("id", true, (McpServerToolCallContent)null!)); - - // ToolCallContent (JsonConstructor) overload - Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, (ToolCallContent)null!)); - Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, new CodeInterpreterToolCallContent("call1"))); - Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, new ImageGenerationToolCallContent("call1"))); + Assert.Throws("requestId", () => new ToolApprovalResponseContent(null!, true, new FunctionCallContent("FCC1", "TestFunction"))); + Assert.Throws("requestId", () => new ToolApprovalResponseContent("", true, new FunctionCallContent("FCC1", "TestFunction"))); + Assert.Throws("requestId", () => new ToolApprovalResponseContent("\r\t\n ", true, new FunctionCallContent("FCC1", "TestFunction"))); + Assert.Throws("toolCall", () => new ToolApprovalResponseContent("id", true, null!)); } public static TheoryData ToolCallContentInstances => new() { new FunctionCallContent("FCC1", "TestFunction", new Dictionary { { "param1", 123 } }), new McpServerToolCallContent("MCC1", "TestTool", "TestServer") { Arguments = new Dictionary { { "arg1", "value1" } } }, + new CodeInterpreterToolCallContent("CI1") { Inputs = [new DataContent("print('hello')"u8.ToArray(), "text/x-python")] }, + new ImageGenerationToolCallContent("IG1"), }; [Theory] diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 52823c80a5e..bd11a3f2739 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1560,6 +1560,129 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) } } + [Theory] + [InlineData("user")] + [InlineData("tool")] + public async Task ToolApprovalResponse_NonMcpToolCall_SilentlyIgnored_NonStreaming(string role) + { + // When a ToolApprovalResponseContent wraps a non-MCP tool call (e.g. CodeInterpreterToolCallContent + // or ImageGenerationToolCallContent), the client should silently drop it from the request payload. + // For the user role, the text content is preserved; for the tool role, the function result is preserved. + string input = role == "user" + ? """ + { + "model": "gpt-4o-mini", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello" + } + ] + } + ] + } + """ + : """ + { + "model": "gpt-4o-mini", + "input": [ + { + "type": "function_call_output", + "call_id": "call-1", + "output": "tool result" + } + ] + } + """; + + const string Output = """ + { + "id": "resp_fallthrough_test_001", + "object": "response", + "created_at": 1741891428, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "type": "message", + "id": "msg_fallthrough_test_001", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Hello! How can I help?", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "usage": { + "input_tokens": 10, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 8, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18 + }, + "user": null, + "metadata": {} + } + """; + + using VerbatimHttpHandler handler = new(input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + // Build a message with role-appropriate content + non-MCP approval responses that should be silently dropped. + var codeInterpreterApproval = new ToolApprovalResponseContent("req-ci-1", true, new CodeInterpreterToolCallContent("ci-call-1")); + var imageGenApproval = new ToolApprovalResponseContent("req-ig-1", false, new ImageGenerationToolCallContent("ig-call-1")); + + AIContent anchorContent = role == "user" + ? new TextContent("hello") + : new FunctionResultContent("call-1", "tool result"); + + var messages = new List + { + new(new ChatRole(role), [anchorContent, codeInterpreterApproval, imageGenApproval]), + }; + + var response = await client.GetResponseAsync(messages); + + Assert.NotNull(response); + var message = Assert.Single(response.Messages); + Assert.Equal(ChatRole.Assistant, message.Role); + Assert.Equal("Hello! How can I help?", message.Text); + } + [Theory] [InlineData(false)] [InlineData(true)]