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)