DEV Community

Cover image for AI Agents in Semantic Kernel: ChatCompletionAgent, AgentGroupChat, and Orchestration
Brian Spann
Brian Spann

Posted on

AI Agents in Semantic Kernel: ChatCompletionAgent, AgentGroupChat, and Orchestration

Agents represent the evolution from AI assistants that respond to AI systems that reason and act. An agent doesn't just answer questions—it breaks down problems, uses tools, collaborates with other agents, and drives toward goals autonomously.

In Part 4, we built production RAG systems. Now we'll explore Semantic Kernel's agent framework—from single agents with tools to multi-agent orchestration.

What Makes an Agent?

An agent combines three capabilities:

  1. Reasoning: Understanding context and planning actions
  2. Tool Use: Executing functions to interact with the world
  3. Memory: Maintaining state across interactions

Semantic Kernel provides two main agent types:

  • ChatCompletionAgent: Single agent with conversation and tools
  • AgentGroupChat: Multiple agents collaborating on tasks

ChatCompletionAgent: Your First Agent

Let's build a customer support agent that can look up orders, process refunds, and handle inquiries:

using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;

// Create the kernel with plugins
var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(
        deploymentName: "gpt-4o",
        endpoint: config["AzureOpenAI:Endpoint"]!,
        credential: new DefaultAzureCredential())
    .Build();

// Register plugins
kernel.Plugins.AddFromObject(new OrderPlugin(orderService), "Orders");
kernel.Plugins.AddFromObject(new CustomerPlugin(customerService), "Customers");
kernel.Plugins.AddFromObject(new RefundPlugin(paymentService), "Refunds");

// Create the agent
var supportAgent = new ChatCompletionAgent
{
    Name = "SupportAgent",
    Instructions = """
        You are a helpful customer support agent for TechShop.

        Your capabilities:
        - Look up order status and details
        - Check customer account information
        - Process refunds for eligible orders
        - Answer questions about policies

        Guidelines:
        - Always verify the customer's identity before sharing order details
        - Be empathetic but professional
        - If you can't help, escalate to a human agent
        - Never share sensitive payment information

        Available tools will help you access customer and order data.
        """,
    Kernel = kernel,
    Arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings
    {
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
    })
};
Enter fullscreen mode Exit fullscreen mode

Running Agent Conversations

// Create a conversation history
var history = new ChatHistory();

// Simulate customer interaction
history.AddUserMessage("Hi, I need to check on my order. It's ORD-789456");

// Get agent response
await foreach (var response in supportAgent.InvokeAsync(history))
{
    Console.WriteLine($"[{supportAgent.Name}]: {response.Content}");
    history.Add(response);
}

// Continue the conversation
history.AddUserMessage("It's been 2 weeks and hasn't arrived. Can I get a refund?");

await foreach (var response in supportAgent.InvokeAsync(history))
{
    Console.WriteLine($"[{supportAgent.Name}]: {response.Content}");
    history.Add(response);
}

// The agent will:
// 1. Call get_order_status to check the order
// 2. See it's overdue
// 3. Call process_refund if eligible
// 4. Respond with confirmation
Enter fullscreen mode Exit fullscreen mode

Streaming Agent Responses

For real-time UX, stream responses as they generate:

public async IAsyncEnumerable<string> StreamAgentResponseAsync(
    ChatCompletionAgent agent,
    ChatHistory history,
    string userMessage)
{
    history.AddUserMessage(userMessage);

    await foreach (var chunk in agent.InvokeStreamingAsync(history))
    {
        if (!string.IsNullOrEmpty(chunk.Content))
        {
            yield return chunk.Content;
        }
    }
}

// Usage in an API endpoint
app.MapPost("/chat", async (ChatRequest request, ChatCompletionAgent agent) =>
{
    return Results.Stream(async stream =>
    {
        var writer = new StreamWriter(stream);
        await foreach (var chunk in StreamAgentResponseAsync(agent, history, request.Message))
        {
            await writer.WriteAsync(chunk);
            await writer.FlushAsync();
        }
    }, "text/event-stream");
});
Enter fullscreen mode Exit fullscreen mode

