Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Generators\Microsoft.Agents.AI.Workflows.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : Workflow
/// 1. HandleAsync(string message): Handles the initial task to create a slogan.
/// 2. HandleAsync(Feedback message): Handles feedback to improve the slogan.
/// </summary>
internal sealed class SloganWriterExecutor : Executor
internal sealed partial class SloganWriterExecutor : Executor
{
private readonly AIAgent _agent;
private AgentSession? _session;
Expand All @@ -133,10 +133,7 @@ public SloganWriterExecutor(string id, IChatClient chatClient) : base(id)
this._agent = new ChatClientAgent(chatClient, agentOptions);
}

protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<string, SloganResult>(this.HandleAsync)
.AddHandler<FeedbackResult, SloganResult>(this.HandleAsync);

[MessageHandler]
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._session ??= await this._agent.CreateSessionAsync(cancellationToken);
Expand All @@ -149,6 +146,7 @@ public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContex
return sloganResult;
}

[MessageHandler]
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var feedbackMessage = $"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@

<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Generators\Microsoft.Agents.AI.Workflows.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace WorkflowAsAnAgentObservabilitySample;

internal static class WorkflowHelper
internal static partial class WorkflowHelper
{
/// <summary>
/// Creates a workflow that uses two language agents to process input concurrently.
Expand Down Expand Up @@ -50,21 +50,16 @@ private static AIAgent GetLanguageAgent(string targetLanguage, IChatClient chatC
/// <summary>
/// Executor that starts the concurrent processing by sending messages to the agents.
/// </summary>
private sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
private sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder
.AddHandler<List<ChatMessage>>(this.RouteMessages)
.AddHandler<TurnToken>(this.RouteTurnTokenAsync);
}

private ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
[MessageHandler]
internal ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
{
return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
}

private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
[MessageHandler]
internal ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
{
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
}
Expand All @@ -73,7 +68,8 @@ private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context,
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
/// </summary>
private sealed class ConcurrentAggregationExecutor() : Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
[YieldsOutput(typeof(List<ChatMessage>))]
private sealed partial class ConcurrentAggregationExecutor() : Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
{
private readonly List<ChatMessage> _messages = [];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -11,6 +11,10 @@

<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Generators\Microsoft.Agents.AI.Workflows.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />

<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ internal sealed class CriticDecision
/// Executor that creates or revises content based on user requests or critic feedback.
/// This executor demonstrates multiple message handlers for different input types.
/// </summary>
internal sealed class WriterExecutor : Executor
internal sealed partial class WriterExecutor : Executor
{
private readonly AIAgent _agent;

Expand All @@ -213,15 +213,11 @@ Maintain the same topic and length requirements.
);
}

protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder
.AddHandler<string, ChatMessage>(this.HandleInitialRequestAsync)
.AddHandler<CriticDecision, ChatMessage>(this.HandleRevisionRequestAsync);

/// <summary>
/// Handles the initial writing request from the user.
/// </summary>
private async ValueTask<ChatMessage> HandleInitialRequestAsync(
[MessageHandler]
public async ValueTask<ChatMessage> HandleInitialRequestAsync(
string message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
Expand All @@ -232,7 +228,8 @@ private async ValueTask<ChatMessage> HandleInitialRequestAsync(
/// <summary>
/// Handles revision requests from the critic with feedback.
/// </summary>
private async ValueTask<ChatMessage> HandleRevisionRequestAsync(
[MessageHandler]
public async ValueTask<ChatMessage> HandleRevisionRequestAsync(
CriticDecision decision,
IWorkflowContext context,
CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public ValueTask ResetAsync()
}

/// <inheritdoc/>
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (this.Model.Disabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Extensions.AI;

Expand All @@ -25,6 +26,7 @@ public ValueTask ResetAsync()
return default;
}

[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// No state to restore if we're starting from the beginning.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public ValueTask ResetAsync()
return default;
}

[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (this._action is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public ValueTask ResetAsync()
}

/// <inheritdoc/>
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public ValueTask ResetAsync()
}

/// <inheritdoc/>
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
DeclarativeWorkflowContext declarativeContext = new(context, this._state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static MethodAnalysisResult AnalyzeHandlerMethod(
string classKey = GetClassKey(classSymbol);
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
bool hasManualConfigureRoutes = HasConfigureRoutesDefined(classSymbol);
bool configureProtocol = HasConfigureProtocolDefined(classSymbol);

// Extract class metadata
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
Expand All @@ -78,7 +78,7 @@ public static MethodAnalysisResult AnalyzeHandlerMethod(
string? genericParameters = GetGenericParameters(classSymbol);
bool isNested = classSymbol.ContainingType != null;
string containingTypeChain = GetContainingTypeChain(classSymbol);
bool baseHasConfigureRoutes = BaseHasConfigureRoutes(classSymbol);
bool baseHasConfigureProtocol = BaseHasConfigureProtocol(classSymbol);
ImmutableEquatableArray<string> classSendTypes = GetClassLevelTypes(classSymbol, SendsMessageAttributeName);
ImmutableEquatableArray<string> classYieldTypes = GetClassLevelTypes(classSymbol, YieldsOutputAttributeName);

Expand All @@ -96,8 +96,8 @@ public static MethodAnalysisResult AnalyzeHandlerMethod(

return new MethodAnalysisResult(
classKey, @namespace, className, genericParameters, isNested, containingTypeChain,
baseHasConfigureRoutes, classSendTypes, classYieldTypes,
isPartialClass, derivesFromExecutor, hasManualConfigureRoutes,
baseHasConfigureProtocol, classSendTypes, classYieldTypes,
isPartialClass, derivesFromExecutor, configureProtocol,
classLocation,
handler,
Diagnostics: new ImmutableEquatableArray<DiagnosticInfo>(methodDiagnostics.ToImmutable()));
Expand Down Expand Up @@ -152,7 +152,7 @@ public static AnalysisResult CombineHandlerMethodResults(IEnumerable<MethodAnaly
if (first.HasManualConfigureRoutes)
{
allDiagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.ConfigureRoutesAlreadyDefined,
DiagnosticDescriptors.ConfigureProtocolAlreadyDefined,
classLocation,
first.ClassName));
return AnalysisResult.WithDiagnostics(allDiagnostics.ToImmutable());
Expand All @@ -175,7 +175,7 @@ public static AnalysisResult CombineHandlerMethodResults(IEnumerable<MethodAnaly
first.GenericParameters,
first.IsNested,
first.ContainingTypeChain,
first.BaseHasConfigureRoutes,
first.BaseHasConfigureProtocol,
new ImmutableEquatableArray<HandlerInfo>(handlers),
first.ClassSendTypes,
first.ClassYieldTypes);
Expand Down Expand Up @@ -211,7 +211,7 @@ public static ImmutableArray<ClassProtocolInfo> AnalyzeClassProtocolAttribute(
string classKey = GetClassKey(classSymbol);
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
bool hasManualConfigureRoutes = HasConfigureRoutesDefined(classSymbol);
bool hasManualConfigureProtocol = HasConfigureProtocolDefined(classSymbol);

string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
? null
Expand Down Expand Up @@ -240,7 +240,7 @@ public static ImmutableArray<ClassProtocolInfo> AnalyzeClassProtocolAttribute(
containingTypeChain,
isPartialClass,
derivesFromExecutor,
hasManualConfigureRoutes,
hasManualConfigureProtocol,
classLocation,
typeName,
attributeKind));
Expand All @@ -251,12 +251,16 @@ public static ImmutableArray<ClassProtocolInfo> AnalyzeClassProtocolAttribute(
}

/// <summary>
/// Combines ClassProtocolInfo results into an AnalysisResult for classes that only have protocol attributes
/// (no [MessageHandler] methods). This generates only ConfigureSentTypes/ConfigureYieldTypes overrides.
/// Combines ClassProtocolInfo results into an AnalysisResult for classes that only have IO attributes
/// (no [MessageHandler] methods). This generates only .SendsMessage/.YieldsMessage calls in the protocol
/// configuration.
/// </summary>
/// <remarks>
/// This is likely to be seen combined with the basic one-method <c>Executor%lt;TIn&gt;</c> or <c>Executor&lt;TIn, TOut&gt;</c>
/// </remarks>
/// <param name="protocolInfos">The protocol info entries for the class.</param>
/// <returns>The combined analysis result.</returns>
public static AnalysisResult CombineProtocolOnlyResults(IEnumerable<ClassProtocolInfo> protocolInfos)
public static AnalysisResult CombineOutputOnlyResults(IEnumerable<ClassProtocolInfo> protocolInfos)
{
List<ClassProtocolInfo> protocols = protocolInfos.ToList();
if (protocols.Count == 0)
Expand Down Expand Up @@ -317,7 +321,7 @@ public static AnalysisResult CombineProtocolOnlyResults(IEnumerable<ClassProtoco
first.GenericParameters,
first.IsNested,
first.ContainingTypeChain,
BaseHasConfigureRoutes: false, // Not relevant for protocol-only
BaseHasConfigureProtocol: false, // Not relevant for protocol-only
Handlers: ImmutableEquatableArray<HandlerInfo>.Empty,
ClassSendTypes: new ImmutableEquatableArray<string>(sendTypes.ToImmutable()),
ClassYieldTypes: new ImmutableEquatableArray<string>(yieldTypes.ToImmutable()));
Expand Down Expand Up @@ -394,12 +398,12 @@ private static bool DerivesFromExecutor(INamedTypeSymbol classSymbol)
}

/// <summary>
/// Checks if this class directly defines ConfigureRoutes (not inherited).
/// Checks if this class directly defines ConfigureProtocol (not inherited).
/// If so, we skip generation to avoid conflicting with user's manual implementation.
/// </summary>
private static bool HasConfigureRoutesDefined(INamedTypeSymbol classSymbol)
private static bool HasConfigureProtocolDefined(INamedTypeSymbol classSymbol)
{
foreach (var member in classSymbol.GetMembers("ConfigureRoutes"))
foreach (var member in classSymbol.GetMembers("ConfigureProtocol"))
{
if (member is IMethodSymbol method && !method.IsAbstract &&
SymbolEqualityComparer.Default.Equals(method.ContainingType, classSymbol))
Expand All @@ -412,22 +416,22 @@ private static bool HasConfigureRoutesDefined(INamedTypeSymbol classSymbol)
}

/// <summary>
/// Checks if any base class (between this class and Executor) defines ConfigureRoutes.
/// If so, generated code should call base.ConfigureRoutes() to preserve inherited handlers.
/// Checks if any base class (between this class and Executor) defines ConfigureProtocol.
/// If so, generated code should call base.ConfigureProtocol() to preserve inherited handlers.
/// </summary>
private static bool BaseHasConfigureRoutes(INamedTypeSymbol classSymbol)
private static bool BaseHasConfigureProtocol(INamedTypeSymbol classSymbol)
{
INamedTypeSymbol? baseType = classSymbol.BaseType;
while (baseType != null)
{
string fullName = baseType.OriginalDefinition.ToDisplayString();
// Stop at Executor - its ConfigureRoutes is abstract/empty
// Stop at Executor - its ConfigureProtocol is abstract/empty
if (fullName == ExecutorTypeName)
{
return false;
}

foreach (var member in baseType.GetMembers("ConfigureRoutes"))
foreach (var member in baseType.GetMembers("ConfigureProtocol"))
{
if (member is IMethodSymbol method && !method.IsAbstract)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ private static DiagnosticDescriptor Register(DiagnosticDescriptor descriptor)
/// <summary>
/// MAFGENWF006: ConfigureRoutes already defined.
/// </summary>
public static readonly DiagnosticDescriptor ConfigureRoutesAlreadyDefined = Register(new(
public static readonly DiagnosticDescriptor ConfigureProtocolAlreadyDefined = Register(new(
id: "MAFGENWF006",
title: "ConfigureRoutes already defined",
messageFormat: "Class '{0}' already defines ConfigureRoutes; [MessageHandler] methods will be ignored",
title: "ConfigureProtocol already defined",
messageFormat: "Class '{0}' already defines ConfigureProtocol; [MessageHandler] methods will be ignored",
category: Category,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private static IEnumerable<AnalysisResult> CombineAllResults(
{
if (!processedClasses.Contains(kvp.Key))
{
yield return SemanticAnalyzer.CombineProtocolOnlyResults(kvp.Value);
yield return SemanticAnalyzer.CombineOutputOnlyResults(kvp.Value);
}
}
}
Expand Down
Loading
Loading