AI for .NET Developers · Lesson 2 of 6
Semantic Kernel Fundamentals — Plugins and Planners
What Semantic Kernel Provides
Semantic Kernel is Microsoft's SDK for building AI-powered applications in .NET.
It provides:
→ A kernel that orchestrates AI model calls (OpenAI, Azure OpenAI, Ollama)
→ Plugins: groups of functions (native C# or prompt-based) the AI can call
→ Chat history: manages conversation state across multiple turns
→ Function calling: the AI automatically invokes your C# functions
→ Planners: the AI creates and executes multi-step plans
→ Memory / vector search: semantic search over embeddings
Use Semantic Kernel when:
→ Building copilot-style features in a .NET application
→ You need the AI to call application functions (not just generate text)
→ You want to abstract the LLM provider (Azure OpenAI, OpenAI, Ollama)Kernel Setup
// NuGet: Microsoft.SemanticKernel, Microsoft.SemanticKernel.Connectors.AzureOpenAI
using Microsoft.SemanticKernel;
var builder = Kernel.CreateBuilder();
// Azure OpenAI:
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://clinical-openai.openai.azure.com/",
apiKey: configuration["AzureOpenAI:ApiKey"]!);
// Or OpenAI directly:
// builder.AddOpenAIChatCompletion(modelId: "gpt-4o", apiKey: "sk-...");
// Or Ollama for local models:
// builder.AddOllamaChatCompletion(modelId: "llama3.2", endpoint: new Uri("http://localhost:11434"));
// Register DI services (plugins can use IServiceProvider)
builder.Services.AddScoped<IPrescriptionRepository, PrescriptionRepository>();
var kernel = builder.Build();
// Register in ASP.NET Core DI:
builder.Services.AddSingleton(kernel);Prompt Functions
// Inline prompt function — the AI generates text from a template
var summariseFunction = kernel.CreateFunctionFromPrompt(
"""
You are a clinical pharmacist assistant. Summarise the following prescription information
for a ward nurse. Use plain language. Highlight any INR concerns.
Prescription details:
{{$prescriptionDetails}}
Provide a brief summary (2-3 sentences maximum).
""",
new PromptExecutionSettings
{
Temperature = 0.3, // low temperature for clinical context — more deterministic
MaxTokens = 200
});
var result = await kernel.InvokeAsync(summariseFunction, new KernelArguments
{
["prescriptionDetails"] = prescriptionDetails
});
var summary = result.GetValue<string>();Native Functions (Plugins)
// A plugin is a class with methods the AI can call via function calling
// The AI reads the Description attributes to understand when to call each function
public sealed class PrescriptionPlugin
{
private readonly IPrescriptionRepository _repository;
public PrescriptionPlugin(IPrescriptionRepository repository) =>
_repository = repository;
[KernelFunction("get_prescription")]
[Description("Gets a prescription by patient MRN. Returns medication name, dose, and current status.")]
public async Task<string> GetPrescriptionAsync(
[Description("The patient's MRN (Medical Record Number)")] string mrn,
CancellationToken ct = default)
{
var prescription = await _repository.GetByPatientMrnAsync(PatientMrn.Of(mrn), ct);
if (prescription is null) return $"No active prescription found for MRN {mrn}.";
return $"Medication: {prescription.MedicationName.Value}, " +
$"Dose: {prescription.DoseAmount} {prescription.DoseUnit}, " +
$"Status: {prescription.Status}";
}
[KernelFunction("check_inr_history")]
[Description("Returns the last 5 INR values for a patient to assess Warfarin dosing trends.")]
public async Task<string> GetInrHistoryAsync(
[Description("Patient MRN")] string mrn,
CancellationToken ct = default)
{
var history = await _repository.GetInrHistoryAsync(PatientMrn.Of(mrn), 5, ct);
if (!history.Any()) return $"No INR history found for MRN {mrn}.";
var lines = history.Select(h => $"{h.RecordedAt:yyyy-MM-dd}: INR {h.Value}");
return string.Join("\n", lines);
}
}
// Register plugin with the kernel:
kernel.Plugins.AddFromObject(new PrescriptionPlugin(prescriptionRepository), "Prescription");Chat with Function Calling
// A pharmacist copilot that can answer questions by calling your C# functions
public sealed class PharmacistCopilotService
{
private readonly Kernel _kernel;
private readonly IChatCompletionService _chat;
public PharmacistCopilotService(Kernel kernel)
{
_kernel = kernel;
_chat = kernel.GetRequiredService<IChatCompletionService>();
}
public async Task<string> AskAsync(string question, ChatHistory history, CancellationToken ct)
{
history.AddUserMessage(question);
var executionSettings = new AzureOpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
Temperature = 0.2,
MaxTokens = 500
};
var response = await _chat.GetChatMessageContentAsync(
history,
executionSettings,
_kernel,
ct);
history.AddAssistantMessage(response.Content!);
return response.Content!;
}
}
// Usage in an API endpoint:
var copilot = serviceProvider.GetRequiredService<PharmacistCopilotService>();
var history = new ChatHistory(systemMessage:
"You are a pharmacist assistant for a clinical ward system. " +
"You have access to patient prescription data. " +
"Only provide clinically relevant information. " +
"If asked about dosing decisions, always recommend consulting a qualified pharmacist.");
var answer = await copilot.AskAsync(
"What is the current Warfarin dose for patient MRN001 and what was their last INR?",
history, ct);
// The AI automatically calls GetPrescriptionAsync("MRN001") and GetInrHistoryAsync("MRN001")
// then composes an answer from the returned dataPrompt Template Files
// Store prompts in files — easier to manage, version control, and modify
// Plugins/ClinicalSummary/skprompt.txt:
"""
You are a clinical documentation assistant.
Summarise the patient admission for the following clinical data.
Include: primary diagnosis, key medications, ward, and any alerts.
Use concise clinical language suitable for handover documentation.
Patient data:
{{$patientData}}
"""
// Plugins/ClinicalSummary/config.json:
{
"schema": 1,
"description": "Generates a clinical summary for patient handover",
"execution_settings": {
"default": {
"max_tokens": 400,
"temperature": 0.2
}
}
}
// Load from directory:
kernel.Plugins.AddFromPromptDirectory("Plugins");
// Invoke:
var summary = await kernel.InvokeAsync("ClinicalSummary", "Summarise",
new KernelArguments { ["patientData"] = patientDataJson });Production issue I've seen: A team deployed a Semantic Kernel-powered copilot with
Temperature = 0.9for a clinical prescription query feature. The AI was generating plausible-sounding but fabricated prescription details for MRNs that didn't exist in the database. The native function returned "No prescription found" and the AI, with high temperature, would extrapolate: "Based on typical Warfarin therapy, the patient likely receives..." — pure hallucination in a clinical context. Fix:Temperature = 0.1or lower for factual queries; system prompt explicitly instructing "if no data is found, say 'no data found' — do not infer or estimate"; and output validation before displaying to clinical staff.
Key Takeaway
Semantic Kernel orchestrates LLM calls and native C# function calls — the AI decides which functions to call based on descriptions. Define plugins as C# classes with
[KernelFunction]and[Description]attributes. Use low temperature (0.1-0.3) for clinical applications — determinism over creativity. Always include a system message that defines scope and instructs the model to report missing data rather than inferring it. The kernel abstracts the LLM provider — switching between Azure OpenAI, OpenAI, and Ollama requires only a configuration change.