Multi-Agent Orchestration with AgentGroupChat

Real power comes from multiple specialized agents working together. Let's build a content creation team:

// Research Agent - finds information
var researcher = new ChatCompletionAgent
{
    Name = "Researcher",
    Instructions = """
        You are a thorough research specialist.
        Your job is to find accurate, relevant information on topics.
        Always cite sources and distinguish between facts and opinions.
        Focus on gathering comprehensive data before conclusions.
        """,
    Kernel = kernelWithSearchPlugin  // Has web search tools
};

// Writer Agent - creates content
var writer = new ChatCompletionAgent
{
    Name = "Writer",
    Instructions = """
        You are a skilled content writer.
        Your job is to take research and create engaging, well-structured content.
        Use clear language, compelling narratives, and proper formatting.
        Always incorporate the research provided - don't make up facts.
        """,
    Kernel = kernel
};

// Editor Agent - reviews and improves
var editor = new ChatCompletionAgent
{
    Name = "Editor",
    Instructions = """
        You are a meticulous editor.
        Your job is to review content for:
        - Factual accuracy (cross-reference with research)
        - Grammar and style
        - Clarity and flow
        - Engagement and readability

        Provide specific, actionable feedback.
        When content is publication-ready, say "APPROVED".
        """,
    Kernel = kernel
};

// Create the group chat
var groupChat = new AgentGroupChat(researcher, writer, editor);
Enter fullscreen mode Exit fullscreen mode

Selection Strategies

How does the system know which agent should speak next?

// 1. Sequential - each agent speaks in order
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        SelectionStrategy = new SequentialSelectionStrategy()
    }
};

// 2. Round Robin - cycles through agents
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        SelectionStrategy = new RoundRobinSelectionStrategy()
    }
};

// 3. Kernel Function - LLM decides who speaks next
var selectionFunction = kernel.CreateFunctionFromPrompt("""
    Based on the conversation, determine which agent should respond next.

    Agents:
    - Researcher: When facts or information are needed
    - Writer: When content needs to be created or revised
    - Editor: When content needs review or final approval

    Conversation so far:
    {{$history}}

    Last message was from: {{$lastAgent}}

    Who should speak next? Respond with just the agent name:
    """);

var groupChat = new AgentGroupChat(researcher, writer, editor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        SelectionStrategy = new KernelFunctionSelectionStrategy(selectionFunction, kernel)
        {
            ResultParser = result => result.GetValue<string>()?.Trim() ?? "Writer"
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Termination Strategies

When should the conversation end?

// 1. Maximum turns
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        TerminationStrategy = new MaximumIterationTerminationStrategy(10)
    }
};

// 2. Keyword-based termination
var terminationFunction = kernel.CreateFunctionFromPrompt("""
    Review the conversation and determine if the task is complete.
    The task is complete when the Editor has said "APPROVED".

    Conversation:
    {{$history}}

    Is the task complete? Answer 'yes' or 'no':
    """);

var groupChat = new AgentGroupChat(researcher, writer, editor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        TerminationStrategy = new KernelFunctionTerminationStrategy(terminationFunction, kernel)
        {
            ResultParser = result => 
                result.GetValue<string>()?.Trim().Equals("yes", StringComparison.OrdinalIgnoreCase) ?? false
        }
    }
};

// 3. Combined strategies
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        TerminationStrategy = new AggregatorTerminationStrategy(
            new MaximumIterationTerminationStrategy(15),
            new KernelFunctionTerminationStrategy(terminationFunction, kernel))
    }
};
Enter fullscreen mode Exit fullscreen mode

Running the Group Chat

// Kick off with a task
groupChat.AddChatMessage(new ChatMessageContent(
    AuthorRole.User,
    "Write a 500-word blog post about the benefits of AI pair programming for developers."));

