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
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();
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); }
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; } }
Note:
GetChatMessageAsync
is illustrative — adapt to the exact SK API in your version (e.g.,IChatCompletionService.GetChatMessageContentAsync
withChatHistory
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(); }
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); }
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"); } }
Production readiness checklist
Below are recommended (and mostly simple) steps to make this production-ready:
- Secure secrets
- Use managed secret stores: Azure Key Vault / AWS Secrets Manager. Don’t store API keys in appsettings.json in prod.
- Request validation & input sanitization
- Validate incoming webhook (Twilio signature validation). Reject unknown callers.
- Resiliency
- Wrap outbound calls to language models with Polly policies: retry + exponential backoff + circuit breaker.
- Observability
- Use ILogger everywhere. Add structured logging. Add metrics (Prometheus, App Insights).
- Log prompts and responses selectively (avoid logging PII).
- Rate limiting & quotas
- Prevent abuse: per-session and per-phone number throttling.
- Cost control
- Limit token usage per user, cache frequent answers, and use shorter models for basic FAQs, switching to larger ones only when needed.
- Testing
- Unit test
IAgentService
by mockingIKernel
or by using a test double. - Integration tests for the plugin functions and webhook endpoints.
- CI/CD & containerization
- Dockerfile + GitHub Actions / Azure Pipelines.
- Deploy behind an API gateway; use HTTPS and validate TLS.
- Data persistence
- Replace
IMemoryCache
session store with durable storage (Redis, Cosmos DB, Postgres) if you want long-lived memory across restarts.
- Privacy & Compliance
* Add consent flows if you store customer data. Provide data deletion APIs.
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); }
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)