Creating Custom Skills with Microsoft Semantic Kernel

Summary: This post explores how to create and implement custom skills in Microsoft Semantic Kernel, covering both semantic (AI-powered) and native (code-based) skills. Learn how to extend Semantic Kernel’s capabilities to solve domain-specific problems in your .NET applications.

Introduction

Since Microsoft introduced Semantic Kernel in March 2023, developers have been exploring its potential for orchestrating AI capabilities in their applications. One of Semantic Kernel’s most powerful features is its extensibility through custom skills.

Skills in Semantic Kernel are collections of related functions that provide specific capabilities to your applications. They come in two flavors:

  1. Semantic Skills: AI-powered functions defined using natural language prompts
  2. Native Skills: Traditional code functions written in C# or other programming languages

By creating custom skills, you can extend Semantic Kernel’s capabilities to address domain-specific problems and integrate AI seamlessly with your existing business logic.

In this post, we’ll dive deep into creating both semantic and native skills, explore best practices for skill design, and demonstrate how to combine different skills to build sophisticated AI-powered features in your .NET applications.

Understanding Skills in Semantic Kernel

Before we start creating custom skills, let’s understand how skills fit into the Semantic Kernel architecture.

The Skill Architecture

In Semantic Kernel, skills are collections of functions that provide specific capabilities. Each function within a skill performs a specific task. For example, a TextProcessingSkill might include functions like Summarize, Translate, and ExtractKeywords.

Skills can be imported into the kernel and then invoked by name. This modular approach allows you to:

  • Organize related functionality
  • Reuse skills across different applications
  • Combine skills to create complex workflows
  • Share skills with the community

Semantic vs. Native Skills

The two types of skills in Semantic Kernel serve different purposes:

Semantic Skills leverage AI models to perform tasks defined by natural language prompts. They’re ideal for:

  • Text generation and transformation
  • Content summarization and analysis
  • Creative tasks like writing and ideation
  • Tasks that benefit from the AI’s knowledge and reasoning

Native Skills use traditional code to perform well-defined operations. They’re best for:

  • Deterministic operations with predictable outputs
  • Integration with external systems and APIs
  • Performance-critical operations
  • Operations requiring precise control

Most real-world applications will use a combination of both types to leverage the strengths of each.

Creating Semantic Skills

Let’s start by creating semantic skills, which use natural language prompts to define AI-powered functions.

Setting Up Your Project

First, let’s create a new console application and install the Semantic Kernel package:

csharp

dotnet new console -n SemanticKernelSkills
cd SemanticKernelSkills
dotnet add package Microsoft.SemanticKernel

Now, let’s set up the basic kernel with Azure OpenAI:

csharp

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // Initialize the kernel with Azure OpenAI
        IKernel kernel = Kernel.Builder
            .WithAzureChatCompletionService(
                deploymentName: "gpt-35-turbo", // Your Azure OpenAI deployment name
                endpoint: "https://your-resource-name.openai.azure.com/",
                apiKey: "your-azure-openai-api-key" )
            .Build();

        // We'll add our skills here
        
        Console.WriteLine("Semantic Kernel initialized successfully!");
    }
}

Creating a Semantic Skill Using Prompt Files

The most common way to create semantic skills is by defining them in text files. Let’s create a WritingSkill with functions for different writing tasks:

  1. First, create a directory structure for your skills:
skills/
└── WritingSkill/
    ├── BlogIntroduction.txt
    ├── Summarize.txt
    └── Translate.txt
  1. Define the prompts for each function:

BlogIntroduction.txt:

Write an engaging introduction paragraph for a blog post about {{$topic}}.
The introduction should be approximately 3-4 sentences long, hook the reader,
and briefly mention the key points that will be covered in the post.
Target audience: {{$audience}}
Tone: {{$tone}}

Summarize.txt:

Summarize the following text in a concise way that captures the main points.
Keep the summary to {{$max_length}} sentences.

Text to summarize:
{{$input}}

Translate.txt:

Translate the following text from {{$source_language}} to {{$target_language}}.
Maintain the original meaning, tone, and nuance as much as possible.

Text to translate:
{{$input}}
  1. Import the skill into your kernel:

csharp

// Import the semantic skill from the directory
var writingSkill = kernel.ImportSemanticSkillFromDirectory(
    Path.Combine(Directory.GetCurrentDirectory(), "skills"), "WritingSkill");

Console.WriteLine("Writing skill imported with the following functions:");
foreach (var function in writingSkill)
{
    Console.WriteLine($"- {function.Key}");
}

