Run native code with Semantic Kernel

pink circles of semantic kernel

In the how to create semantic functions section, we showed how you could create a semantic function that retrieves a user's intent, but what do you do once you have the intent? In most cases, you want to perform some sort of task based on the intent. For example, if the user wants to send an email, you'll need to make the necessary API calls to actually send an email.

Automating tasks like these are the primary purpose of AI apps. In this section, we'll show how you can create a simple native function that can perform a task LLMs cannot do easily on their own: arithmetic. In a subsequent tutorial we'll demonstrate how to combine native functions with semantic functions to correctly answer word problems like What is the square root of 634? and What is 42 plus 1513?

If you want to see the final solution to this article, you can check out the following samples in the public documentation repository. Use the link to the previous solution if you want to follow along.

Language Link to previous solution Link to final solution
C# Open solution in GitHub Open solution in GitHub
Python Open solution in GitHub Open solution in GitHub

Why use native functions?

Large language models are great at generating text, but there are several tasks they cannot perform on their own. These include, but are not limited to:

  • Retrieve data from external data sources
  • Knowing what time it is
  • Performing complex math
  • Completing tasks in the real world
  • Memorizing and recalling information

Augmenting large language models with native functions

Thankfully, these tasks can already be completed by computers using native code. With native functions, you can author these features as functions that can later be called by the kernel. This allows you to combine the power of large language models with the power of native code.

For example, if you simply asked a large language model What is the square root of 634?, it would likely return back a number that is close to the square root of 634, but not the exact answer. This is because large language models are trained to predict the next word in a sequence, not to perform math.

Giving the kernel the ability to perform math

To solve this problem, we'll demonstrate how to create native functions that can perform arithmetic based on a user's intent. At the end of this section you will have the following supported functions.

Plugin Function Type Description
Orchestrator Plugin GetIntent Semantic Gets the intent of the user
Orchestrator Plugin GetNumbers Semantic Gets the numbers from a user's request
Orchestrator Plugin RouteRequest Native Routes the request to the appropriate function
Math Plugin Sqrt Native Takes the square root of a number
Math Plugin Multiple Native Multiplies two numbers together

In this article, we'll start with a simple example by demonstrating how to create a Sqrt function. In the Using multiple inputs and outputs article, we'll then show how to create functions that require multiple inputs (like the Multiply function). Finally, in the Calling nested functions article, we'll show how to create the RouteRequest and GetNumbers functions which combine native and semantic functions together.

Finding a home for your native functions

You can place native functions in the same plugin directory as your semantic functions. For example, to create native functions for a plugin called MyPlugin, you can create a new file called MyPlugin.cs in the same directory as your semantic functions.

MyPluginsDirectory
│
└─── MyPlugin
     │
     └─── MyFirstSemanticFunction
     │    └─── skprompt.txt
     │    └─── config.json
     └─── MyOtherSemanticFunctions
     |    | ...  
     │
     └─── MyPlugin.cs

Tip

It's ok if you have a plugin folder with both native and semantic functions. The kernel will load both functions into the same plugin namespace. What's important is that you don't have two functions with the same name within the same plugin namespace. If you do, the last function loaded will take precedence.

Creating the folder for the Math plugin

Since we're giving our kernel the ability to perform math, we'll create a new plugin called MathPlugin. To do this, we'll create a MathPlugin folder along with a file to store all its native functions. Depending on the language you're using, you'll create either a C# or Python file.

Plugins
│
└─── OrchestratorPlugin
|    │
|    └─── GetIntent
|         └─── skprompt.txt
|         └─── config.json
|
└─── MathPlugin
     │
     └─── Math.cs

Creating your native functions

Open up the Math.cs or Math.py file you created earlier and follow the instructions below to create the Sqrt function. This function will take a single number as an input and return the square root of that number.

Defining the class for your plugin

All native functions must be defined as public methods of a class that represents your plugin. To begin, create a class called Math in your Math.cs or Math.py file.

using System.ComponentModel;
using System.Globalization;
using Microsoft.SemanticKernel;

namespace Plugins.MathPlugin;

public class Math
{
}

Use the SKFunction decorator to define a native function

Now that you have a class for your plugin, you can add the Sqrt function. To make sure Semantic Kernel knows this is a native function, use the SKFunction decorator above your new method. This decorator will tell the kernel that this method is a native function and will automatically register it with the kernel when the plugin is loaded.

using System.ComponentModel;
using System.Globalization;
using Microsoft.SemanticKernel;

namespace Plugins.MathPlugin;

public class Math
{
    [SKFunction, Description("Take the square root of a number")]
    public double Sqrt([Description("The number to take a square root of")] double input)
    {
        return System.Math.Sqrt(input);
    }
}

Notice how we've added a description to the function and each of its parameters with the Description attribute. This description will be used in the future by the planner to automatically create a plan using these functions. In our case, we're telling planner that this function can Take the square root of a number.

Running your native function

Now that you've created your first native function, you can import it and run it using the following code. Notice how calling a native function is the same as calling a semantic function. This is one of the benefits of using the kernel, both semantic and native functions are treated identically.

using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;

IKernel kernel = new KernelBuilder()
    // Add a text or chat completion service using either:
    // .WithAzureTextCompletionService()
    // .WithAzureChatCompletionService()
    // .WithOpenAITextCompletionService()
    // .WithOpenAIChatCompletionService()
    .Build();

// Import the Math Plugin
var mathPlugin = kernel.ImportFunctions(new Plugins.MathPlugin.Math(), "MathPlugin");

// Make a request that runs the Sqrt function
var result = await kernel.RunAsync("12", mathPlugin["Sqrt"]);
Console.WriteLine(result.GetValue<double>());

The code should output 3.4641016151377544 since it's the square root of 12.

Take the next step

Now that you can create a simple native function, you can now learn how to create native functions that accept multiple input parameters. This will be helpful to create functions like addition, multiplication, subtraction, and division which all require multiple inputs.