Semantic Kernel Advanced: Plugins, Planners, and Production AI Pipelines
Go beyond the basics with Semantic Kernel. Covers custom plugins, function calling, the Handlebars planner, memory and embeddings, process framework, filters, prompt caching, and production patterns.
What Semantic Kernel Adds Beyond Direct API Calls
The raw Claude/OpenAI SDK is simple: send messages, receive a response. Semantic Kernel adds:
- Plugins — C# functions the AI can discover and call
- Planners — the AI generates a multi-step plan to accomplish a goal
- Memory — vector store integration for context-aware recall
- Process framework — orchestrate multi-step workflows with state
- Filters — middleware for function invocations (logging, auth, rate limiting)
- Prompt templates — reusable, parameterised prompts
Setup
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI
dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
dotnet add package Microsoft.SemanticKernel.Plugins.Core
dotnet add package Microsoft.SemanticKernel.Memory.Qdrant// Program.cs
builder.Services.AddKernel()
.AddAzureOpenAIChatCompletion(
deploymentName: builder.Configuration["AzureOpenAI:Deployment"]!,
endpoint: builder.Configuration["AzureOpenAI:Endpoint"]!,
apiKey: builder.Configuration["AzureOpenAI:ApiKey"]!)
.Plugins.AddFromType<OrderPlugin>()
.Plugins.AddFromType<ProductPlugin>()
.Plugins.AddFromType<CustomerPlugin>();Plugins
Plugins expose C# methods to the AI as callable functions.
public class OrderPlugin
{
private readonly IOrderRepository _orders;
private readonly IMediator _mediator;
public OrderPlugin(IOrderRepository orders, IMediator mediator)
{
_orders = orders;
_mediator = mediator;
}
[KernelFunction, Description("Get order details by ID")]
public async Task<string> GetOrderAsync(
[Description("The order UUID")] string orderId,
CancellationToken ct)
{
if (!Guid.TryParse(orderId, out var id))
return "Invalid order ID.";
var order = await _orders.GetByIdAsync(OrderId.From(id), ct);
if (order is null) return $"Order {orderId} not found.";
return $"Order {order.Id.Value}: {order.Status}, total {order.Total}, " +
$"created {order.CreatedAt:yyyy-MM-dd}";
}
[KernelFunction, Description("List recent orders, optionally filtered by status")]
public async Task<string> ListOrdersAsync(
[Description("Filter by status: Pending, Submitted, Shipped, Cancelled. Empty = all")] string? status,
[Description("Maximum orders to return (1-50)")] int limit = 10,
CancellationToken ct = default)
{
var orders = await _orders.GetRecentAsync(Math.Clamp(limit, 1, 50), ct);
if (!string.IsNullOrEmpty(status))
orders = orders.Where(o => o.Status.ToString() == status).ToList();
if (!orders.Any()) return "No orders found.";
return string.Join("\n", orders.Select(o =>
$"- {o.Id.Value}: {o.Status} | {o.Total} | {o.CreatedAt:yyyy-MM-dd}"));
}
[KernelFunction, Description("Cancel an order. Only works for pending or submitted orders.")]
public async Task<string> CancelOrderAsync(
[Description("Order ID to cancel")] string orderId,
[Description("Reason for cancellation")] string reason,
CancellationToken ct = default)
{
if (!Guid.TryParse(orderId, out var id)) return "Invalid order ID.";
var order = await _orders.GetByIdAsync(OrderId.From(id), ct);
if (order is null) return $"Order {orderId} not found.";
try
{
order.Cancel(reason);
await _unitOfWork.SaveChangesAsync(ct);
return $"Order {orderId} cancelled successfully.";
}
catch (DomainException ex)
{
return $"Cannot cancel: {ex.Message}";
}
}
}Invoking with Auto Function Calling
public class OrderAssistant
{
private readonly Kernel _kernel;
public OrderAssistant(Kernel kernel) => _kernel = kernel;
public async Task<string> ChatAsync(string userMessage, CancellationToken ct)
{
var history = new ChatHistory();
history.AddSystemMessage(
"You are an order management assistant. Use the available tools to help users " +
"check order status, list orders, and cancel orders when requested. " +
"Always confirm before cancelling an order.");
history.AddUserMessage(userMessage);
var settings = new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
MaxTokens = 1000
};
var chat = _kernel.GetRequiredService<IChatCompletionService>();
var result = await chat.GetChatMessageContentAsync(history, settings, _kernel, ct);
return result.Content ?? "I couldn't process that request.";
}
}
// Usage
var assistant = serviceProvider.GetRequiredService<OrderAssistant>();
var response = await assistant.ChatAsync(
"What are my last 5 orders and what's the status of order abc-123?");
// AI automatically calls ListOrdersAsync and GetOrderAsync, then synthesises the answerPrompt Templates
// Define a reusable prompt template
var orderSummaryPrompt = """
You are a helpful assistant. The user wants a summary of their orders.
Order data:
{{$orderData}}
Provide a concise summary including:
- Total number of orders
- Total spend
- Most recent order
- Any orders that need attention (e.g., stuck in Pending for >24 hours)
""";
var function = _kernel.CreateFunctionFromPrompt(
orderSummaryPrompt,
new OpenAIPromptExecutionSettings { MaxTokens = 500 });
var result = await _kernel.InvokeAsync(function, new KernelArguments
{
["orderData"] = JsonSerializer.Serialize(orders)
});Filters (Middleware for Functions)
// Log all function invocations
public class FunctionLoggingFilter : IFunctionInvocationFilter
{
private readonly ILogger<FunctionLoggingFilter> _logger;
public FunctionLoggingFilter(ILogger<FunctionLoggingFilter> logger) => _logger = logger;
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
_logger.LogInformation(
"AI invoking function {Plugin}.{Function}",
context.Function.PluginName,
context.Function.Name);
var sw = Stopwatch.StartNew();
try
{
await next(context);
_logger.LogInformation(
"Function {Function} completed in {Elapsed}ms",
context.Function.Name, sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "Function {Function} failed", context.Function.Name);
throw;
}
}
}
// Prevent destructive operations without confirmation
public class ConfirmationFilter : IFunctionInvocationFilter
{
private static readonly HashSet<string> DestructiveFunctions = ["CancelOrder", "DeleteOrder"];
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
if (DestructiveFunctions.Contains(context.Function.Name))
{
// Add context so AI asks for confirmation
context.Arguments["requiresConfirmation"] = "true";
}
await next(context);
}
}
// Register
builder.Services.AddKernel()
.Services.AddSingleton<IFunctionInvocationFilter, FunctionLoggingFilter>()
.Services.AddSingleton<IFunctionInvocationFilter, ConfirmationFilter>();Memory and Embeddings
builder.Services.AddSingleton<ISemanticTextMemory>(sp =>
{
var embeddingGen = sp.GetRequiredService<ITextEmbeddingGenerationService>();
var memoryStore = new QdrantMemoryStore("http://localhost:6333", vectorSize: 1536);
return new SemanticTextMemory(memoryStore, embeddingGen);
});
// Save to memory
public class KnowledgeBaseService
{
private readonly ISemanticTextMemory _memory;
public async Task IndexOrderAsync(Order order, CancellationToken ct)
{
await _memory.SaveInformationAsync(
collection: "orders",
id: order.Id.Value.ToString(),
text: $"Order {order.Id.Value}: {order.Status}, " +
$"customer {order.CustomerId.Value}, " +
$"total {order.Total.Amount} {order.Total.Currency}, " +
$"created {order.CreatedAt:yyyy-MM-dd}",
cancellationToken: ct);
}
// Semantic search
public async Task<string> SearchOrdersAsync(string query, CancellationToken ct)
{
var results = _memory.SearchAsync("orders", query, limit: 5, cancellationToken: ct);
var matches = new List<string>();
await foreach (var result in results)
matches.Add($"{result.Metadata.Id}: {result.Metadata.Text} (score: {result.Relevance:F2})");
return matches.Count == 0
? "No relevant orders found."
: string.Join("\n", matches);
}
}Multi-Turn Conversation with History Management
public class ConversationService
{
private readonly Kernel _kernel;
private readonly Dictionary<string, ChatHistory> _sessions = new();
public async Task<string> ChatAsync(string sessionId, string message, CancellationToken ct)
{
if (!_sessions.TryGetValue(sessionId, out var history))
{
history = new ChatHistory();
history.AddSystemMessage("You are an order management assistant...");
_sessions[sessionId] = history;
}
history.AddUserMessage(message);
// Trim history to avoid token limits (keep last 20 messages)
if (history.Count > 22)
history.RemoveRange(1, history.Count - 22); // keep system message
var result = await _kernel
.GetRequiredService<IChatCompletionService>()
.GetChatMessageContentAsync(history,
new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
},
_kernel, ct);
history.AddAssistantMessage(result.Content ?? "");
return result.Content ?? "";
}
}Interview Questions
Q: What is a Semantic Kernel plugin and how does it differ from a raw function call? A plugin wraps C# methods with descriptions and parameter metadata so the AI can discover and invoke them automatically. The AI decides which functions to call and in what order based on the user's intent. With raw function calling, you manually provide the tool schema; SK generates it from method attributes.
Q: What is the purpose of filters in Semantic Kernel? Middleware that runs around function invocations — equivalent to ASP.NET Core middleware but for AI function calls. Use cases: logging all AI-initiated function calls, rate limiting expensive operations, requiring confirmation before destructive actions, injecting security context.
Q: How do you prevent a Semantic Kernel agent from making unauthorized API calls? Use function invocation filters to check permissions before executing. Limit what plugins are registered (principle of least privilege). Validate and sanitise all AI-provided parameters. Log all function calls for audit. For destructive operations, require explicit confirmation in the prompt or filter.
Q: What is semantic memory and when do you use it? Persistent vector storage for the AI to recall relevant context. When a user asks "show me orders similar to last month's issues", you embed the query, search the vector store for semantically similar order records, and feed the results as context. Use it when the context is too large for the context window or when you need cross-session recall.
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.