mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
190ca75b6a
* Add workflow builder edge tests Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/3c3d5324-cdcd-4a38-8c67-94e4e78e29c5 Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Strengthen workflow edge helper tests Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Normalize edge helper bad input validation Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Clarify edge helper target validation Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Use explicit target parameter names Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Document workflow edge test helpers Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Clarify null element validation messages Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Add repeated chain executor coverage Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Preserve Throw helper validation style Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Cover empty switch case targets Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Relax builder null assertion parameter checks Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/af831ee2-0a99-4427-9ffd-a3b5022c1b3b Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Inline ValidateTargets into call sites Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/cb9a6a6a-02c7-41a8-a4b4-da16ad62ef86 Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Refactor ForwardExcept with TFM-specialized TryGetNonEnumeratedCount Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/b081f61f-93ce-45dc-abbd-82c465395470 Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Use TFM-specialized count check: TryGetNonEnumeratedCount for NET6+, ICollection pattern for NETFX Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/8ec28a43-e7b7-456e-8d8e-921511b4accc Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Apply TFM-specialized count check to ForwardMessage as well Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/9238ea32-a3e8-4b83-9683-484ad400071f Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Address review feedback: simplify Throw.IfNull in SwitchBuilder per westey-m suggestion Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/299950fd-4457-47f3-a373-f65d601b7ea5 Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Use indexed parameter name in SwitchBuilder Throw.IfNull: executors[index] Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c5655707-5b0b-44f3-98a9-5f3961e32cfe Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Revert #if NET6_0_OR_GREATER back to #if NET; inline executorIndex++ Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c5655707-5b0b-44f3-98a9-5f3961e32cfe Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * Add comment explaining unusual Throw.IfNull use for null elements inside collection Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c5655707-5b0b-44f3-98a9-5f3961e32cfe Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> Co-authored-by: Jacob Alber <jaalber@microsoft.com>
215 lines
12 KiB
C#
215 lines
12 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI.Workflows;
|
|
|
|
/// <summary>
|
|
/// Provides extension methods for configuring and building workflows using the WorkflowBuilder type.
|
|
/// </summary>
|
|
/// <remarks>These extension methods simplify the process of connecting executors, adding external calls, and
|
|
/// constructing workflows with output aggregation. They are intended to streamline workflow graph construction and
|
|
/// promote common patterns for chaining and aggregating workflow steps.</remarks>
|
|
public static class WorkflowBuilderExtensions
|
|
{
|
|
/// <summary>
|
|
/// Adds edges to the workflow that forward messages of the specified type from the source executor to
|
|
/// one or more target executors.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">The type of message to forward.</typeparam>
|
|
/// <param name="builder">The <see cref="WorkflowBuilder"/> to which the edges will be added.</param>
|
|
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
|
/// <param name="target">The target executor to which messages will be forwarded.</param>
|
|
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
|
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, ExecutorBinding target)
|
|
{
|
|
Throw.IfNull(target, nameof(target));
|
|
|
|
return builder.ForwardMessage<TMessage>(source, [target], condition: null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds edges to the workflow that forward messages of the specified type from the source executor to
|
|
/// one or more target executors.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">The type of message to forward.</typeparam>
|
|
/// <param name="builder">The <see cref="WorkflowBuilder"/> to which the edges will be added.</param>
|
|
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
|
/// <param name="targets">The target executors to which messages will be forwarded.</param>
|
|
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
|
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, IEnumerable<ExecutorBinding> targets)
|
|
=> builder.ForwardMessage<TMessage>(source, targets, condition: null);
|
|
|
|
/// <summary>
|
|
/// Adds edges to the workflow that forward messages of the specified type from the source executor to
|
|
/// one or more target executors.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">The type of message to forward.</typeparam>
|
|
/// <param name="builder">The <see cref="WorkflowBuilder"/> to which the edges will be added.</param>
|
|
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
|
/// <param name="targets">The target executors to which messages will be forwarded.</param>
|
|
/// <param name="condition">An optional condition that messages must satisfy to be forwarded. If <see langword="null"/>,
|
|
/// all messages of type <typeparamref name="TMessage"/> will be forwarded.</param>
|
|
/// <returns>The updated <see cref="WorkflowBuilder"/> instance.</returns>
|
|
public static WorkflowBuilder ForwardMessage<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, IEnumerable<ExecutorBinding> targets, Func<TMessage, bool>? condition = null)
|
|
{
|
|
Throw.IfNull(builder);
|
|
Throw.IfNull(source);
|
|
Throw.IfNull(targets);
|
|
|
|
Func<object?, bool> predicate = WorkflowBuilder.CreateConditionFunc<TMessage>(IsAllowedTypeAndMatchingCondition)!;
|
|
|
|
#if NET
|
|
if (targets.TryGetNonEnumeratedCount(out int count) && count == 1)
|
|
#else
|
|
if (targets is ICollection<ExecutorBinding> { Count: 1 })
|
|
#endif
|
|
{
|
|
return builder.AddEdge(source, Throw.IfNull(targets.First(), nameof(targets)), predicate);
|
|
}
|
|
|
|
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, targets.Select(ValidateTarget)));
|
|
|
|
// The reason we can check for "not null" here is that CreateConditionFunc<T> will do the correct unwrapping
|
|
// logic for PortableValues.
|
|
bool IsAllowedTypeAndMatchingCondition(TMessage? message) => message != null && (condition == null || condition(message));
|
|
|
|
ExecutorBinding ValidateTarget(ExecutorBinding target) => Throw.IfNull(target, nameof(targets));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds edges from the specified source to the provided executors, excluding messages of a specified type.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">The type of messages to exclude from being forwarded to the executors.</typeparam>
|
|
/// <param name="builder">The <see cref="WorkflowBuilder"/> instance to which the edges will be added.</param>
|
|
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
|
/// <param name="target">The target executor to which messages, except those of type <typeparamref name="TMessage"/>, will be forwarded.</param>
|
|
/// <returns>The updated <see cref="WorkflowBuilder"/> instance with the added edges.</returns>
|
|
public static WorkflowBuilder ForwardExcept<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, ExecutorBinding target)
|
|
{
|
|
Throw.IfNull(target, nameof(target));
|
|
|
|
return builder.ForwardExcept<TMessage>(source, [target]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds edges from the specified source to the provided executors, excluding messages of a specified type.
|
|
/// </summary>
|
|
/// <typeparam name="TMessage">The type of messages to exclude from being forwarded to the executors.</typeparam>
|
|
/// <param name="builder">The <see cref="WorkflowBuilder"/> instance to which the edges will be added.</param>
|
|
/// <param name="source">The source executor from which messages will be forwarded.</param>
|
|
/// <param name="targets">The target executors to which messages, except those of type <typeparamref name="TMessage"/>, will be forwarded.</param>
|
|
/// <returns>The updated <see cref="WorkflowBuilder"/> instance with the added edges.</returns>
|
|
public static WorkflowBuilder ForwardExcept<TMessage>(this WorkflowBuilder builder, ExecutorBinding source, IEnumerable<ExecutorBinding> targets)
|
|
{
|
|
Throw.IfNull(builder);
|
|
Throw.IfNull(source);
|
|
Throw.IfNull(targets);
|
|
|
|
Func<object?, bool> predicate = WorkflowBuilder.CreateConditionFunc<TMessage>((Func<object?, bool>)IsAllowedType)!;
|
|
|
|
#if NET
|
|
if (targets.TryGetNonEnumeratedCount(out int count) && count == 1)
|
|
#else
|
|
if (targets is ICollection<ExecutorBinding> { Count: 1 })
|
|
#endif
|
|
{
|
|
return builder.AddEdge(source, Throw.IfNull(targets.First(), nameof(targets)), predicate);
|
|
}
|
|
|
|
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, targets.Select(ValidateTarget)));
|
|
|
|
// The reason we can check for "null" here is that CreateConditionFunc<T> will do the correct unwrapping
|
|
// logic for PortableValues.
|
|
static bool IsAllowedType(object? message) => message is null;
|
|
|
|
ExecutorBinding ValidateTarget(ExecutorBinding target) => Throw.IfNull(target, nameof(targets));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a sequential chain of executors to the workflow, connecting each executor in order so that each is
|
|
/// executed after the previous one.
|
|
/// </summary>
|
|
/// <remarks>Each executor in the chain is connected so that execution flows from the source to each subsequent
|
|
/// executor in the order provided.</remarks>
|
|
/// <param name="builder">The workflow builder to which the executor chain will be added. </param>
|
|
/// <param name="source">The initial executor in the chain. Cannot be null.</param>
|
|
/// <param name="executors">An ordered sequence of executors to be added to the chain after the source.</param>
|
|
/// <returns>The original workflow builder instance with the specified executor chain added.</returns>
|
|
/// <param name="allowRepetition">If set to <see langword="true"/>, the same executor can be added to the chain multiple times.</param>
|
|
/// <exception cref="ArgumentException">Thrown if there is a cycle in the chain.</exception>
|
|
public static WorkflowBuilder AddChain(this WorkflowBuilder builder, ExecutorBinding source, IList<ExecutorBinding> executors, bool allowRepetition = false)
|
|
{
|
|
Throw.IfNull(builder);
|
|
Throw.IfNull(source);
|
|
Throw.IfNull(executors);
|
|
|
|
HashSet<string> seenExecutors = [source.Id];
|
|
|
|
foreach (var executor in executors)
|
|
{
|
|
Throw.IfNull(executor, nameof(executors));
|
|
|
|
if (!allowRepetition && seenExecutors.Contains(executor.Id))
|
|
{
|
|
throw new ArgumentException($"Executor '{executor.Id}' is already in the chain.", nameof(executors));
|
|
}
|
|
seenExecutors.Add(executor.Id);
|
|
|
|
builder.AddEdge(source, executor, idempotent: true);
|
|
source = executor;
|
|
}
|
|
|
|
return builder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an external call to the workflow by connecting the specified source to a new input port with the given
|
|
/// request and response types.
|
|
/// </summary>
|
|
/// <remarks>This method creates a bidirectional connection between the source and the new input port,
|
|
/// allowing the workflow to send requests and receive responses through the specified external call. The port is
|
|
/// configured to handle messages of the specified request and response types.</remarks>
|
|
/// <typeparam name="TRequest">The type of the request message that the external call will accept.</typeparam>
|
|
/// <typeparam name="TResponse">The type of the response message that the external call will produce.</typeparam>
|
|
/// <param name="builder">The workflow builder to which the external call will be added. </param>
|
|
/// <param name="source">The source executor representing the external system or process to connect. Cannot be null.</param>
|
|
/// <param name="portId">The unique identifier for the input port that will handle the external call. Cannot be null.</param>
|
|
/// <returns>The original workflow builder instance with the external call added.</returns>
|
|
public static WorkflowBuilder AddExternalCall<TRequest, TResponse>(this WorkflowBuilder builder, ExecutorBinding source, string portId)
|
|
{
|
|
Throw.IfNull(builder);
|
|
Throw.IfNull(source);
|
|
Throw.IfNull(portId);
|
|
|
|
RequestPort port = new(portId, typeof(TRequest), typeof(TResponse));
|
|
return builder.AddEdge(source, port)
|
|
.AddEdge(port, source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a switch step to the workflow, allowing conditional branching based on the specified source executor.
|
|
/// </summary>
|
|
/// <remarks>Use this method to introduce conditional logic into a workflow, enabling execution to follow
|
|
/// different paths based on the outcome of the source executor. The switch configuration defines the available
|
|
/// branches and their associated conditions.</remarks>
|
|
/// <param name="builder">The workflow builder to which the switch step will be added. Cannot be null.</param>
|
|
/// <param name="source">The source executor that determines the branching condition for the switch. Cannot be null.</param>
|
|
/// <param name="configureSwitch">An action used to configure the switch builder, specifying the branches and their conditions. Cannot be null.</param>
|
|
/// <returns>The workflow builder instance with the configured switch step added.</returns>
|
|
public static WorkflowBuilder AddSwitch(this WorkflowBuilder builder, ExecutorBinding source, Action<SwitchBuilder> configureSwitch)
|
|
{
|
|
Throw.IfNull(builder);
|
|
Throw.IfNull(source);
|
|
Throw.IfNull(configureSwitch);
|
|
|
|
SwitchBuilder switchBuilder = new();
|
|
configureSwitch(switchBuilder);
|
|
|
|
return switchBuilder.ReduceToFanOut(builder, source);
|
|
}
|
|
}
|