Using the Semantic Skill

Now let’s use our semantic skill to generate a blog introduction:

csharp

// Create variables for the function
var variables = new ContextVariables();
variables.Set("topic", ".NET MAUI for cross-platform development");
variables.Set("audience", "professional .NET developers");
variables.Set("tone", "informative yet enthusiastic");

// Execute the function
Console.WriteLine("\nGenerating blog introduction...");
var result = await kernel.RunAsync(variables, writingSkill["BlogIntroduction"]);

Console.WriteLine("\nGenerated Introduction:");
Console.WriteLine(result);

Let’s also try the summarization function:

csharp

// Sample text to summarize
string longText = @"
Microsoft Semantic Kernel is an open-source SDK that allows developers to integrate AI services 
like OpenAI's GPT-4 and Azure OpenAI Service into their applications. It provides a framework for 
combining AI capabilities with traditional programming, enabling developers to create more intelligent 
applications. Semantic Kernel orchestrates the use of different AI plugins (skills) and the communication 
between them and your application. It's designed to be flexible, extensible, and adaptable to different 
AI services and models. The SDK supports multiple programming languages including C# and Python, 
making it accessible to a wide range of developers.
";

// Create variables for the function
variables = new ContextVariables();
variables.Set("input", longText);
variables.Set("max_length", "2");

// Execute the function
Console.WriteLine("\nSummarizing text...");
result = await kernel.RunAsync(variables, writingSkill["Summarize"]);

Console.WriteLine("\nSummary:");
Console.WriteLine(result);

Creating a Semantic Skill Programmatically

You can also create semantic skills directly in code, which is useful for dynamic scenarios:

csharp

// Define a prompt template
string recipePrompt = @"
Create a recipe based on the following ingredients: {{$ingredients}}.
The recipe should be suitable for a {{$meal_type}} and serve {{$servings}} people.
Include cooking time, ingredients list, and step-by-step instructions.
";

// Create a semantic function from the prompt
var recipeFunction = kernel.CreateSemanticFunction(
    recipePrompt,
    maxTokens: 1000,
    temperature: 0.7,
    functionName: "CreateRecipe",
    skillName: "CookingSkill");

// Use the function
variables = new ContextVariables();
variables.Set("ingredients", "chicken, rice, bell peppers, onions, garlic, olive oil");
variables.Set("meal_type", "dinner");
variables.Set("servings", "4");

Console.WriteLine("\nGenerating recipe...");
result = await kernel.RunAsync(variables, recipeFunction);

Console.WriteLine("\nGenerated Recipe:");
Console.WriteLine(result);

Creating Native Skills

Native skills are traditional code functions that you can integrate into Semantic Kernel. They’re perfect for deterministic operations and integrating with external systems.

Creating a Simple Native Skill

Let’s create a MathSkill that performs basic mathematical operations:

csharp

using Microsoft.SemanticKernel.SkillDefinition;
using System;
using System.ComponentModel;
using System.Threading.Tasks;

public class MathSkill
{
    [SKFunction, Description("Add two numbers together.")]
    public double Add(
        [Description("First number to add")] double a,
        [Description("Second number to add")] double b)
    {
        return a + b;
    }

    [SKFunction, Description("Subtract the second number from the first.")]
    public double Subtract(
        [Description("Number to subtract from")] double a,
        [Description("Number to subtract")] double b)
    {
        return a - b;
    }

    [SKFunction, Description("Multiply two numbers together.")]
    public double Multiply(
        [Description("First number to multiply")] double a,
        [Description("Second number to multiply")] double b)
    {
        return a * b;
    }

    [SKFunction, Description("Divide the first number by the second.")]
    public double Divide(
        [Description("Number to divide")] double a,
        [Description("Number to divide by")] double b)
    {
        if (b == 0)
            throw new ArgumentException("Cannot divide by zero.");
        return a / b;
    }
}

Now, let’s import and use this native skill:

csharp

// Import the native skill
var mathSkill = kernel.ImportSkill(new MathSkill(), "Math");

Console.WriteLine("\nMath skill imported with the following functions:");
foreach (var function in mathSkill)
{
    Console.WriteLine($"- {function.Key}");
}

// Use the math skill
variables = new ContextVariables();
variables.Set("a", "10");
variables.Set("b", "5");

Console.WriteLine("\nPerforming math operations...");

