DEV Community

Cover image for Building Production-Ready AI Agents with Semantic Kernel
Sebastian Van Rooyen
Sebastian Van Rooyen Subscriber

Posted on

Building Production-Ready AI Agents with Semantic Kernel

If you’re looking to build a production-ready AI agent with Semantic Kernel, I can help. Let’s chat.

Semantic Kernel is one of the most pragmatic ways to build agentic AI in the .NET world. It gives you a first-class abstraction for prompts, tools/functions, memory and orchestration — without tying you to a single monolithic UI or brittle glue code. In this post I’ll explain why it’s powerful, compare it to other approaches, and show a clean, production-ready .NET architecture that follows SOLID principles, separation of concerns, and demonstrates how to wire a WhatsApp webhook → agent flow.

TL;DR: Semantic Kernel = prompts + tool calling + memory + orchestration. Combine that with a properly layered .NET app and you get maintainable, testable, deployable agents.


Why Semantic Kernel? (short & practical)

  • First-class function/tool calling — expose C# functions to the model as “tools” and let the model call them. That makes grounding, accuracy and deterministic side effects easier.
  • Memory primitives — store short/long term memory pieces and retrieve them for context.
  • Composable prompts / semantic functions — encapsulate prompts with parameters and reuse them.
  • Language-model agnostic — works with OpenAI, Azure OpenAI, and others (via connectors).
  • Designed for agents — not just single-prompt Q&A; it supports planners and multi-step workflows.

Compared to other frameworks:

  • Plain SDK usage (raw OpenAI SDK): great for one-off prompts but you must build your own tooling, prompt management and tool integration.
  • RAG libraries (LangChain, LlamaIndex): great for retrieval and pipelines — Semantic Kernel provides more opinionated tooling for function calling and memory especially tailored for .NET.
  • Full agent platforms (closed SaaS): those can be easier for non-devs but often lock you in. SK keeps you in code and composable.

Project structure (what we’ll build)

NexfluenceAgent/
 ├─ src/
 │  ├─ Nexfluence.Api/            # Web API (webhook)
 │  ├─ Nexfluence.Core/           # Domain models & interfaces
 │  ├─ Nexfluence.Services/       # Agent service, prompt service
 │  └─ Nexfluence.Plugins/        # Business plugin (catalog, FAQs)
 ├─ tests/
 └─ dockerfile
Enter fullscreen mode Exit fullscreen mode

We’ll show key parts: DI, controller, service, plugin. All code is idiomatic .NET 8 and uses async.


SOLID in practice — design goals

  • Single Responsibility: each class has one reason to change (controller: HTTP; service: agent orchestration; plugin: business facts).
  • Open/Closed: add new tools/skills via plugins without editing core logic.
  • Liskov: services implement interfaces and can be replaced by mocks in tests.
  • Interface Segregation: small focused interfaces (IAgentService, IPromptBuilder).
  • Dependency Inversion: high level modules depend on abstractions (interfaces), not concrete SK types.

Key code snippets

Note: these are compacted for clarity — treat them as copy-pasteable starting points.

Program.cs — wiring DI, Kernel, resilience, and web API

// Program.cs (minimal)
using Microsoft.SemanticKernel;
using Microsoft.Extensions.Caching.Memory;
using Nexfluence.Core;
using Nexfluence.Services;
using Nexfluence.Plugins;

var builder = WebApplication.CreateBuilder(args);

// Configuration & secrets
var openAiKey = builder.Configuration["OpenAI:ApiKey"] ?? throw new InvalidOperationException("Missing OpenAI key");

// 1) Create Kernel (DI-friendly)
var kernel = Kernel.Builder
    .WithOpenAIChatCompletionService(modelId: "gpt-4o-mini", apiKey: openAiKey)
    .Build();

kernel.Plugins.AddFromType<BusinessPlugin>("business");

builder.Services.AddSingleton<IKernel>(kernel);

// 2) App services (DI)
builder.Services.AddMemoryCache();
builder.Services.AddScoped<IAgentService, SemanticKernelAgentService>();
builder.Services.AddScoped<IPromptBuilder, PromptBuilder>();

