Skip to content

Commit 2ef3f59

Browse files
committed
feat: Expand ChatForwardingExecutor handled types
Make ChatForwardingExecutor match the input types of ChatProtocolExecutor.
1 parent 69d68d5 commit 2ef3f59

File tree

1 file changed

+49
-7
lines changed

1 file changed

+49
-7
lines changed

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

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading;
46
using System.Threading.Tasks;
57
using Microsoft.Extensions.AI;
68

79
namespace Microsoft.Agents.AI.Workflows;
810

11+
/// <summary>
12+
/// Provides configuration options for <see cref="ChatForwardingExecutor"/>.
13+
/// </summary>
14+
public class ChatForwardingExecutorOptions
15+
{
16+
/// <summary>
17+
/// Gets or sets the chat role to use when converting string messages to <see cref="ChatMessage"/> instances.
18+
/// If set, the executor will accept string messages and convert them to chat messages with this role.
19+
/// </summary>
20+
public ChatRole? StringMessageChatRole { get; set; }
21+
}
22+
923
/// <summary>
1024
/// A ChatProtocol executor that forwards all messages it receives. Useful for splitting inputs into parallel
1125
/// processing paths.
@@ -14,15 +28,43 @@ namespace Microsoft.Agents.AI.Workflows;
1428
/// multiple chat-related types, enabling flexible message forwarding scenarios. Thread safety and reusability are
1529
/// ensured by its design.</remarks>
1630
/// <param name="id">The unique identifier for the executor instance. Used to distinguish this executor within the system.</param>
17-
public sealed class ChatForwardingExecutor(string id) : Executor(id, declareCrossRunShareable: true), IResettableExecutor
31+
/// <param name="options">Optional configuration settings for the executor. If null, default options are used.</param>
32+
public sealed class ChatForwardingExecutor(string id, ChatForwardingExecutorOptions? options = null) : Executor(id, declareCrossRunShareable: true), IResettableExecutor
1833
{
34+
private readonly ChatRole? _stringMessageChatRole = options?.StringMessageChatRole;
35+
1936
/// <inheritdoc/>
20-
protected sealed override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
21-
routeBuilder
22-
.AddHandler<string>((message, context, cancellationToken) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken))
23-
.AddHandler<ChatMessage>((message, context, cancellationToken) => context.SendMessageAsync(message, cancellationToken: cancellationToken))
24-
.AddHandler<List<ChatMessage>>((messages, context, cancellationToken) => context.SendMessageAsync(messages, cancellationToken: cancellationToken))
25-
.AddHandler<TurnToken>((turnToken, context, cancellationToken) => context.SendMessageAsync(turnToken, cancellationToken: cancellationToken));
37+
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
38+
{
39+
if (this._stringMessageChatRole.HasValue)
40+
{
41+
routeBuilder = routeBuilder.AddHandler<string>(
42+
(message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)));
43+
}
44+
45+
return routeBuilder.AddHandler<ChatMessage>(ForwardMessageAsync)
46+
.AddHandler<IEnumerable<ChatMessage>>(ForwardMessagesAsync)
47+
.AddHandler<ChatMessage[]>(ForwardMessagesAsync)
48+
.AddHandler<List<ChatMessage>>(ForwardMessagesAsync)
49+
.AddHandler<TurnToken>(ForwardTurnTokenAsync);
50+
}
51+
52+
private static ValueTask ForwardMessageAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken)
53+
=> context.SendMessageAsync(message, cancellationToken);
54+
55+
// Note that this can be used to split a turn into multiple parallel turns taken, which will cause streaming ChatMessages
56+
// to overlap.
57+
private static ValueTask ForwardTurnTokenAsync(TurnToken message, IWorkflowContext context, CancellationToken cancellationToken)
58+
=> context.SendMessageAsync(message, cancellationToken);
59+
60+
// TODO: This is not ideal, but until we have a way of guaranteeing correct routing of interfaces across serialization
61+
// boundaries, we need to do type unification. It behaves better when used as a handler in ChatProtocolExecutor because
62+
// it is a strictly contravariant use, whereas this forces invariance on the type because it is directly forwarded.
63+
private static ValueTask ForwardMessagesAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
64+
=> context.SendMessageAsync(messages is List<ChatMessage> messageList ? messageList : messages.ToList(), cancellationToken);
65+
66+
private static ValueTask ForwardMessagesAsync(ChatMessage[] messages, IWorkflowContext context, CancellationToken cancellationToken)
67+
=> context.SendMessageAsync(messages, cancellationToken);
2668

2769
/// <inheritdoc/>
2870
public ValueTask ResetAsync() => default;

0 commit comments

Comments
 (0)