mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add AgentWorkflowBuilder group chat (#861)
* Add AgentWorkflowBuilder group chat
And fix a variety of issues along the way:
- Use DateTime{Offset}.UtcNow rather than Now
- AIAgentHostExecutor shouldn't be publishing empty messages
- Sequential workflows should be flowing all history and not just the output from the previous agent as the input into the next agent
- Renamed some of the new agent workflow methods... still not super happy with the shape, though
- Simplified handoffs builder, e.g. using a hashset with a custom comparer instead of a dictionary
- Improved multi-service use by trying to change assistant->user role for messages created by other agents
- Changed MessageMerger to rely on M.E.AI's coalescing more and to avoid empty contents / text
- Ensured that messages from ChatClientAgent include MessageId and CreatedAt timestamps
- Avoided including instructions for agents in a handoff workflow that don't have any handoffs
- Removed the unnecessary end function in handoffs
- Improved naming of executors to include agent name for debuggability
- Use "N" formatting with Guid.ToString everywhere, to avoid the unnecessary extra dash character which is also not valid in various places (like function tool names)
- Replace `params T[]` with `params IEnumerable<T>` to make public APIs more flexible in what they consume
* Address feedback
- Fix unintentional provider change in sample
This commit is contained in:
committed by
GitHub
Unverified
parent
7c70c33157
commit
03ef7f054f
+1
-1
@@ -166,7 +166,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
|
||||
// Generate a random email ID and store the email content to the shared state
|
||||
var newEmail = new Email
|
||||
{
|
||||
EmailId = Guid.NewGuid().ToString(),
|
||||
EmailId = Guid.NewGuid().ToString("N"),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
@@ -190,7 +190,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
|
||||
// Generate a random email ID and store the email content
|
||||
var newEmail = new Email
|
||||
{
|
||||
EmailId = Guid.NewGuid().ToString(),
|
||||
EmailId = Guid.NewGuid().ToString("N"),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisEx
|
||||
// Generate a random email ID and store the email content
|
||||
var newEmail = new Email
|
||||
{
|
||||
EmailId = Guid.NewGuid().ToString(),
|
||||
EmailId = Guid.NewGuid().ToString("N"),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
@@ -62,7 +62,7 @@ internal sealed class FileReadExecutor() : ReflectingExecutor<FileReadExecutor>(
|
||||
// Read file content from embedded resource
|
||||
string fileContent = Resources.Read(message);
|
||||
// Store file content in a shared state for access by other executors
|
||||
string fileID = Guid.NewGuid().ToString();
|
||||
string fileID = Guid.NewGuid().ToString("N");
|
||||
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
|
||||
|
||||
return fileID;
|
||||
|
||||
+29
-6
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
@@ -30,7 +31,7 @@ public static class Program
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
var client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
|
||||
|
||||
Console.Write("Choose workflow type ('sequential', 'concurrent', 'handoffs'): ");
|
||||
Console.Write("Choose workflow type ('sequential', 'concurrent', 'handoffs', 'groupchat'): ");
|
||||
switch (Console.ReadLine())
|
||||
{
|
||||
case "sequential":
|
||||
@@ -58,10 +59,9 @@ public static class Program
|
||||
"You determine which agent to use based on the user's homework question. ALWAYS handoff to another agent.",
|
||||
"triage_agent",
|
||||
"Routes messages to the appropriate specialist agent");
|
||||
var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent)
|
||||
.WithHandoff(triageAgent, [mathTutor, historyTutor])
|
||||
.WithHandoff(mathTutor, triageAgent)
|
||||
.WithHandoff(historyTutor, triageAgent)
|
||||
var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
|
||||
.WithHandoffs(triageAgent, [mathTutor, historyTutor])
|
||||
.WithHandoffs([mathTutor, historyTutor], triageAgent)
|
||||
.Build();
|
||||
|
||||
List<ChatMessage> messages = [];
|
||||
@@ -72,22 +72,45 @@ public static class Program
|
||||
messages.AddRange(await RunWorkflowAsync(workflow, messages));
|
||||
}
|
||||
|
||||
case "groupchat":
|
||||
await RunWorkflowAsync(
|
||||
AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents => new AgentWorkflowBuilder.RoundRobinGroupChatManager(agents) { MaximumIterationCount = 5 })
|
||||
.AddParticipants(from lang in (string[])["French", "Spanish", "English"] select GetTranslationAgent(lang, client))
|
||||
.Build(),
|
||||
[new(ChatRole.User, "Hello, world!")]);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid workflow type.");
|
||||
}
|
||||
|
||||
static async Task<List<ChatMessage>> RunWorkflowAsync(Workflow<List<ChatMessage>> workflow, List<ChatMessage> messages)
|
||||
{
|
||||
string? lastExecutorId = null;
|
||||
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is AgentRunUpdateEvent e)
|
||||
{
|
||||
Console.WriteLine($"{e.ExecutorId}: {e.Data}");
|
||||
if (e.ExecutorId != lastExecutorId)
|
||||
{
|
||||
lastExecutorId = e.ExecutorId;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(e.ExecutorId);
|
||||
}
|
||||
|
||||
Console.Write(e.Update.Text);
|
||||
if (e.Update.Contents.OfType<FunctionCallContent>().FirstOrDefault() is FunctionCallContent call)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" [Calling function '{call.Name}' with arguments: {JsonSerializer.Serialize(call.Arguments)}]");
|
||||
}
|
||||
}
|
||||
else if (evt is WorkflowCompletedEvent completed)
|
||||
{
|
||||
Console.WriteLine();
|
||||
return (List<ChatMessage>)completed.Data!;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user