Skip to content

Commit 2cfaf73

Browse files
[.Net] add SendAsync api to iterate group chat step by step (#3214)
* add SendAsync api and tests * update example to use new sendAsync API
1 parent cf29a2f commit 2cfaf73

File tree

5 files changed

+140
-55
lines changed

5 files changed

+140
-55
lines changed

dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs

+49-50
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33

44
using System.Text;
55
using System.Text.Json;
6-
using AutoGen;
76
using AutoGen.BasicSample;
87
using AutoGen.Core;
98
using AutoGen.DotnetInteractive;
109
using AutoGen.OpenAI;
1110
using AutoGen.OpenAI.Extension;
11+
using Azure.AI.OpenAI;
1212
using FluentAssertions;
1313

1414
public partial class Example07_Dynamic_GroupChat_Calculate_Fibonacci
@@ -49,10 +49,11 @@ public async Task<string> ReviewCodeBlock(
4949
#endregion reviewer_function
5050

5151
#region create_coder
52-
public static async Task<IAgent> CreateCoderAgentAsync()
52+
public static async Task<IAgent> CreateCoderAgentAsync(OpenAIClient client, string deployModel)
5353
{
54-
var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo();
55-
var coder = new GPTAgent(
54+
var coder = new OpenAIChatAgent(
55+
openAIClient: client,
56+
modelName: deployModel,
5657
name: "coder",
5758
systemMessage: @"You act as dotnet coder, you write dotnet code to resolve task. Once you finish writing code, ask runner to run the code for you.
5859
@@ -70,8 +71,8 @@ public static async Task<IAgent> CreateCoderAgentAsync()
7071
```
7172
7273
If your code is incorrect, runner will tell you the error message. Fix the error and send the code again.",
73-
config: gpt3Config,
7474
temperature: 0.4f)
75+
.RegisterMessageConnector()
7576
.RegisterPrintMessage();
7677

7778
return coder;
@@ -81,9 +82,8 @@ public static async Task<IAgent> CreateCoderAgentAsync()
8182
#region create_runner
8283
public static async Task<IAgent> CreateRunnerAgentAsync(InteractiveService service)
8384
{
84-
var runner = new AssistantAgent(
85+
var runner = new DefaultReplyAgent(
8586
name: "runner",
86-
systemMessage: "You run dotnet code",
8787
defaultReply: "No code available.")
8888
.RegisterDotnetCodeBlockExectionHook(interactiveService: service)
8989
.RegisterMiddleware(async (msgs, option, agent, _) =>
@@ -105,45 +105,38 @@ public static async Task<IAgent> CreateRunnerAgentAsync(InteractiveService servi
105105
#endregion create_runner
106106

107107
#region create_admin
108-
public static async Task<IAgent> CreateAdminAsync()
108+
public static async Task<IAgent> CreateAdminAsync(OpenAIClient client, string deployModel)
109109
{
110-
var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo();
111-
var admin = new GPTAgent(
110+
var admin = new OpenAIChatAgent(
111+
openAIClient: client,
112+
modelName: deployModel,
112113
name: "admin",
113-
systemMessage: "You are group admin, terminate the group chat once task is completed by saying [TERMINATE] plus the final answer",
114-
temperature: 0,
115-
config: gpt3Config)
116-
.RegisterMiddleware(async (msgs, option, agent, _) =>
117-
{
118-
var reply = await agent.GenerateReplyAsync(msgs, option);
119-
if (reply is TextMessage textMessage && textMessage.Content.Contains("TERMINATE") is true)
120-
{
121-
var content = $"{textMessage.Content}\n\n {GroupChatExtension.TERMINATE}";
122-
123-
return new TextMessage(Role.Assistant, content, from: reply.From);
124-
}
125-
126-
return reply;
127-
});
114+
temperature: 0)
115+
.RegisterMessageConnector()
116+
.RegisterPrintMessage();
128117

129118
return admin;
130119
}
131120
#endregion create_admin
132121

133122
#region create_reviewer
134-
public static async Task<IAgent> CreateReviewerAgentAsync()
123+
public static async Task<IAgent> CreateReviewerAgentAsync(OpenAIClient openAIClient, string deployModel)
135124
{
136125
var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo();
137126
var functions = new Example07_Dynamic_GroupChat_Calculate_Fibonacci();
138-
var reviewer = new GPTAgent(
139-
name: "code_reviewer",
140-
systemMessage: @"You review code block from coder",
141-
config: gpt3Config,
142-
functions: [functions.ReviewCodeBlockFunctionContract.ToOpenAIFunctionDefinition()],
127+
var functionCallMiddleware = new FunctionCallMiddleware(
128+
functions: [functions.ReviewCodeBlockFunctionContract],
143129
functionMap: new Dictionary<string, Func<string, Task<string>>>()
144130
{
145-
{ nameof(ReviewCodeBlock), functions.ReviewCodeBlockWrapper },
146-
})
131+
{ nameof(functions.ReviewCodeBlock), functions.ReviewCodeBlockWrapper },
132+
});
133+
var reviewer = new OpenAIChatAgent(
134+
openAIClient: openAIClient,
135+
name: "code_reviewer",
136+
systemMessage: @"You review code block from coder",
137+
modelName: deployModel)
138+
.RegisterMessageConnector()
139+
.RegisterStreamingMiddleware(functionCallMiddleware)
147140
.RegisterMiddleware(async (msgs, option, innerAgent, ct) =>
148141
{
149142
var maxRetry = 3;
@@ -229,16 +222,19 @@ public static async Task RunWorkflowAsync()
229222
Directory.CreateDirectory(workDir);
230223
}
231224

225+
var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo();
226+
var openaiClient = new OpenAIClient(new Uri(config.Endpoint), new Azure.AzureKeyCredential(config.ApiKey));
227+
232228
using var service = new InteractiveService(workDir);
233229
var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service);
234230

235231
await service.StartAsync(workDir, default);
236232

237233
#region create_workflow
238-
var reviewer = await CreateReviewerAgentAsync();
239-
var coder = await CreateCoderAgentAsync();
234+
var reviewer = await CreateReviewerAgentAsync(openaiClient, config.DeploymentName);
235+
var coder = await CreateCoderAgentAsync(openaiClient, config.DeploymentName);
240236
var runner = await CreateRunnerAgentAsync(service);
241-
var admin = await CreateAdminAsync();
237+
var admin = await CreateAdminAsync(openaiClient, config.DeploymentName);
242238

243239
var admin2CoderTransition = Transition.Create(admin, coder);
244240
var coder2ReviewerTransition = Transition.Create(coder, reviewer);
@@ -335,39 +331,42 @@ public static async Task RunAsync()
335331
Directory.CreateDirectory(workDir);
336332
}
337333

334+
var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo();
335+
var openaiClient = new OpenAIClient(new Uri(config.Endpoint), new Azure.AzureKeyCredential(config.ApiKey));
336+
338337
using var service = new InteractiveService(workDir);
339338
var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service);
340339

341340
await service.StartAsync(workDir, default);
342341
#region create_group_chat
343-
var reviewer = await CreateReviewerAgentAsync();
344-
var coder = await CreateCoderAgentAsync();
342+
var reviewer = await CreateReviewerAgentAsync(openaiClient, config.DeploymentName);
343+
var coder = await CreateCoderAgentAsync(openaiClient, config.DeploymentName);
345344
var runner = await CreateRunnerAgentAsync(service);
346-
var admin = await CreateAdminAsync();
345+
var admin = await CreateAdminAsync(openaiClient, config.DeploymentName);
347346
var groupChat = new GroupChat(
348347
admin: admin,
349348
members:
350349
[
351-
admin,
352350
coder,
353351
runner,
354352
reviewer,
355353
]);
356354

357-
admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat);
358355
coder.SendIntroduction("I will write dotnet code to resolve task", groupChat);
359356
reviewer.SendIntroduction("I will review dotnet code", groupChat);
360357
runner.SendIntroduction("I will run dotnet code once the review is done", groupChat);
361358

362-
var groupChatManager = new GroupChatManager(groupChat);
363-
var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10);
364-
365-
// the last message is from admin, which is the termination message
366-
var lastMessage = conversationHistory.Last();
367-
lastMessage.From.Should().Be("admin");
368-
lastMessage.IsGroupChatTerminateMessage().Should().BeTrue();
369-
lastMessage.Should().BeOfType<TextMessage>();
370-
lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString());
359+
var task = "What's the 39th of fibonacci number?";
360+
var taskMessage = new TextMessage(Role.User, task);
361+
await foreach (var message in groupChat.SendAsync([taskMessage], maxRound: 10))
362+
{
363+
// teminate chat if message is from runner and run successfully
364+
if (message.From == "runner" && message.GetContent().Contains(the39thFibonacciNumber.ToString()))
365+
{
366+
Console.WriteLine($"The 39th of fibonacci number is {the39thFibonacciNumber}");
367+
break;
368+
}
369+
}
371370
#endregion create_group_chat
372371
}
373372
}

dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs

+33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Runtime.CompilerServices;
8+
using System.Threading;
79

810
namespace AutoGen.Core;
911

@@ -23,6 +25,36 @@ public static void AddInitializeMessage(this IAgent agent, string message, IGrou
2325
groupChat.SendIntroduction(msg);
2426
}
2527

28+
/// <summary>
29+
/// Send messages to a <see cref="IGroupChat"/> and return new messages from the group chat.
30+
/// </summary>
31+
/// <param name="groupChat"></param>
32+
/// <param name="chatHistory"></param>
33+
/// <param name="maxRound"></param>
34+
/// <param name="cancellationToken"></param>
35+
/// <returns></returns>
36+
public static async IAsyncEnumerable<IMessage> SendAsync(
37+
this IGroupChat groupChat,
38+
IEnumerable<IMessage> chatHistory,
39+
int maxRound = 10,
40+
[EnumeratorCancellation]
41+
CancellationToken cancellationToken = default)
42+
{
43+
while (maxRound-- > 0)
44+
{
45+
var messages = await groupChat.CallAsync(chatHistory, maxRound: 1, cancellationToken);
46+
var lastMessage = messages.Last();
47+
48+
yield return lastMessage;
49+
if (lastMessage.IsGroupChatTerminateMessage())
50+
{
51+
yield break;
52+
}
53+
54+
chatHistory = messages;
55+
}
56+
}
57+
2658
/// <summary>
2759
/// Send an instruction message to the group chat.
2860
/// </summary>
@@ -78,6 +110,7 @@ public static bool IsGroupChatClearMessage(this IMessage message)
78110
return message.GetContent()?.Contains(CLEAR_MESSAGES) ?? false;
79111
}
80112

113+
[Obsolete]
81114
public static IEnumerable<IMessage> ProcessConversationForAgent(
82115
this IGroupChat groupChat,
83116
IEnumerable<IMessage> initialMessages,

dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ namespace AutoGen.Core;
1313
/// If the last message is from one of the candidates, the next agent will be the next candidate in the list.
1414
/// </para>
1515
/// <para>
16-
/// Otherwise, no agent will be selected. In this case, the orchestrator will return an empty list.
16+
/// Otherwise, the first agent in <see cref="OrchestrationContext.Candidates"/> will be returned.
1717
/// </para>
1818
/// <para>
19-
/// This orchestrator always return a single agent.
2019
/// </para>
2120
/// </summary>
2221
public class RoundRobinOrchestrator : IOrchestrator
@@ -29,7 +28,7 @@ public class RoundRobinOrchestrator : IOrchestrator
2928

3029
if (lastMessage == null)
3130
{
32-
return null;
31+
return context.Candidates.FirstOrDefault();
3332
}
3433

3534
var candidates = context.Candidates.ToList();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// GroupChatTests.cs
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using FluentAssertions;
9+
using Xunit;
10+
11+
namespace AutoGen.Tests;
12+
13+
public class GroupChatTests
14+
{
15+
[Fact]
16+
public async Task ItSendMessageTestAsync()
17+
{
18+
var alice = new DefaultReplyAgent("Alice", "I am alice");
19+
var bob = new DefaultReplyAgent("Bob", "I am bob");
20+
21+
var groupChat = new GroupChat([alice, bob]);
22+
23+
var chatHistory = new List<IMessage>();
24+
25+
var maxRound = 10;
26+
await foreach (var message in groupChat.SendAsync(chatHistory, maxRound))
27+
{
28+
chatHistory.Add(message);
29+
}
30+
31+
chatHistory.Count().Should().Be(10);
32+
}
33+
34+
[Fact]
35+
public async Task ItTerminateConversationWhenAgentReturnTerminateKeyWord()
36+
{
37+
var alice = new DefaultReplyAgent("Alice", "I am alice");
38+
var bob = new DefaultReplyAgent("Bob", "I am bob");
39+
var cathy = new DefaultReplyAgent("Cathy", $"I am cathy, {GroupChatExtension.TERMINATE}");
40+
41+
var groupChat = new GroupChat([alice, bob, cathy]);
42+
43+
var chatHistory = new List<IMessage>();
44+
45+
var maxRound = 10;
46+
await foreach (var message in groupChat.SendAsync(chatHistory, maxRound))
47+
{
48+
chatHistory.Add(message);
49+
}
50+
51+
chatHistory.Count().Should().Be(3);
52+
chatHistory.Last().From.Should().Be("Cathy");
53+
}
54+
}

dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public async Task ItReturnNullIfLastMessageIsNotFromCandidates()
8484
}
8585

8686
[Fact]
87-
public async Task ItReturnEmptyListIfNoChatHistory()
87+
public async Task ItReturnTheFirstAgentInTheListIfNoChatHistory()
8888
{
8989
var orchestrator = new RoundRobinOrchestrator();
9090
var context = new OrchestrationContext
@@ -98,6 +98,6 @@ public async Task ItReturnEmptyListIfNoChatHistory()
9898
};
9999

100100
var result = await orchestrator.GetNextSpeakerAsync(context);
101-
result.Should().BeNull();
101+
result!.Name.Should().Be("Alice");
102102
}
103103
}

0 commit comments

Comments
 (0)