var addResult = await kernel.RunAsync(variables, mathSkill["Add"]);
Console.WriteLine($"10 + 5 = {addResult}");

var subtractResult = await kernel.RunAsync(variables, mathSkill["Subtract"]);
Console.WriteLine($"10 - 5 = {subtractResult}");

var multiplyResult = await kernel.RunAsync(variables, mathSkill["Multiply"]);
Console.WriteLine($"10 * 5 = {multiplyResult}");

var divideResult = await kernel.RunAsync(variables, mathSkill["Divide"]);
Console.WriteLine($"10 / 5 = {divideResult}");

Creating a More Complex Native Skill

Let’s create a more complex native skill that interacts with external APIs. We’ll create a WeatherSkill that fetches weather data:

csharp

using Microsoft.SemanticKernel.SkillDefinition;
using System;
using System.ComponentModel;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

public class WeatherSkill
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

    public WeatherSkill(string apiKey )
    {
        _httpClient = new HttpClient( );
        _apiKey = apiKey;
    }

    [SKFunction, Description("Get the current weather for a location.")]
    public async Task<string> GetCurrentWeather(
        [Description("The city and country, e.g., 'Seattle, US'")] string location)
    {
        try
        {
            // Call OpenWeatherMap API (you'll need to sign up for a free API key)
            string url = $"https://api.openweathermap.org/data/2.5/weather?q={location}&appid={_apiKey}&units=metric";
            HttpResponseMessage response = await _httpClient.GetAsync(url );
            response.EnsureSuccessStatusCode();
            
            string responseBody = await response.Content.ReadAsStringAsync();
            using JsonDocument doc = JsonDocument.Parse(responseBody);
            
            // Extract relevant weather information
            double temperature = doc.RootElement.GetProperty("main").GetProperty("temp").GetDouble();
            string weatherDescription = doc.RootElement.GetProperty("weather")[0].GetProperty("description").GetString();
            double humidity = doc.RootElement.GetProperty("main").GetProperty("humidity").GetDouble();
            
            return $"Current weather in {location}: {weatherDescription}, temperature: {temperature}°C, humidity: {humidity}%";
        }
        catch (Exception ex)
        {
            return $"Error getting weather: {ex.Message}";
        }
    }

    [SKFunction, Description("Get the 5-day weather forecast for a location.")]
    public async Task<string> GetForecast(
        [Description("The city and country, e.g., 'Seattle, US'")] string location)
    {
        try
        {
            // Call OpenWeatherMap API for forecast
            string url = $"https://api.openweathermap.org/data/2.5/forecast?q={location}&appid={_apiKey}&units=metric";
            HttpResponseMessage response = await _httpClient.GetAsync(url );
            response.EnsureSuccessStatusCode();
            
            string responseBody = await response.Content.ReadAsStringAsync();
            using JsonDocument doc = JsonDocument.Parse(responseBody);
            
            // Process and format the forecast data
            var forecast = new System.Text.StringBuilder();
            forecast.AppendLine($"5-day weather forecast for {location}:");
            
            var forecastList = doc.RootElement.GetProperty("list");
            string currentDate = "";
            
            foreach (var item in forecastList.EnumerateArray())
            {
                string dt = item.GetProperty("dt_txt").GetString();
                string date = dt.Split(' ')[0];
                string time = dt.Split(' ')[1];
                
                // Only include one forecast per day (noon)
                if (date != currentDate && time.StartsWith("12:"))
                {
                    currentDate = date;
                    double temp = item.GetProperty("main").GetProperty("temp").GetDouble();
                    string weather = item.GetProperty("weather")[0].GetProperty("description").GetString();
                    
                    forecast.AppendLine($"{date}: {weather}, {temp}°C");
                }
            }
            
            return forecast.ToString();
        }
        catch (Exception ex)
        {
            return $"Error getting forecast: {ex.Message}";
        }
    }
}

To use this skill:

csharp

// Import the weather skill (you'll need an OpenWeatherMap API key)
var weatherSkill = kernel.ImportSkill(new WeatherSkill("your-openweathermap-api-key"), "Weather");

// Use the weather skill
variables = new ContextVariables();
variables.Set("location", "Seattle, US");

Console.WriteLine("\nGetting weather information...");
var weatherResult = await kernel.RunAsync(variables, weatherSkill["GetCurrentWeather"]);
Console.WriteLine(weatherResult);

var forecastResult = await kernel.RunAsync(variables, weatherSkill["GetForecast"]);
Console.WriteLine(forecastResult);