.NET & C# Development · Lesson 169 of 229
Semantic Kernel Deep Dive — Agents, Plugins, and Memory in .NET
Semantic Kernel Deep Dive — Agents, Plugins, and Memory in .NET
Semantic Kernel (SK) is Microsoft's open-source SDK for building AI-powered applications in .NET. It orchestrates LLM calls, tools, memory, and multi-agent workflows with a consistent programming model.
Architecture Overview
Kernel
│
├─ Services (IChatCompletionService, IEmbeddingGenerationService)
│ └─ Providers: OpenAI, Azure OpenAI, Ollama, HuggingFace
│
├─ Plugins (functions the AI can call)
│ ├─ KernelFunction from C# method
│ ├─ KernelFunction from prompt template
│ └─ KernelFunction from OpenAPI spec
│
├─ Memory / Vector Store
│ └─ Qdrant, Redis, Azure AI Search, in-memory
│
└─ Agents
├─ ChatCompletionAgent (single agent)
├─ OpenAIAssistantAgent (OpenAI Assistants API)
└─ AgentGroupChat (multi-agent collaboration)Step 1: Kernel Setup
<PackageReference Include="Microsoft.SemanticKernel" Version="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Core" Version="1.*" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.*" />// Program.cs
builder.Services.AddKernel()
.AddOpenAIChatCompletion(
modelId: "gpt-4o",
apiKey: builder.Configuration["OpenAI:ApiKey"]!)
.AddOpenAITextEmbeddingGeneration(
modelId: "text-embedding-3-small",
apiKey: builder.Configuration["OpenAI:ApiKey"]!);
// Or Azure OpenAI
builder.Services.AddKernel()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: builder.Configuration["AzureOpenAI:Endpoint"]!,
credentials: new DefaultAzureCredential());Step 2: Plugins — Functions the AI Can Call
// Define a plugin as a C# class with KernelFunction methods
public class OrderPlugin(IOrderRepository repo, IInventoryService inventory)
{
[KernelFunction]
[Description("Get the current status and details of a customer order")]
public async Task<string> GetOrderStatusAsync(
[Description("The order ID")] int orderId,
CancellationToken ct = default)
{
var order = await repo.GetByIdAsync(orderId, ct);
if (order is null) return $"Order {orderId} not found.";
return $"Order {orderId}: Status={order.Status}, Total={order.Total:C}, Items={order.Items.Count}";
}
[KernelFunction]
[Description("Cancel an order that is still in Pending status")]
public async Task<string> CancelOrderAsync(
[Description("The order ID to cancel")] int orderId,
[Description("Reason for cancellation")] string reason,
CancellationToken ct = default)
{
var result = await repo.CancelAsync(orderId, reason, ct);
return result ? $"Order {orderId} cancelled." : $"Cannot cancel order {orderId} — it may already be shipped.";
}
[KernelFunction]
[Description("Check if a product is in stock")]
public async Task<string> CheckStockAsync(
[Description("Product ID")] int productId,
[Description("Required quantity")] int quantity,
CancellationToken ct = default)
{
var available = await inventory.CheckAsync(productId, quantity, ct);
return available ? $"Product {productId} has sufficient stock." : $"Product {productId} is out of stock.";
}
}
// Register plugin in DI and add to kernel
builder.Services.AddSingleton<OrderPlugin>();
// In the service that uses the kernel:
public class OrderAssistant(Kernel kernel, OrderPlugin plugin)
{
public Kernel GetConfiguredKernel()
{
kernel.Plugins.AddFromObject(plugin, "Orders");
return kernel;
}
}Step 3: Prompt Templates as Functions
// Prompt template stored as a string or file
const string ClassifyPrompt = """
You are an order classification expert.
Classify the following customer message into one of: [order_status, cancellation, complaint, general]
Customer message: {{$input}}
Respond with ONLY the category name.
""";
// Create a kernel function from the template
var classifyFunction = kernel.CreateFunctionFromPrompt(
ClassifyPrompt,
new OpenAIPromptExecutionSettings { MaxTokens = 20, Temperature = 0 });
// Invoke it
var result = await kernel.InvokeAsync(classifyFunction,
new KernelArguments { ["input"] = "I want to know where my package is" });
Console.WriteLine(result.GetValue<string>()); // "order_status"Step 4: ChatCompletionAgent with Auto Function Calling
// An agent that can use plugins automatically
public class CustomerSupportAgent(Kernel kernel)
{
public async Task<string> HandleAsync(string userMessage, ChatHistory history, CancellationToken ct)
{
var agent = new ChatCompletionAgent
{
Name = "CustomerSupport",
Instructions = """
You are a helpful customer support agent for an e-commerce platform.
Use the available tools to answer questions about orders.
Always confirm actions before executing them.
Be concise and professional.
""",
Kernel = kernel,
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
}),
};
history.AddUserMessage(userMessage);
var responses = new StringBuilder();
await foreach (var content in agent.InvokeAsync(history, cancellationToken: ct))
{
responses.Append(content.Content);
}
var reply = responses.ToString();
history.AddAssistantMessage(reply);
return reply;
}
}Step 5: Multi-Agent Group Chat
// Multiple specialised agents collaborating
var researchAgent = new ChatCompletionAgent
{
Name = "Researcher",
Instructions = "You research and analyse order data. Provide facts only.",
Kernel = kernel,
};
var writerAgent = new ChatCompletionAgent
{
Name = "Writer",
Instructions = "You write clear, customer-friendly summaries based on the researcher's findings.",
Kernel = kernel,
};
// Termination: stop when the writer says "DONE"
var terminationStrategy = new KernelFunctionTerminationStrategy(
kernel.CreateFunctionFromPrompt("Return 'yes' if the last message contains 'DONE', else 'no'"),
kernel)
{
ResultParser = result => result.GetValue<string>()?.Contains("yes", StringComparison.OrdinalIgnoreCase) ?? false,
MaximumIterations = 6,
};
// Selection: alternate between agents
var selectionStrategy = new KernelFunctionSelectionStrategy(
kernel.CreateFunctionFromPrompt("""
Given this history, who should speak next: Researcher or Writer?
Only return the agent name.
History: {{$history}}
"""),
kernel)
{
ResultParser = result => result.GetValue<string>() ?? "Researcher",
};
var groupChat = new AgentGroupChat(researchAgent, writerAgent)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = terminationStrategy,
SelectionStrategy = selectionStrategy,
},
};
groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User,
"Analyse the top 5 orders from last month and write a summary for the CEO."));
await foreach (var response in groupChat.InvokeAsync(ct))
{
Console.WriteLine($"[{response.AuthorName}]: {response.Content}");
}Step 6: Memory and Vector Search
// Add vector store (in-memory for dev, Qdrant for prod)
builder.Services.AddSingleton<IVectorStore, InMemoryVectorStore>();
// Or Qdrant
builder.Services.AddQdrantVectorStore("localhost");
// Define a memory record
public class ProductRecord
{
[VectorStoreRecordKey]
public string Id { get; set; } = "";
[VectorStoreRecordData]
public string Name { get; set; } = "";
[VectorStoreRecordData]
public string Description { get; set; } = "";
[VectorStoreRecordVector(Dimensions: 1536)]
public ReadOnlyMemory<float> Embedding { get; set; }
}
// Store and retrieve
public class ProductMemory(IVectorStore store, ITextEmbeddingGenerationService embedder)
{
public async Task StoreAsync(Product product, CancellationToken ct)
{
var collection = store.GetCollection<string, ProductRecord>("products");
await collection.CreateCollectionIfNotExistsAsync(ct);
var embedding = await embedder.GenerateEmbeddingAsync(
$"{product.Name} {product.Description}", cancellationToken: ct);
await collection.UpsertAsync(new ProductRecord
{
Id = product.Id.ToString(),
Name = product.Name,
Description = product.Description,
Embedding = embedding,
}, cancellationToken: ct);
}
public async Task<List<ProductRecord>> SearchAsync(string query, int topK = 5, CancellationToken ct = default)
{
var collection = store.GetCollection<string, ProductRecord>("products");
var embedding = await embedder.GenerateEmbeddingAsync(query, cancellationToken: ct);
var results = await collection.VectorizedSearchAsync(embedding,
new VectorSearchOptions { Top = topK }, ct);
var records = new List<ProductRecord>();
await foreach (var r in results.Results)
records.Add(r.Record);
return records;
}
}Step 7: Filters — Cross-Cutting Concerns
// Filters intercept function calls — add logging, validation, safety checks
public class SafetyFilter(ILogger<SafetyFilter> logger) : IFunctionInvocationFilter
{
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
logger.LogInformation("SK calling function: {Plugin}.{Function}",
context.Function.PluginName, context.Function.Name);
// Block dangerous operations in certain contexts
if (context.Function.Name == "CancelOrder" &&
context.Arguments["reason"]?.ToString()?.Length < 10)
{
context.Result = new FunctionResult(context.Function, "Cancellation reason is too short.");
return; // don't invoke the actual function
}
await next(context);
logger.LogInformation("SK function result: {Result}", context.Result);
}
}
// Register the filter
builder.Services.AddSingleton<IFunctionInvocationFilter, SafetyFilter>();Interview Answer
"Semantic Kernel is Microsoft's orchestration SDK for AI in .NET. The Kernel is the central object — you add AI services (chat completion, embeddings) and plugins (C# classes with [KernelFunction] methods). The LLM automatically discovers and calls plugin functions when tool calling is enabled. For single-agent workflows: ChatCompletionAgent with ToolCallBehavior.AutoInvokeKernelFunctions handles the loop automatically. For multi-agent: AgentGroupChat coordinates specialised agents with selection and termination strategies. Memory uses IVectorStore — an abstraction over Qdrant, Redis, Azure AI Search — you store embeddings with VectorStoreRecordVector, then search with VectorizedSearchAsync. Filters (IFunctionInvocationFilter) add cross-cutting concerns like logging, safety checks, and rate limiting without modifying plugin code. SK integrates with Microsoft.Extensions.AI at the service level, so you can swap providers in DI without changing any business code."