Python Kernel Updates: A Closer Look at Our Redesigned Plugin and Function Integration

Evan Mattson

Eduard van Valkenburg

As we approach a stable v1.0 version of the Python Semantic Kernel SDK, we analysed the methods used to add plugins and functions to the kernel. We realised that the variety of available methods might confuse developers. For instance, when should one use import_plugin_from_object() versus import_native_plugin_from_directory()? Or create_function_from_prompt() versus register_function_from_method()? Our goal has always been to keep the SDK as Pythonic and simple as possible, and the complexity of these interactions was not aligning with that principle. This realisation prompted a major simplification in how we handle plugins and functions.

As of our beta package version 0.9.6b1, you can now simply use one of the following: kernel.add_plugin(), kernel.add_plugins(), kernel.add_function(), or kernel.add_functions() and for the really special cases kernel.add_function_from_openai and kernel.add_function_from_openapi.

Adding Plugins to the Kernel

There are four ways to add a plugin:

  • Directly as a KernelPlugin instance—other parameters will be ignored.
  • As a custom class with methods decorated by kernel_function.
  • As a dictionary where one or more methods are decorated with kernel_function.
  • From a directory, with parent_directory and plugin_name.

The add_plugin method always returns the plugin in case you want to store that or use it.

To demonstrate, here’s how you can add a plugin to the kernel. First, let’s create a sample plugin:

class LightPlugin(KernelBaseModel):
    is_on: bool = False
    @kernel_function(
        name="get_state",
        description="Gets the state of the light.",
    )
    def get_state(
        self,
    ) -> Annotated[str, "the output is a string"]:
        """Returns the state result of the light."""
        return "On" if self.is_on else "Off"
    @kernel_function(
        name="change_state",
        description="Changes the state of the light.",
    )
    def change_state(
        self,
        new_state: Annotated[bool, "the new state of the light"],
    ) -> Annotated[str, "the output is a string"]:
        """Changes the state of the light."""
        self.is_on = new_state
        state = self.get_state()
        print(f"The light is now: {state}")
        return state
Next, add the plugin to the kernel:
light_plugin = kernel.add_plugin(
        plugin=LightPlugin(),
        plugin_name="LightPlugin",
    )
From Directory
To load a plugin from a directory, you can use:
kernel.add_plugin(parent_directory=cur_dir, plugin_name="email_plugin")

The behaviour of this function is now also different then what it was (when it was called kernel.import_plugin_from_directory), the following now happens:

  • We loop through the files and folders inside of the directory specified, which is parsed to parent_directory/plugin_name, we do not go deeper then the first level.
    • If it finds a directory, it looks for two files, named ‘config.json’ and ‘skprompt.txt’, this is then loaded as a KernelFunctionFromPrompt.
    • If it finds a .yaml or .yml file, it loads those as yaml prompts to a KernelFunctionFromPrompt as well.
    • If it finds a .py file, this is loaded and it looks for classes within it, those classes are loaded and any methods within that have the @kernel_function decorator are loaded as KernelFunctionFromMethod
      • If you want to initialise the class in the file with parameter, there is a parameter called class_init_arguments that you can use, this is a dict with the key corresponding to the name of the class (not the name of the file), and the value should be a dict with the parameter you want to initialise with.
      • One other note, previously the file had to be called ‘native_function.py’ that restriction is lifted, and there can now also be multiple classes in a single file, but since all functions within those classes are parsed and added to a single plugin, there is not much to be gained from that, other then perhaps different class init parameters.

Finally, all the functions it found, regardless of source are added in a single KernelPlugin with the name specified in the call.

The plural version works, similarly, except it only takes KernelPlugin objects or classes with decorated methods.

