Getting Started with Semantic Kernel — Part 2: Kernel Functions & Plugins

Welcome back to our Semantic Kernel series! In Part 1, we built our first Kernel instance and used it to generate text from simple prompts. That was cool, but a truly intelligent system needs to do things, not just talk about them.
Today, we're taking a massive leap forward. We'll explore how to turn our Semantic Kernel from a sophisticated chatbot into an agent that can interact with the real world by calling our C# code. This is where the magic of Kernel Functions and Plugins truly shines, allowing the AI model to perform actions and solve problems in a structured way.
Let's dive in!
Kernel Functions: The Building Blocks of Action
At its core, a Kernel Function is a capability that the Semantic Kernel can invoke. These can be defined in two primary ways: from prompt templates or directly from C# code.
1. Prompt-Based Kernel Functions
First, let's look at how we can define a function purely through a prompt template. This is useful for tasks that are still primarily text-generation but require more structured input and output, or when you want to package a specific prompt for reuse.
We define a prompt template with placeholders (like {{$length}} and {{$topic}}) that act as variables. Then, we use kernel.CreateFunctionFromPrompt to turn this template into an invokable KernelFunction:
var promptTemplate = @"Write a {{$length}} sentence story about {{$topic}}.";
KernelFunction promptFunction = kernel.CreateFunctionFromPrompt(
promptTemplate, settings,
functionName: "StoryGenerator"
);
Notice the settings parameter. This is an OpenAIPromptExecutionSettings object where we can fine-tune how the underlying LLM executes the prompt. For example, we can control creativity with Temperature, limit output length with MaxTokens, or define StopSequences to tell the model when to stop generating text.
var settings = new OpenAIPromptExecutionSettings
{
Temperature = Math.Clamp(0.7f, 0.0f, 1.0f),
TopP = Math.Clamp(1.0f, 0.0f, 1.0f),
MaxTokens = Math.Max(1, 500),
StopSequences = new List<string> { "." },
PresencePenalty = Math.Clamp(0.0f, -2.0f, 2.0f),
FrequencyPenalty = Math.Clamp(0.0f, -2.0f, 2.0f)
};
To invoke this function, we use kernel.InvokeAsync, passing our promptFunction and a KernelArguments object to supply values for our variables:
var promptResult = await kernel.InvokeAsync(promptFunction, new KernelArguments
{
["length"] = "three",
["topic"] = "a brave astronaut"
});
Console.WriteLine(promptResult);
This gives us a simple, reusable way to generate specific types of content, but it's still just generating text.
2. Native C# Kernel Functions
Now, for the really exciting part: exposing your own C# methods as KernelFunctions. This is how the AI model can directly call your application's code to perform actions, interact with databases, call external APIs, or do complex calculations.
To create a native C# function, you simply define a public method in a class and decorate it with the [KernelFunction] attribute. I also highly recommend adding a [Description] attribute, as this provides crucial context to the AI model about what your function does and what its parameters mean. This description is what the model uses to decide when and how to call your function.
Here's an example with a TextAnalysisPlugin that can count words or detect sentiment:
public class TextAnalysisPlugin
{
[KernelFunction]
[Description("Counts the number of words in a given text.")]
public int CountWords([Description("The text to analyze.")] string input)
{
if (string.IsNullOrWhiteSpace(input)) return 0;
int wordCount = input.Split(new[] { ' ', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length;
return wordCount;
}
[KernelFunction]
[Description("Detects the sentiment of a given text.")]
public Task<string> DetectSentiment([Description("The text to analyze.")] string input)
{
if (string.IsNullOrWhiteSpace(input)) return Task.FromResult("Neutral");
string[] positiveWords = { "good", "great", "happy", "awesome", "excellent" };
string[] negativeWords = { "bad", "sad", "terrible", "horrible", "awful" };
int positiveCount = input.Split().Count(word => positiveWords.Contains(word.ToLower()));
int negativeCount = input.Split().Count(word => negativeWords.Contains(word.ToLower()));
string sentiment = positiveCount > negativeCount ? "Positive" :
negativeCount > positiveCount ? "Negative" : "Neutral";
return Task.FromResult(sentiment);
}
}
To make these functions available to the kernel, we ImportPluginFromObject:
var textAnalysisPlugin = kernel.ImportPluginFromObject(new TextAnalysisPlugin());
Once imported, you can invoke these functions explicitly, just like the prompt-based ones:
Console.WriteLine("\n2. Testing typed function - CountWords :");
var typedResult = await kernel.InvokeAsync(textAnalysisPlugin["CountWords"], new KernelArguments
{
["input"] = "On April 12, 1961, Soviet cosmonaut Yuri Gagarin made history..."
});
Console.WriteLine(typedResult);
Console.WriteLine("\n3. Testing typed function - DetectSentiment");
var moralResult = await kernel.InvokeAsync(textAnalysisPlugin["DetectSentiment"], new KernelArguments
{
["input"] = "On April 12, 1961, the Soviet cosmonaut Yuri Gagarin made history..."
});
Console.WriteLine(moralResult);
Plugins and Automatic Function Calling: The AI Takes Control
Here's where Semantic Kernel truly distinguishes itself. Instead of you explicitly telling the kernel which function to call, you can let the AI model figure it out on its own. This is called automatic function calling (or tool calling).
Imagine you have a MathPlugin with Add and Multiply functions:
public class MathPlugin
{
[KernelFunction]
public double Add(double a, double b) => a + b;
[KernelFunction]
public string Multiply([Description("First number")] double a,
[Description("Second number")] double b)
{
return quot;{a} x {b} = {a * b}";
}
}
To enable automatic function calling, you need to register your plugin with kernel.Plugins.AddFromObject and, critically, set ToolCallBehavior.AutoInvokeKernelFunctions in your OpenAIPromptExecutionSettings.
var mathPlugins = new MathPlugin();
kernel.Plugins.AddFromObject(mathPlugins);
OpenAIPromptExecutionSettings settings = new()
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
MaxTokens = null,
Temperature = 1,
ChatSystemPrompt = @"
Assistant is a large language model.
This assistant uses plugins to interact with the software."
};
Now, when you provide a prompt that requires arithmetic, the model, aware of the MathPlugin and its capabilities (thanks to the [Description] attributes and ToolCallBehavior), will automatically generate a request to call the appropriate C# method, execute it, and then use the result to formulate its answer. This is the monumental shift from "the model writes text" to "the model calls my code"!
var Prompt = "If I am 37 years old and my daughter is 7, what is our total combined age multiplied by 2 and the result plus 5";
KernelArguments kernelArgument = new(settings);
var result = await kernel.InvokePromptAsync(Prompt, kernelArgument);
Console.WriteLine(result);
The model will break down the problem: first, it identifies the need to Add the ages, then Multiply the sum by 2, and finally add 5. It orchestrates these calls to your C# methods behind the scenes, giving you the final answer.
Intercepting Function Calls with Filters
Sometimes, you might want to add custom logic around every function invocation, such as logging, telemetry, or security checks. Semantic Kernel provides IFunctionInvocationFilter for this purpose.
You can create a simple filter that implements IFunctionInvocationFilter:
public class MathFilter() : IFunctionInvocationFilter
{
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
{
// Perform some actions before function invocation
Debug.WriteLine("Before function invocation: " + context.Function.Name);
await next(context);
// Perform some actions after function invocation
Debug.WriteLine("After function invocation: " + context.Function.Name);
}
}
Then, add it to your kernel's FunctionInvocationFilters collection:
kernel.FunctionInvocationFilters.Add(new MathFilter());
Now, every time a kernel function (whether prompt-based or native C#) is invoked, your OnFunctionInvocationAsync method will be called, allowing you to hook into the lifecycle of function execution.
Wrapping Up
Today, we've unlocked a new dimension of capability with Microsoft Semantic Kernel, moving from simple text generation to intelligent action. We learned about:
- Kernel Functions defined from prompt templates with
OpenAIPromptExecutionSettingsand dynamicKernelArguments. - Exposing native C# methods as
KernelFunctions using[KernelFunction]and[Description]attributes. - Organizing functions into Plugins and making them available to the kernel with
kernel.ImportPluginFromObject. - The powerful concept of automatic function calling using
ToolCallBehavior.AutoInvokeKernelFunctions, letting the model decide when to execute your C# code. - Intercepting function invocations with
IFunctionInvocationFilter.
This is a fundamental shift, empowering your AI applications to interact with the world through code. In the next part of our series, we'll continue building on this foundation, exploring more advanced plugin scenarios and how to orchestrate complex tasks. You can find all the code for this series in the egarim/IntroductionToSemanticKernel2025 repository.
Stay tuned!