mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: .NET: Executor source gen for workflow executor routing (#3131)
* Roslyn Source Generators for Workflow Executor Routing. * Update dotnet/src/Microsoft.Agents.AI.Workflows.Generators/ExecutorRouteGenerator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * WIP. * All fixed up except dangling sends/yields attriutes, working on that next. * Add protocol-only generation for SendsMessage/YieldsOutput attributes * Ensuring collections that can change order are sorted to enable pipeline caching. * Improvents per PR feedback. --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
4940d0ef36
commit
ea7818d390
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI.Workflows.Generators.Models;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Generators.Generation;
|
||||
|
||||
/// <summary>
|
||||
/// Generates source code for executor route configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This builder produces a partial class file that overrides <c>ConfigureRoutes</c> to register
|
||||
/// handlers discovered via [MessageHandler] attributes. It may also generate <c>ConfigureSentTypes</c>
|
||||
/// and <c>ConfigureYieldTypes</c> overrides when [SendsMessage] or [YieldsOutput] attributes are present.
|
||||
/// </remarks>
|
||||
internal static class SourceBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the complete source file for an executor's generated partial class.
|
||||
/// </summary>
|
||||
/// <param name="info">The analyzed executor information containing class metadata and handler details.</param>
|
||||
/// <returns>The generated C# source code as a string.</returns>
|
||||
public static string Generate(ExecutorInfo info)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// File header
|
||||
sb.AppendLine("// <auto-generated/>");
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine();
|
||||
|
||||
// Using directives
|
||||
sb.AppendLine("using System;");
|
||||
sb.AppendLine("using System.Collections.Generic;");
|
||||
sb.AppendLine("using Microsoft.Agents.AI.Workflows;");
|
||||
sb.AppendLine();
|
||||
|
||||
// Namespace
|
||||
if (!string.IsNullOrWhiteSpace(info.Namespace))
|
||||
{
|
||||
sb.AppendLine($"namespace {info.Namespace};");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// For nested classes, we must emit partial declarations for each containing type.
|
||||
// Example: if MyExecutor is nested in Outer.Inner, we emit:
|
||||
// partial class Outer { partial class Inner { partial class MyExecutor { ... } } }
|
||||
string indent = "";
|
||||
if (info.IsNested)
|
||||
{
|
||||
foreach (string containingType in info.ContainingTypeChain.Split('.'))
|
||||
{
|
||||
sb.AppendLine($"{indent}partial class {containingType}");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
indent += " ";
|
||||
}
|
||||
}
|
||||
|
||||
// Class declaration
|
||||
sb.AppendLine($"{indent}partial class {info.ClassName}{info.GenericParameters}");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string memberIndent = indent + " ";
|
||||
bool hasContent = false;
|
||||
|
||||
// Only generate ConfigureRoutes if there are handlers
|
||||
if (info.Handlers.Count > 0)
|
||||
{
|
||||
GenerateConfigureRoutes(sb, info, memberIndent);
|
||||
hasContent = true;
|
||||
}
|
||||
|
||||
// Only generate protocol overrides if [SendsMessage] or [YieldsOutput] attributes are present.
|
||||
// Without these attributes, we rely on the base class defaults.
|
||||
if (info.ShouldGenerateProtocolOverrides)
|
||||
{
|
||||
if (hasContent)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
GenerateConfigureSentTypes(sb, info, memberIndent);
|
||||
sb.AppendLine();
|
||||
GenerateConfigureYieldTypes(sb, info, memberIndent);
|
||||
}
|
||||
|
||||
// Close class
|
||||
sb.AppendLine($"{indent}}}");
|
||||
|
||||
// Close nested classes
|
||||
if (info.IsNested)
|
||||
{
|
||||
string[] containingTypes = info.ContainingTypeChain.Split('.');
|
||||
for (int i = containingTypes.Length - 1; i >= 0; i--)
|
||||
{
|
||||
indent = new string(' ', i * 4);
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the ConfigureRoutes override that registers all [MessageHandler] methods.
|
||||
/// </summary>
|
||||
private static void GenerateConfigureRoutes(StringBuilder sb, ExecutorInfo info, string indent)
|
||||
{
|
||||
sb.AppendLine($"{indent}protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string bodyIndent = indent + " ";
|
||||
|
||||
// If a base class has its own ConfigureRoutes, chain to it first to preserve inherited handlers.
|
||||
if (info.BaseHasConfigureRoutes)
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}routeBuilder = base.ConfigureRoutes(routeBuilder);");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Generate handler registrations using fluent AddHandler calls.
|
||||
// RouteBuilder.AddHandler<TIn> registers a void handler; AddHandler<TIn, TOut> registers one with a return value.
|
||||
if (info.Handlers.Count == 1)
|
||||
{
|
||||
HandlerInfo handler = info.Handlers[0];
|
||||
sb.AppendLine($"{bodyIndent}return routeBuilder");
|
||||
sb.Append($"{bodyIndent} .AddHandler");
|
||||
AppendHandlerGenericArgs(sb, handler);
|
||||
sb.AppendLine($"(this.{handler.MethodName});");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiple handlers: chain fluent calls, semicolon only on the last one.
|
||||
sb.AppendLine($"{bodyIndent}return routeBuilder");
|
||||
|
||||
for (int i = 0; i < info.Handlers.Count; i++)
|
||||
{
|
||||
HandlerInfo handler = info.Handlers[i];
|
||||
|
||||
sb.Append($"{bodyIndent} .AddHandler");
|
||||
AppendHandlerGenericArgs(sb, handler);
|
||||
sb.Append($"(this.{handler.MethodName})");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Remove last newline without using that System.Environment which is banned from use in analyzers
|
||||
var newLineLength = new StringBuilder().AppendLine().Length;
|
||||
sb.Remove(sb.Length - newLineLength, newLineLength);
|
||||
sb.AppendLine(";");
|
||||
}
|
||||
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends generic type arguments for AddHandler based on whether the handler returns a value.
|
||||
/// </summary>
|
||||
private static void AppendHandlerGenericArgs(StringBuilder sb, HandlerInfo handler)
|
||||
{
|
||||
// Handlers returning ValueTask use single type arg; ValueTask<T> uses two.
|
||||
if (handler.HasOutput && handler.OutputTypeName != null)
|
||||
{
|
||||
sb.Append($"<{handler.InputTypeName}, {handler.OutputTypeName}>");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"<{handler.InputTypeName}>");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates ConfigureSentTypes override declaring message types this executor sends via context.SendMessageAsync.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Types come from [SendsMessage] attributes on the class or individual handler methods.
|
||||
/// This enables workflow protocol validation at build time.
|
||||
/// </remarks>
|
||||
private static void GenerateConfigureSentTypes(StringBuilder sb, ExecutorInfo info, string indent)
|
||||
{
|
||||
sb.AppendLine($"{indent}protected override ISet<Type> ConfigureSentTypes()");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string bodyIndent = indent + " ";
|
||||
|
||||
sb.AppendLine($"{bodyIndent}var types = base.ConfigureSentTypes();");
|
||||
|
||||
foreach (var type in info.ClassSendTypes)
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
}
|
||||
|
||||
foreach (var handler in info.Handlers)
|
||||
{
|
||||
foreach (var type in handler.SendTypes)
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($"{bodyIndent}return types;");
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates ConfigureYieldTypes override declaring message types this executor yields via context.YieldOutputAsync.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Types come from [YieldsOutput] attributes and handler return types (ValueTask<T>).
|
||||
/// This enables workflow protocol validation at build time.
|
||||
/// </remarks>
|
||||
private static void GenerateConfigureYieldTypes(StringBuilder sb, ExecutorInfo info, string indent)
|
||||
{
|
||||
sb.AppendLine($"{indent}protected override ISet<Type> ConfigureYieldTypes()");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string bodyIndent = indent + " ";
|
||||
|
||||
sb.AppendLine($"{bodyIndent}var types = base.ConfigureYieldTypes();");
|
||||
|
||||
// Track types to avoid emitting duplicate Add calls (the set handles runtime dedup,
|
||||
// but cleaner generated code is easier to read).
|
||||
var addedTypes = new HashSet<string>();
|
||||
|
||||
foreach (var type in info.ClassYieldTypes)
|
||||
{
|
||||
if (addedTypes.Add(type))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var handler in info.Handlers)
|
||||
{
|
||||
foreach (var type in handler.YieldTypes)
|
||||
{
|
||||
if (addedTypes.Add(type))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
}
|
||||
}
|
||||
|
||||
// Handler return types (ValueTask<T>) are implicitly yielded.
|
||||
if (handler.HasOutput && handler.OutputTypeName != null && addedTypes.Add(handler.OutputTypeName))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({handler.OutputTypeName}));");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($"{bodyIndent}return types;");
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user