// 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; /// /// Generates source code for executor route configuration. /// /// /// This builder produces a partial class file that overrides ConfigureRoutes to register /// handlers discovered via [MessageHandler] attributes. It may also generate ConfigureSentTypes /// and ConfigureYieldTypes overrides when [SendsMessage] or [YieldsOutput] attributes are present. /// internal static class SourceBuilder { internal const string IndentUnit = " "; /// /// Generates the complete source file for an executor's generated partial class. /// /// The analyzed executor information containing class metadata and handler details. /// The generated C# source code as a string. public static string Generate(ExecutorInfo info) { var sb = new StringBuilder(); // File header sb.AppendLine("// "); 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 += 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(); } /// /// Generates the ConfigureRoutes override that registers all [MessageHandler] methods. /// 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 registers a void handler; AddHandler 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}}}"); } /// /// Appends generic type arguments for AddHandler based on whether the handler returns a value. /// private static void AppendHandlerGenericArgs(StringBuilder sb, HandlerInfo handler) { // Handlers returning ValueTask use single type arg; ValueTask uses two. if (handler.HasOutput && handler.OutputTypeName != null) { sb.Append($"<{handler.InputTypeName}, {handler.OutputTypeName}>"); } else { sb.Append($"<{handler.InputTypeName}>"); } } /// /// Generates ConfigureSentTypes override declaring message types this executor sends via context.SendMessageAsync. /// /// /// Types come from [SendsMessage] attributes on the class or individual handler methods. /// This enables workflow protocol validation at build time. /// 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(); 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); } } } /// /// Generates ConfigureYieldTypes override declaring message types this executor yields via context.YieldOutputAsync. /// /// /// Types come from [YieldsOutput] attributes and handler return types (ValueTask<T>). /// This enables workflow protocol validation at build time. /// 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(); 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); } } } }