mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Conditional edge routing sample.
This commit is contained in:
@@ -187,16 +187,25 @@ public class DurableWorkflowRunner
|
||||
|
||||
foreach (WorkflowExecutionLevel level in plan.Levels)
|
||||
{
|
||||
if (level.Executors.Count == 1)
|
||||
// Filter executors based on edge conditions from their predecessors
|
||||
List<WorkflowExecutorInfo> eligibleExecutors = GetEligibleExecutors(level.Executors, results, plan, logger);
|
||||
|
||||
if (eligibleExecutors.Count == 0)
|
||||
{
|
||||
WorkflowExecutorInfo executorInfo = level.Executors[0];
|
||||
// No eligible executors at this level, continue to next level
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eligibleExecutors.Count == 1)
|
||||
{
|
||||
WorkflowExecutorInfo executorInfo = eligibleExecutors[0];
|
||||
string input = GetExecutorInput(executorInfo.ExecutorId, initialInput, results, plan);
|
||||
results[executorInfo.ExecutorId] = await this.ExecuteExecutorAsync(context, executorInfo, input, logger).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<Task<(string Id, string Result)>> tasks = [];
|
||||
foreach (WorkflowExecutorInfo executorInfo in level.Executors)
|
||||
foreach (WorkflowExecutorInfo executorInfo in eligibleExecutors)
|
||||
{
|
||||
string input = GetExecutorInput(executorInfo.ExecutorId, initialInput, results, plan);
|
||||
tasks.Add(this.ExecuteExecutorWithIdAsync(context, executorInfo, input, logger));
|
||||
@@ -212,6 +221,115 @@ public class DurableWorkflowRunner
|
||||
return GetFinalResult(plan, results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters executors based on their incoming edge conditions.
|
||||
/// An executor is eligible if all its incoming edges have conditions that evaluate to true,
|
||||
/// or if the edges have no conditions.
|
||||
/// </summary>
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing workflow types registered at startup.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing workflow types registered at startup.")]
|
||||
private static List<WorkflowExecutorInfo> GetEligibleExecutors(
|
||||
List<WorkflowExecutorInfo> executors,
|
||||
Dictionary<string, string> results,
|
||||
WorkflowExecutionPlan plan,
|
||||
ILogger logger)
|
||||
{
|
||||
List<WorkflowExecutorInfo> eligible = [];
|
||||
|
||||
foreach (WorkflowExecutorInfo executorInfo in executors)
|
||||
{
|
||||
List<string> predecessors = plan.Predecessors[executorInfo.ExecutorId];
|
||||
|
||||
// Root executor (no predecessors) is always eligible
|
||||
if (predecessors.Count == 0)
|
||||
{
|
||||
eligible.Add(executorInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any predecessor's edge condition allows this executor to run
|
||||
bool isEligible = false;
|
||||
foreach (string predecessorId in predecessors)
|
||||
{
|
||||
// Get the condition for this edge (predecessor -> current executor)
|
||||
if (!plan.EdgeConditions.TryGetValue((predecessorId, executorInfo.ExecutorId), out Func<object?, bool>? condition))
|
||||
{
|
||||
// No condition registered for this edge, assume it's eligible
|
||||
isEligible = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (condition is null)
|
||||
{
|
||||
// Edge has no condition, always eligible
|
||||
isEligible = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Evaluate the condition using the predecessor's result
|
||||
if (results.TryGetValue(predecessorId, out string? predecessorResult))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the predecessor's output type for proper deserialization
|
||||
Type? predecessorOutputType = plan.ExecutorOutputTypes.GetValueOrDefault(predecessorId);
|
||||
|
||||
// Deserialize the predecessor result to the expected type for condition evaluation
|
||||
object? resultObject = DeserializeForCondition(predecessorResult, predecessorOutputType);
|
||||
if (condition(resultObject))
|
||||
{
|
||||
isEligible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to evaluate condition for edge from '{PredecessorId}' to '{ExecutorId}'", predecessorId, executorInfo.ExecutorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEligible)
|
||||
{
|
||||
eligible.Add(executorInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogExecutorSkipped(executorInfo.ExecutorId);
|
||||
}
|
||||
}
|
||||
|
||||
return eligible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a JSON string result into an object for condition evaluation.
|
||||
/// </summary>
|
||||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing workflow types registered at startup.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing workflow types registered at startup.")]
|
||||
private static object? DeserializeForCondition(string json, Type? targetType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (targetType is null)
|
||||
{
|
||||
return JsonSerializer.Deserialize<object>(json);
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize(json, targetType);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// If it's not valid JSON, return the string as-is
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string Id, string Result)> ExecuteExecutorWithIdAsync(
|
||||
TaskOrchestrationContext context,
|
||||
WorkflowExecutorInfo executorInfo,
|
||||
|
||||
@@ -130,4 +130,10 @@ internal static partial class Logs
|
||||
Level = LogLevel.Information,
|
||||
Message = "Activity executed for executor '{ExecutorId}' with result: {Result}")]
|
||||
public static partial void LogActivityExecuted(this ILogger logger, string executorId, string result);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 17,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Executor '{ExecutorId}' skipped due to edge condition evaluation")]
|
||||
public static partial void LogExecutorSkipped(this ILogger logger, string executorId);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,17 @@ public sealed class WorkflowExecutionPlan
|
||||
/// </summary>
|
||||
public Dictionary<string, List<string>> Successors { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Maps edge connections (sourceId, targetId) to their condition functions.
|
||||
/// The condition function takes the predecessor's result and returns true if the edge should be followed.
|
||||
/// </summary>
|
||||
public Dictionary<(string SourceId, string TargetId), Func<object?, bool>?> EdgeConditions { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Maps executor IDs to their output types (for proper deserialization during condition evaluation).
|
||||
/// </summary>
|
||||
public Dictionary<string, Type?> ExecutorOutputTypes { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this workflow has any parallel execution opportunities.
|
||||
/// </summary>
|
||||
@@ -89,6 +100,7 @@ public static class WorkflowHelper
|
||||
|
||||
Dictionary<string, ExecutorBinding> executors = workflow.ReflectExecutors();
|
||||
Dictionary<string, HashSet<EdgeInfo>> edges = workflow.ReflectEdges();
|
||||
Dictionary<(string SourceId, string TargetId), Func<object?, bool>?> edgeConditions = workflow.GetEdgeConditions();
|
||||
|
||||
WorkflowExecutionPlan plan = new();
|
||||
|
||||
@@ -97,12 +109,15 @@ public static class WorkflowHelper
|
||||
Dictionary<string, List<string>> predecessors = [];
|
||||
Dictionary<string, int> inDegree = [];
|
||||
|
||||
// Initialize all executors
|
||||
foreach (string executorId in executors.Keys)
|
||||
// Initialize all executors and extract their output types
|
||||
foreach (KeyValuePair<string, ExecutorBinding> executor in executors)
|
||||
{
|
||||
successors[executorId] = [];
|
||||
predecessors[executorId] = [];
|
||||
inDegree[executorId] = 0;
|
||||
successors[executor.Key] = [];
|
||||
predecessors[executor.Key] = [];
|
||||
inDegree[executor.Key] = 0;
|
||||
|
||||
// Extract output type from executor type (e.g., Executor<TInput, TOutput> -> TOutput)
|
||||
plan.ExecutorOutputTypes[executor.Key] = GetExecutorOutputType(executor.Value.ExecutorType);
|
||||
}
|
||||
|
||||
// Build the graph from edges
|
||||
@@ -124,6 +139,12 @@ public static class WorkflowHelper
|
||||
}
|
||||
}
|
||||
|
||||
// Store edge conditions in the plan
|
||||
foreach (KeyValuePair<(string SourceId, string TargetId), Func<object?, bool>?> condition in edgeConditions)
|
||||
{
|
||||
plan.EdgeConditions[condition.Key] = condition.Value;
|
||||
}
|
||||
|
||||
// Store the graph structure in the plan
|
||||
foreach (string executorId in executors.Keys)
|
||||
{
|
||||
@@ -213,4 +234,41 @@ public static class WorkflowHelper
|
||||
return typeName.Contains("AIAgentHostExecutor", StringComparison.OrdinalIgnoreCase) &&
|
||||
assemblyName.Contains("Microsoft.Agents.AI", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the output type from an executor type.
|
||||
/// For Executor<TInput, TOutput>, returns TOutput.
|
||||
/// For Executor<TInput>, returns null (void output).
|
||||
/// </summary>
|
||||
/// <param name="executorType">The executor type to analyze.</param>
|
||||
/// <returns>The output type, or null if the executor has no typed output.</returns>
|
||||
private static Type? GetExecutorOutputType(Type executorType)
|
||||
{
|
||||
// Walk up the inheritance chain to find Executor<TInput, TOutput> or Executor<TInput>
|
||||
Type? currentType = executorType;
|
||||
while (currentType is not null)
|
||||
{
|
||||
if (currentType.IsGenericType)
|
||||
{
|
||||
Type genericDefinition = currentType.GetGenericTypeDefinition();
|
||||
Type[] genericArgs = currentType.GetGenericArguments();
|
||||
|
||||
// Check for Executor<TInput, TOutput> (2 type parameters)
|
||||
if (genericArgs.Length == 2 && genericDefinition.Name.StartsWith("Executor", StringComparison.Ordinal))
|
||||
{
|
||||
return genericArgs[1]; // TOutput
|
||||
}
|
||||
|
||||
// Check for Executor<TInput> (1 type parameter) - void return
|
||||
if (genericArgs.Length == 1 && genericDefinition.Name.StartsWith("Executor", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,30 @@ public class Workflow
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the condition functions for direct edges, keyed by (sourceId, targetId) tuple.
|
||||
/// </summary>
|
||||
/// <returns>A dictionary mapping edge connections to their condition functions (null if no condition).</returns>
|
||||
/// <remarks>This method creates a new dictionary each time it is called to ensure thread safety.</remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Method creates a new collection on each call.")]
|
||||
public Dictionary<(string SourceId, string TargetId), Func<object?, bool>?> GetEdgeConditions()
|
||||
{
|
||||
Dictionary<(string SourceId, string TargetId), Func<object?, bool>?> conditions = [];
|
||||
|
||||
foreach (KeyValuePair<string, HashSet<Edge>> edgeGroup in this.Edges)
|
||||
{
|
||||
foreach (Edge edge in edgeGroup.Value)
|
||||
{
|
||||
if (edge.DirectEdgeData is DirectEdgeData directEdge)
|
||||
{
|
||||
conditions[(directEdge.SourceId, directEdge.SinkId)] = directEdge.Condition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conditions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all executor bindings in the workflow, keyed by their ID.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user