diff --git a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs index 004e0f05544..dd4fcada967 100644 --- a/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs +++ b/dotnet/sample/AutoGen.BasicSamples/Example07_Dynamic_GroupChat_Calculate_Fibonacci.cs @@ -3,12 +3,12 @@ using System.Text; using System.Text.Json; -using AutoGen; using AutoGen.BasicSample; using AutoGen.Core; using AutoGen.DotnetInteractive; using AutoGen.OpenAI; using AutoGen.OpenAI.Extension; +using Azure.AI.OpenAI; using FluentAssertions; public partial class Example07_Dynamic_GroupChat_Calculate_Fibonacci @@ -49,10 +49,11 @@ public async Task ReviewCodeBlock( #endregion reviewer_function #region create_coder - public static async Task CreateCoderAgentAsync() + public static async Task CreateCoderAgentAsync(OpenAIClient client, string deployModel) { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var coder = new GPTAgent( + var coder = new OpenAIChatAgent( + openAIClient: client, + modelName: deployModel, name: "coder", 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. @@ -70,8 +71,8 @@ public static async Task CreateCoderAgentAsync() ``` If your code is incorrect, runner will tell you the error message. Fix the error and send the code again.", - config: gpt3Config, temperature: 0.4f) + .RegisterMessageConnector() .RegisterPrintMessage(); return coder; @@ -81,9 +82,8 @@ public static async Task CreateCoderAgentAsync() #region create_runner public static async Task CreateRunnerAgentAsync(InteractiveService service) { - var runner = new AssistantAgent( + var runner = new DefaultReplyAgent( name: "runner", - systemMessage: "You run dotnet code", defaultReply: "No code available.") .RegisterDotnetCodeBlockExectionHook(interactiveService: service) .RegisterMiddleware(async (msgs, option, agent, _) => @@ -105,45 +105,38 @@ public static async Task CreateRunnerAgentAsync(InteractiveService servi #endregion create_runner #region create_admin - public static async Task CreateAdminAsync() + public static async Task CreateAdminAsync(OpenAIClient client, string deployModel) { - var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); - var admin = new GPTAgent( + var admin = new OpenAIChatAgent( + openAIClient: client, + modelName: deployModel, name: "admin", - systemMessage: "You are group admin, terminate the group chat once task is completed by saying [TERMINATE] plus the final answer", - temperature: 0, - config: gpt3Config) - .RegisterMiddleware(async (msgs, option, agent, _) => - { - var reply = await agent.GenerateReplyAsync(msgs, option); - if (reply is TextMessage textMessage && textMessage.Content.Contains("TERMINATE") is true) - { - var content = $"{textMessage.Content}\n\n {GroupChatExtension.TERMINATE}"; - - return new TextMessage(Role.Assistant, content, from: reply.From); - } - - return reply; - }); + temperature: 0) + .RegisterMessageConnector() + .RegisterPrintMessage(); return admin; } #endregion create_admin #region create_reviewer - public static async Task CreateReviewerAgentAsync() + public static async Task CreateReviewerAgentAsync(OpenAIClient openAIClient, string deployModel) { var gpt3Config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); var functions = new Example07_Dynamic_GroupChat_Calculate_Fibonacci(); - var reviewer = new GPTAgent( - name: "code_reviewer", - systemMessage: @"You review code block from coder", - config: gpt3Config, - functions: [functions.ReviewCodeBlockFunctionContract.ToOpenAIFunctionDefinition()], + var functionCallMiddleware = new FunctionCallMiddleware( + functions: [functions.ReviewCodeBlockFunctionContract], functionMap: new Dictionary>>() { - { nameof(ReviewCodeBlock), functions.ReviewCodeBlockWrapper }, - }) + { nameof(functions.ReviewCodeBlock), functions.ReviewCodeBlockWrapper }, + }); + var reviewer = new OpenAIChatAgent( + openAIClient: openAIClient, + name: "code_reviewer", + systemMessage: @"You review code block from coder", + modelName: deployModel) + .RegisterMessageConnector() + .RegisterStreamingMiddleware(functionCallMiddleware) .RegisterMiddleware(async (msgs, option, innerAgent, ct) => { var maxRetry = 3; @@ -229,16 +222,19 @@ public static async Task RunWorkflowAsync() Directory.CreateDirectory(workDir); } + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var openaiClient = new OpenAIClient(new Uri(config.Endpoint), new Azure.AzureKeyCredential(config.ApiKey)); + using var service = new InteractiveService(workDir); var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); await service.StartAsync(workDir, default); #region create_workflow - var reviewer = await CreateReviewerAgentAsync(); - var coder = await CreateCoderAgentAsync(); + var reviewer = await CreateReviewerAgentAsync(openaiClient, config.DeploymentName); + var coder = await CreateCoderAgentAsync(openaiClient, config.DeploymentName); var runner = await CreateRunnerAgentAsync(service); - var admin = await CreateAdminAsync(); + var admin = await CreateAdminAsync(openaiClient, config.DeploymentName); var admin2CoderTransition = Transition.Create(admin, coder); var coder2ReviewerTransition = Transition.Create(coder, reviewer); @@ -335,39 +331,42 @@ public static async Task RunAsync() Directory.CreateDirectory(workDir); } + var config = LLMConfiguration.GetAzureOpenAIGPT3_5_Turbo(); + var openaiClient = new OpenAIClient(new Uri(config.Endpoint), new Azure.AzureKeyCredential(config.ApiKey)); + using var service = new InteractiveService(workDir); var dotnetInteractiveFunctions = new DotnetInteractiveFunction(service); await service.StartAsync(workDir, default); #region create_group_chat - var reviewer = await CreateReviewerAgentAsync(); - var coder = await CreateCoderAgentAsync(); + var reviewer = await CreateReviewerAgentAsync(openaiClient, config.DeploymentName); + var coder = await CreateCoderAgentAsync(openaiClient, config.DeploymentName); var runner = await CreateRunnerAgentAsync(service); - var admin = await CreateAdminAsync(); + var admin = await CreateAdminAsync(openaiClient, config.DeploymentName); var groupChat = new GroupChat( admin: admin, members: [ - admin, coder, runner, reviewer, ]); - admin.SendIntroduction("Welcome to my group, work together to resolve my task", groupChat); coder.SendIntroduction("I will write dotnet code to resolve task", groupChat); reviewer.SendIntroduction("I will review dotnet code", groupChat); runner.SendIntroduction("I will run dotnet code once the review is done", groupChat); - var groupChatManager = new GroupChatManager(groupChat); - var conversationHistory = await admin.InitiateChatAsync(groupChatManager, "What's the 39th of fibonacci number?", maxRound: 10); - - // the last message is from admin, which is the termination message - var lastMessage = conversationHistory.Last(); - lastMessage.From.Should().Be("admin"); - lastMessage.IsGroupChatTerminateMessage().Should().BeTrue(); - lastMessage.Should().BeOfType(); - lastMessage.GetContent().Should().Contain(the39thFibonacciNumber.ToString()); + var task = "What's the 39th of fibonacci number?"; + var taskMessage = new TextMessage(Role.User, task); + await foreach (var message in groupChat.SendAsync([taskMessage], maxRound: 10)) + { + // teminate chat if message is from runner and run successfully + if (message.From == "runner" && message.GetContent().Contains(the39thFibonacciNumber.ToString())) + { + Console.WriteLine($"The 39th of fibonacci number is {the39thFibonacciNumber}"); + break; + } + } #endregion create_group_chat } } diff --git a/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs b/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs index 45728023b96..a5009e21155 100644 --- a/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs +++ b/dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; namespace AutoGen.Core; @@ -23,6 +25,36 @@ public static void AddInitializeMessage(this IAgent agent, string message, IGrou groupChat.SendIntroduction(msg); } + /// + /// Send messages to a and return new messages from the group chat. + /// + /// + /// + /// + /// + /// + public static async IAsyncEnumerable SendAsync( + this IGroupChat groupChat, + IEnumerable chatHistory, + int maxRound = 10, + [EnumeratorCancellation] + CancellationToken cancellationToken = default) + { + while (maxRound-- > 0) + { + var messages = await groupChat.CallAsync(chatHistory, maxRound: 1, cancellationToken); + var lastMessage = messages.Last(); + + yield return lastMessage; + if (lastMessage.IsGroupChatTerminateMessage()) + { + yield break; + } + + chatHistory = messages; + } + } + /// /// Send an instruction message to the group chat. /// @@ -78,6 +110,7 @@ public static bool IsGroupChatClearMessage(this IMessage message) return message.GetContent()?.Contains(CLEAR_MESSAGES) ?? false; } + [Obsolete] public static IEnumerable ProcessConversationForAgent( this IGroupChat groupChat, IEnumerable initialMessages, diff --git a/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs b/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs index 0f8b8e483c6..af5efdc0e9e 100644 --- a/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs +++ b/dotnet/src/AutoGen.Core/Orchestrator/RoundRobinOrchestrator.cs @@ -13,10 +13,9 @@ namespace AutoGen.Core; /// If the last message is from one of the candidates, the next agent will be the next candidate in the list. /// /// -/// Otherwise, no agent will be selected. In this case, the orchestrator will return an empty list. +/// Otherwise, the first agent in will be returned. /// /// -/// This orchestrator always return a single agent. /// /// public class RoundRobinOrchestrator : IOrchestrator @@ -29,7 +28,7 @@ public class RoundRobinOrchestrator : IOrchestrator if (lastMessage == null) { - return null; + return context.Candidates.FirstOrDefault(); } var candidates = context.Candidates.ToList(); diff --git a/dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs b/dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs new file mode 100644 index 00000000000..7a7a27be9b1 --- /dev/null +++ b/dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// GroupChatTests.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Tests; + +public class GroupChatTests +{ + [Fact] + public async Task ItSendMessageTestAsync() + { + var alice = new DefaultReplyAgent("Alice", "I am alice"); + var bob = new DefaultReplyAgent("Bob", "I am bob"); + + var groupChat = new GroupChat([alice, bob]); + + var chatHistory = new List(); + + var maxRound = 10; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound)) + { + chatHistory.Add(message); + } + + chatHistory.Count().Should().Be(10); + } + + [Fact] + public async Task ItTerminateConversationWhenAgentReturnTerminateKeyWord() + { + var alice = new DefaultReplyAgent("Alice", "I am alice"); + var bob = new DefaultReplyAgent("Bob", "I am bob"); + var cathy = new DefaultReplyAgent("Cathy", $"I am cathy, {GroupChatExtension.TERMINATE}"); + + var groupChat = new GroupChat([alice, bob, cathy]); + + var chatHistory = new List(); + + var maxRound = 10; + await foreach (var message in groupChat.SendAsync(chatHistory, maxRound)) + { + chatHistory.Add(message); + } + + chatHistory.Count().Should().Be(3); + chatHistory.Last().From.Should().Be("Cathy"); + } +} diff --git a/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs b/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs index e14bf85cf21..17897860a14 100644 --- a/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs +++ b/dotnet/test/AutoGen.Tests/Orchestrator/RoundRobinOrchestratorTests.cs @@ -84,7 +84,7 @@ public async Task ItReturnNullIfLastMessageIsNotFromCandidates() } [Fact] - public async Task ItReturnEmptyListIfNoChatHistory() + public async Task ItReturnTheFirstAgentInTheListIfNoChatHistory() { var orchestrator = new RoundRobinOrchestrator(); var context = new OrchestrationContext @@ -98,6 +98,6 @@ public async Task ItReturnEmptyListIfNoChatHistory() }; var result = await orchestrator.GetNextSpeakerAsync(context); - result.Should().BeNull(); + result!.Name.Should().Be("Alice"); } }