// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
///
/// Provides extension methods for configuring and building workflows using the WorkflowBuilder type.
///
/// 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.
public static class WorkflowBuilderExtensions
{
///
/// Adds edges to the workflow that forward messages of the specified type from the source executor to
/// one or more target executors.
///
/// The type of message to forward.
/// The to which the edges will be added.
/// The source executor from which messages will be forwarded.
/// The target executors to which messages will be forwarded.
/// The updated instance.
public static WorkflowBuilder ForwardMessage(this WorkflowBuilder builder, ExecutorIsh source, params IEnumerable executors)
{
Throw.IfNull(executors);
Func predicate = WorkflowBuilder.CreateConditionFunc((Func)IsAllowedType)!;
#if NET
if (executors.TryGetNonEnumeratedCount(out int count) && count == 1)
#else
if (executors is ICollection { Count: 1 })
#endif
{
return builder.AddEdge(source, executors.First(), predicate);
}
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, executors));
// The reason we can check for "not null" here is that CreateConditionFunc will do the correct unwrapping
// logic for PortableValues.
static bool IsAllowedType(object? message) => message is not null;
}
///
/// Adds edges from the specified source to the provided executors, excluding messages of a specified type.
///
/// The type of messages to exclude from being forwarded to the executors.
/// The instance to which the edges will be added.
/// The source executor from which messages will be forwarded.
/// The target executors to which messages, except those of type , will be forwarded.
/// The updated instance with the added edges.
public static WorkflowBuilder ForwardExcept(this WorkflowBuilder builder, ExecutorIsh source, params IEnumerable executors)
{
Throw.IfNull(executors);
Func predicate = WorkflowBuilder.CreateConditionFunc((Func)IsAllowedType)!;
#if NET
if (executors.TryGetNonEnumeratedCount(out int count) && count == 1)
#else
if (executors is ICollection { Count: 1 })
#endif
{
return builder.AddEdge(source, executors.First(), predicate);
}
return builder.AddSwitch(source, (switch_) => switch_.AddCase(predicate, executors));
// The reason we can check for "null" here is that CreateConditionFunc will do the correct unwrapping
// logic for PortableValues.
static bool IsAllowedType(object? message) => message is null;
}
///
/// Adds a sequential chain of executors to the workflow, connecting each executor in order so that each is
/// executed after the previous one.
///
/// Each executor in the chain is connected so that execution flows from the source to each subsequent
/// executor in the order provided.
/// The workflow builder to which the executor chain will be added.
/// The initial executor in the chain. Cannot be null.
/// An ordered array of executors to be added to the chain after the source.
/// The original workflow builder instance with the specified executor chain added.
/// Thrown if there is a cycle in the chain.
public static WorkflowBuilder AddChain(this WorkflowBuilder builder, ExecutorIsh source, params IEnumerable executors)
{
Throw.IfNull(builder);
Throw.IfNull(source);
HashSet seenExecutors = [source.Id];
foreach (var executor in executors)
{
Throw.IfNull(executor, nameof(executors));
if (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);
source = executor;
}
return builder;
}
///
/// Adds an external call to the workflow by connecting the specified source to a new input port with the given
/// request and response types.
///
/// 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.
/// The type of the request message that the external call will accept.
/// The type of the response message that the external call will produce.
/// The workflow builder to which the external call will be added.
/// The source executor representing the external system or process to connect. Cannot be null.
/// The unique identifier for the input port that will handle the external call. Cannot be null.
/// The original workflow builder instance with the external call added.
public static WorkflowBuilder AddExternalCall(this WorkflowBuilder builder, ExecutorIsh source, string portId)
{
Throw.IfNull(builder);
Throw.IfNull(source);
Throw.IfNull(portId);
InputPort port = new(portId, typeof(TRequest), typeof(TResponse));
return builder.AddEdge(source, port)
.AddEdge(port, source);
}
///
/// Adds a switch step to the workflow, allowing conditional branching based on the specified source executor.
///
/// 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.
/// The workflow builder to which the switch step will be added. Cannot be null.
/// The source executor that determines the branching condition for the switch. Cannot be null.
/// An action used to configure the switch builder, specifying the branches and their conditions. Cannot be null.
/// The workflow builder instance with the configured switch step added.
public static WorkflowBuilder AddSwitch(this WorkflowBuilder builder, ExecutorIsh source, Action configureSwitch)
{
Throw.IfNull(builder);
Throw.IfNull(source);
Throw.IfNull(configureSwitch);
SwitchBuilder switchBuilder = new();
configureSwitch(switchBuilder);
return switchBuilder.ReduceToFanOut(builder, source);
}
}