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
@@ -123,7 +123,7 @@ appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.
|
||||
|
||||
// Create a parent span for the entire agent session
|
||||
using var sessionActivity = activitySource.StartActivity("Agent Session");
|
||||
var sessionId = Guid.NewGuid().ToString();
|
||||
var sessionId = Guid.NewGuid().ToString("N");
|
||||
sessionActivity?
|
||||
.SetTag("agent.name", "OpenTelemetryDemoAgent")
|
||||
.SetTag("session.id", sessionId)
|
||||
|
||||
+7
-6
@@ -31,6 +31,8 @@ namespace SampleApp
|
||||
// Custom agent that parrot's the user input back in upper case.
|
||||
internal sealed class UpperCaseParrotAgent : AIAgent
|
||||
{
|
||||
public override string? Name => "UpperCaseParrotAgent";
|
||||
|
||||
public override AgentThread GetNewThread()
|
||||
=> new CustomAgentThread();
|
||||
|
||||
@@ -51,7 +53,7 @@ namespace SampleApp
|
||||
return new AgentRunResponse
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = Guid.NewGuid().ToString(),
|
||||
ResponseId = Guid.NewGuid().ToString("N"),
|
||||
Messages = responseMessages
|
||||
};
|
||||
}
|
||||
@@ -75,8 +77,8 @@ namespace SampleApp
|
||||
AuthorName = this.DisplayName,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = message.Contents,
|
||||
ResponseId = Guid.NewGuid().ToString(),
|
||||
MessageId = Guid.NewGuid().ToString()
|
||||
ResponseId = Guid.NewGuid().ToString("N"),
|
||||
MessageId = Guid.NewGuid().ToString("N")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -86,7 +88,7 @@ namespace SampleApp
|
||||
// Clone the message and update its author to be the agent.
|
||||
var messageClone = x.Clone();
|
||||
messageClone.Role = ChatRole.Assistant;
|
||||
messageClone.MessageId = Guid.NewGuid().ToString();
|
||||
messageClone.MessageId = Guid.NewGuid().ToString("N");
|
||||
messageClone.AuthorName = agentName;
|
||||
|
||||
// Clone and convert any text content to upper case.
|
||||
@@ -109,8 +111,7 @@ namespace SampleApp
|
||||
/// </summary>
|
||||
internal sealed class CustomAgentThread : InMemoryAgentThread
|
||||
{
|
||||
internal CustomAgentThread()
|
||||
: base() { }
|
||||
internal CustomAgentThread() { }
|
||||
|
||||
internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
: base(serializedThreadState, jsonSerializerOptions) { }
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace SampleApp
|
||||
|
||||
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
this.ThreadDbKey ??= Guid.NewGuid().ToString();
|
||||
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
|
||||
|
||||
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
@@ -21,7 +21,7 @@ AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true);
|
||||
|
||||
// Create TracerProvider with console exporter
|
||||
// This will output the telemetry data to the console.
|
||||
string sourceName = Guid.NewGuid().ToString();
|
||||
string sourceName = Guid.NewGuid().ToString("N");
|
||||
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.AddSource(sourceName)
|
||||
.AddConsoleExporter()
|
||||
|
||||
+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