Skip to content

Commit d95b528

Browse files
committed
feat: Add support for Send/Yield annotations with basic Executor
* Adds annotations to Declarative workflow executors
1 parent 1ec41b6 commit d95b528

14 files changed

Lines changed: 63 additions & 74 deletions

File tree

dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ private static AIAgent GetLanguageAgent(string targetLanguage, IChatClient chatC
5353
private sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
5454
{
5555
[MessageHandler]
56-
private ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
56+
internal ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
5757
{
5858
return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
5959
}
6060

6161
[MessageHandler]
62-
private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
62+
internal ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
6363
{
6464
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
6565
}

dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ Maintain the same topic and length requirements.
214214
/// Handles the initial writing request from the user.
215215
/// </summary>
216216
[MessageHandler]
217-
private async ValueTask<ChatMessage> HandleInitialRequestAsync(
217+
public async ValueTask<ChatMessage> HandleInitialRequestAsync(
218218
string message,
219219
IWorkflowContext context,
220220
CancellationToken cancellationToken = default)
@@ -226,7 +226,7 @@ private async ValueTask<ChatMessage> HandleInitialRequestAsync(
226226
/// Handles revision requests from the critic with feedback.
227227
/// </summary>
228228
[MessageHandler]
229-
private async ValueTask<ChatMessage> HandleRevisionRequestAsync(
229+
public async ValueTask<ChatMessage> HandleRevisionRequestAsync(
230230
CriticDecision decision,
231231
IWorkflowContext context,
232232
CancellationToken cancellationToken = default)

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public ValueTask ResetAsync()
6060
}
6161

6262
/// <inheritdoc/>
63+
[SendsMessage(typeof(ActionExecutorResult))]
6364
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
6465
{
6566
if (this.Model.Disabled)

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
7+
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
78
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
89
using Microsoft.Extensions.AI;
910

@@ -25,6 +26,7 @@ public ValueTask ResetAsync()
2526
return default;
2627
}
2728

29+
[SendsMessage(typeof(ActionExecutorResult))]
2830
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
2931
{
3032
// No state to restore if we're starting from the beginning.

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public ValueTask ResetAsync()
4040
return default;
4141
}
4242

43+
[SendsMessage(typeof(ActionExecutorResult))]
4344
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
4445
{
4546
if (this._action is not null)

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/ActionExecutor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public ValueTask ResetAsync()
7373
}
7474

7575
/// <inheritdoc/>
76+
[SendsMessage(typeof(ActionExecutorResult))]
7677
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
7778
{
7879
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken).ConfigureAwait(false);

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/RootExecutor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public ValueTask ResetAsync()
5454
}
5555

5656
/// <inheritdoc/>
57+
[SendsMessage(typeof(ActionExecutorResult))]
5758
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
5859
{
5960
DeclarativeWorkflowContext declarativeContext = new(context, this._state);

dotnet/src/Microsoft.Agents.AI.Workflows/Executor.cs

Lines changed: 12 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -201,24 +201,6 @@ protected Executor(string id, ExecutorOptions? options = null, bool declareCross
201201
/// RouteBuilder.</remarks>
202202
/// <returns>An instance of <see cref="ExecutorProtocol"/> that represents the fully configured protocol.</returns>
203203
protected abstract ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder);
204-
//{
205-
// // TODO: Eventually we want this to be the primary way to configure the protocol, but for now
206-
// // we will drive it from the RouteBuilder for backward compatibility.
207-
// this.ConfigureRoutes(protocolBuilder.RouteBuilder);
208-
209-
// // Avoid re-entrancy issues. TODO: Remove old ConfigureXYZ calls once this is the primary configuration path.
210-
// //this._configuringProtocol = true;
211-
// //protocolBuilder.SendsMessageTypes(this.ConfigureSentTypes());
212-
// //protocolBuilder.YieldsOutputTypes(this.ConfigureYieldTypes());
213-
// //this._configuringProtocol = false;
214-
215-
// return protocolBuilder;
216-
//}
217-
218-
/// <summary>
219-
/// Override this method to register handlers for the executor.
220-
/// </summary>
221-
//protected virtual RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder;
222204

223205
internal void AttachRequestContext(IExternalRequestContext externalRequestContext)
224206
{
@@ -228,9 +210,6 @@ internal void AttachRequestContext(IExternalRequestContext externalRequestContex
228210
// .AttachRequestContext()
229211
// >>> only usable now
230212

231-
// This can be removed once we can obsolete the old RegisterRoutes/RegisterXYZTypes methods in favour of the ConfigureProtocol
232-
// method. Then instead of relying on the Executor instance to drive routing through .MessageRouter, the execution environment
233-
// will rely on the Protocol instance directly.
234213
this.DelayedPortRegistrations.ApplyPortRegistrations(externalRequestContext);
235214
_ = this.Protocol; // Force protocol to be built if not already done.
236215
}
@@ -245,34 +224,6 @@ internal void AttachRequestContext(IExternalRequestContext externalRequestContex
245224
protected internal virtual ValueTask InitializeAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
246225
=> default;
247226

248-
///// <summary>
249-
///// Override this method to declare the types of messages this executor can send.
250-
///// </summary>
251-
///// <returns></returns>
252-
//protected virtual ISet<Type> ConfigureSentTypes()
253-
//{
254-
// if (this.Options.AutoSendMessageHandlerResultObject && !this._configuringProtocol)
255-
// {
256-
// return this.Router.DefaultOutputTypes;
257-
// }
258-
259-
// return new HashSet<Type>();
260-
//}
261-
262-
///// <summary>
263-
///// Override this method to declare the types of messages this executor can yield as workflow outputs.
264-
///// </summary>
265-
///// <returns></returns>
266-
//protected virtual ISet<Type> ConfigureYieldTypes()
267-
//{
268-
// if (this.Options.AutoYieldOutputHandlerResultObject && !this._configuringProtocol)
269-
// {
270-
// return this.Router.DefaultOutputTypes;
271-
// }
272-
273-
// return new HashSet<Type>();
274-
//}
275-
276227
internal MessageRouter Router => this.Protocol.Router;
277228

278229
/// <summary>
@@ -387,8 +338,6 @@ protected internal virtual ValueTask InitializeAsync(IWorkflowContext context, C
387338
/// <returns></returns>
388339
public bool CanHandle(Type messageType) => this.Protocol.CanHandle(messageType);
389340

390-
//internal bool CanHandle(TypeId messageType) => this.Protocol.CanHandle(messageType);
391-
392341
internal bool CanOutput(Type messageType) => this.Protocol.CanOutput(messageType);
393342
}
394343

@@ -404,7 +353,12 @@ public abstract class Executor<TInput>(string id, ExecutorOptions? options = nul
404353
{
405354
/// <inheritdoc/>
406355
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
407-
=> protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler<TInput>(this.HandleAsync));
356+
{
357+
Func<TInput, IWorkflowContext, CancellationToken, ValueTask> handlerDelegate = this.HandleAsync;
358+
359+
return protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(handlerDelegate))
360+
.AddHandlerAttributeTypes(handlerDelegate.Method);
361+
}
408362

409363
/// <inheritdoc/>
410364
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
@@ -424,7 +378,12 @@ public abstract class Executor<TInput, TOutput>(string id, ExecutorOptions? opti
424378
{
425379
/// <inheritdoc/>
426380
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
427-
=> protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler<TInput, TOutput>(this.HandleAsync));
381+
{
382+
Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>> handlerDelegate = this.HandleAsync;
383+
384+
return protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(handlerDelegate))
385+
.AddHandlerAttributeTypes(handlerDelegate.Method);
386+
}
428387

429388
/// <inheritdoc/>
430389
public abstract ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);

dotnet/src/Microsoft.Agents.AI.Workflows/FunctionExecutor.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal static Func<TInput, IWorkflowContext, CancellationToken, ValueTask> Wra
3131
if (handlerSync.Method != null)
3232
{
3333
MethodInfo method = handlerSync.Method;
34-
(sentTypes, yieldedTypes) = GetAttributeTypes(method);
34+
(sentTypes, yieldedTypes) = method.GetAttributeTypes();
3535
}
3636
else
3737
{
@@ -47,19 +47,10 @@ ValueTask RunActionAsync(TInput input, IWorkflowContext workflowContext, Cancell
4747
}
4848
}
4949

50-
private static (IEnumerable<Type> Sent, IEnumerable<Type> Yielded) GetAttributeTypes(MethodInfo method)
51-
{
52-
IEnumerable<SendsMessageAttribute> sendsMessageAttrs = method.GetCustomAttributes<SendsMessageAttribute>();
53-
IEnumerable<YieldsOutputAttribute> yieldsOutputAttrs = method.GetCustomAttributes<YieldsOutputAttribute>();
54-
// TODO: Should we include [MessageHandler]?
55-
56-
return (Sent: sendsMessageAttrs.Select(attr => attr.Type), Yielded: yieldsOutputAttrs.Select(attr => attr.Type));
57-
}
58-
5950
/// <inheritdoc/>
6051
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
6152
{
62-
(IEnumerable<Type> attributeSentTypes, IEnumerable<Type> attributeYieldTypes) = GetAttributeTypes(handlerAsync.Method);
53+
(IEnumerable<Type> attributeSentTypes, IEnumerable<Type> attributeYieldTypes) = handlerAsync.Method.GetAttributeTypes();
6354

6455
return base.ConfigureProtocol(protocolBuilder)
6556
.SendsMessageTypes(attributeSentTypes.Concat(sentMessageTypes ?? []))

dotnet/src/Microsoft.Agents.AI.Workflows/ProtocolBuilder.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,25 @@
22

33
using System;
44
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Reflection;
57
using Microsoft.Agents.AI.Workflows.Execution;
68
using Microsoft.Shared.Diagnostics;
79

810
namespace Microsoft.Agents.AI.Workflows;
911

12+
internal static class MethodAttributeExtensions
13+
{
14+
public static (IEnumerable<Type> Sent, IEnumerable<Type> Yielded) GetAttributeTypes(this MethodInfo method)
15+
{
16+
IEnumerable<SendsMessageAttribute> sendsMessageAttrs = method.GetCustomAttributes<SendsMessageAttribute>();
17+
IEnumerable<YieldsOutputAttribute> yieldsOutputAttrs = method.GetCustomAttributes<YieldsOutputAttribute>();
18+
// TODO: Should we include [MessageHandler]?
19+
20+
return (Sent: sendsMessageAttrs.Select(attr => attr.Type), Yielded: yieldsOutputAttrs.Select(attr => attr.Type));
21+
}
22+
}
23+
1024
/// <summary>
1125
/// .
1226
/// </summary>
@@ -20,6 +34,23 @@ internal ProtocolBuilder(DelayedExternalRequestContext delayRequestContext)
2034
this.RouteBuilder = new RouteBuilder(delayRequestContext);
2135
}
2236

37+
internal ProtocolBuilder AddHandlerAttributeTypes(MethodInfo method, bool registerSentTypes = true, bool registerYieldTypes = true)
38+
{
39+
(IEnumerable<Type> sentTypes, IEnumerable<Type> yieldTypes) = method.GetAttributeTypes();
40+
41+
if (registerSentTypes)
42+
{
43+
this._sendTypes.UnionWith(sentTypes);
44+
}
45+
46+
if (registerYieldTypes)
47+
{
48+
this._yieldTypes.UnionWith(yieldTypes);
49+
}
50+
51+
return this;
52+
}
53+
2354
/// <summary>
2455
/// Adds the specified type to the set of declared "sent" message types for the protocol. Objects of these types will be allowed to be
2556
/// sent through the Executor's outgoing edges, via <see cref="IWorkflowContext.SendMessageAsync"/>.

0 commit comments

Comments
 (0)