Featured image of post Semantic Kernel - From semantic functions to prompt functions

Semantic Kernel - From semantic functions to prompt functions

Let's review the changes in semantic functions introduced in Semantic Kernel 1.0.

The recent 1.0 release of Semantic Kernel introduced multiple breaking changes, with the goal to align the naming and the features with the AI industry. In the previous post, we focused on the basic scenarios, like setting up the connection to an AI service and executing a basic prompt. In this post, instead, we’ll cover one of the features that was heavily affected by the breaking changes: semantic functions, which are now called prompt functions. In this post, we’re going to review the changes and see how to migrate our existing code to the new version.

# From semantic functions to prompt functions

Due to the rebranding into prompt functions, now we have also a different method to import a semantic function inside the kernel. Instead of using ImportSemanticFunctionsFromDirectory(), we must use a new method called ImportPluginFromPromptDirectory(), as in the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
string apiKey = configuration["AzureOpenAI:ApiKey"];
string deploymentName = configuration["AzureOpenAI:DeploymentName"];
string endpoint = configuration["AzureOpenAI:Endpoint"];

var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey)
    .Build();

var kernel = kernelBuilder.Build();

var pluginsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Plugins", "MailPlugin");

kernel.ImportPluginFromPromptDirectory(pluginsDirectory, "MailPlugin");

Compared to the previous code, there’s an important difference. In the original version, it was enough to pass as parameter the path of the entire Plugins folder (YourProject/Plugins), then the kernel was able to figure out the specific plugin folder to use by leveraging the name of the plugin (MailPlugin, in our case). In the new version, we must pass, instead, the full path of the folder which contains the specific plugin (YourProject/Plugins/MailPlugin, in our case).

# Changing the configuration of the prompt

When you create a prompt function using the classic approach we have learned about in the original post, you have two files inside the folder which represents your plugin: one for the prompt (named skprompt.txt) and one for the configuration (named config.json). The configuration file includes the configuration of the prompt, like the LLM parameters, the description, etc. This is an example of the configuration file we have seen in the original post:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "schema": 1,
  "type": "completion",
  "description": "Write a business mail",
  "completion": {
    "max_tokens": 500,
    "temperature": 0.0,
    "top_p": 0.0,
    "presence_penalty": 0.0,
    "frequency_penalty": 0.0
  },
  "input": {
    "parameters": [
      {
        "name": "input",
        "description": "The text to convert into a business mail.",
        "defaultValue": "",
        "required": true 
      }
    ]
  }
}

Semantic Kernel 1.0 introduced a few minor changes in the way you define the input and output parameters, as in the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "schema": 1,
  "type": "completion",
  "description": "Write a business mail",
  "completion": {
    "max_tokens": 500,
    "temperature": 0.0,
    "top_p": 0.0,
    "presence_penalty": 0.0,
    "frequency_penalty": 0.0
  },
  "input_variables": [
    {
      "name": "input",
      "description": "The text to convert into a business mail.",
      "is_required": true,
      "default": ""
    }
  ]
}

As you can notice, we don’t have anymore the input property with, as a child, the parameters collection. Instead, we have a new property called input_variables, which is a collection of objects, each of them representing a parameter. The defaultValue property has been renamed to default and the required property has been renamed to is_required. Changing the configuration of the prompt to use the new schema is critically important when we use planners and automatic function calling, otherwise they won’t be able to figure out which the input parameter are accepted by the prompt.

# Executing the function

Another important change involves function storage. Functions are no longer stored in the kernel’s Functions collection. Instead, they now reside in the Plugins collection. This means that, to get a reference to a function, we must call the GetFunction() method exposed by the Plugins collection passing, as before, the name of the plugin and the name of the function we want to use.

1
2
3
4
5
6
7
8
var function = kernel.Plugins.GetFunction("MailPlugin", "WriteBusinessMail");

KernelArguments variables = new KernelArguments
{
    { "input", "Tell David that I'm going to finish the business plan by the end of the week." }
};

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