// 3) Observability & resiliency recommendations
// - Use ILogger<T> everywhere (provided by builder)
// - Consider Polly policies for outbound requests (e.g. OpenAI, Twilio)
// - Add health checks / metrics

builder.Services.AddControllers();
var app = builder.Build();

app.MapControllers();
app.Run();
Enter fullscreen mode Exit fullscreen mode

Domain interfaces (Nexfluence.Core)

// IAgentService.cs
public interface IAgentService
{
    Task<string> HandleMessageAsync(string sessionId, string userMessage, CancellationToken ct = default);
}

// IPromptBuilder.cs
public interface IPromptBuilder
{
    string BuildSystemPrompt();
    string BuildUserPrompt(string userMessage);
}
Enter fullscreen mode Exit fullscreen mode

Agent service — orchestrates SK calls (Single Responsibility + DIP)

// SemanticKernelAgentService.cs
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;

public class SemanticKernelAgentService : IAgentService
{
    private readonly IKernel _kernel;
    private readonly IPromptBuilder _promptBuilder;
    private readonly ILogger<SemanticKernelAgentService> _log;
    private readonly IMemoryCache _cache;

    public SemanticKernelAgentService(IKernel kernel, IPromptBuilder promptBuilder,
        ILogger<SemanticKernelAgentService> log, IMemoryCache cache)
    {
        _kernel = kernel;
        _promptBuilder = promptBuilder;
        _log = log;
        _cache = cache;
    }

    public async Task<string> HandleMessageAsync(string sessionId, string userMessage, CancellationToken ct = default)
    {
        // Simple per-session memory example (could be replaced with persistent store)
        var memoryKey = $"session:{sessionId}:history";
        var history = _cache.GetOrCreate(memoryKey, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromMinutes(30);
            return new List<string>();
        });

        history.Add(userMessage);
        _cache.Set(memoryKey, history);

        // Build system + user prompt
        var system = _promptBuilder.BuildSystemPrompt();
        var userPrompt = _promptBuilder.BuildUserPrompt(userMessage);

        // Create semantic function (encapsulated prompt)
        var prompt = $"{system}\n\nUser: {userPrompt}";

        // Use the Kernel chat completion service
        var chat = _kernel.GetService<IChatCompletion>();
        var result = await chat.GetChatMessageAsync(prompt, ct: ct);

        // If SK supports tool/function calling, prefer that path by configuring execution settings
        var reply = result?.Content ?? "Sorry, I couldn't process that right now.";
        _log.LogInformation("Agent reply: {Reply}", reply);

        // Store assistant reply in history
        history.Add(reply);
        _cache.Set(memoryKey, history);

        return reply;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: GetChatMessageAsync is illustrative — adapt to the exact SK API in your version (e.g., IChatCompletionService.GetChatMessageContentAsync with ChatHistory if available).

Prompt builder — interface segregation & single responsibility

// PromptBuilder.cs
public class PromptBuilder : IPromptBuilder
{
    public string BuildSystemPrompt() =>
        "You are Nexfluence's WhatsApp assistant. Be helpful, concise, and prefer business plugin for factual answers.";

    public string BuildUserPrompt(string userMessage) =>
        userMessage.Trim();
}
Enter fullscreen mode Exit fullscreen mode

Business plugin (expose facts as functions) — Open/Closed: add more functions without changing core

// BusinessPlugin.cs
using Microsoft.SemanticKernel;
using System.ComponentModel;

public class BusinessPlugin
{
    private static readonly List<Product> Catalog = new()
    {
        new("Blue Hoodie","S",29.99m), new("Blue Hoodie","M",29.99m), new("Black T-Shirt","M",14.99m),
    };

    [KernelFunction("list_products")]
    [Description("List product names.")]
    public Task<List<string>> ListProductsAsync()
        => Task.FromResult(Catalog.Select(p => p.Name).Distinct().ToList());

    [KernelFunction("get_price")]
    [Description("Get price by name and size.")]
    public Task<decimal?> GetPriceAsync(string name, string size)
    {
        var item = Catalog.FirstOrDefault(p => 
            p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
            p.Size.Equals(size, StringComparison.OrdinalIgnoreCase));
        return Task.FromResult(item?.Price);
    }