// Let agents collaborate
Console.WriteLine("=== Agent Collaboration Starting ===\n");

await foreach (var message in groupChat.InvokeAsync())
{
    var agentName = message.AuthorName ?? "Unknown";
    Console.WriteLine($"--- {agentName} ---");
    Console.WriteLine(message.Content);
    Console.WriteLine();
}

Console.WriteLine("=== Collaboration Complete ===");

// Get the final history
var finalHistory = await groupChat.GetChatMessagesAsync().ToListAsync();
var finalContent = finalHistory.Last(m => m.AuthorName == "Writer").Content;
Console.WriteLine($"\nFinal Article:\n{finalContent}");
Enter fullscreen mode Exit fullscreen mode

Agent Filters for Observability

Monitor and control agent behavior with filters:

public class AgentLoggingFilter : IAutoFunctionInvocationFilter
{
    private readonly ILogger _logger;
    private readonly Stopwatch _stopwatch = new();

    public AgentLoggingFilter(ILogger<AgentLoggingFilter> logger) => _logger = logger;

    public async Task OnAutoFunctionInvocationAsync(
        AutoFunctionInvocationContext context,
        Func<AutoFunctionInvocationContext, Task> next)
    {
        _stopwatch.Restart();

        _logger.LogInformation(
            "Agent invoking function {Plugin}.{Function} with args: {Args}",
            context.Function.PluginName,
            context.Function.Name,
            JsonSerializer.Serialize(context.Arguments));

        try
        {
            await next(context);

            _stopwatch.Stop();

            _logger.LogInformation(
                "Function {Function} completed in {ElapsedMs}ms. Result: {Result}",
                context.Function.Name,
                _stopwatch.ElapsedMilliseconds,
                context.Result.GetValue<object>()?.ToString()?[..Math.Min(200, context.Result.ToString()!.Length)]);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Function {Function} failed", context.Function.Name);
            throw;
        }
    }
}

// Register the filter
builder.Services.AddSingleton<IAutoFunctionInvocationFilter, AgentLoggingFilter>();
Enter fullscreen mode Exit fullscreen mode

Safety Filter

Prevent dangerous operations:

public class SafetyFilter : IAutoFunctionInvocationFilter
{
    private readonly HashSet<string> _dangerousFunctions = new()
    {
        "delete_account",
        "transfer_funds",
        "modify_permissions"
    };

    public async Task OnAutoFunctionInvocationAsync(
        AutoFunctionInvocationContext context,
        Func<AutoFunctionInvocationContext, Task> next)
    {
        var functionName = context.Function.Name;

        if (_dangerousFunctions.Contains(functionName))
        {
            // Require confirmation for dangerous operations
            context.Result = new FunctionResult(
                context.Function,
                "This operation requires human approval. Please confirm with the user.");
            return;  // Don't call the actual function
        }

        await next(context);
    }
}
Enter fullscreen mode Exit fullscreen mode

Human-in-the-Loop Patterns

Keep humans in control for critical decisions:

public class HumanApprovalPlugin
{
    private readonly IApprovalService _approvalService;

    [KernelFunction("request_approval")]
    [Description("Requests human approval for an action before proceeding")]
    public async Task<ApprovalResult> RequestApprovalAsync(
        [Description("The action requiring approval")] string action,
        [Description("Why this action is needed")] string justification,
        [Description("Risk level: low, medium, high")] string riskLevel)
    {
        var request = new ApprovalRequest
        {
            Action = action,
            Justification = justification,
            RiskLevel = Enum.Parse<RiskLevel>(riskLevel, ignoreCase: true),
            RequestedAt = DateTime.UtcNow
        };

        // This could send a Slack message, email, or push notification
        var approval = await _approvalService.RequestApprovalAsync(request);

        return new ApprovalResult
        {
            Approved = approval.IsApproved,
            ApprovedBy = approval.ApproverName,
            Message = approval.IsApproved 
                ? "Approval granted. You may proceed."
                : $"Approval denied. Reason: {approval.DenialReason}"
        };
    }
}