The rest of the changes are the same ones we have learned about in the previous post:

  • ContextVariables has been renamed to KernelArguments.
  • The RunAsync() method has been renamed to InvokeAsync() and we must pass the parameters in the reverse order (first the function, then the variables). If you prefer, you can use also the InvokeStreamingAsync() method, if you prefer to stream the response as it gets generated, instead of waiting for it to be fully available.

# Using YAML to define a prompt function

One of the most relevant new features in 1.0 around prompt functions is the ability to define them using a YAML file. The advantage of this approach, over the traditional one, is that we don’t have anymore to maintain two different files, one for the prompt and one for the configuration, but we can consolidate everything in a single file. Let’s see how it works.

The first step is to add a YAML file in your project. When you use this approach, you don’t have to follow a specific folder structure for your plugin, since we’re going to read the YAML file as a string. To keep a consistent structure, I’ve created a folder called MailPluginYaml inside the Plugins folder and I’ve added a file called WriteBusinessMail.yaml, which is defined like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
name: WriteBusinessMail
template: |
  Rewrite the text between triple backticks into a business mail. Use a professional tone, be clear and concise. Sign the mail as AI Assistant. Text: ```{{$input}}```.  
template_format: semantic-kernel
description: A function that generates a business mail.
input_variables:
  - name: input
    description: The text to be rewritten into a business mail.
    is_required: true
output_variable:
  description: The generated business mail.

The declaration should be easy to understand: with the various properties, we define the key features of the function, like the name, the template, the description, the inputs and the outputs. All these information will be used by Semantic Kernel to determine if the function is a good candidate to solve a specific problem.

Now that we have a prompt function defined using a YAML file, the only difference is the method we must use to load the plugin in the kernel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var writeMailYaml = File.ReadAllText($"{pluginsDirectory}\\MailPluginYaml\\WriteBusinessMail.yaml");
var function = kernel.CreateFunctionFromPromptYaml(writeMailYaml);

KernelArguments variables = new KernelArguments
{
    { "input", "Tell David that I'm going to finish the business plan by the end of the week." }
};

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

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

First, we use File.ReadAllText() to read the content of the YAML file as a string. Then, we pass the string to the CreateFunctionFromPromptYaml() method, which returns a reference to the function. The rest of the code is the same as the previous example: we define the input variables using a KernelArguments collection and we pass it to the InvokeAsync() method, together with the function.

It’s important to highlight that the CreateFunctionFromPromptYaml() method only generates a function out of the YAML definition, but it doesn’t store it into the kernel. This means that you can use this semantic prompt if you explicitly invoke it (like we did in the previous example), but if you want to use a more automated approach (like with the planner), it wouldn’t work.

If you want to store it into the kernel, you have to use the KernelPluginFactory helper to create a KernelPlugin object and then import it using the ImportPluginFromObject() method, as in the following example:

1
2
3
4
5
var writeMailYaml = File.ReadAllText($"{pluginsDirectory}\\MailPluginYaml\\WriteBusinessMail.yaml");
var function = kernel.CreateFunctionFromPromptYaml(writeMailYaml);

var plugin = KernelPluginFactory.CreateFromFunctions("MailPlugin", new[] { function });
kernel.Plugins.Add(plugin);

In our case, we already have a function that we want to embed into a plugin, so we use the CreateFromFunctions() method of the KernePluginFactory class, which accepts as input the name of the plugin and a collection of functions which, in our case, contains only one element.

# Wrapping up

In this post, you have learned how to migrate semantic functions to prompt functions, using the new syntax and the new features included in Semantic Kernel 1.0. You have also learned how to transition from the classic plugins approach, based on a text file with the prompt and a configuration file with the parameters, to a new approach based on a single YAML file.

You will find the updated example in the GitHub repository of the project.

Happy coding!

Built with Hugo
Theme Stack designed by Jimmy