mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* refactor: [BREAKING] Config => ExecutorConfig Make the Config name less likely to collide with other classes by renaming to ExecutorConfig. Makes Configured and related classes internal as they do not need to be part of the public surface. * fix: Make RouteBuilder explicit in SourceGen to avoid conflicts
249 lines
8.9 KiB
C#
249 lines
8.9 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
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
|
|
{
|
|
internal const string IndentUnit = " ";
|
|
|
|
/// <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();
|
|
sb.AppendLine("using RouteBuilder = Microsoft.Agents.AI.Workflows.RouteBuilder;");
|
|
|
|
// 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 += IndentUnit;
|
|
}
|
|
}
|
|
|
|
// Class declaration
|
|
sb.AppendLine($"{indent}partial class {info.ClassName}{info.GenericParameters}");
|
|
sb.AppendLine($"{indent}{{");
|
|
|
|
string memberIndent = indent + IndentUnit;
|
|
|
|
// ConfigureProtocol
|
|
sb.AppendLine($"{memberIndent}protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)");
|
|
sb.AppendLine($"{memberIndent}{{");
|
|
|
|
string bodyIndent = memberIndent + IndentUnit;
|
|
|
|
if (info.BaseHasConfigureProtocol)
|
|
{
|
|
sb.Append($"{bodyIndent}return base.ConfigureProtocol(protocolBuilder)");
|
|
bodyIndent += " ";
|
|
}
|
|
else
|
|
{
|
|
sb.Append($"{bodyIndent}return protocolBuilder");
|
|
}
|
|
|
|
// Only generate protocol overrides if [SendsMessage] or [YieldsOutput] attributes are present.
|
|
// Without these attributes, we rely on the base class defaults.
|
|
if (info.ShouldGenerateSentMessageRegistrations)
|
|
{
|
|
GenerateConfigureSentTypes(sb, info, bodyIndent);
|
|
}
|
|
|
|
if (info.ShouldGenerateYieldedOutputRegistrations)
|
|
{
|
|
GenerateConfigureYieldTypes(sb, info, bodyIndent);
|
|
}
|
|
|
|
// Only generate ConfigureRoutes if there are handlers
|
|
if (info.Handlers.Count > 0)
|
|
{
|
|
GenerateConfigureRoutes(sb, info, bodyIndent);
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine(";");
|
|
}
|
|
|
|
// Close ConfigureProtocol
|
|
sb.AppendLine($"{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(".ConfigureRoutes(ConfigureRoutes);");
|
|
|
|
sb.AppendLine($"{indent}void ConfigureRoutes(RouteBuilder routeBuilder)");
|
|
sb.AppendLine($"{indent}{{");
|
|
|
|
string bodyIndent = indent + IndentUnit;
|
|
|
|
// 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}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}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)
|
|
{
|
|
// 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.ClassSendTypes.Where(type => addedTypes.Add(type)))
|
|
{
|
|
sb.AppendLine($".SendsMessage<{type}>()");
|
|
sb.Append(indent);
|
|
}
|
|
|
|
foreach (var handler in info.Handlers)
|
|
{
|
|
foreach (var type in handler.SendTypes.Where(type => addedTypes.Add(type)))
|
|
{
|
|
sb.AppendLine($".SendsMessage<{type}>()");
|
|
sb.Append(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)
|
|
{
|
|
// 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.Where(type => addedTypes.Add(type)))
|
|
{
|
|
sb.AppendLine($".YieldsOutput<{type}>()");
|
|
sb.Append(indent);
|
|
}
|
|
|
|
foreach (var handler in info.Handlers)
|
|
{
|
|
foreach (var type in handler.YieldTypes.Where(type => addedTypes.Add(type)))
|
|
{
|
|
sb.AppendLine($".YieldsOutput<{type}>()");
|
|
sb.Append(indent);
|
|
}
|
|
}
|
|
}
|
|
}
|