You can supply them as a list of KernelPlugins or a dict with KernelPlugins or objects, for instance:

  • kernel.add_plugins(plugins={"MathPlugin": MathPlugin(), "TimePlugin": TimePlugin()})
  • kernel.add_plugins([KernelPlugin.from_object(MathPlugin()), previously_created_kernel_plugin]
The reason the list only accepts KernelPlugins is that otherwise the name is unknown, the key will serve as the name when using a dict (unless the dict contains a KernelPlugin, in which case the name from the is used)
Notice the simplicity in adding plugins. No need to remember specific method names or decide which method fits which scenario.

 

Add Functions to the Kernel

Adding functions follows a similar streamlined process. When you add a function, it associates with the specified plugin. If the plugin doesn’t exist, the kernel creates it and adds the functions. Be default Kernel.add_function returns the newly created function, but there is an optional flag (return_plugin: bool) to return the plugin instead. Kernel.add_functions('plugin_name', functions=[func1, func2]) always returns the created or updated plugin.

Here’s how you might add a function:

chat_function = kernel.add_function(
    prompt=system_message + """{{$chat_history}}{{$user_input}}""",
    function_name="chat",
    plugin_name="chat",
    prompt_execution_settings=req_settings,
)
And similarly, you can add multiple functions at once:
@kernel_function(name="func1")
def func1(arg1: str) -> str:
    return "test"

@kernel_function(name="func2")
def func2(arg1: str) -> str:
    return "test"

plugin = kernel.add_functions(plugin_name="test", functions=[func1, func2])

A key change that was made now is that whenever a function is added to a plugin (regardless of how) that function is copied and the metadata of that function is updated to reflect the name of the plugin, this means that this is valid, notice that the function that is returned no longer equals the function that was given.

@kernel_function(name="func1") def func1(arg1: str) -> str:
    return "test"

func2 = kernel.add_function(plugin_name='test_plugin', function=func1)
func1 != func2

One important note, when you add two functions with the same name, the function is overridden, the KernelPlugin works like a dict in that regard.

Under the covers

To make this work and cleanup the code, we made some changes under the covers that are good to know.

We removed the KernelPluginCollection class, as it was just a dict of plugin names and plugins and we could just as easily use a dict instead, the benefit of that is less code for us to maintain and a fully dict like experience (thinks like kernel.plugins['name'] just work).

One of the big changes we made under the covers for this is to move all the logic of loading and parsing to either the KernelPlugin or KernelFunction class, for instance when add_plugin is called with the directory info (and no plugin), it calls KernelPlugin.from_directory and then adds the returned plugin to the plugins dict inside of the Kernel, this reduced the amount of code in the Kernel tremendously.

All the initialisation methods for plugins are:

  • KernelPlugin(name, description, functions)
  • KernelPlugin.from_object(plugin_name, object, description)
  • KernelPlugin.from_directory(plugin_name, parent_directory, description, class_init_arguments)
  • KernelPlugin.from_openapi(plugin_name, openapi_document_path, execution_settings, description)
  • KernelPlugin.from_openai(plugin_name, plugin_url, plugin_str, execution_parameters, description)
  • KernelPlugin.from_python_file(plugin_name, py_file (path to), description, class_init_arguments)

and for functions:

  • KernelFunctionFromPrompt(function_name, plugin_name, description, prompt, template_format, prompt_template, prompt_template_config, prompt_execution_settings)
    • Alias: KernelFunction.from_prompt
  • KernelFunctionFromPrompt.from_yaml(yaml_str, plugin_name (optional))
  • KernelFunctionFromPrompt.from_directory(path, plugin_name (optional))
  • KernelFunctionFromMethod(method, plugin_name, stream_method) (where the method has name, description, etc. defined through the decorator)
    • Alias: KernelFunction.from_method

Summary

In this post, we’ve outlined significant improvements made to the Python Semantic Kernel SDK as we gear up for the release of version 1.0. Our focus has been on simplifying the processes involved in adding plugins and functions to the kernel, thereby adhering more closely to Pythonic principles and making our SDK easier to use. You’ve seen the streamlined methods, kernel.add_plugin(), kernel.add_plugins(), kernel.add_function(), and kernel.add_functions(), which replace the older, more confusing options.

As we continue to refine the SDK, we look forward to your feedback on these updates and how they impact your development work. Your insights are invaluable as we strive to make the Python Semantic Kernel SDK the best tool it can be.

Please reach out if you have any questions or feedback through our Semantic Kernel GitHub Discussion Channel. We look forward to hearing from you! We would also love your support: if you’ve enjoyed using Semantic Kernel, please give us a star on GitHub.

0 comments

Leave a comment

Feedback usabilla icon