-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathAgentWorkflowBuilder.cs
More file actions
188 lines (165 loc) · 9.59 KB
/
Copy pathAgentWorkflowBuilder.cs
File metadata and controls
188 lines (165 loc) · 9.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Specialized;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
/// <summary>
/// Provides utility methods for constructing common patterns of workflows composed of agents.
/// </summary>
public static partial class AgentWorkflowBuilder
{
/// <summary>
/// Builds a <see cref="Workflow"/> composed of a pipeline of agents where the output of one agent is the input to the next.
/// </summary>
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
/// <returns>The built workflow composed of the supplied <paramref name="agents"/>, in the order in which they were yielded from the source.</returns>
public static Workflow BuildSequential(params IEnumerable<AIAgent> agents)
=> BuildSequentialCore(workflowName: null, agents);
/// <summary>
/// Builds a <see cref="Workflow"/> composed of a pipeline of agents where the output of one agent is the input to the next.
/// </summary>
/// <param name="workflowName">The name of workflow.</param>
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
/// <returns>The built workflow composed of the supplied <paramref name="agents"/>, in the order in which they were yielded from the source.</returns>
public static Workflow BuildSequential(string workflowName, params IEnumerable<AIAgent> agents)
=> BuildSequentialCore(workflowName, agents);
private static Workflow BuildSequentialCore(string? workflowName, params IEnumerable<AIAgent> agents)
{
Throw.IfNull(agents);
// Create a builder that chains the agents together in sequence. The workflow simply begins
// with the first agent in the sequence.
WorkflowBuilder? builder = null;
ExecutorBinding? previous = null;
foreach (var agent in agents)
{
AgentRunStreamingExecutor agentExecutor = new(agent, includeInputInOutput: true);
if (builder is null)
{
builder = new WorkflowBuilder(agentExecutor);
}
else
{
Debug.Assert(previous is not null);
builder.AddEdge(previous, agentExecutor);
}
previous = agentExecutor;
}
if (previous is null)
{
Throw.ArgumentException(nameof(agents), "At least one agent must be provided to build a sequential workflow.");
}
// Add an ending executor that batches up all messages from the last agent
// so that it's published as a single list result.
Debug.Assert(builder is not null);
OutputMessagesExecutor end = new();
builder = builder.AddEdge(previous, end).WithOutputFrom(end);
if (workflowName is not null)
{
builder = builder.WithName(workflowName);
}
return builder.Build();
}
/// <summary>
/// Builds a <see cref="Workflow"/> composed of agents that operate concurrently on the same input,
/// aggregating their outputs into a single collection.
/// </summary>
/// <param name="agents">The set of agents to compose into a concurrent workflow.</param>
/// <param name="aggregator">
/// The aggregation function that accepts a list of the output messages from each <paramref name="agents"/> and produces
/// a single result list. If <see langword="null"/>, the default behavior is to return a list containing the last message
/// from each agent that produced at least one message.
/// </param>
/// <returns>The built workflow composed of the supplied concurrent <paramref name="agents"/>.</returns>
public static Workflow BuildConcurrent(
IEnumerable<AIAgent> agents,
Func<IList<List<ChatMessage>>, List<ChatMessage>>? aggregator = null)
=> BuildConcurrentCore(workflowName: null, agents, aggregator);
/// <summary>
/// Builds a <see cref="Workflow"/> composed of agents that operate concurrently on the same input,
/// aggregating their outputs into a single collection.
/// </summary>
/// <param name="workflowName">The name of the workflow.</param>
/// <param name="agents">The set of agents to compose into a concurrent workflow.</param>
/// <param name="aggregator">
/// The aggregation function that accepts a list of the output messages from each <paramref name="agents"/> and produces
/// a single result list. If <see langword="null"/>, the default behavior is to return a list containing the last message
/// from each agent that produced at least one message.
/// </param>
/// <returns>The built workflow composed of the supplied concurrent <paramref name="agents"/>.</returns>
public static Workflow BuildConcurrent(
string workflowName,
IEnumerable<AIAgent> agents,
Func<IList<List<ChatMessage>>, List<ChatMessage>>? aggregator = null)
=> BuildConcurrentCore(workflowName, agents, aggregator);
private static Workflow BuildConcurrentCore(
string? workflowName,
IEnumerable<AIAgent> agents,
Func<IList<List<ChatMessage>>, List<ChatMessage>>? aggregator = null)
{
Throw.IfNull(agents);
// A workflow needs a starting executor, so we create one that forwards everything to each agent.
ChatForwardingExecutor start = new("Start");
WorkflowBuilder builder = new(start);
// For each agent, we create an executor to host it and an accumulator to batch up its output messages,
// so that the final accumulator receives a single list of messages from each agent. Otherwise, the
// accumulator would not be able to determine what came from what agent, as there's currently no
// provenance tracking exposed in the workflow context passed to a handler.
ExecutorBinding[] agentExecutors = (from agent in agents select (ExecutorBinding)new AgentRunStreamingExecutor(agent, includeInputInOutput: false)).ToArray();
ExecutorBinding[] accumulators = [.. from agent in agentExecutors select (ExecutorBinding)new CollectChatMessagesExecutor($"Batcher/{agent.Id}")];
builder.AddFanOutEdge(start, targets: agentExecutors);
for (int i = 0; i < agentExecutors.Length; i++)
{
builder.AddEdge(agentExecutors[i], accumulators[i]);
}
// Create the accumulating executor that will gather the results from each agent, and connect
// each agent's accumulator to it. If no aggregation function was provided, we default to returning
// the last message from each agent
aggregator ??= static lists => (from list in lists where list.Count > 0 select list.Last()).ToList();
Func<string, string, ValueTask<ConcurrentEndExecutor>> endFactory =
(string _, string __) => new(new ConcurrentEndExecutor(agentExecutors.Length, aggregator));
ExecutorBinding end = endFactory.BindExecutor(ConcurrentEndExecutor.ExecutorId);
builder.AddFanInEdge(end, sources: accumulators);
builder = builder.WithOutputFrom(end);
if (workflowName is not null)
{
builder = builder.WithName(workflowName);
}
return builder.Build();
}
/// <summary>Creates a new <see cref="HandoffsWorkflowBuilder"/> using <paramref name="initialAgent"/> as the starting agent in the workflow.</summary>
/// <param name="initialAgent">The agent that will receive inputs provided to the workflow.</param>
/// <returns>The builder for creating a workflow based on handoffs.</returns>
/// <remarks>
/// Handoffs between agents are achieved by the current agent invoking an <see cref="AITool"/> provided to an agent
/// via <see cref="ChatClientAgentOptions"/>'s <see cref="ChatClientAgentOptions.ChatOptions"/>.<see cref="ChatOptions.Tools"/>.
/// The <see cref="AIAgent"/> must be capable of understanding those <see cref="AgentRunOptions"/> provided. If the agent
/// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur.
/// </remarks>
public static HandoffsWorkflowBuilder CreateHandoffBuilderWith(AIAgent initialAgent)
{
Throw.IfNull(initialAgent);
return new(initialAgent);
}
/// <summary>Creates a new <see cref="GroupChatWorkflowBuilder"/> with <paramref name="managerFactory"/>.</summary>
/// <param name="managerFactory">
/// Function that will create the <see cref="GroupChatManager"/> for the workflow instance. The manager will be
/// provided with the set of agents that will participate in the group chat.
/// </param>
/// <returns>The builder for creating a workflow based on handoffs.</returns>
/// <remarks>
/// Handoffs between agents are achieved by the current agent invoking an <see cref="AITool"/> provided to an agent
/// via <see cref="ChatClientAgentOptions"/>'s <see cref="ChatClientAgentOptions.ChatOptions"/>.<see cref="ChatOptions.Tools"/>.
/// The <see cref="AIAgent"/> must be capable of understanding those <see cref="AgentRunOptions"/> provided. If the agent
/// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur.
/// </remarks>
public static GroupChatWorkflowBuilder CreateGroupChatBuilderWith(Func<IReadOnlyList<AIAgent>, GroupChatManager> managerFactory)
{
Throw.IfNull(managerFactory);
return new GroupChatWorkflowBuilder(managerFactory);
}
}