Featured image of post Semantic Kernel - Native plugins

Semantic Kernel - Native plugins

In this post, we're going to explore how you can build native plugins for Semantic Kernel

Updated on 17th November 2023 to reflect the latest changes in the Semantic Kernel APIs:

  • All the Azure OpenAI extension methods have been renamed from Azure to AzureOpenAI. For example, WithAzureChatCompletionService() is now WithAzureOpenAIChatCompletionService().

In the previous post, we learned how to create a semantic function plugin for Semantic Kernel. A semantic function is just a prompt, however, by including it in a function inside a plugin, we made it easier to reuse and test it.

In this post, we’re going to explore another type of plugins: native functions. As the name suggests, these plugins are native to the platform you’re using, so they can be written in C#. Python or Java. Since they support native code, they can perform more than just executing a prompt; we can virtually execute any operation which is supported by the platform.

Creating a native plugin

For this sample, we’re going to build a plugin which is able to retrieve the information about the United States population in a given year. This might be a peculiar scenario, but there’s a reason why we’re using it: a platform called DataUSA provides a set of free APIs that we can use to retrieve various data about the United States, like the population number, the number of universities, etc. The APIs are free to use and they don’t require any authentication, so they’re perfect for our sample.

These APIs behave like any other REST APIs. As such, this is the type of operation that requires using native APIs: we need to perform a GET request to a given URL, then we need to parse the response and extract the information we need. We can’t perform this operation just with a prompt.

The first step to create the native function is the same as we have seen in the previous post: we create a folder, called Plugins and, inside it, we create a subfolder for our plugin. We’re going to call it UnitedStatesPlugin. Inside this folder, we’re going to add a new class, which will host our function, called UnitedStatesPlugin.cs. This is how the project looks like:

A project with a plugin with a native function

Inside the class, we can define one or more functions, which will be exposed as Semantic Kernel functions. From a code perspective, these functions are just normal C# functions, so we can use any C# feature we want. The only requirement is that we must decorate the function with the [SKFunction] attribute, which is defined in the Microsoft.SemanticKernel namespace. This way, Semantic Kernel will know that the plugin exposes a function, whose name will match the name of the method. Let’s take a look at the following sample:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class UnitedStatesPlugin
{
    [SKFunction, Description("Get the United States population for a specific year")]
    public async Task<string> GetPopulation([Description("The year")]int year)
    {
        string request = "https://datausa.io/api/data?drilldowns=Nation&measures=Population";
        HttpClient client = new HttpClient();
        var result = await client.GetFromJsonAsync<UnitedStatesResult>(request);
        var populationData = result.data.FirstOrDefault(x => x.Year == year.ToString());
        string response = $"The population number in the United States in {year} was {populationData.Population}";
        return response;
    }
}

As you can see, the function is just a normal C# method, which returns a string (using a Task, since the method is asynchronous). The only difference is that it’s decorated with the SKFunction attribute. This attribute accepts a parameter, which is the description of the function. We have also added a [Description] attribute also to the year parameter. These two attributes replace the information that, in a semantic function, we were storing in the config.json file. We’ll come back to the importance of providing this information when we’re going to introduce the planner.

The rest of the code is easy to understand: we use the HttpClient class to perform a GET request to the DataUSA API. By using the GetFromJsonAsync<T>() method, we can deserialize the response into a C# class, which maps the content of the JSON response (you can see it here. Finally, we filter the resulting collection to extract only the data for the year we’re interested in and we return the result.

Testing the plugin

Now that we have created the plugin, we can test it. To do so, we need to add it to the Semantic Kernel configuration. First, we create a new instance of the kernel as usual:

1
2
3
4
5
6
7
8
9
string apiKey = "my-api-key";
string deploymentName = "deployment-name";
string endpoint = "endpoint-url";

var kernelBuilder = new KernelBuilder();
kernelBuilder.
    WithAzureOpenAIChatCompletionService(deploymentName, endpoint, apiKey);

var kernel = kernelBuilder.Build();

Then, we can add the plugin to the kernel, this time using the ImportFunctions() method:

1
kernel.ImportFunctions(new UnitedStatesPlugin(), "UnitedStatesPlugin");

As parameters, we supply first a new instance of the class we have previously created, then we provide a name for the plugin.

Finally, we can retrieve the function defined in the plugin in the same way we did for the semantic function plugin:

1
var function = kernel.Functions.GetFunction("UnitedStatesPlugin", "GetPopulation");

The first parameter is the name of the plugin, the second one is the name of the method declared in the UnitedStatesPlugin class.

Finally, we execute the plugin in the usual way: we create a ContextVariables collection with the input, then we call RunAsync() on the kernel passing the collection and the function as parameter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ContextVariables variables = new ContextVariables
{
    { "input", "2015" }
};

var result = await kernel.RunAsync(
    variables,
    function
);

Console.WriteLine(result.GetValue<string>());
Console.ReadLine();

In this case, we are supplying as input 2015, so we’re expecting to get back the population number of United States in 2015. If we did everything correct, this is exactly the information we’re going to get back:

1
The population number in the United States in 2015 was 316515021

Wrapping up

In this post, we have seen another type of plugins: native ones. Compared to the semantic functions, they are slightly more complex to define, since we need to create a real class with real code. On the other side, however, they are more powerful, since they can perform any operation we want, not just executing a prompt.

In the next post, we’re going to explore the last type of plugins: OpenAI plugins. Stay tuned!

In the meantime, you can find the full source code of the sample on GitHub, in the SemanticKernel.NativeFunction project.

Built with Hugo
Theme Stack designed by Jimmy