// In agent instructions:
var agent = new ChatCompletionAgent
{
    Instructions = """
        Before performing any action with risk level 'high', you MUST:
        1. Call request_approval with the action details
        2. Wait for approval
        3. Only proceed if approved

        Never skip the approval step for high-risk actions.
        """
};
Enter fullscreen mode Exit fullscreen mode

Interactive Approval Flow

public class InteractiveAgent
{
    private readonly ChatCompletionAgent _agent;
    private readonly IUserInterface _ui;

    public async Task<string> ProcessWithApprovalAsync(string userRequest)
    {
        var history = new ChatHistory();
        history.AddUserMessage(userRequest);

        await foreach (var response in _agent.InvokeAsync(history))
        {
            // Check if agent is requesting approval
            if (response.Content?.Contains("APPROVAL_REQUIRED:") == true)
            {
                var approvalRequest = ParseApprovalRequest(response.Content);

                // Show to user
                var approved = await _ui.RequestApprovalAsync(
                    $"The agent wants to: {approvalRequest.Action}\n" +
                    $"Reason: {approvalRequest.Justification}\n\n" +
                    "Do you approve?");

                // Continue with approval result
                history.Add(response);
                history.AddUserMessage(approved 
                    ? "APPROVED - proceed with the action"
                    : "DENIED - do not proceed");

                // Get final response
                await foreach (var finalResponse in _agent.InvokeAsync(history))
                {
                    return finalResponse.Content ?? "";
                }
            }

            return response.Content ?? "";
        }

        return "";
    }
}
Enter fullscreen mode Exit fullscreen mode

Agent Patterns for Production

Supervisor Pattern

One agent coordinates others:

var supervisor = new ChatCompletionAgent
{
    Name = "Supervisor",
    Instructions = """
        You are a supervisor agent that coordinates a team of specialists.

        Your team:
        - DataAnalyst: For data queries and analysis
        - ContentWriter: For creating written content
        - QualityChecker: For reviewing and validating work

        Your job:
        1. Understand the user's request
        2. Delegate to the appropriate specialist(s)
        3. Review their work
        4. Provide the final response to the user

        Always explain your delegation decisions.
        """,
    Kernel = kernel
};

var dataAnalyst = new ChatCompletionAgent { Name = "DataAnalyst", /* ... */ };
var contentWriter = new ChatCompletionAgent { Name = "ContentWriter", /* ... */ };
var qualityChecker = new ChatCompletionAgent { Name = "QualityChecker", /* ... */ };

var supervisedChat = new AgentGroupChat(supervisor, dataAnalyst, contentWriter, qualityChecker)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        SelectionStrategy = new KernelFunctionSelectionStrategy(
            kernel.CreateFunctionFromPrompt("""
                The Supervisor always speaks first and last.
                Between, specialists speak when delegated to.

                Conversation: {{$history}}

                Who speaks next?
                """),
            kernel)
    }
};
Enter fullscreen mode Exit fullscreen mode

Debate Pattern

Agents argue different perspectives:

var optimist = new ChatCompletionAgent
{
    Name = "Optimist",
    Instructions = "You argue for the benefits and opportunities in every situation. Be enthusiastic but factual."
};

var skeptic = new ChatCompletionAgent
{
    Name = "Skeptic", 
    Instructions = "You identify risks, challenges, and potential problems. Be critical but constructive."
};

var moderator = new ChatCompletionAgent
{
    Name = "Moderator",
    Instructions = """
        You synthesize the debate between Optimist and Skeptic.
        After they've each spoken twice, summarize:
        - Key benefits identified
        - Key risks identified
        - Balanced recommendation
        """
};

