Table of Contents

GroupChat invokes agents in a dynamic way. On one hand, It relies on its admin agent to intellegently determines the next speaker based on conversation context, and on the other hand, it also allows you to control the conversation flow by using a Graph. This makes it a more dynamic yet controlable way to determine the next speaker agent. You can use GroupChat to create a dynamic group chat with multiple agents working together to resolve a given task.

Note

In GroupChat, when only the group admin is used to determine the next speaker agent, it's recommented to use a more powerful llm model, such as gpt-4 to ensure the best experience.

Use GroupChat to implement a code interpreter chat flow

The following example shows how to create a dynamic group chat with GroupChat. In this example, we will create a dynamic group chat with 4 agents: admin, coder, reviewer and runner. Each agent has its own role in the group chat:

Code interpreter group chat

  • admin: create task for group to work on and terminate the conversation when task is completed. In this example, the task to resolve is to calculate the 39th Fibonacci number.
  • coder: a dotnet coder who can write code to resolve tasks.
  • reviewer: a dotnet code reviewer who can review code written by coder. In this example, reviewer will examine if the code written by coder follows the condition below:
    • has only one csharp code block.
    • use top-level statements.
    • is dotnet code snippet.
    • print the result of the code snippet to console.
  • runner: a dotnet code runner who can run code written by coder and print the result.
flowchart LR
  subgraph Group Chat
    B[Amin]
    C[Coder]
    D[Reviewer]
    E[Runner]
  end
Note

The complete code of this example can be found in Example07_Dynamic_GroupChat_Calculate_Fibonacci

Create group chat

The code below shows how to create a dynamic group chat with GroupChat. In this example, we will create a dynamic group chat with 4 agents: admin, coder, reviewer and runner. In this case we don't pass a workflow to the group chat, so the group chat will use driven by the admin agent.

var reviewer = await CreateReviewerAgentAsync(gpt4o);
var coder = await CreateCoderAgentAsync(gpt4o);
var runner = await CreateRunnerAgentAsync(kernel);
var admin = await CreateAdminAsync(gpt4o);
var groupChat = new GroupChat(
    admin: admin,
    members:
    [
        coder,
        runner,
        reviewer,
    ]);

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 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;
    }
}
Tip

You can set up initial context for the group chat using SendIntroduction. The initial context can help group admin orchestrates the conversation flow.

Output:

GroupChat

Below are break-down of how agents are created and their roles in the group chat.

  • Create admin agent

The code below shows how to create admin agent. admin agent will create a task for group to work on and terminate the conversation when task is completed.

public static async Task<IAgent> CreateAdminAsync(ChatClient client)
{
    var admin = new OpenAIChatAgent(
        chatClient: client,
        name: "admin",
        temperature: 0)
        .RegisterMessageConnector()
        .RegisterPrintMessage();

    return admin;
}
  • Create coder agent
public static async Task<IAgent> CreateCoderAgentAsync(ChatClient client)
{
    var coder = new OpenAIChatAgent(
        chatClient: client,
        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.

        Here're some rules to follow on writing dotnet code:
        - put code between ```csharp and ```
        - Avoid adding `using` keyword when creating disposable object. e.g `var httpClient = new HttpClient()`
        - Try to use `var` instead of explicit type.
        - Try avoid using external library, use .NET Core library instead.
        - Use top level statement to write code.
        - Always print out the result to console. Don't write code that doesn't print out anything.
        
        If you need to install nuget packages, put nuget packages in the following format:
        ```nuget
        nuget_package_name
        ```
        
        If your code is incorrect, runner will tell you the error message. Fix the error and send the code again.",
        temperature: 0.4f)
        .RegisterMessageConnector()
        .RegisterPrintMessage();

    return coder;
}
  • Create reviewer agent

The code below shows how to create reviewer agent. reviewer agent is a dotnet code reviewer who can review code written by coder. In this example, a function is used to examine if the code written by coder follows the condition.

public struct CodeReviewResult
{
    public bool HasMultipleCodeBlocks { get; set; }
    public bool IsTopLevelStatement { get; set; }
    public bool IsDotnetCodeBlock { get; set; }
    public bool IsPrintResultToConsole { get; set; }
}

/// <summary>
/// review code block
/// </summary>
/// <param name="hasMultipleCodeBlocks">true if there're multipe csharp code blocks</param>
/// <param name="isTopLevelStatement">true if the code is in top level statement</param>
/// <param name="isDotnetCodeBlock">true if the code block is csharp code block</param>
/// <param name="isPrintResultToConsole">true if the code block print out result to console</param>
[Function]
public async Task<string> ReviewCodeBlock(
    bool hasMultipleCodeBlocks,
    bool isTopLevelStatement,
    bool isDotnetCodeBlock,
    bool isPrintResultToConsole)
{
    var obj = new CodeReviewResult
    {
        HasMultipleCodeBlocks = hasMultipleCodeBlocks,
        IsTopLevelStatement = isTopLevelStatement,
        IsDotnetCodeBlock = isDotnetCodeBlock,
        IsPrintResultToConsole = isPrintResultToConsole,
    };

    return JsonSerializer.Serialize(obj);
}
Tip

