Skip to content

Commit

Permalink
[.Net] add SendAsync api to iterate group chat step by step (#3214)
Browse files Browse the repository at this point in the history
* add SendAsync api and tests

* update example to use new sendAsync API
  • Loading branch information
LittleLittleCloud authored Jul 29, 2024
1 parent cf29a2f commit 2cfaf73
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,10 +49,11 @@ public async Task<string> ReviewCodeBlock(
#endregion reviewer_function

#region create_coder
public static async Task<IAgent> CreateCoderAgentAsync()
public static async Task<IAgent> 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.
Expand All @@ -70,8 +71,8 @@ public static async Task<IAgent> 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;
Expand All @@ -81,9 +82,8 @@ public static async Task<IAgent> CreateCoderAgentAsync()
#region create_runner
public static async Task<IAgent> 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, _) =>
Expand All @@ -105,45 +105,38 @@ public static async Task<IAgent> CreateRunnerAgentAsync(InteractiveService servi
#endregion create_runner

#region create_admin
public static async Task<IAgent> CreateAdminAsync()
public static async Task<IAgent> 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<IAgent> CreateReviewerAgentAsync()
public static async Task<IAgent> 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<string, Func<string, Task<string>>>()
{
{ 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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<TextMessage>();
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
}
}
33 changes: 33 additions & 0 deletions dotnet/src/AutoGen.Core/Extension/GroupChatExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;

namespace AutoGen.Core;

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

/// <summary>
/// Send messages to a <see cref="IGroupChat"/> and return new messages from the group chat.
/// </summary>
/// <param name="groupChat"></param>
/// <param name="chatHistory"></param>
/// <param name="maxRound"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async IAsyncEnumerable<IMessage> SendAsync(
this IGroupChat groupChat,
IEnumerable<IMessage> 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;
}
}

/// <summary>
/// Send an instruction message to the group chat.
/// </summary>
Expand Down Expand Up @@ -78,6 +110,7 @@ public static bool IsGroupChatClearMessage(this IMessage message)
return message.GetContent()?.Contains(CLEAR_MESSAGES) ?? false;
}

[Obsolete]
public static IEnumerable<IMessage> ProcessConversationForAgent(
this IGroupChat groupChat,
IEnumerable<IMessage> initialMessages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </para>
/// <para>
/// Otherwise, no agent will be selected. In this case, the orchestrator will return an empty list.
/// Otherwise, the first agent in <see cref="OrchestrationContext.Candidates"/> will be returned.
/// </para>
/// <para>
/// This orchestrator always return a single agent.
/// </para>
/// </summary>
public class RoundRobinOrchestrator : IOrchestrator
Expand All @@ -29,7 +28,7 @@ public class RoundRobinOrchestrator : IOrchestrator

if (lastMessage == null)
{
return null;
return context.Candidates.FirstOrDefault();
}

var candidates = context.Candidates.ToList();
Expand Down
54 changes: 54 additions & 0 deletions dotnet/test/AutoGen.Tests/GroupChat/GroupChatTests.cs
Original file line number Diff line number Diff line change
@@ -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<IMessage>();

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<IMessage>();

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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -98,6 +98,6 @@ public async Task ItReturnEmptyListIfNoChatHistory()
};

var result = await orchestrator.GetNextSpeakerAsync(context);
result.Should().BeNull();
result!.Name.Should().Be("Alice");
}
}

0 comments on commit 2cfaf73

Please sign in to comment.