var debate = new AgentGroupChat(optimist, skeptic, moderator)
{
    ExecutionSettings = new AgentGroupChatSettings
    {
        SelectionStrategy = new SequentialSelectionStrategy(),
        TerminationStrategy = new MaximumIterationTerminationStrategy(5)
    }
};
Enter fullscreen mode Exit fullscreen mode

Specialist Router

Route to specialized agents based on intent:

public class AgentRouter
{
    private readonly Dictionary<string, ChatCompletionAgent> _specialists;
    private readonly Kernel _kernel;

    public async Task<ChatCompletionAgent> RouteAsync(string userMessage)
    {
        var classificationPrompt = $"""
            Classify this user message into one category:
            - billing: Payment, invoices, subscriptions
            - technical: Product issues, bugs, how-to
            - sales: Pricing, features, comparisons
            - general: Other inquiries

            Message: {userMessage}

            Category (one word):
            """;

        var category = await _kernel.InvokePromptAsync<string>(classificationPrompt);

        return _specialists.GetValueOrDefault(
            category?.Trim().ToLower() ?? "general",
            _specialists["general"]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Agents

public class AgentTests
{
    [Fact]
    public async Task SupportAgent_LooksUpOrder_WhenAsked()
    {
        // Arrange
        var mockOrderPlugin = new Mock<IOrderPlugin>();
        mockOrderPlugin
            .Setup(p => p.GetOrderAsync("ORD-123", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Order { Id = "ORD-123", Status = "Shipped" });

        var kernel = CreateTestKernel();
        kernel.Plugins.AddFromObject(mockOrderPlugin.Object, "Orders");

        var agent = new ChatCompletionAgent
        {
            Name = "Support",
            Instructions = "Help customers with order inquiries.",
            Kernel = kernel,
            Arguments = new KernelArguments(new OpenAIPromptExecutionSettings
            {
                FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
            })
        };

        // Act
        var history = new ChatHistory();
        history.AddUserMessage("What's the status of order ORD-123?");

        var responses = await agent.InvokeAsync(history).ToListAsync();

        // Assert
        mockOrderPlugin.Verify(p => p.GetOrderAsync("ORD-123", It.IsAny<CancellationToken>()), Times.Once);
        Assert.Contains("Shipped", responses.Last().Content);
    }

    [Fact]
    public async Task AgentGroupChat_CompletesTask_WithTermination()
    {
        // Arrange
        var agents = CreateTestAgents();
        var groupChat = new AgentGroupChat(agents.ToArray())
        {
            ExecutionSettings = new AgentGroupChatSettings
            {
                TerminationStrategy = new MaximumIterationTerminationStrategy(5)
            }
        };

        // Act
        groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "Write a haiku about testing."));
        var messages = await groupChat.InvokeAsync().ToListAsync();

        // Assert
        Assert.NotEmpty(messages);
        Assert.True(messages.Count <= 5);
    }
}
Enter fullscreen mode Exit fullscreen mode

Series Conclusion

Across this five-part series, we've built a complete understanding of Semantic Kernel:

  1. Fundamentals: Kernel architecture, services, functions, and templates
  2. Plugins: Native functions, OpenAPI, and MCP integration
  3. Memory: Vector stores, embeddings, and semantic search
  4. RAG: Production retrieval patterns and evaluation
  5. Agents: Autonomous reasoning with multi-agent orchestration

Semantic Kernel provides the building blocks for sophisticated AI applications. The patterns we've explored—from simple chat completions to multi-agent collaboration—form a foundation you can extend for your specific use cases.

What's Next for You

  • Start small: Build a single-agent assistant with 2-3 plugins
  • Add memory: Give your agent persistent knowledge with RAG
  • Scale up: Experiment with multi-agent patterns for complex workflows
  • Measure: Implement evaluation to track and improve quality
  • Secure: Add filters, approval flows, and monitoring for production

The AI agent landscape is evolving rapidly. Semantic Kernel's abstractions help you stay flexible as new models, protocols, and patterns emerge.

Happy building! 🚀


This concludes our 5-part series on Semantic Kernel.

Top comments (0)