diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
index af8b19c8d84..ffdf33f7645 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs
@@ -28,8 +28,6 @@ namespace Microsoft.Extensions.AI;
// [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")]
// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")]
// [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")]
-// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")]
-// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")]
// [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")]
// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")]
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs
index d3ec7ab8f0b..064dbcd95be 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalRequestContent.cs
@@ -8,7 +8,7 @@
namespace Microsoft.Extensions.AI;
///
-/// Represents a request for user approval of a function call.
+/// Represents a request for user approval of a call content.
///
[Experimental("MEAI001")]
public sealed class FunctionApprovalRequestContent : UserInputRequestContent
@@ -17,25 +17,25 @@ 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.
+ /// The call content that requires user approval.
/// is .
/// is empty or composed entirely of whitespace.
- /// is .
- public FunctionApprovalRequestContent(string id, FunctionCallContent functionCall)
+ /// is .
+ public FunctionApprovalRequestContent(string id, AIContent callContent)
: base(id)
{
- FunctionCall = Throw.IfNull(functionCall);
+ CallContent = Throw.IfNull(callContent);
}
///
- /// Gets the function call that pre-invoke approval is required for.
+ /// Gets the call content that pre-invoke approval is required for.
///
- public FunctionCallContent FunctionCall { get; }
+ public AIContent CallContent { get; }
///
- /// Creates a to indicate whether the function call is approved or rejected based on the value of .
+ /// Creates a to indicate whether the call is approved or rejected based on the value of .
///
- /// if the function call is approved; otherwise, .
+ /// if the call is approved; otherwise, .
/// The representing the approval response.
- public FunctionApprovalResponseContent CreateResponse(bool approved) => new(Id, approved, FunctionCall);
+ public FunctionApprovalResponseContent CreateResponse(bool approved) => new(Id, approved, CallContent);
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs
index 948dc6a1347..c856b2e21d5 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionApprovalResponseContent.cs
@@ -8,7 +8,7 @@
namespace Microsoft.Extensions.AI;
///
-/// Represents a response to a function approval request.
+/// Represents a response to an approval request.
///
[Experimental("MEAI001")]
public sealed class FunctionApprovalResponseContent : UserInputResponseContent
@@ -16,17 +16,17 @@ 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.
+ /// The ID that uniquely identifies the approval request/response pair.
+ /// if the call is approved; otherwise, .
+ /// The call content that requires user approval.
/// is .
/// is empty or composed entirely of whitespace.
- /// is .
- public FunctionApprovalResponseContent(string id, bool approved, FunctionCallContent functionCall)
+ /// is .
+ public FunctionApprovalResponseContent(string id, bool approved, AIContent callContent)
: base(id)
{
Approved = approved;
- FunctionCall = Throw.IfNull(functionCall);
+ CallContent = Throw.IfNull(callContent);
}
///
@@ -35,7 +35,7 @@ public FunctionApprovalResponseContent(string id, bool approved, FunctionCallCon
public bool Approved { get; }
///
- /// Gets the function call for which approval was requested.
+ /// Gets the call content for which approval was requested.
///
- public FunctionCallContent FunctionCall { get; }
+ public AIContent CallContent { 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 8f302d901b4..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs
+++ /dev/null
@@ -1,41 +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.Diagnostics;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a request for user approval of an MCP server tool call.
-///
-[Experimental("MEAI001")]
-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 0e239a79d7f..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs
+++ /dev/null
@@ -1,32 +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;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents a response to an MCP server tool approval request.
-///
-[Experimental("MEAI001")]
-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/UserInputRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
index b2a2e0e6e95..3e77607a3fd 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputRequestContent.cs
@@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI;
[Experimental("MEAI001")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(FunctionApprovalRequestContent), "functionApprovalRequest")]
-[JsonDerivedType(typeof(McpServerToolApprovalRequestContent), "mcpServerToolApprovalRequest")]
public class UserInputRequestContent : AIContent
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
index 6902f047282..f17ae2a964d 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/UserInputResponseContent.cs
@@ -14,7 +14,6 @@ namespace Microsoft.Extensions.AI;
[Experimental("MEAI001")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(FunctionApprovalResponseContent), "functionApprovalResponse")]
-[JsonDerivedType(typeof(McpServerToolApprovalResponseContent), "mcpServerToolApprovalResponse")]
public class UserInputResponseContent : AIContent
{
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
index d01294836bc..51ed08441f1 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs
@@ -55,8 +55,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
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);
@@ -129,8 +127,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(FunctionApprovalResponseContent))]
[JsonSerializable(typeof(McpServerToolCallContent))]
[JsonSerializable(typeof(McpServerToolResultContent))]
- [JsonSerializable(typeof(McpServerToolApprovalRequestContent))]
- [JsonSerializable(typeof(McpServerToolApprovalResponseContent))]
[JsonSerializable(typeof(CodeInterpreterToolCallContent))]
[JsonSerializable(typeof(CodeInterpreterToolResultContent))]
[JsonSerializable(typeof(ResponseContinuationToken))]
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index eb39754d5fd..cf8ec8a8823 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -212,7 +212,7 @@ internal static IEnumerable ToChatMessages(IEnumerable ToChatMessages(IEnumerable
break;
case McpToolCallApprovalRequestItem mtcari:
- yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
+ yield return CreateUpdate(new FunctionApprovalRequestContent(mtcari.Id, new McpServerToolCallContent(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
{
Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
RawRepresentation = mtcari,
@@ -855,7 +851,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable rawRep,
- McpServerToolApprovalResponseContent mcpResp => ResponseItem.CreateMcpApprovalResponseItem(mcpResp.Id, mcpResp.Approved),
+ FunctionApprovalResponseContent { CallContent: McpServerToolCallContent mcpCall } farc => ResponseItem.CreateMcpApprovalResponseItem(mcpCall.CallId, farc.Approved),
_ => null
};
@@ -1052,8 +1048,8 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
}
break;
- case McpServerToolApprovalResponseContent mcpApprovalResponseContent:
- yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved);
+ case FunctionApprovalResponseContent { CallContent: McpServerToolCallContent mcpCall } farc:
+ yield return ResponseItem.CreateMcpApprovalResponseItem(mcpCall.CallId, farc.Approved);
break;
}
}
@@ -1090,12 +1086,12 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))));
break;
- case McpServerToolApprovalRequestContent mcpApprovalRequestContent:
+ case FunctionApprovalRequestContent { CallContent: McpServerToolCallContent mcpCall }:
yield return ResponseItem.CreateMcpApprovalRequestItem(
- mcpApprovalRequestContent.Id,
- mcpApprovalRequestContent.ToolCall.ServerName,
- mcpApprovalRequestContent.ToolCall.ToolName,
- BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(mcpApprovalRequestContent.ToolCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)));
+ mcpCall.CallId,
+ mcpCall.ServerName,
+ mcpCall.ToolName,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(mcpCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)));
break;
case McpServerToolCallContent mstcc:
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
index 74f9bf554fa..86e4d011638 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
@@ -1245,7 +1245,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
///
/// 1. Remove all and from the .
/// 2. Recreate for any that haven't been executed yet.
- /// 3. Genreate failed for any rejected .
+ /// 3. Generate failed for any rejected .
/// 4. add all the new content items to and return them as the pre-invocation history.
///
private static (List? preDownstreamCallHistory, List? approvals) ProcessFunctionApprovalResponses(
@@ -1326,15 +1326,15 @@ private static (List? approvals, List? approvals, List 0 })
{
Throw.InvalidOperationException(
- $"FunctionApprovalRequestContent found with FunctionCall.CallId(s) '{string.Join(", ", approvalRequestCallIds)}' that have no matching FunctionApprovalResponseContent.");
+ $"FunctionApprovalRequestContent found with CallId(s) '{string.Join(", ", approvalRequestCallIds)}' that have no matching FunctionApprovalResponseContent.");
}
// 2nd iteration, over all approval responses:
@@ -1393,7 +1393,7 @@ private static (List? approvals, List? approvals, List? targetList = ref approvalResponse.Approved ? ref approvedFunctionCalls : ref rejectedFunctionCalls;
ChatMessage? requestMessage = null;
- _ = allApprovalRequestsMessages?.TryGetValue(approvalResponse.FunctionCall.CallId, out requestMessage);
+ _ = allApprovalRequestsMessages?.TryGetValue(fcc.CallId, out requestMessage);
(targetList ??= []).Add(new() { Response = approvalResponse, RequestMessage = requestMessage });
}
@@ -1418,7 +1418,7 @@ private static (List? approvals, ListThe for the rejected function calls.
private static List? GenerateRejectedFunctionResults(List? rejections) =>
rejections is { Count: > 0 } ?
- rejections.ConvertAll(static m => (AIContent)new FunctionResultContent(m.Response.FunctionCall.CallId, "Error: Tool call invocation was rejected by user.")) :
+ rejections.ConvertAll(static m => (AIContent)new FunctionResultContent(m.FunctionCallContent.CallId, "Error: Tool call invocation was rejected by user.")) :
null;
///
@@ -1459,7 +1459,7 @@ private static (List? approvals, List? approvals, List
{
// The FRC that is generated here is already added to originalMessages by ProcessFunctionCallsAsync.
var modeAndMessages = await ProcessFunctionCallsAsync(
- originalMessages, options, toolMap, notInvokedApprovals.Select(x => x.Response.FunctionCall).ToList(), 0, consecutiveErrorCount, isStreaming, cancellationToken);
+ originalMessages, options, toolMap, notInvokedApprovals.Select(x => x.FunctionCallContent).ToList(), 0, consecutiveErrorCount, isStreaming, cancellationToken);
consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
return (modeAndMessages.MessagesAdded, modeAndMessages.ShouldTerminate, consecutiveErrorCount);
@@ -1720,9 +1720,10 @@ public enum FunctionInvocationStatus
Exception,
}
- private struct ApprovalResultWithRequestMessage
+ private readonly struct ApprovalResultWithRequestMessage
{
- public FunctionApprovalResponseContent Response { get; set; }
- public ChatMessage? RequestMessage { get; set; }
+ public FunctionApprovalResponseContent Response { get; init; }
+ public ChatMessage? RequestMessage { get; init; }
+ public FunctionCallContent FunctionCallContent => (FunctionCallContent)Response.CallContent;
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs
index e5734ccd7cf..71ee60c5983 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs
@@ -74,8 +74,8 @@ public void Serialization_DerivedTypes_Roundtrips()
new FunctionApprovalResponseContent("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 FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
+ new FunctionApprovalResponseContent("request123", approved: true, new McpServerToolCallContent("call456", "myTool2", "myServer2"))
]);
var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs
index 924243a7d1c..bb199def89b 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalRequestContentTests.cs
@@ -19,7 +19,7 @@ public void Constructor_InvalidArguments_Throws()
Assert.Throws("id", () => new FunctionApprovalRequestContent("", functionCall));
Assert.Throws("id", () => new FunctionApprovalRequestContent("\r\t\n ", functionCall));
- Assert.Throws("functionCall", () => new FunctionApprovalRequestContent("id", null!));
+ Assert.Throws("callContent", () => new FunctionApprovalRequestContent("id", null!));
}
[Theory]
@@ -33,7 +33,7 @@ public void Constructor_Roundtrips(string id)
FunctionApprovalRequestContent content = new(id, functionCall);
Assert.Same(id, content.Id);
- Assert.Same(functionCall, content.FunctionCall);
+ Assert.Same(functionCall, content.CallContent);
}
[Theory]
@@ -51,7 +51,7 @@ public void CreateResponse_ReturnsExpectedResponse(bool approved)
Assert.NotNull(response);
Assert.Same(id, response.Id);
Assert.Equal(approved, response.Approved);
- Assert.Same(functionCall, response.FunctionCall);
+ Assert.Same(functionCall, response.CallContent);
}
[Fact]
@@ -64,8 +64,11 @@ public void Serialization_Roundtrips()
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);
+ Assert.NotNull(deserializedContent.CallContent);
+
+ var deserializedFunctionCall = Assert.IsType(deserializedContent.CallContent);
+ var originalFunctionCall = (FunctionCallContent)content.CallContent;
+ Assert.Equal(originalFunctionCall.CallId, deserializedFunctionCall.CallId);
+ Assert.Equal(originalFunctionCall.Name, deserializedFunctionCall.Name);
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs
index 67d2f13cf49..209daeeccac 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionApprovalResponseContentTests.cs
@@ -18,7 +18,7 @@ public void Constructor_InvalidArguments_Throws()
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!));
+ Assert.Throws("callContent", () => new FunctionApprovalResponseContent("id", true, null!));
}
[Theory]
@@ -32,7 +32,7 @@ public void Constructor_Roundtrips(string id, bool approved)
Assert.Same(id, content.Id);
Assert.Equal(approved, content.Approved);
- Assert.Same(functionCall, content.FunctionCall);
+ Assert.Same(functionCall, content.CallContent);
}
[Fact]
@@ -46,8 +46,11 @@ public void Serialization_Roundtrips()
Assert.NotNull(deserializedContent);
Assert.Equal(content.Id, deserializedContent.Id);
Assert.Equal(content.Approved, deserializedContent.Approved);
- Assert.NotNull(deserializedContent.FunctionCall);
- Assert.Equal(content.FunctionCall.CallId, deserializedContent.FunctionCall.CallId);
- Assert.Equal(content.FunctionCall.Name, deserializedContent.FunctionCall.Name);
+ Assert.NotNull(deserializedContent.CallContent);
+
+ var deserializedFunctionCall = Assert.IsType(deserializedContent.CallContent);
+ var originalFunctionCall = (FunctionCallContent)content.CallContent;
+ Assert.Equal(originalFunctionCall.CallId, deserializedFunctionCall.CallId);
+ Assert.Equal(originalFunctionCall.Name, deserializedFunctionCall.Name);
}
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
index fc4dac9cabb..9661972112f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputRequestContentTests.cs
@@ -33,16 +33,19 @@ public void Constructor_Roundtrips(string id)
[Fact]
public void Serialization_DerivedTypes_Roundtrips()
{
- UserInputRequestContent content = new FunctionApprovalRequestContent("request123", new FunctionCallContent("call123", "functionName", new Dictionary { { "param1", 123 } }));
+ FunctionApprovalRequestContent 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);
+ 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")),
+
+ // Uncomment once McpServerToolCallContent is no longer experimental.
+ // new FunctionApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")),
];
var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputRequestContentArray);
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
index 2442e57272d..39d6307fff5 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UserInputResponseContentTests.cs
@@ -40,7 +40,9 @@ public void Serialization_DerivedTypes_Roundtrips()
UserInputResponseContent[] contents =
[
new FunctionApprovalResponseContent("request123", true, new FunctionCallContent("call123", "functionName")),
- new McpServerToolApprovalResponseContent("request123", true),
+
+ // Uncomment once McpServerToolCallContent is no longer experimental.
+ // new FunctionApprovalResponseContent("request123", true, new McpServerToolCallContent("call123", "myTool", "myServer")),
];
var serializedContents = JsonSerializer.Serialize(contents, TestJsonSerializerContext.Default.UserInputResponseContentArray);
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
index 830563a60e1..ffde9ad653a 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
@@ -144,7 +144,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);
}
@@ -156,8 +156,8 @@ public async Task RemoteMCP_CallTool_ApprovalRequired()
SkipIfNotEnabled();
await RunAsync(false, false, false);
- await RunAsync(true, true, false);
await RunAsync(false, false, true);
+ await RunAsync(true, true, false);
await RunAsync(true, true, true);
async Task RunAsync(bool streaming, bool requireSpecific, bool useConversationId)
@@ -198,8 +198,12 @@ 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 =>
+ {
+ var mcpCallContent = Assert.IsType(c.CallContent);
+ return new FunctionApprovalResponseContent(mcpCallContent.CallId, true, c.CallContent);
+ })
.ToArray());
if (approvalResponse.Contents.Count == 0)
{
@@ -407,8 +411,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.CallContent);
+ Assert.Equal("search_events", mcpCallContent.ToolName);
input.Add(new ChatMessage(ChatRole.Tool, [approvalRequest.CreateResponse(true)]));
response = streaming ?
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index 94d767f67d4..9e55383d751 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -1292,7 +1292,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
{
Tools = [new HostedMcpServerTool("deepwiki", new Uri("https://mcp.deepwiki.com/mcp"))]
};
- McpServerToolApprovalRequestContent approvalRequest;
+ FunctionApprovalRequestContent approvalRequest;
using (VerbatimHttpHandler handler = new(input, output))
using (HttpClient httpClient = new(handler))
@@ -1302,7 +1302,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;
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
index 7c42c0edaf9..e1cb552543d 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs
@@ -9,7 +9,7 @@
using System.Threading.Tasks;
using Xunit;
-namespace Microsoft.Extensions.AI.ChatCompletion;
+namespace Microsoft.Extensions.AI;
public class FunctionInvokingChatClientApprovalsTests
{
@@ -571,11 +571,11 @@ public async Task ApprovalRequestWithoutApprovalResponseThrowsAsync()
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("FunctionApprovalRequestContent found with CallId(s) 'callId1' that have no matching FunctionApprovalResponseContent.", 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("FunctionApprovalRequestContent found with CallId(s) 'callId1' that have no matching FunctionApprovalResponseContent.", invokeStreamingException.Message);
}
[Fact]
@@ -816,24 +816,27 @@ async IAsyncEnumerable YieldInnerClientUpdates(
break;
case 2:
var approvalRequest1 = update.Contents.OfType().First();
- Assert.Equal("callId1", approvalRequest1.FunctionCall.CallId);
- Assert.Equal("Func1", approvalRequest1.FunctionCall.Name);
+ var functionCall1 = Assert.IsType(approvalRequest1.CallContent);
+ Assert.Equal("callId1", functionCall1.CallId);
+ Assert.Equal("Func1", functionCall1.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 functionCall2 = Assert.IsType(approvalRequest2.CallContent);
+ Assert.Equal("callId2", functionCall2.CallId);
+ Assert.Equal("Func2", functionCall2.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 functionCall3 = Assert.IsType(approvalRequest3.CallContent);
+ Assert.Equal("callId1", functionCall3.CallId);
+ Assert.Equal("Func3", functionCall3.Name);
// Fifth content can be yielded immediately, since we previously encountered a function call that requires approval.
Assert.Equal(5, updateYieldCount);
@@ -844,6 +847,217 @@ async IAsyncEnumerable YieldInnerClientUpdates(
}
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task FunctionCallReplacedWithApproval_MixedWithMcpApprovalAsync(bool useAdditionalTools)
+ {
+ AITool[] tools =
+ [
+ new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")),
+ new HostedMcpServerTool("myServer", "https://localhost/mcp")
+ ];
+
+ var options = new ChatOptions
+ {
+ Tools = useAdditionalTools ? null : tools
+ };
+
+ List input =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ ];
+
+ List downstreamClientOutput =
+ [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func"),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ])
+ ];
+
+ List expectedOutput =
+ [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ])
+ ];
+
+ await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null);
+ await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, expectedOutput, additionalTools: useAdditionalTools ? tools : null);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ApprovedApprovalResponseIsExecuted_MixedWithMcpApprovalAsync(bool useAdditionalTools)
+ {
+ AITool[] tools =
+ [
+ new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")),
+ new HostedMcpServerTool("myServer", "https://localhost/mcp")
+ ];
+
+ var options = new ChatOptions
+ {
+ Tools = useAdditionalTools ? null : tools
+ };
+
+ List input =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]) { MessageId = "resp1" },
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ ];
+
+ List expectedDownstreamClientInput =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId2", true, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func")
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ new FunctionResultContent("callId1", result: "Result 1")
+ ]),
+ ];
+
+ List downstreamClientOutput =
+ [
+ new ChatMessage(ChatRole.Assistant, [
+ new McpServerToolResultContent("callId2") { Output = [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") { Output = [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, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAdditionalTools, bool approveMcp)
+ {
+ AITool[] tools =
+ [
+ new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func")),
+ new HostedMcpServerTool("myServer", "https://localhost/mcp")
+ ];
+
+ var options = new ChatOptions
+ {
+ Tools = useAdditionalTools ? null : tools
+ };
+
+ List input =
+ [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]) { MessageId = "resp1" },
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId1", !approveMcp, new FunctionCallContent("callId1", "Func")),
+ new FunctionApprovalResponseContent("callId2", approveMcp, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ ];
+
+ List expectedDownstreamClientInput = [
+ new ChatMessage(ChatRole.User, "hello"),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionApprovalRequestContent("callId2", new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.User,
+ [
+ new FunctionApprovalResponseContent("callId2", approveMcp, new McpServerToolCallContent("callId2", "McpCall", "myServer"))
+ ]),
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func")
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ approveMcp ?
+ new FunctionResultContent("callId1", result: "Error: Tool call invocation was rejected by user.") :
+ new FunctionResultContent("callId1", result: "Result 1")
+ ]),
+ ];
+
+ List downstreamClientOutput =
+ [
+ new ChatMessage(ChatRole.Assistant, [
+ new TextContent("world"),
+ .. approveMcp ?
+ [new McpServerToolResultContent("callId2") { Output = [new TextContent("Result 2")] }] :
+ Array.Empty()
+ ])
+ ];
+
+ List output = [
+ new ChatMessage(ChatRole.Assistant,
+ [
+ new FunctionCallContent("callId1", "Func"),
+ ]),
+ new ChatMessage(ChatRole.Tool,
+ [
+ approveMcp ?
+ new FunctionResultContent("callId1", result: "Error: Tool call invocation was rejected by user.") :
+ new FunctionResultContent("callId1", result: "Result 1")
+ ]),
+ new ChatMessage(ChatRole.Assistant, [
+ new TextContent("world"),
+ .. approveMcp ?
+ [new McpServerToolResultContent("callId2") { Output = [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,