    public record Product(string Name, string Size, decimal Price);
}
Enter fullscreen mode Exit fullscreen mode

Controller — contract, validation, and Twilio-compatible webhook (SRP + testable)

// WhatsAppWebhookController.cs
using Microsoft.AspNetCore.Mvc;
using Twilio.TwiML;
using Nexfluence.Core;

[ApiController]
[Route("api/[controller]")]
public class WhatsAppWebhookController : ControllerBase
{
    private readonly IAgentService _agentService;
    private readonly ILogger<WhatsAppWebhookController> _log;

    public WhatsAppWebhookController(IAgentService agentService, ILogger<WhatsAppWebhookController> log)
    {
        _agentService = agentService;
        _log = log;
    }

    [HttpPost]
    public async Task<IActionResult> Receive([FromForm] string Body, [FromForm] string From, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(Body) || string.IsNullOrWhiteSpace(From))
            return BadRequest(new { error = "Body and From are required" });

        var sessionId = From; // simple session mapping

        var reply = await _agentService.HandleMessageAsync(sessionId, Body, ct);

        var twiml = new MessagingResponse();
        twiml.Message(reply);

        return Content(twiml.ToString(), "application/xml");
    }
}
Enter fullscreen mode Exit fullscreen mode

Production readiness checklist

Below are recommended (and mostly simple) steps to make this production-ready:

  1. Secure secrets
  • Use managed secret stores: Azure Key Vault / AWS Secrets Manager. Don’t store API keys in appsettings.json in prod.
  1. Request validation & input sanitization
  • Validate incoming webhook (Twilio signature validation). Reject unknown callers.
  1. Resiliency
  • Wrap outbound calls to language models with Polly policies: retry + exponential backoff + circuit breaker.
  1. Observability
  • Use ILogger everywhere. Add structured logging. Add metrics (Prometheus, App Insights).
  • Log prompts and responses selectively (avoid logging PII).
  1. Rate limiting & quotas
  • Prevent abuse: per-session and per-phone number throttling.
  1. Cost control
  • Limit token usage per user, cache frequent answers, and use shorter models for basic FAQs, switching to larger ones only when needed.
  1. Testing
  • Unit test IAgentService by mocking IKernel or by using a test double.
  • Integration tests for the plugin functions and webhook endpoints.
  1. CI/CD & containerization
  • Dockerfile + GitHub Actions / Azure Pipelines.
  • Deploy behind an API gateway; use HTTPS and validate TLS.
  1. Data persistence
  • Replace IMemoryCache session store with durable storage (Redis, Cosmos DB, Postgres) if you want long-lived memory across restarts.
  1. Privacy & Compliance
* Add consent flows if you store customer data. Provide data deletion APIs.
Enter fullscreen mode Exit fullscreen mode

Example: Twilio signature validation (security)

// Validate Twilio signature before processing webhook (pseudo)
public bool ValidateTwilioRequest(HttpRequest request, string twilioAuthToken)
{
    var signature = request.Headers["X-Twilio-Signature"].FirstOrDefault();
    var url = $"{request.Scheme}://{request.Host}{request.Path}";
    var form = request.HasFormContentType ? request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()) : new Dictionary<string,string>();
    return Twilio.Security.RequestValidator.Validate(url, form, signature, twilioAuthToken);
}
Enter fullscreen mode Exit fullscreen mode

Small note on costs & model selection

For FAQ and short replies use cheaper models or instruction-tuned smaller models. For multi-step planners or creative tasks, escalate to a larger model. You can implement model routing in IAgentService based on intent.


Final words — how to extend

  • Add cart and order plugins (tools that perform DB writes and trigger downstream workflows).
  • Add a human handoff skill (if confidence low, forward to human agent).
  • Add analytics: conversation funnels, deflection rate, average response time.

Summary

Semantic Kernel + clean .NET architecture gives you:

  • composable AI agents,
  • clear separation of concerns,
  • testable services,
  • and a path to production with standard engineering practices.

Top comments (0)