Learnixo

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

C#
// 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

C#
// 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)

C#
// 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

C#
// 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 data

Prompt Template Files

C#
// 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.9 for 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.1 or 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.