// 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) => builder.ForwardMessage(source, condition: null, executors); /// /// 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. /// An optional condition that messages must satisfy to be forwarded. If , /// all messages of type will be forwarded. /// The target executors to which messages will be forwarded. /// The updated instance. public static WorkflowBuilder ForwardMessage(this WorkflowBuilder builder, ExecutorIsh source, Func? condition = null, params IEnumerable executors) { Throw.IfNull(executors); Func predicate = WorkflowBuilder.CreateConditionFunc(IsAllowedTypeAndMatchingCondition)!; #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. bool IsAllowedTypeAndMatchingCondition(TMessage? message) => message != null && (condition == null || condition(message)); } /// /// 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. /// If set to , the same executor can be added to the chain multiple times. /// 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, bool allowRepetition = false, params IEnumerable executors) { Throw.IfNull(builder); Throw.IfNull(source); HashSet 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; } /// /// 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); RequestPort 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); } }