From 92925a8bc77faf8c9ef386fabe5cc32293b6b7bd Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:40:45 -0700 Subject: [PATCH] .NET Workflows - Add support for tool approval (#1685) * Draft * Nullable init * Complete * Consistency * Test fix * Typo * Comment * Updated * Fix identifier * Test fix * Comment typo * Better naming * Comment * Tweak comment --- .../Declarative/ExecuteWorkflow/Program.cs | 38 +++++++++++--- ...Request.cs => AgentFunctionToolRequest.cs} | 10 ++-- ...sponse.cs => AgentFunctionToolResponse.cs} | 23 ++++---- .../Events/AnswerRequest.cs | 27 ++++++++++ .../{InputResponse.cs => AnswerResponse.cs} | 10 ++-- .../Events/InputRequest.cs | 22 -------- .../Events/UserInputRequest.cs | 30 +++++++++++ .../Events/UserInputResponse.cs | 52 +++++++++++++++++++ .../Interpreter/WorkflowActionVisitor.cs | 37 ++++++++----- ...oft.Agents.AI.Workflows.Declarative.csproj | 1 + .../ObjectModel/InvokeAzureAgentExecutor.cs | 35 +++++++++---- .../ObjectModel/QuestionExecutor.cs | 4 +- .../WorkflowAgentProvider.cs | 4 +- .../Framework/WorkflowHarness.cs | 2 +- ...Test.cs => FunctionCallingWorkflowTest.cs} | 6 +-- .../Events/AgentFunctionToolRequestTest.cs | 48 +++++++++++++++++ .../Events/AgentFunctionToolResponseTest.cs | 46 ++++++++++++++++ .../Events/AgentToolRequestTest.cs | 28 ---------- .../Events/AgentToolResponseTest.cs | 27 ---------- .../Events/EventTest.cs | 5 +- .../Events/UserInputRequestTest.cs | 46 ++++++++++++++++ .../Events/UserInputResponseTest.cs | 46 ++++++++++++++++ ...questTest.cs => UserMessageRequestTest.cs} | 9 ++-- ...onseTest.cs => UserMessageResponseTest.cs} | 14 +++-- 24 files changed, 424 insertions(+), 146 deletions(-) rename dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/{AgentToolRequest.cs => AgentFunctionToolRequest.cs} (66%) rename dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/{AgentToolResponse.cs => AgentFunctionToolResponse.cs} (59%) create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs rename dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/{InputResponse.cs => AnswerResponse.cs} (71%) delete mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputRequest.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs rename dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/{ToolInputWorkflowTest.cs => FunctionCallingWorkflowTest.cs} (93%) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolRequestTest.cs delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolResponseTest.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs rename dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/{InputRequestTest.cs => UserMessageRequestTest.cs} (56%) rename dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/{InputResponseTest.cs => UserMessageResponseTest.cs} (53%) diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs index dcb5221908..4b26aa14f1 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ExecuteWorkflow/Program.cs @@ -336,9 +336,11 @@ internal sealed class Program request.Data.TypeId.TypeName switch { // Request for human input - _ when request.Data.TypeId.IsMatch() => HandleInputRequest(request.DataAs()!), + _ when request.Data.TypeId.IsMatch() => HandleUserMessageRequest(request.DataAs()!), // Request for function tool invocation. (Only active when functions are defined and IncludeFunctions is true.) - _ when request.Data.TypeId.IsMatch() => await this.HandleToolRequestAsync(request.DataAs()!), + _ when request.Data.TypeId.IsMatch() => await this.HandleToolRequestAsync(request.DataAs()!), + // Request for user input, such as function or mcp tool approval + _ when request.Data.TypeId.IsMatch() => HandleUserInputRequest(request.DataAs()!), // Unknown request type. _ => throw new InvalidOperationException($"Unsupported external request type: {request.GetType().Name}."), }; @@ -346,7 +348,7 @@ internal sealed class Program /// /// Handle request for human input. /// - private static InputResponse HandleInputRequest(InputRequest request) + private static AnswerResponse HandleUserMessageRequest(AnswerRequest request) { string? userInput; do @@ -358,7 +360,7 @@ internal sealed class Program } while (string.IsNullOrWhiteSpace(userInput)); - return new InputResponse(userInput); + return new AnswerResponse(userInput); } /// @@ -368,13 +370,13 @@ internal sealed class Program /// This handler is only active when is set to true and /// one or more instances are defined in the constructor. /// - private async ValueTask HandleToolRequestAsync(AgentToolRequest request) + private async ValueTask HandleToolRequestAsync(AgentFunctionToolRequest request) { Task[] functionTasks = request.FunctionCalls.Select(functionCall => InvokesToolAsync(functionCall)).ToArray(); await Task.WhenAll(functionTasks); - return AgentToolResponse.Create(request, functionTasks.Select(task => task.Result)); + return AgentFunctionToolResponse.Create(request, functionTasks.Select(task => task.Result)); async Task InvokesToolAsync(FunctionCallContent functionCall) { @@ -385,6 +387,30 @@ internal sealed class Program } } + /// + /// Handle request for user input for mcp and function tool approval. + /// + private static UserInputResponse HandleUserInputRequest(UserInputRequest request) + { + return UserInputResponse.Create(request, ProcessRequests()); + + IEnumerable ProcessRequests() + { + foreach (UserInputRequestContent approvalRequest in request.InputRequests) + { + // Here we are explicitly approving all requests. + // In a real-world scenario, you would replace this logic to either solicit user approval or implement a more complex approval process. + yield return + approvalRequest switch + { + McpServerToolApprovalRequestContent mcpApprovalRequest => mcpApprovalRequest.CreateResponse(approved: true), + FunctionApprovalRequestContent functionApprovalRequest => functionApprovalRequest.CreateResponse(approved: true), + _ => throw new NotSupportedException($"Unsupported request of type {approvalRequest.GetType().Name}"), + }; + } + } + } + private static string? ParseWorkflowFile(string[] args) { string? workflowFile = args.FirstOrDefault(); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentToolRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolRequest.cs similarity index 66% rename from dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentToolRequest.cs rename to dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolRequest.cs index b1fe34eda1..5f9f878e79 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentToolRequest.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolRequest.cs @@ -7,9 +7,9 @@ using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.Declarative.Events; /// -/// Represents a request for user input. +/// Represents one or more function tool requests. /// -public sealed class AgentToolRequest +public sealed class AgentFunctionToolRequest { /// /// The name of the agent associated with the tool request. @@ -17,14 +17,14 @@ public sealed class AgentToolRequest public string AgentName { get; } /// - /// A list of tool requests. + /// A list of function tool requests. /// public IList FunctionCalls { get; } [JsonConstructor] - internal AgentToolRequest(string agentName, IList? functionCalls = null) + internal AgentFunctionToolRequest(string agentName, IList functionCalls) { this.AgentName = agentName; - this.FunctionCalls = functionCalls ?? []; + this.FunctionCalls = functionCalls; } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentToolResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolResponse.cs similarity index 59% rename from dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentToolResponse.cs rename to dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolResponse.cs index 29a7f98954..e03414bd2e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentToolResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AgentFunctionToolResponse.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.Declarative.Events; /// -/// Represents a user input response. +/// Represents one or more function tool responses. /// -public sealed class AgentToolResponse +public sealed class AgentFunctionToolResponse { /// /// The name of the agent associated with the tool response. @@ -22,32 +22,31 @@ public sealed class AgentToolResponse /// public IList FunctionResults { get; } - /// - /// Initializes a new instance of the class. - /// [JsonConstructor] - internal AgentToolResponse(string agentName, IList functionResults) + internal AgentFunctionToolResponse(string agentName, IList functionResults) { this.AgentName = agentName; this.FunctionResults = functionResults; } /// - /// Factory method to create an from an + /// Factory method to create an from an /// Ensures that all function calls in the request have a corresponding result. /// /// The tool request. - /// On or more function results - /// An that can be provided to the workflow. - /// Not all have a corresponding . - public static AgentToolResponse Create(AgentToolRequest toolRequest, params IEnumerable functionResults) + /// One or more function results + /// An that can be provided to the workflow. + /// Not all have a corresponding . + public static AgentFunctionToolResponse Create(AgentFunctionToolRequest toolRequest, params IEnumerable functionResults) { HashSet callIds = [.. toolRequest.FunctionCalls.Select(call => call.CallId)]; HashSet resultIds = [.. functionResults.Select(call => call.CallId)]; + if (!callIds.SetEquals(resultIds)) { throw new DeclarativeActionException($"Missing results for: {string.Join(",", callIds.Except(resultIds))}"); } - return new AgentToolResponse(toolRequest.AgentName, [.. functionResults]); + + return new AgentFunctionToolResponse(toolRequest.AgentName, [.. functionResults]); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs new file mode 100644 index 0000000000..845696a180 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerRequest.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Workflows.Declarative.Events; + +/// +/// Represents a request for user input in response to a `Question` action. +/// +public sealed class AnswerRequest +{ + /// + /// An optional prompt for the user. + /// + /// + /// This prompt is utilized for the "Question" action type in the Declarative Workflow, + /// but is redundant when the user is responding to an agent since the agent's message + /// is the implicit prompt. + /// + public string? Prompt { get; } + + [JsonConstructor] + internal AnswerRequest(string? prompt = null) + { + this.Prompt = prompt; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerResponse.cs similarity index 71% rename from dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs rename to dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerResponse.cs index 65454d2994..00903f43f0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/AnswerResponse.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Events; /// /// Represents a user input response. /// -public sealed class InputResponse +public sealed class AnswerResponse { /// /// The response value. @@ -16,20 +16,20 @@ public sealed class InputResponse public ChatMessage Value { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The response value. [JsonConstructor] - public InputResponse(ChatMessage value) + public AnswerResponse(ChatMessage value) { this.Value = value; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The response value. - public InputResponse(string value) + public AnswerResponse(string value) { this.Value = new ChatMessage(ChatRole.User, value); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputRequest.cs deleted file mode 100644 index 45ce8c217d..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.AI.Workflows.Declarative.Events; - -/// -/// Represents a request for user input. -/// -public sealed class InputRequest -{ - /// - /// The prompt message to display to the user. - /// - public string Prompt { get; } - - [JsonConstructor] - internal InputRequest(string prompt) - { - this.Prompt = prompt; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs new file mode 100644 index 0000000000..1426025d74 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.Declarative.Events; + +/// +/// Represents one or more user-input requests. +/// +public sealed class UserInputRequest +{ + /// + /// The name of the agent associated with the tool request. + /// + public string AgentName { get; } + + /// + /// A list of user input requests. + /// + public IList InputRequests { get; } + + [JsonConstructor] + internal UserInputRequest(string agentName, IList inputRequests) + { + this.AgentName = agentName; + this.InputRequests = inputRequests; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs new file mode 100644 index 0000000000..edb9f3b7cc --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/UserInputResponse.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.Declarative.Events; + +/// +/// Represents one or more user-input responses. +/// +public sealed class UserInputResponse +{ + /// + /// The name of the agent associated with the tool request. + /// + public string AgentName { get; } + + /// + /// A list of approval responses. + /// + public IList InputResponses { get; } + + [JsonConstructor] + internal UserInputResponse(string agentName, IList inputResponses) + { + this.AgentName = agentName; + this.InputResponses = inputResponses; + } + + /// + /// Factory method to create an from a + /// Ensures that all requests have a corresponding result. + /// + /// The input request. + /// One or more responses + /// An that can be provided to the workflow. + /// Not all have a corresponding . + public static UserInputResponse Create(UserInputRequest inputRequest, params IEnumerable inputResponses) + { + HashSet callIds = [.. inputRequest.InputRequests.OfType().Select(call => call.Id)]; + HashSet resultIds = [.. inputResponses.Select(call => call.Id)]; + + if (!callIds.SetEquals(resultIds)) + { + throw new DeclarativeActionException($"Missing responses for: {string.Join(",", callIds.Except(resultIds))}"); + } + + return new UserInputResponse(inputRequest.AgentName, [.. inputResponses]); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index ce511040cd..a4ba035d6c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -249,13 +249,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor // Define input action string inputId = QuestionExecutor.Steps.Input(action.Id); - RequestPortAction inputPort = new(RequestPort.Create(inputId)); + RequestPortAction inputPort = new(RequestPort.Create(inputId)); this._workflowModel.AddNode(inputPort, action.ParentId); this._workflowModel.AddLinkFromPeer(action.ParentId, inputId); // Capture input response string captureId = QuestionExecutor.Steps.Capture(action.Id); - this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId); + this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId); // Transition to post action if complete this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId, QuestionExecutor.IsComplete); @@ -315,22 +315,31 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this.ContinueWith(action); // Transition to post action if complete string postId = Steps.Post(action.Id); - this._workflowModel.AddLink(action.Id, postId, result => !InvokeAzureAgentExecutor.RequiresInput(result)); + this._workflowModel.AddLink(action.Id, postId, InvokeAzureAgentExecutor.RequiresNothing); - // Define input action - string inputId = InvokeAzureAgentExecutor.Steps.Input(action.Id); - RequestPortAction inputPort = new(RequestPort.Create(inputId)); - this._workflowModel.AddNode(inputPort, action.ParentId); - this._workflowModel.AddLink(action.Id, inputId, InvokeAzureAgentExecutor.RequiresInput); + // Define request-port for function calling action + string functionCallingPortId = InvokeAzureAgentExecutor.Steps.FunctionTool(action.Id); + RequestPortAction functionCallingPort = new(RequestPort.Create(functionCallingPortId)); + this._workflowModel.AddNode(functionCallingPort, action.ParentId); + this._workflowModel.AddLink(action.Id, functionCallingPort.Id, InvokeAzureAgentExecutor.RequiresFunctionCall); - // Input port always transitions to resume + // Define request-port for user input, such as: mcp tool & function tool approval + string userInputPortId = InvokeAzureAgentExecutor.Steps.UserInput(action.Id); + RequestPortAction userInputPort = new(RequestPort.Create(userInputPortId)); + this._workflowModel.AddNode(userInputPort, action.ParentId); + this._workflowModel.AddLink(action.Id, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput); + + // Request ports always transitions to resume string resumeId = InvokeAzureAgentExecutor.Steps.Resume(action.Id); - this._workflowModel.AddNode(new DelegateActionExecutor(resumeId, this._workflowState, action.ResumeAsync), action.ParentId); - this._workflowModel.AddLink(inputId, resumeId); - // Transition to request port if more input is required - this._workflowModel.AddLink(resumeId, inputId, InvokeAzureAgentExecutor.RequiresInput); + this._workflowModel.AddNode(new DelegateActionExecutor(resumeId, this._workflowState, action.ResumeAsync), action.ParentId); + this._workflowModel.AddLink(functionCallingPortId, resumeId); + this._workflowModel.AddLink(userInputPortId, resumeId); + // Transition to appropriate request port if more function calling is requested + this._workflowModel.AddLink(resumeId, functionCallingPortId, InvokeAzureAgentExecutor.RequiresFunctionCall); + // Transition to appropriate request port if more user input is requested + this._workflowModel.AddLink(resumeId, userInputPortId, InvokeAzureAgentExecutor.RequiresUserInput); // Transition to post action if complete - this._workflowModel.AddLink(resumeId, postId, result => !InvokeAzureAgentExecutor.RequiresInput(result)); + this._workflowModel.AddLink(resumeId, postId, InvokeAzureAgentExecutor.RequiresNothing); // Define post action this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj index 9e23c5f727..b3b2a86ab4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj @@ -4,6 +4,7 @@ $(ProjectsTargetFrameworks) $(ProjectsDebugTargetFrameworks) preview + $(NoWarn);MEAI001 diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index b26add9f81..f9b9775ad1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -21,12 +21,16 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA { public static class Steps { - public static string Input(string id) => $"{id}_{nameof(Input)}"; + public static string UserInput(string id) => $"{id}_{nameof(UserInput)}"; + public static string FunctionTool(string id) => $"{id}_{nameof(FunctionTool)}"; public static string Resume(string id) => $"{id}_{nameof(Resume)}"; } - // Input is requested by a message other than ActionExecutorResult. - public static bool RequiresInput(object? message) => message is not ActionExecutorResult; + public static bool RequiresFunctionCall(object? message) => message is AgentFunctionToolRequest; + + public static bool RequiresUserInput(object? message) => message is UserInputRequest; + + public static bool RequiresNothing(object? message) => message is ActionExecutorResult; private AzureAgentUsage AgentUsage => Throw.IfNull(this.Model.Agent, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}"); private AzureAgentInput? AgentInput => this.Model.Input; @@ -42,7 +46,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return default; } - public ValueTask ResumeAsync(IWorkflowContext context, AgentToolResponse message, CancellationToken cancellationToken) => + public ValueTask ResumeAsync(IWorkflowContext context, AgentFunctionToolResponse message, CancellationToken cancellationToken) => this.InvokeAgentAsync(context, [message.FunctionResults.ToChatMessage()], cancellationToken); public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) @@ -64,12 +68,20 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA if (string.IsNullOrEmpty(agentResponse.Text)) { // Identify function calls that have no associated result. - List functionCalls = this.GetOrphanedFunctionCalls(agentResponse); - isComplete = functionCalls.Count == 0; - - if (!isComplete) + List inputRequests = GetUserInputRequests(agentResponse); + if (inputRequests.Count > 0) { - AgentToolRequest toolRequest = new(agentName, functionCalls); + isComplete = false; + UserInputRequest approvalRequest = new(agentName, inputRequests.OfType().ToArray()); + await context.SendMessageAsync(approvalRequest, targetId: null, cancellationToken).ConfigureAwait(false); + } + + // Identify function calls that have no associated result. + List functionCalls = GetOrphanedFunctionCalls(agentResponse); + if (functionCalls.Count > 0) + { + isComplete = false; + AgentFunctionToolRequest toolRequest = new(agentName, functionCalls); await context.SendMessageAsync(toolRequest, targetId: null, cancellationToken).ConfigureAwait(false); } } @@ -95,7 +107,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return userInput?.ToChatMessages(); } - private List GetOrphanedFunctionCalls(AgentRunResponse agentResponse) + private static List GetOrphanedFunctionCalls(AgentRunResponse agentResponse) { HashSet functionResultIds = [.. agentResponse.Messages @@ -117,6 +129,9 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return functionCalls; } + private static List GetUserInputRequests(AgentRunResponse agentResponse) => + agentResponse.Messages.SelectMany(m => m.Contents.OfType()).ToList(); + private string? GetConversationId() { if (this.Model.ConversationId is null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 1c6e1596df..f4035d7258 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -75,12 +75,12 @@ internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider age public async ValueTask PrepareResponseAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) { int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false); - InputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); + AnswerRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt)); await context.SendMessageAsync(inputRequest, targetId: null, cancellationToken).ConfigureAwait(false); await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false); } - public async ValueTask CaptureResponseAsync(IWorkflowContext context, InputResponse message, CancellationToken cancellationToken) + public async ValueTask CaptureResponseAsync(IWorkflowContext context, AnswerResponse message, CancellationToken cancellationToken) { FormulaValue? extractedValue = null; if (message.Value is null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs index 9db57aba35..1cb79db9f7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs @@ -16,8 +16,8 @@ public abstract class WorkflowAgentProvider /// /// Gets or sets a collection of additional tools an agent is able to automatically invoke. /// If an agent is configured with a function tool that is not available, a is executed - /// that provides an that describes the function calls requested. The caller may - /// then respond with a corrsponding that includes the results of the function calls. + /// that provides an that describes the function calls requested. The caller may + /// then respond with a corrsponding that includes the results of the function calls. /// /// /// These will not impact the requests sent to the model by the . diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs index 4e5e775b92..391a4b63d6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs @@ -31,7 +31,7 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId) string inputText = testcase.Setup.Responses[responseCount].Value; Console.WriteLine($"INPUT: {inputText}"); ++responseCount; - WorkflowEvents runEvents = await this.ResumeAsync(new InputResponse(inputText)).ConfigureAwait(false); + WorkflowEvents runEvents = await this.ResumeAsync(new AnswerResponse(inputText)).ConfigureAwait(false); workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. runEvents.Events]); requestCount = (workflowEvents.InputEvents.Count + 1) / 2; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/ToolInputWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs similarity index 93% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/ToolInputWorkflowTest.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs index 995a92bfc3..419c37e0eb 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/ToolInputWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs @@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; /// /// Tests execution of workflow created by . /// -public sealed class ToolInputWorkflowTest(ITestOutputHelper output) : IntegrationTest(output) +public sealed class FunctionCallingWorkflowTest(ITestOutputHelper output) : IntegrationTest(output) { [Fact] public Task ValidateAutoInvokeAsync() => @@ -47,7 +47,7 @@ public sealed class ToolInputWorkflowTest(ITestOutputHelper output) : Integratio Assert.False(autoInvoke); RequestInfoEvent inputEvent = workflowEvents.InputEvents[workflowEvents.InputEvents.Count - 1]; - AgentToolRequest? toolRequest = inputEvent.Request.Data.As(); + AgentFunctionToolRequest? toolRequest = inputEvent.Request.Data.As(); Assert.NotNull(toolRequest); List<(FunctionCallContent, AIFunction)> functionCalls = []; @@ -66,7 +66,7 @@ public sealed class ToolInputWorkflowTest(ITestOutputHelper output) : Integratio ++responseCount; - WorkflowEvents runEvents = await harness.ResumeAsync(AgentToolResponse.Create(toolRequest, functionResults)).ConfigureAwait(false); + WorkflowEvents runEvents = await harness.ResumeAsync(AgentFunctionToolResponse.Create(toolRequest, functionResults)).ConfigureAwait(false); workflowEvents = new WorkflowEvents([.. workflowEvents.Events, .. runEvents.Events]); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs new file mode 100644 index 0000000000..64639251aa --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolRequestTest.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; + +/// +/// Verify class +/// +public sealed class AgentFunctionToolRequestTest(ITestOutputHelper output) : EventTest(output) +{ + [Fact] + public void VerifySerializationEmpty() + { + // Arrange & Act + AgentFunctionToolRequest copy = VerifyEventSerialization(new AgentFunctionToolRequest("testagent", [])); + + // Assert + Assert.Equal("testagent", copy.AgentName); + Assert.Empty(copy.FunctionCalls); + } + + [Fact] + public void VerifySerializationWithRequests() + { + // Arrange & Act + AgentFunctionToolRequest copy = + VerifyEventSerialization( + new AgentFunctionToolRequest( + "agent", + [ + new FunctionCallContent("call1", "result1"), + new FunctionCallContent("call2", "result2", new Dictionary() { { "name", "Clam Chowder" } }), + ])); + + // Assert + Assert.Equal("agent", copy.AgentName); + Assert.Equal(2, copy.FunctionCalls.Count); + Assert.IsType(copy.FunctionCalls[0]); + Assert.Null(copy.FunctionCalls[0].Arguments); + Assert.IsType(copy.FunctionCalls[1]); + Assert.NotNull(copy.FunctionCalls[1].Arguments); + Assert.NotEmpty(copy.FunctionCalls[1].Arguments!); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs new file mode 100644 index 0000000000..9467d67117 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentFunctionToolResponseTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; + +/// +/// Verify class +/// +public sealed class AgentFunctionToolResponseTest(ITestOutputHelper output) : EventTest(output) +{ + [Fact] + public void VerifySerializationEmpty() + { + // Arrange & Act + AgentFunctionToolResponse copy = VerifyEventSerialization(new AgentFunctionToolResponse("testagent", [])); + + // Assert + Assert.Equal("testagent", copy.AgentName); + Assert.Empty(copy.FunctionResults); + } + + [Fact] + public void VerifySerializationWithResults() + { + // Arrange & Act + AgentFunctionToolResponse copy = + VerifyEventSerialization( + new AgentFunctionToolResponse( + "agent", + [ + new FunctionResultContent("call1", "result1"), + new FunctionResultContent("call2", "result2"), + ])); + + // Assert + Assert.Equal("agent", copy.AgentName); + Assert.Equal(2, copy.FunctionResults.Count); + Assert.IsType(copy.FunctionResults[0]); + Assert.Equal("call1", copy.FunctionResults[0].CallId); + Assert.IsType(copy.FunctionResults[1]); + Assert.Equal("call2", copy.FunctionResults[1].CallId); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolRequestTest.cs deleted file mode 100644 index 61be304bd4..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolRequestTest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Base class for event tests. -/// -public sealed class AgentToolRequestTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerialization() - { - AgentToolRequest copy = - VerifyEventSerialization( - new AgentToolRequest( - "agent", - [ - new FunctionCallContent("call1", "result1"), - new FunctionCallContent("call2", "result2", new Dictionary() { { "name", "Clam Chowder" } }) - ])); - Assert.Equal("agent", copy.AgentName); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolResponseTest.cs deleted file mode 100644 index f6258cb33e..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/AgentToolResponseTest.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows.Declarative.Events; -using Microsoft.Extensions.AI; -using Xunit.Abstractions; - -namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; - -/// -/// Base class for event tests. -/// -public sealed class AgentToolResponseTest(ITestOutputHelper output) : EventTest(output) -{ - [Fact] - public void VerifySerialization() - { - AgentToolResponse copy = - VerifyEventSerialization( - new AgentToolResponse( - "agent", - [ - new FunctionResultContent("call1", "result1"), - new FunctionResultContent("call2", "result2") - ])); - Assert.Equal("agent", copy.AgentName); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs index 0f573aba7e..7dc1c895d3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using Microsoft.Extensions.AI; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests; @@ -12,9 +13,9 @@ public abstract class EventTest(ITestOutputHelper output) : WorkflowTest(output) { protected static TEvent VerifyEventSerialization(TEvent source) { - string? text = JsonSerializer.Serialize(source); + string? text = JsonSerializer.Serialize(source, AIJsonUtilities.DefaultOptions); Assert.NotNull(text); - TEvent? copy = JsonSerializer.Deserialize(text); + TEvent? copy = JsonSerializer.Deserialize(text, AIJsonUtilities.DefaultOptions); Assert.NotNull(copy); return copy; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs new file mode 100644 index 0000000000..c190e93193 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputRequestTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; + +/// +/// Verify class +/// +public sealed class UserInputRequestTest(ITestOutputHelper output) : EventTest(output) +{ + [Fact] + public void VerifySerializationEmpty() + { + // Arrange & Act + UserInputRequest copy = VerifyEventSerialization(new UserInputRequest("test agent", [])); + + // Assert + Assert.Equal("test agent", copy.AgentName); + Assert.Empty(copy.InputRequests); + } + + [Fact] + public void VerifySerializationWithRequests() + { + // Arrange & Act + UserInputRequest copy = + VerifyEventSerialization( + new UserInputRequest( + "agent", + [ + new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")), + new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")), + ])); + + // Assert + Assert.Equal("agent", copy.AgentName); + Assert.Equal(2, copy.InputRequests.Count); + McpServerToolApprovalRequestContent mcpRequest = Assert.IsType(copy.InputRequests[0]); + Assert.Equal("call1", mcpRequest.Id); + FunctionApprovalRequestContent functionRequest = Assert.IsType(copy.InputRequests[1]); + Assert.Equal("call2", functionRequest.Id); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs new file mode 100644 index 0000000000..2d36d01611 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserInputResponseTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; + +/// +/// Verify class +/// +public sealed class UserInputResponseTest(ITestOutputHelper output) : EventTest(output) +{ + [Fact] + public void VerifySerializationEmpty() + { + // Arrange & Act + UserInputResponse copy = VerifyEventSerialization(new UserInputResponse("testagent", [])); + + // Assert + Assert.Equal("testagent", copy.AgentName); + Assert.Empty(copy.InputResponses); + } + + [Fact] + public void VerifySerializationWithResponses() + { + // Arrange & Act + UserInputResponse copy = + VerifyEventSerialization( + new UserInputResponse( + "agent", + [ + new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true), + new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")).CreateResponse(approved: true), + ])); + + // Assert + Assert.Equal("agent", copy.AgentName); + Assert.Equal(2, copy.InputResponses.Count); + McpServerToolApprovalResponseContent mcpResponse = Assert.IsType(copy.InputResponses[0]); + Assert.Equal("call1", mcpResponse.Id); + FunctionApprovalResponseContent functionResponse = Assert.IsType(copy.InputResponses[1]); + Assert.Equal("call2", functionResponse.Id); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageRequestTest.cs similarity index 56% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequestTest.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageRequestTest.cs index d9242307dc..db6ab351ce 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequestTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageRequestTest.cs @@ -6,14 +6,17 @@ using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; /// -/// Base class for event tests. +/// Verify class /// -public sealed class InputRequestTest(ITestOutputHelper output) : EventTest(output) +public sealed class UserMessageRequestTest(ITestOutputHelper output) : EventTest(output) { [Fact] public void VerifySerialization() { - InputRequest copy = VerifyEventSerialization(new InputRequest("wassup")); + // Arrange & Act + AnswerRequest copy = VerifyEventSerialization(new AnswerRequest("wassup")); + + // Assert Assert.Equal("wassup", copy.Prompt); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageResponseTest.cs similarity index 53% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponseTest.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageResponseTest.cs index 015d80e246..efeef6052d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponseTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/UserMessageResponseTest.cs @@ -7,21 +7,27 @@ using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; /// -/// Base class for event tests. +/// Verify class /// -public sealed class InputResponseTest(ITestOutputHelper output) : EventTest(output) +public sealed class UserMessageResponseTest(ITestOutputHelper output) : EventTest(output) { [Fact] public void VerifySerializationText() { - InputResponse copy = VerifyEventSerialization(new InputResponse("test response")); + // Arrange & Act + AnswerResponse copy = VerifyEventSerialization(new AnswerResponse("test response")); + + // Assert Assert.Equal("test response", copy.Value.Text); } [Fact] public void VerifySerializationMessage() { - InputResponse copy = VerifyEventSerialization(new InputResponse(new ChatMessage(ChatRole.User, "test response"))); + // Arrange & Act + AnswerResponse copy = VerifyEventSerialization(new AnswerResponse(new ChatMessage(ChatRole.User, "test response"))); + + // Assert Assert.Equal("test response", copy.Value.Text); } }