mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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
This commit is contained in:
@@ -336,9 +336,11 @@ internal sealed class Program
|
||||
request.Data.TypeId.TypeName switch
|
||||
{
|
||||
// Request for human input
|
||||
_ when request.Data.TypeId.IsMatch<InputRequest>() => HandleInputRequest(request.DataAs<InputRequest>()!),
|
||||
_ when request.Data.TypeId.IsMatch<AnswerRequest>() => HandleUserMessageRequest(request.DataAs<AnswerRequest>()!),
|
||||
// Request for function tool invocation. (Only active when functions are defined and IncludeFunctions is true.)
|
||||
_ when request.Data.TypeId.IsMatch<AgentToolRequest>() => await this.HandleToolRequestAsync(request.DataAs<AgentToolRequest>()!),
|
||||
_ when request.Data.TypeId.IsMatch<AgentFunctionToolRequest>() => await this.HandleToolRequestAsync(request.DataAs<AgentFunctionToolRequest>()!),
|
||||
// Request for user input, such as function or mcp tool approval
|
||||
_ when request.Data.TypeId.IsMatch<UserInputRequest>() => HandleUserInputRequest(request.DataAs<UserInputRequest>()!),
|
||||
// Unknown request type.
|
||||
_ => throw new InvalidOperationException($"Unsupported external request type: {request.GetType().Name}."),
|
||||
};
|
||||
@@ -346,7 +348,7 @@ internal sealed class Program
|
||||
/// <summary>
|
||||
/// Handle request for human input.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -368,13 +370,13 @@ internal sealed class Program
|
||||
/// This handler is only active when <see cref="IncludeFunctions"/> is set to true and
|
||||
/// one or more <see cref="AIFunction"/> instances are defined in the constructor.
|
||||
/// </remarks>
|
||||
private async ValueTask<AgentToolResponse> HandleToolRequestAsync(AgentToolRequest request)
|
||||
private async ValueTask<AgentFunctionToolResponse> HandleToolRequestAsync(AgentFunctionToolRequest request)
|
||||
{
|
||||
Task<FunctionResultContent>[] 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<FunctionResultContent> InvokesToolAsync(FunctionCallContent functionCall)
|
||||
{
|
||||
@@ -385,6 +387,30 @@ internal sealed class Program
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle request for user input for mcp and function tool approval.
|
||||
/// </summary>
|
||||
private static UserInputResponse HandleUserInputRequest(UserInputRequest request)
|
||||
{
|
||||
return UserInputResponse.Create(request, ProcessRequests());
|
||||
|
||||
IEnumerable<UserInputResponseContent> 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();
|
||||
|
||||
+5
-5
@@ -7,9 +7,9 @@ using Microsoft.Extensions.AI;
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input.
|
||||
/// Represents one or more function tool requests.
|
||||
/// </summary>
|
||||
public sealed class AgentToolRequest
|
||||
public sealed class AgentFunctionToolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
@@ -17,14 +17,14 @@ public sealed class AgentToolRequest
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tool requests.
|
||||
/// A list of function tool requests.
|
||||
/// </summary>
|
||||
public IList<FunctionCallContent> FunctionCalls { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AgentToolRequest(string agentName, IList<FunctionCallContent>? functionCalls = null)
|
||||
internal AgentFunctionToolRequest(string agentName, IList<FunctionCallContent> functionCalls)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.FunctionCalls = functionCalls ?? [];
|
||||
this.FunctionCalls = functionCalls;
|
||||
}
|
||||
}
|
||||
+11
-12
@@ -8,9 +8,9 @@ using Microsoft.Extensions.AI;
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user input response.
|
||||
/// Represents one or more function tool responses.
|
||||
/// </summary>
|
||||
public sealed class AgentToolResponse
|
||||
public sealed class AgentFunctionToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool response.
|
||||
@@ -22,32 +22,31 @@ public sealed class AgentToolResponse
|
||||
/// </summary>
|
||||
public IList<FunctionResultContent> FunctionResults { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputResponse"/> class.
|
||||
/// </summary>
|
||||
[JsonConstructor]
|
||||
internal AgentToolResponse(string agentName, IList<FunctionResultContent> functionResults)
|
||||
internal AgentFunctionToolResponse(string agentName, IList<FunctionResultContent> functionResults)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.FunctionResults = functionResults;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an <see cref="AgentToolResponse"/> from an <see cref="AgentToolRequest"/>
|
||||
/// Factory method to create an <see cref="AgentFunctionToolResponse"/> from an <see cref="AgentFunctionToolRequest"/>
|
||||
/// Ensures that all function calls in the request have a corresponding result.
|
||||
/// </summary>
|
||||
/// <param name="toolRequest">The tool request.</param>
|
||||
/// <param name="functionResults">On or more function results</param>
|
||||
/// <returns>An <see cref="AgentToolResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static AgentToolResponse Create(AgentToolRequest toolRequest, params IEnumerable<FunctionResultContent> functionResults)
|
||||
/// <param name="functionResults">One or more function results</param>
|
||||
/// <returns>An <see cref="AgentFunctionToolResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentFunctionToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static AgentFunctionToolResponse Create(AgentFunctionToolRequest toolRequest, params IEnumerable<FunctionResultContent> functionResults)
|
||||
{
|
||||
HashSet<string> callIds = [.. toolRequest.FunctionCalls.Select(call => call.CallId)];
|
||||
HashSet<string> 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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input in response to a `Question` action.
|
||||
/// </summary>
|
||||
public sealed class AnswerRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// An optional prompt for the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public string? Prompt { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal AnswerRequest(string? prompt = null)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
/// <summary>
|
||||
/// Represents a user input response.
|
||||
/// </summary>
|
||||
public sealed class InputResponse
|
||||
public sealed class AnswerResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The response value.
|
||||
@@ -16,20 +16,20 @@ public sealed class InputResponse
|
||||
public ChatMessage Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputResponse"/> class.
|
||||
/// Initializes a new instance of the <see cref="AnswerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
[JsonConstructor]
|
||||
public InputResponse(ChatMessage value)
|
||||
public AnswerResponse(ChatMessage value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputResponse"/> class.
|
||||
/// Initializes a new instance of the <see cref="AnswerResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The response value.</param>
|
||||
public InputResponse(string value)
|
||||
public AnswerResponse(string value)
|
||||
{
|
||||
this.Value = new ChatMessage(ChatRole.User, value);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for user input.
|
||||
/// </summary>
|
||||
public sealed class InputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The prompt message to display to the user.
|
||||
/// </summary>
|
||||
public string Prompt { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal InputRequest(string prompt)
|
||||
{
|
||||
this.Prompt = prompt;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more user-input requests.
|
||||
/// </summary>
|
||||
public sealed class UserInputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of user input requests.
|
||||
/// </summary>
|
||||
public IList<AIContent> InputRequests { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal UserInputRequest(string agentName, IList<AIContent> inputRequests)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.InputRequests = inputRequests;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one or more user-input responses.
|
||||
/// </summary>
|
||||
public sealed class UserInputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the agent associated with the tool request.
|
||||
/// </summary>
|
||||
public string AgentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of approval responses.
|
||||
/// </summary>
|
||||
public IList<AIContent> InputResponses { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal UserInputResponse(string agentName, IList<AIContent> inputResponses)
|
||||
{
|
||||
this.AgentName = agentName;
|
||||
this.InputResponses = inputResponses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create an <see cref="UserInputResponse"/> from a <see cref="UserInputRequest"/>
|
||||
/// Ensures that all requests have a corresponding result.
|
||||
/// </summary>
|
||||
/// <param name="inputRequest">The input request.</param>
|
||||
/// <param name="inputResponses">One or more responses</param>
|
||||
/// <returns>An <see cref="UserInputResponse"/> that can be provided to the workflow.</returns>
|
||||
/// <exception cref="DeclarativeActionException">Not all <see cref="AgentFunctionToolRequest.FunctionCalls"/> have a corresponding <see cref="FunctionResultContent"/>.</exception>
|
||||
public static UserInputResponse Create(UserInputRequest inputRequest, params IEnumerable<UserInputResponseContent> inputResponses)
|
||||
{
|
||||
HashSet<string> callIds = [.. inputRequest.InputRequests.OfType<UserInputRequestContent>().Select(call => call.Id)];
|
||||
HashSet<string> 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]);
|
||||
}
|
||||
}
|
||||
+23
-14
@@ -249,13 +249,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
// Define input action
|
||||
string inputId = QuestionExecutor.Steps.Input(action.Id);
|
||||
RequestPortAction inputPort = new(RequestPort.Create<InputRequest, InputResponse>(inputId));
|
||||
RequestPortAction inputPort = new(RequestPort.Create<AnswerRequest, AnswerResponse>(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<InputResponse>(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId);
|
||||
this.ContinueWith(new DelegateActionExecutor<AnswerResponse>(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<AgentToolRequest, AgentToolResponse>(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<AgentFunctionToolRequest, AgentFunctionToolResponse>(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<UserInputRequest, UserInputResponse>(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<AgentToolResponse>(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<AgentFunctionToolResponse>(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);
|
||||
|
||||
+1
@@ -4,6 +4,7 @@
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
+25
-10
@@ -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<FunctionCallContent> functionCalls = this.GetOrphanedFunctionCalls(agentResponse);
|
||||
isComplete = functionCalls.Count == 0;
|
||||
|
||||
if (!isComplete)
|
||||
List<UserInputRequestContent> inputRequests = GetUserInputRequests(agentResponse);
|
||||
if (inputRequests.Count > 0)
|
||||
{
|
||||
AgentToolRequest toolRequest = new(agentName, functionCalls);
|
||||
isComplete = false;
|
||||
UserInputRequest approvalRequest = new(agentName, inputRequests.OfType<AIContent>().ToArray());
|
||||
await context.SendMessageAsync(approvalRequest, targetId: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Identify function calls that have no associated result.
|
||||
List<FunctionCallContent> 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<FunctionCallContent> GetOrphanedFunctionCalls(AgentRunResponse agentResponse)
|
||||
private static List<FunctionCallContent> GetOrphanedFunctionCalls(AgentRunResponse agentResponse)
|
||||
{
|
||||
HashSet<string> functionResultIds =
|
||||
[.. agentResponse.Messages
|
||||
@@ -117,6 +129,9 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
|
||||
return functionCalls;
|
||||
}
|
||||
|
||||
private static List<UserInputRequestContent> GetUserInputRequests(AgentRunResponse agentResponse) =>
|
||||
agentResponse.Messages.SelectMany(m => m.Contents.OfType<UserInputRequestContent>()).ToList();
|
||||
|
||||
private string? GetConversationId()
|
||||
{
|
||||
if (this.Model.ConversationId is null)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -16,8 +16,8 @@ public abstract class WorkflowAgentProvider
|
||||
/// <summary>
|
||||
/// 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 <see cref="RequestPort"/> is executed
|
||||
/// that provides an <see cref="AgentToolRequest"/> that describes the function calls requested. The caller may
|
||||
/// then respond with a corrsponding <see cref="AgentToolResponse"/> that includes the results of the function calls.
|
||||
/// that provides an <see cref="AgentFunctionToolRequest"/> that describes the function calls requested. The caller may
|
||||
/// then respond with a corrsponding <see cref="AgentFunctionToolResponse"/> that includes the results of the function calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These will not impact the requests sent to the model by the <see cref="FunctionInvokingChatClient"/>.
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
|
||||
/// <summary>
|
||||
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
|
||||
/// </summary>
|
||||
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<AgentToolRequest>();
|
||||
AgentFunctionToolRequest? toolRequest = inputEvent.Request.Data.As<AgentFunctionToolRequest>();
|
||||
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]);
|
||||
}
|
||||
|
||||
+48
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="AgentFunctionToolRequest"/> class
|
||||
/// </summary>
|
||||
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<string, object?>() { { "name", "Clam Chowder" } }),
|
||||
]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("agent", copy.AgentName);
|
||||
Assert.Equal(2, copy.FunctionCalls.Count);
|
||||
Assert.IsType<FunctionCallContent>(copy.FunctionCalls[0]);
|
||||
Assert.Null(copy.FunctionCalls[0].Arguments);
|
||||
Assert.IsType<FunctionCallContent>(copy.FunctionCalls[1]);
|
||||
Assert.NotNull(copy.FunctionCalls[1].Arguments);
|
||||
Assert.NotEmpty(copy.FunctionCalls[1].Arguments!);
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="AgentFunctionToolResponse"/> class
|
||||
/// </summary>
|
||||
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<FunctionResultContent>(copy.FunctionResults[0]);
|
||||
Assert.Equal("call1", copy.FunctionResults[0].CallId);
|
||||
Assert.IsType<FunctionResultContent>(copy.FunctionResults[1]);
|
||||
Assert.Equal("call2", copy.FunctionResults[1].CallId);
|
||||
}
|
||||
}
|
||||
-28
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for event tests.
|
||||
/// </summary>
|
||||
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<string, object?>() { { "name", "Clam Chowder" } })
|
||||
]));
|
||||
Assert.Equal("agent", copy.AgentName);
|
||||
}
|
||||
}
|
||||
-27
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for event tests.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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>(TEvent source)
|
||||
{
|
||||
string? text = JsonSerializer.Serialize(source);
|
||||
string? text = JsonSerializer.Serialize(source, AIJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(text);
|
||||
TEvent? copy = JsonSerializer.Deserialize<TEvent>(text);
|
||||
TEvent? copy = JsonSerializer.Deserialize<TEvent>(text, AIJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
+46
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="UserInputRequest"/> class
|
||||
/// </summary>
|
||||
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<McpServerToolApprovalRequestContent>(copy.InputRequests[0]);
|
||||
Assert.Equal("call1", mcpRequest.Id);
|
||||
FunctionApprovalRequestContent functionRequest = Assert.IsType<FunctionApprovalRequestContent>(copy.InputRequests[1]);
|
||||
Assert.Equal("call2", functionRequest.Id);
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Verify <see cref="UserInputResponse"/> class
|
||||
/// </summary>
|
||||
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<McpServerToolApprovalResponseContent>(copy.InputResponses[0]);
|
||||
Assert.Equal("call1", mcpResponse.Id);
|
||||
FunctionApprovalResponseContent functionResponse = Assert.IsType<FunctionApprovalResponseContent>(copy.InputResponses[1]);
|
||||
Assert.Equal("call2", functionResponse.Id);
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -6,14 +6,17 @@ using Xunit.Abstractions;
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for event tests.
|
||||
/// Verify <see cref="AnswerRequest"/> class
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -7,21 +7,27 @@ using Xunit.Abstractions;
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for event tests.
|
||||
/// Verify <see cref="AnswerResponse"/> class
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user