Run native code with 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.