You can use FunctionAttribute to generate type-safe function definition and function call wrapper for the function. For more information, please check out Create type safe function call.

public static async Task<IAgent> CreateReviewerAgentAsync(ChatClient chatClient)
{
    var functions = new Example07_Dynamic_GroupChat_Calculate_Fibonacci();
    var functionCallMiddleware = new FunctionCallMiddleware(
        functions: [functions.ReviewCodeBlockFunctionContract],
        functionMap: new Dictionary<string, Func<string, Task<string>>>()
        {
            { nameof(functions.ReviewCodeBlock), functions.ReviewCodeBlockWrapper },
        });
    var reviewer = new OpenAIChatAgent(
        chatClient: chatClient,
        name: "code_reviewer",
        systemMessage: @"You review code block from coder")
        .RegisterMessageConnector()
        .RegisterStreamingMiddleware(functionCallMiddleware)
        .RegisterMiddleware(async (msgs, option, innerAgent, ct) =>
        {
            var maxRetry = 3;
            var reply = await innerAgent.GenerateReplyAsync(msgs, option, ct);
            while (maxRetry-- > 0)
            {
                if (reply.GetToolCalls() is var toolCalls && toolCalls.Count == 1 && toolCalls[0].FunctionName == nameof(ReviewCodeBlock))
                {
                    var toolCallResult = reply.GetContent();
                    var reviewResultObj = JsonSerializer.Deserialize<CodeReviewResult>(toolCallResult);
                    var reviews = new List<string>();
                    if (reviewResultObj.HasMultipleCodeBlocks)
                    {
                        var fixCodeBlockPrompt = @"There're multiple code blocks, please combine them into one code block";
                        reviews.Add(fixCodeBlockPrompt);
                    }

                    if (reviewResultObj.IsDotnetCodeBlock is false)
                    {
                        var fixCodeBlockPrompt = @"The code block is not csharp code block, please write dotnet code only";
                        reviews.Add(fixCodeBlockPrompt);
                    }

                    if (reviewResultObj.IsTopLevelStatement is false)
                    {
                        var fixCodeBlockPrompt = @"The code is not top level statement, please rewrite your dotnet code using top level statement";
                        reviews.Add(fixCodeBlockPrompt);
                    }

                    if (reviewResultObj.IsPrintResultToConsole is false)
                    {
                        var fixCodeBlockPrompt = @"The code doesn't print out result to console, please print out result to console";
                        reviews.Add(fixCodeBlockPrompt);
                    }

                    if (reviews.Count > 0)
                    {
                        var sb = new StringBuilder();
                        sb.AppendLine("There're some comments from code reviewer, please fix these comments");
                        foreach (var review in reviews)
                        {
                            sb.AppendLine($"- {review}");
                        }

                        return new TextMessage(Role.Assistant, sb.ToString(), from: "code_reviewer");
                    }
                    else
                    {
                        var msg = new TextMessage(Role.Assistant, "The code looks good, please ask runner to run the code for you.")
                        {
                            From = "code_reviewer",
                        };

                        return msg;
                    }
                }
                else
                {
                    var originalContent = reply.GetContent();
                    var prompt = $@"Please convert the content to ReviewCodeBlock function arguments.

    ## Original Content
    {originalContent}";

                    reply = await innerAgent.SendAsync(prompt, msgs, ct);
                }
            }

            throw new Exception("Failed to review code block");
        })
        .RegisterPrintMessage();

    return reviewer;
}
  • Create runner agent
Tip

AutoGen provides a built-in support for running code snippet. For more information, please check out Execute code snippet.

public static async Task<IAgent> CreateRunnerAgentAsync(Kernel kernel)
{
    var runner = new DefaultReplyAgent(
        name: "runner",
        defaultReply: "No code available.")
        .RegisterMiddleware(async (msgs, option, agent, _) =>
        {
            if (msgs.Any() || msgs.All(msg => msg.From != "coder"))
            {
                return new TextMessage(Role.Assistant, "No code available. Coder please write code");
            }
            else
            {
                var coderMsg = msgs.Last(msg => msg.From == "coder");
                if (coderMsg.ExtractCodeBlock("```csharp", "```") is string code)
                {
                    var codeResult = await kernel.RunSubmitCodeCommandAsync(code, "csharp");

                    codeResult = $"""
                    [RUNNER_RESULT]
                    {codeResult}
                    """;

                    return new TextMessage(Role.Assistant, codeResult)
                    {
                        From = "runner",
                    };
                }
                else
                {
                    return new TextMessage(Role.Assistant, "No code available. Coder please write code");
                }
            }
        })
        .RegisterPrintMessage();

    return runner;
}