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/CompatibilitySuppressions.xml b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..b2eebebb7c0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,774 @@ + + + + + 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/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 + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + 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/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 + + + CP0001 + T:Microsoft.Extensions.AI.FunctionApprovalRequestContent + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0001 + 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/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 + 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.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/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.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/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 + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + lib/net462/Microsoft.Extensions.AI.Abstractions.dll + true + + + 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 + + + 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.ImageGenerationToolCallContent.#ctor + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + 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.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.ImageGenerationToolResultContent.#ctor + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net8.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + 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.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.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 + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolCallContent.#ctor + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + 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 + + + CP0002 + M:Microsoft.Extensions.AI.CodeInterpreterToolResultContent.#ctor + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + 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 + + + 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.ImageGenerationToolCallContent.#ctor + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + 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.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.ImageGenerationToolResultContent.#ctor + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + lib/net9.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + 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 + + + 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 + + + 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 + + + CP0002 + 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.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.CodeInterpreterToolResultContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + 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.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.ImageGenerationToolCallContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + 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.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.ImageGenerationToolResultContent.#ctor + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.AI.Abstractions.dll + true + + + CP0002 + 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.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.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 + + \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index af8b19c8d84..61f159afc3f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -18,20 +18,20 @@ namespace Microsoft.Extensions.AI; [JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")] [JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")] [JsonDerivedType(typeof(UsageContent), typeDiscriminator: "usage")] +[JsonDerivedType(typeof(ToolApprovalRequestContent), typeDiscriminator: "toolApprovalRequest")] +[JsonDerivedType(typeof(ToolApprovalResponseContent), typeDiscriminator: "toolApprovalResponse")] +[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")] +// [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..c155b0228b8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs @@ -15,20 +15,17 @@ 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. + 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..9384c927cb7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs @@ -11,20 +11,17 @@ 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. + 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/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs deleted file mode 100644 index f5a394cd63d..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs +++ /dev/null @@ -1,43 +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 a function call. -/// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class FunctionApprovalRequestContent : UserInputRequestContent -{ - /// - /// 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. - /// is . - public FunctionApprovalRequestContent(string id, FunctionCallContent functionCall) - : base(id) - { - FunctionCall = Throw.IfNull(functionCall); - } - - /// - /// Gets the function call that pre-invoke approval is required for. - /// - public FunctionCallContent FunctionCall { 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, . - /// 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 }; -} 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 5cc04c61442..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs +++ /dev/null @@ -1,47 +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 response to a function approval request. -/// -[Experimental(DiagnosticIds.Experiments.AIFunctionApprovals, UrlFormat = DiagnosticIds.UrlFormat)] -public sealed class FunctionApprovalResponseContent : UserInputResponseContent -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID that uniquely identifies the function approval request/response pair. - /// if the function call is approved; otherwise, . - /// The function call that requires user approval. - /// is . - /// is empty or composed entirely of whitespace. - /// is . - public FunctionApprovalResponseContent(string id, bool approved, FunctionCallContent functionCall) - : base(id) - { - Approved = approved; - FunctionCall = Throw.IfNull(functionCall); - } - - /// - /// Gets a value indicating whether the user approved the request. - /// - public bool Approved { get; } - - /// - /// Gets the function call for which approval was requested. - /// - 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 6ec7febc486..f83e3c94f4b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.AI; /// Represents a function call request. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class FunctionCallContent : AIContent +public class FunctionCallContent : ToolCallContent { /// /// Initializes a new instance of the class. @@ -24,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. /// @@ -51,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 d5eb4884709..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,7 +12,7 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a function call. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class FunctionResultContent : AIContent +public class FunctionResultContent : ToolResultContent { /// /// Initializes a new instance of the class. @@ -26,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/ImageGenerationToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs index 9e22207cf00..45522e3d599 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolCallContent.cs @@ -10,17 +10,14 @@ 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. + 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..796583130cf 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ImageGenerationToolResultContent.cs @@ -15,20 +15,17 @@ 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. + 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/InputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/InputRequestContent.cs new file mode 100644 index 00000000000..8272191f648 --- /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(ToolApprovalRequestContent), "toolApprovalRequest")] +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..54f9642d9e6 --- /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(ToolApprovalResponseContent), "toolApprovalResponse")] +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..5f0bf047781 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.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; @@ -13,36 +11,37 @@ 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 : ToolCallContent { /// /// 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)) { - CallId = Throw.IfNullOrWhitespace(callId); - ToolName = Throw.IfNullOrWhitespace(toolName); + Name = Throw.IfNullOrWhitespace(name); ServerName = serverName; } /// - /// Gets the tool call ID. + /// Gets the name of the tool requested. /// - public string CallId { get; } - - /// - /// Gets the name of the tool called. - /// - public string ToolName { get; } + public string Name { get; } /// /// Gets the name of the MCP server that hosts the tool. @@ -50,7 +49,7 @@ public McpServerToolCallContent(string callId, string toolName, string? serverNa public string? ServerName { get; } /// - /// Gets or sets the arguments used for the tool call. + /// Gets or sets the arguments requested to be provided to the tool. /// - public IReadOnlyDictionary? Arguments { get; set; } + 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 a8798328019..684cbf688c3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.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; @@ -16,8 +14,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 : ToolResultContent { /// /// Initializes a new instance of the class. @@ -26,17 +23,12 @@ public sealed class McpServerToolResultContent : AIContent /// is . /// is empty or composed entirely of whitespace. public McpServerToolResultContent(string callId) + : base(Throw.IfNullOrWhitespace(callId)) { - CallId = Throw.IfNullOrWhitespace(callId); } /// - /// Gets the tool call ID. + /// Gets or sets the output contents of the tool call. /// - public string CallId { get; } - - /// - /// Gets or sets the output of the tool call. - /// - public IList? Output { get; set; } + 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..da5e75d49a8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalRequestContent.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.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. + /// The tool call that requires approval before execution. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + [JsonConstructor] + public ToolApprovalRequestContent(string requestId, ToolCallContent toolCall) + : base(requestId) + { + ToolCall = Throw.IfNull(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) => + 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 new file mode 100644 index 00000000000..ab073f36b2d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs @@ -0,0 +1,46 @@ +// 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 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 tool call that was subject to approval. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + [JsonConstructor] + public ToolApprovalResponseContent(string requestId, bool approved, ToolCallContent toolCall) + : base(requestId) + { + Approved = approved; + ToolCall = Throw.IfNull(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..8cc504dc540 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolCallContent.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; +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")] + +// 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 +{ + /// + /// Initializes a new instance of the class. + /// + /// The tool call ID. + /// is . + internal 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..64e1b2c5510 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolResultContent.cs @@ -0,0 +1,44 @@ +// 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")] + +// 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 +{ + /// + /// Initializes a new instance of the class. + /// + /// The tool call ID for which this is the result. + /// is . + internal 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/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..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 @@ -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" } ] }, @@ -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" @@ -1930,51 +1922,7 @@ ] }, { - "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalRequestContent : Microsoft.Extensions.AI.UserInputRequestContent", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionApprovalRequestContent(string id, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.CreateResponse(bool approved, string? reason = null);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalRequestContent.FunctionCall { get; }", - "Stage": "Experimental" - } - ] - }, - { - "Type": "sealed class Microsoft.Extensions.AI.FunctionApprovalResponseContent : Microsoft.Extensions.AI.UserInputResponseContent", - "Stage": "Experimental", - "Methods": [ - { - "Member": "Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionApprovalResponseContent(string id, bool approved, Microsoft.Extensions.AI.FunctionCallContent functionCall);", - "Stage": "Experimental" - } - ], - "Properties": [ - { - "Member": "bool Microsoft.Extensions.AI.FunctionApprovalResponseContent.Approved { get; }", - "Stage": "Experimental" - }, - { - "Member": "Microsoft.Extensions.AI.FunctionCallContent Microsoft.Extensions.AI.FunctionApprovalResponseContent.FunctionCall { get; }", - "Stage": "Experimental" - }, - { - "Member": "string? Microsoft.Extensions.AI.FunctionApprovalResponseContent.Reason { get; set; }", - "Stage": "Experimental" - } - ] - }, - { - "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 +1939,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" @@ -2010,7 +1954,7 @@ ] }, { - "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 +1963,6 @@ } ], "Properties": [ - { - "Member": "string Microsoft.Extensions.AI.FunctionResultContent.CallId { get; }", - "Stage": "Stable" - }, { "Member": "System.Exception? Microsoft.Extensions.AI.FunctionResultContent.Exception { get; set; }", "Stage": "Stable" @@ -2227,145 +2167,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" } ] }, @@ -2607,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" @@ -2701,114 +2627,102 @@ ] }, { - "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.ToolCallContent", + "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": "System.Collections.Generic.IDictionary? Microsoft.Extensions.AI.McpServerToolCallContent.Arguments { get; set; }", + "Stage": "Stable" }, { - "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.CallId { get; }", - "Stage": "Experimental" + "Member": "string Microsoft.Extensions.AI.McpServerToolCallContent.Name { get; }", + "Stage": "Stable" }, { "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.ToolResultContent", + "Stage": "Stable", "Methods": [ { "Member": "Microsoft.Extensions.AI.McpServerToolResultContent.McpServerToolResultContent(string callId);", - "Stage": "Experimental" + "Stage": "Stable" } ], "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" + "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.McpServerToolResultContent.Outputs { get; set; }", + "Stage": "Stable" } ] }, @@ -3348,6 +3262,70 @@ } ] }, + { + "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.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.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": "class Microsoft.Extensions.AI.ToolCallContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.ToolCallContent.CallId { get; }", + "Stage": "Stable" + } + ] + }, + { + "Type": "class Microsoft.Extensions.AI.ToolResultContent : Microsoft.Extensions.AI.AIContent", + "Stage": "Stable", + "Properties": [ + { + "Member": "string Microsoft.Extensions.AI.ToolResultContent.CallId { get; }", + "Stage": "Stable" + } + ] + }, { "Type": "class Microsoft.Extensions.AI.UriContent : Microsoft.Extensions.AI.AIContent", "Stage": "Stable", @@ -3435,38 +3413,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..845769658d1 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -51,17 +51,17 @@ 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); 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. @@ -123,16 +123,19 @@ 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., 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(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.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/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..d5b571308eb 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); @@ -412,9 +431,8 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; case StreamingResponseImageGenerationCallInProgressUpdate imageGenInProgress: - yield return CreateUpdate(new ImageGenerationToolCallContent + yield return CreateUpdate(new ImageGenerationToolCallContent(imageGenInProgress.ItemId) { - ImageId = imageGenInProgress.ItemId, RawRepresentation = imageGenInProgress, }); break; @@ -424,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, }); @@ -447,9 +464,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 ToolApprovalRequestContent(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 +478,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 ToolApprovalResponseContent( + 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 +674,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 +931,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null; + Dictionary? idToContentMapping = null; foreach (ChatMessage input in inputs) { @@ -919,7 +964,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep, - McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved), + ToolApprovalResponseContent { ToolCall: McpServerToolCallContent } toolResp => ResponseItem.CreateMcpApprovalResponseItem(toolResp.RequestId, toolResp.Approved), _ => null }; @@ -1001,6 +1046,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable contents) { @@ -1115,10 +1164,6 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera break; } break; - - case McpServerToolApprovalResponseContent mcpApprovalResponseContent: - yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved); - break; } } @@ -1146,6 +1191,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 +1204,33 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))); break; - case McpServerToolApprovalRequestContent mcpApprovalRequestContent: + case ToolApprovalRequestContent toolReq when toolReq.ToolCall 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; + toolReq.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.Outputs?.OfType().FirstOrDefault() is ErrorContent errorContent) { mtci.Error = BinaryData.FromString(errorContent.Message); } else { - mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []); + mtci.ToolOutput = string.Concat(mstrc.Outputs?.OfType() ?? []); } yield return mtci; @@ -1367,7 +1415,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 +1425,9 @@ private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents.Add(new McpServerToolResultContent(mtci.Id) { RawRepresentation = mtci, - Output = [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)], }); } @@ -1394,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 { @@ -1412,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}")] }); @@ -1430,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/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 9fd95df0424..188b6f3fa64 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,16 +1464,16 @@ private static bool CurrentActivityIsInvokeAgent var content = message.Contents[j]; switch (content) { - case FunctionApprovalRequestContent farc: + 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.FunctionCall.CallId); - (allApprovalRequestsMessages ??= []).Add(farc.Id, message); + _ = (approvalRequestCallIds ??= []).Add(tarc.ToolCall.CallId); + (allApprovalRequestsMessages ??= []).Add(tarc.RequestId, message); break; - case FunctionApprovalResponseContent farc: + 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.FunctionCall.CallId); - (allApprovalResponses ??= []).Add(farc); + _ = approvalRequestCallIds?.Remove(tarc.ToolCall.CallId); + (allApprovalResponses ??= []).Add(tarc); break; case FunctionResultContent frc: @@ -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,18 +1531,18 @@ 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; ChatMessage? requestMessage = null; - _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.Id, out requestMessage); + _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.RequestId, out requestMessage); (targetList ??= []).Add(new() { Response = approvalResponse, RequestMessage = requestMessage }); } @@ -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(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(functionCall.CallId, functionCall); + message.Contents[contentIndex] = new ToolApprovalRequestContent(ComposeApprovalRequestId(functionCall.CallId), functionCall); outputMessages[messageIndex] = message; lastMessageIndex = messageIndex; @@ -1783,8 +1783,11 @@ 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 + /// Execute the provided and return the resulting /// wrapped in objects. /// private async Task<(IList? FunctionResultContentMessages, bool ShouldTerminate, int ConsecutiveErrorCount)> InvokeApprovedFunctionApprovalResponsesAsync( @@ -1800,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); @@ -1900,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 bcb4fb2a241..6a1c6c6b1b2 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; @@ -340,7 +340,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(), }); @@ -349,7 +349,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, @@ -361,7 +361,7 @@ internal static string SerializeChatMessages( m.Parts.Add(new OtelServerToolCallPart { Id = mstcc.CallId, - Name = mstcc.ToolName, + Name = mstcc.Name, ServerToolCall = new OtelMcpToolCall { Arguments = mstcc.Arguments, @@ -376,31 +376,31 @@ internal static string SerializeChatMessages( Id = mstrc.CallId, ServerToolCallResponse = new OtelMcpToolCallResponse { - Output = mstrc.Output, + Output = mstrc.Outputs, }, }); break; - case McpServerToolApprovalRequestContent mstarc: + case ToolApprovalRequestContent fareqc when fareqc.ToolCall is McpServerToolCallContent mcpToolCall: m.Parts.Add(new OtelServerToolCallPart { - Id = mstarc.Id, - Name = mstarc.ToolCall.ToolName, + Id = fareqc.RequestId, + Name = mcpToolCall.Name, ServerToolCall = new OtelMcpApprovalRequest { - Arguments = mstarc.ToolCall.Arguments, - ServerName = mstarc.ToolCall.ServerName, + Arguments = mcpToolCall.Arguments, + ServerName = mcpToolCall.ServerName, }, }); break; - case McpServerToolApprovalResponseContent mstaresp: + case ToolApprovalResponseContent farespc when farespc.ToolCall 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 @@ -927,3 +927,4 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(IEnumerable))] private sealed partial class OtelContext : JsonSerializerContext; } + 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..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,10 +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. /// @@ -119,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 97aa9b04995..cd0a171d798 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,16 +70,25 @@ 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 McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new McpServerToolApprovalResponseContent("request123", approved: true), - new ImageGenerationToolCallContent { ImageId = "img123" }, - new ImageGenerationToolResultContent { ImageId = "img456", Outputs = [new DataContent(new byte[] { 4, 5, 6 }, "image/png")] } + new ToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), + new ToolApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2")), + new ImageGenerationToolCallContent("img123"), + new ImageGenerationToolResultContent("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/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/FunctionApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs deleted file mode 100644 index cc5cc1dd8d9..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs +++ /dev/null @@ -1,93 +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.Text.Json; -using Xunit; - -namespace Microsoft.Extensions.AI.Contents; - -public class FunctionApprovalRequestContentTests -{ - [Fact] - 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("functionCall", () => new FunctionApprovalRequestContent("id", null!)); - } - - [Theory] - [InlineData("abc")] - [InlineData("123")] - [InlineData("!@#")] - public void Constructor_Roundtrips(string id) - { - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - - FunctionApprovalRequestContent content = new(id, functionCall); - - Assert.Same(id, content.Id); - Assert.Same(functionCall, content.FunctionCall); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CreateResponse_ReturnsExpectedResponse(bool approved) - { - string id = "req-1"; - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - - FunctionApprovalRequestContent content = new(id, functionCall); - - var response = content.CreateResponse(approved); - - Assert.NotNull(response); - Assert.Same(id, response.Id); - Assert.Equal(approved, response.Approved); - Assert.Same(functionCall, response.FunctionCall); - Assert.Null(response.Reason); - } - - [Theory] - [InlineData(true, "Approved for testing")] - [InlineData(false, "Rejected due to security concerns")] - [InlineData(true, null)] - [InlineData(false, null)] - public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, string? reason) - { - string id = "req-1"; - FunctionCallContent functionCall = new("FCC1", "TestFunction"); - - FunctionApprovalRequestContent content = new(id, functionCall); - - var response = content.CreateResponse(approved, reason); - - Assert.NotNull(response); - Assert.Same(id, response.Id); - Assert.Equal(approved, response.Approved); - Assert.Same(functionCall, response.FunctionCall); - Assert.Equal(reason, response.Reason); - } - - [Fact] - 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); - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs deleted file mode 100644 index 405955463a1..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs +++ /dev/null @@ -1,59 +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 FunctionApprovalResponseContentTests -{ - [Fact] - 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("functionCall", () => new FunctionApprovalResponseContent("id", true, null!)); - } - - [Theory] - [InlineData("abc", true)] - [InlineData("123", false)] - [InlineData("!@#", true)] - 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.Equal(approved, content.Approved); - Assert.Same(functionCall, content.FunctionCall); - } - - [Theory] - [InlineData(null)] - [InlineData("Custom rejection reason")] - public void Serialization_Roundtrips(string? reason) - { - var content = new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")) - { - Reason = reason - }; - - var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); - var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); - - Assert.NotNull(deserializedContent); - Assert.Equal(content.Id, deserializedContent.Id); - 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); - } -} 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..2cf79cbcbc2 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,31 @@ 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); + 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()); + } + } } 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..bcb50b36c85 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,26 @@ 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); + 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()); + } + } } 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/InputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/InputRequestContentTests.cs new file mode 100644 index 00000000000..5930e4a6c4f --- /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 ToolApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } })), + new ToolApprovalRequestContent("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..e5cae07ea58 --- /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 ToolApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")), + new ToolApprovalResponseContent("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..eb17c95cd43 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,7 +19,7 @@ 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); } @@ -39,12 +40,12 @@ 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.Equal("callId1", c.CallId); - Assert.Equal("toolName", c.ToolName); + Assert.Equal("toolName", c.Name); Assert.Equal("serverName", c.ServerName); } @@ -52,9 +53,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..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,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; @@ -17,7 +17,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.Outputs); } [Fact] @@ -37,10 +37,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.Outputs); + IList outputs = [new TextContent("test result")]; + c.Outputs = outputs; + Assert.Same(outputs, c.Outputs); } [Fact] @@ -55,14 +55,25 @@ public void Serialization_Roundtrips() { var content = new McpServerToolResultContent("call123") { - Output = new List { new TextContent("result") } + Outputs = [new TextContent("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.NotNull(deserializedContent.Outputs); + Assert.Equal("result", Assert.IsType(Assert.Single(deserializedContent.Outputs)).Text); + } } } + diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs new file mode 100644 index 00000000000..f70edfa3e7b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalRequestContentTests.cs @@ -0,0 +1,102 @@ +// 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.Contents; + +public class ToolApprovalRequestContentTests +{ + [Fact] + public void Constructor_InvalidArguments_Throws() + { + 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] + [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(toolCall, content.ToolCall); + } + + [Theory] + [MemberData(nameof(ToolCallContentInstances))] + public void CreateResponse_ReturnsExpectedResponse(ToolCallContent toolCall) + { + string id = "req-1"; + ToolApprovalRequestContent content = new(id, toolCall); + + var response = content.CreateResponse(approved: true); + + Assert.NotNull(response); + Assert.Same(id, response.RequestId); + Assert.True(response.Approved); + Assert.Same(toolCall, response.ToolCall); + Assert.Null(response.Reason); + } + + [Theory] + [InlineData(true, "Approved for testing")] + [InlineData(false, "Rejected due to security concerns")] + [InlineData(true, null)] + [InlineData(false, null)] + public void CreateResponse_WithReason_ReturnsExpectedResponse(bool approved, string? reason) + { + string id = "req-1"; + FunctionCallContent functionCall = new("FCC1", "TestFunction"); + + 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.ToolCall); + Assert.Equal(reason, response.Reason); + } + + [Theory] + [MemberData(nameof(ToolCallContentInstances))] + public void Serialization_Roundtrips(ToolCallContent toolCall) + { + var content = new ToolApprovalRequestContent("request123", toolCall); + + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(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); + Assert.Equal(content.RequestId, deserializedContent.RequestId); + Assert.NotNull(deserializedContent.ToolCall); + 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 new file mode 100644 index 00000000000..37a83712233 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolApprovalResponseContentTests.cs @@ -0,0 +1,99 @@ +// 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.Contents; + +public class ToolApprovalResponseContentTests +{ + [Fact] + public void Constructor_InvalidArguments_Throws() + { + 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] + [MemberData(nameof(ToolCallContentInstances))] + public void Constructor_Roundtrips(ToolCallContent toolCall) + { + ToolApprovalResponseContent content = new("req-1", true, 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); + } + + [Theory] + [MemberData(nameof(ToolCallContentInstances))] + public void Serialization_Roundtrips(ToolCallContent toolCall) + { + var content = new ToolApprovalResponseContent("request123", true, toolCall) + { + Reason = "Approved for testing" + }; + + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(content); + AssertSerializationRoundtrips(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); + Assert.Equal(content.RequestId, deserializedContent.RequestId); + Assert.Equal(content.Approved, deserializedContent.Approved); + Assert.Equal(content.Reason, deserializedContent.Reason); + Assert.NotNull(deserializedContent.ToolCall); + Assert.IsType(content.ToolCall.GetType(), deserializedContent.ToolCall); + Assert.Equal(content.ToolCall.CallId, deserializedContent.ToolCall.CallId); + } + } + + [Theory] + [InlineData(null)] + [InlineData("Custom rejection reason")] + public void Serialization_WithReason_Roundtrips(string? reason) + { + 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); + + Assert.NotNull(deserializedContent); + Assert.Equal(content.RequestId, deserializedContent.RequestId); + 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); + } +} 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..572c6c1a5d7 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolCallContentTests.cs @@ -0,0 +1,47 @@ +// 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() + { + 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 message.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 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(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 new file mode 100644 index 00000000000..f0c8d376f53 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ToolResultContentTests.cs @@ -0,0 +1,46 @@ +// 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() + { + 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 message.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 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(deserialized2.Contents[i]); + Assert.Equal(message.Contents[i].GetType(), deserialized2.Contents[i].GetType()); + } + } +} 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..65e175ceb90 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -39,8 +39,12 @@ 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(ToolCallContent[]))] +[JsonSerializable(typeof(ToolResultContent[]))] [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..0afdd6936a1 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 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.ToolCall); + Assert.Equal("ask_question", nestedMcpCall.Name); + Assert.Equal("deepwiki", nestedMcpCall.ServerName); + + // 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); + 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.ToolCall); + 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 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.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 -> 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.ToolCall); + Assert.Equal("ask_question", nestedMcpCall.Name); + Assert.Equal("deepwiki", nestedMcpCall.ServerName); + Assert.NotNull(nestedMcpCall.RawRepresentation); + Assert.Same(mcpApprovalRequestItem, nestedMcpCall.RawRepresentation); + + // 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); + 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.ToolCall); + 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 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.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") { Outputs = 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..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,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.ToolCall); + 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(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 abd1414d85f..bd11a3f2739 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; + 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; } @@ -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(Assert.Single(result.Outputs!)).Text); Assert.NotNull(response.Usage); Assert.Equal(542, response.Usage.InputTokenCount); @@ -1561,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)] @@ -1798,28 +1920,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(Assert.Single(firstResult.Outputs!)).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(Assert.Single(secondResult.Outputs!)).Text); Assert.NotNull(response.Usage); Assert.Equal(1329, response.Usage.InputTokenCount); @@ -2211,28 +2331,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(Assert.Single(firstResult.Outputs!)).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(Assert.Single(secondResult.Outputs!)).Text); Assert.NotNull(response.Usage); Assert.Equal(1420, response.Usage.InputTokenCount); @@ -2240,6 +2358,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(Assert.Single(toolResult.Outputs!)); + 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 +2624,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 +2639,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() { @@ -5633,12 +6038,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; @@ -5739,7 +6144,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 => @@ -5898,7 +6303,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] @@ -6109,3 +6514,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 0d2fd351c30..4e6f03278ac 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 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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("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 ToolApprovalRequestContent("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 ToolApprovalResponseContent("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 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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", false, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", false, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", false, new FunctionCallContent("callId1", "Func1")) + new ToolApprovalResponseContent("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 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("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 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("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 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("callId1", new FunctionCallContent("callId1", "Func1")), + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")), ]) { MessageId = "resp1" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("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("callId1", new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalRequestContent("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("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("callId3", new FunctionCallContent("callId3", "Func1")), + new ToolApprovalRequestContent("ficc_callId3", new FunctionCallContent("callId3", "Func1")), ]) { MessageId = "resp2" }, new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("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("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("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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("callId1", true, new FunctionCallContent("callId1", "Func1")), - new FunctionApprovalResponseContent("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]), @@ -1175,11 +1175,11 @@ public async Task ApprovalResponsePreservesOriginalRequestMessageMetadata() new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [ - new FunctionApprovalRequestContent("approval-request-id", new FunctionCallContent("function-call-id", "Func1")) + new ToolApprovalRequestContent("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")) + new ToolApprovalResponseContent("approval-request-id", true, new FunctionCallContent("function-call-id", "Func1")) ]), ]; @@ -1205,6 +1205,219 @@ public async Task ApprovalResponsePreservesOriginalRequestMessageMetadata() Assert.Equal(OriginalMessageId, actualOutput[0].MessageId); } + [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 ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) + ]; + + List expectedOutput = + [ + new ChatMessage(ChatRole.Assistant, + [ + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new ToolApprovalRequestContent("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 ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new ToolApprovalResponseContent("ficc_callId1", true, new FunctionCallContent("callId1", "Func")), + new ToolApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + ]; + + List expectedDownstreamClientInput = + [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.User, + [ + new ToolApprovalResponseContent("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") { Outputs = [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") { Outputs = [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 ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func")), + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new ToolApprovalResponseContent("ficc_callId1", approveFuncCall, new FunctionCallContent("callId1", "Func")), + new ToolApprovalResponseContent("callId2", approveMcpCall, new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + ]; + + List expectedDownstreamClientInput = [ + new ChatMessage(ChatRole.User, "hello"), + new ChatMessage(ChatRole.Assistant, + [ + new ToolApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer")) + ]), + new ChatMessage(ChatRole.User, + [ + new ToolApprovalResponseContent("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") { Outputs = [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") { Outputs = [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); + } + private static Task> InvokeAndAssertAsync( ChatOptions? options, List input, @@ -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,28 @@ 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, + }, + FunctionCallContent fcc => new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments) + { + InformationalOnly = fcc.InformationalOnly + }, + ToolApprovalRequestContent tarc => + new ToolApprovalRequestContent(tarc.RequestId, (ToolCallContent)CloneFcc(tarc.ToolCall)), + ToolApprovalResponseContent tarc => + new ToolApprovalResponseContent(tarc.RequestId, tarc.Approved, (ToolCallContent)CloneFcc(tarc.ToolCall)) + { + 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 2ddf757d185..ffcbcdc05ec 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; @@ -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("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("callId1", new FunctionCallContent("callId1", "Func1")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("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("callId1", new FunctionCallContent("callId1", "Func1")) + new ToolApprovalRequestContent("ficc_callId1", new FunctionCallContent("callId1", "Func1")) ]), new ChatMessage(ChatRole.User, [ - new FunctionApprovalResponseContent("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 3f1c9f59bce..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") { Output = [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") { Output = [new TextContent("Tool result")] }] }; + yield return new() { Contents = [new McpServerToolResultContent("mcp-call-1") { Outputs = [new TextContent("Tool result")] }] }; } using var chatClient = innerClient @@ -785,11 +794,11 @@ public async Task McpServerToolApprovalContentTypes_SerializedCorrectly() [ new(ChatRole.Assistant, [ - new McpServerToolApprovalRequestContent("approval-1", toolCall), + new ToolApprovalRequestContent("approval-1", toolCall), ]), new(ChatRole.User, [ - new McpServerToolApprovalResponseContent("approval-1", true), + new ToolApprovalResponseContent("approval-1", true, toolCall), ]), ]; @@ -839,3 +848,4 @@ private sealed class NonSerializableAIContent : AIContent; private static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim(); } + 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) { } }