Welcome back, AI explorers! In our journey through modern AI agent frameworks, we’ve seen how LangGraph builds state machines, AutoGen fosters conversational agents, and CrewAI empowers role-playing teams. Now, it’s time to dive into a framework designed with enterprise integration and modularity at its core: Semantic Kernel (SK).

Semantic Kernel, spearheaded by Microsoft, offers a powerful SDK for integrating Large Language Models (LLMs) with conventional programming languages like Python and C#. It helps you build intelligent applications by weaving together AI capabilities (like natural language understanding and generation) with existing business logic and external services. Think of it as a sophisticated toolkit that allows your code to think and act more intelligently by leveraging LLMs, without completely reinventing your application architecture.

By the end of this chapter, you’ll understand Semantic Kernel’s fundamental concepts like the Kernel itself, Semantic Functions, Native Functions, Skills (also known as Plugins), Context Variables, and Planners. We’ll set up a Python environment, build a simple skill, and then orchestrate multiple skills using a Planner to achieve a more complex goal. Get ready to integrate AI into your applications with enterprise-grade precision!

Core Concepts of Semantic Kernel

Semantic Kernel introduces a structured way to combine the “reasoning” power of LLMs with the “doing” power of traditional code. Let’s break down its key components.

The Kernel: The Brain of Your AI Application

At the heart of every Semantic Kernel application is the Kernel. You can think of the Kernel as the central orchestrator, the “brain” that manages all the AI capabilities and interactions. It’s responsible for:

  1. Loading and managing Skills (Plugins): It knows what capabilities your application has.
  2. Executing functions: Whether they are AI-powered (semantic) or traditional code (native).
  3. Managing context: Passing information between different parts of your application.
  4. Integrating with LLMs: Connecting to OpenAI, Azure OpenAI, or other providers.

Imagine the Kernel as a project manager. It doesn’t do the work itself, but it knows who can do what (skills), gives them the necessary information (context), and makes sure tasks are executed in the right order.

Semantic Functions (Prompts as Code)

Semantic Functions are where the magic of LLMs truly shines in Semantic Kernel. They allow you to define prompts as reusable, invokable functions. Instead of just sending a raw prompt to an LLM, you encapsulate it, give it a name, and define its expected inputs and outputs.

Why are they important?

  • Reusability: Define a prompt once, use it everywhere.
  • Parameterization: Easily pass variables into your prompts.
  • Modularity: Break down complex prompts into smaller, manageable pieces.
  • Version Control: Treat your prompts like code, track changes, and deploy them.

For example, you might have a semantic function called SummarizeText that takes a long piece of text and returns a concise summary. The actual prompt template is hidden within the function, making your application code much cleaner.

Native Functions (Tools as Code)

While Semantic Functions leverage LLMs, Native Functions are your good old reliable traditional code functions written in Python (or C#). These are your tools that perform specific, deterministic actions that LLMs can’t or shouldn’t do, such as:

  • Calling external APIs (e.g., retrieving data from a database, sending an email).
  • Performing complex calculations.
  • Interacting with local file systems.
  • Executing business logic.

Why are they important?

  • Reliability: Deterministic execution, no LLM hallucinations.
  • Efficiency: Faster and cheaper for non-cognitive tasks.
  • Access to external systems: Bridge the gap between AI and your existing infrastructure.

Semantic Kernel allows LLMs (via Planners) to “call” these native functions when they determine a specific tool is needed to fulfill a user’s request. This is the essence of “tool usage” or “function calling” in SK.

Skills (Plugins): Collections of Functions

In Semantic Kernel, a Skill (often referred to as a Plugin in newer documentation and other frameworks) is simply a collection of related Semantic Functions and Native Functions. They act as logical groupings of capabilities.

Think of a skill as a toolbox for a specific domain. For instance:

  • A “TextSkill” might contain Summarize, Translate, ExtractKeywords.
  • An “EmailSkill” might contain SendEmail, DraftEmail, CheckInbox.
  • A “CalendarSkill” might contain CreateEvent, ListEvents, DeleteEvent.

By organizing functions into skills, you create modular, reusable components that can be easily added to your Kernel, making your AI application extensible and manageable.

Context Variables (Memory)

For any multi-step AI workflow, maintaining state and passing information between different operations is crucial. Semantic Kernel uses Context Variables to manage this. The Context object is essentially a dictionary-like structure that holds all the input and output variables, as well as conversational history, errors, and other relevant data during an operation.

When you invoke a function (semantic or native), it receives the current Context, can read variables from it, and can write new variables back into it. This allows functions to build upon each other’s outputs, forming a coherent workflow.

Planners (Orchestration): Automating Skill Execution

This is where Semantic Kernel truly shines in building multi-step agentic workflows. A Planner is an AI-powered component that takes a high-level user goal and automatically figures out the sequence of skills (semantic and native functions) needed to achieve that goal. It essentially “plans” the execution path.

How do Planners work?

  1. The user provides a goal (e.g., “Summarize this document and then email it to John Doe”).
  2. The Planner inspects the available skills and their descriptions (what they do, what inputs they need).
  3. Using an LLM, the Planner generates a plan – a sequence of function calls – to achieve the goal.
  4. The Kernel then executes this plan, passing context between each step.

Semantic Kernel offers different types of planners, such as the SequentialPlanner (for linear workflows) and more advanced planners like the HandlebarsPlanner (which offers more robust control and logic within the plan itself). Planners are the ultimate orchestrators, allowing your AI application to dynamically adapt to user requests by chaining together its available tools.

Here’s a simplified Mermaid diagram illustrating how the Kernel, Skills, and Planners interact:

flowchart TD User_Goal[User Goal Summarize and Email] --> Planner[Planner Determines Steps] Planner -->|Generates Plan| Kernel[Kernel Orchestrates Execution] subgraph Available_Skills["Available Skills"] Skill_A[TextSkill Summarize Extract] Skill_B[EmailSkill SendEmail DraftEmail] end Kernel -->|Invokes| Skill_A Skill_A -->|Output Context| Kernel Kernel -->|Invokes| Skill_B Skill_B -->|Final Output| User_Goal

Setting Up Your Semantic Kernel Environment

To get started with Semantic Kernel, you’ll need Python 3.9+ and pip. We’ll also need an API key for an LLM provider. OpenAI is a common choice, but Azure OpenAI, Anthropic, and others are supported.

Step 1: Install Semantic Kernel

Let’s begin by installing the semantic-kernel package. As of late 2023, the 1.x series is stable and widely adopted for Python. Given the rapid evolution of AI frameworks, always check the official Semantic Kernel Python documentation for the absolute latest stable version as of 2026-03-20. For this guide, we’ll use 1.10.0 as a representative example of a stable 1.x release.

Open your terminal and run:

pip install semantic-kernel==1.10.0 # Please check official docs for the absolute latest stable version on 2026-03-20

This command installs the core Semantic Kernel library.

Step 2: Set Up Your LLM Provider API Key

You’ll need an API key to connect to an LLM. For this example, we’ll use OpenAI. It’s best practice to store API keys securely, typically using environment variables.

  1. Get an OpenAI API Key: If you don’t have one, sign up at platform.openai.com and generate a new secret key.
  2. Set as Environment Variable: On Linux/macOS:
    export OPENAI_API_KEY="sk-YOUR_OPENAI_API_KEY"
    
    On Windows (Command Prompt):
    set OPENAI_API_KEY="sk-YOUR_OPENAI_API_KEY"
    
    On Windows (PowerShell):
    $env:OPENAI_API_KEY="sk-YOUR_OPENAI_API_KEY"
    
    Replace "sk-YOUR_OPENAI_API_KEY" with your actual key. Remember to restart your terminal or IDE after setting the variable for it to take effect.

Step-by-Step Implementation

Now that our environment is ready, let’s build some AI magic! We’ll start with a simple greeting skill and then move on to orchestrating multiple skills with a Planner.

Project 1: Building Your First Semantic Kernel Application - A Simple Greeting Skill

We’ll start by creating a simple Semantic Function that generates a personalized greeting.

Create a new Python file, say sk_greeting.py.

Step 1: Initialize the Kernel

First, we import the necessary components and initialize our Kernel. We’ll also configure it to use OpenAI’s GPT-3.5-turbo model.

import os
import asyncio
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion

# We'll put our async code inside an 'async def main()' function
async def main():
    # Step 1: Initialize the Kernel
    kernel = sk.Kernel()

    # Ensure OPENAI_API_KEY environment variable is set
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        raise ValueError("OPENAI_API_KEY environment variable not set.")

    # Add OpenAI chat completion service to the kernel
    # Use a common model like gpt-3.5-turbo or gpt-4
    kernel.add_service(
        OpenAIChatCompletion(service_id="chat-gpt", ai_model_id="gpt-3.5-turbo", api_key=api_key)
    )

    print("Kernel initialized with OpenAI service.")

Explanation:

  • import asyncio: This is crucial! Semantic Kernel operations are asynchronous, meaning they can run in the background without blocking your program. asyncio is Python’s library for writing concurrent code.
  • async def main():: We define an asynchronous function main to hold our SK logic.
  • kernel = sk.Kernel(): Creates an instance of the Kernel.
  • os.getenv("OPENAI_API_KEY"): Safely retrieves your API key.
  • kernel.add_service(...): This line connects your Kernel to the OpenAI LLM. service_id is a unique identifier you choose, and ai_model_id specifies which GPT model to use. gpt-3.5-turbo is a good balance of cost and performance.
Step 2: Create a Semantic Function

Next, we’ll define a simple semantic function using a prompt template. Add this code inside your main() function.

    # Step 2: Define a simple semantic function
    sk_prompt = """
The user wants a greeting.
Say hello to the user, using their name if provided.
If no name is provided, just say "Hello there!".

User name: {{ $name }}
"""

    # Create the semantic function
    # The `plugin_name` will be the skill name, and `function_name` is the function within that skill.
    # The `description` is crucial for Planners to understand what the function does.
    say_hello_function = kernel.create_function_from_prompt(
        prompt=sk_prompt,
        function_name="SayHello",
        plugin_name="GreetingSkill",
        description="Generates a friendly greeting, optionally using the user's name."
    )

    print("GreetingSkill created.")

Explanation:

  • sk_prompt: This is our prompt template. Notice {{ $name }}. This is a context variable that our function expects. If the name variable is present in the context when the function is called, it will be substituted into the prompt.
  • kernel.create_function_from_prompt(...): This is how you create a semantic function.
    • prompt: Our prompt template.
    • function_name: The specific name of this function within its skill.
    • plugin_name: The name of the skill this function belongs to. We’re creating a skill on the fly here.
    • description: Crucially important! This description tells the Kernel (and especially Planners) what this function does. It’s how the AI understands when to use this “tool.”
Step 3: Invoke the Semantic Function

Let’s call our new function! We’ll pass some input to it via the Context. Add this inside your main() function.

    # Step 3: Invoke the semantic function
    print("\nInvoking SayHello with a name...")
    result_with_name = await kernel.invoke(say_hello_function, sk.ContextVariables(name="Alice"))
    print(f"Result with name: {result_with_name}")

    print("\nInvoking SayHello without a name...")
    result_without_name = await kernel.invoke(say_hello_function) # No name provided
    print(f"Result without name: {result_without_name}")

Explanation:

  • await kernel.invoke(...): This executes the specified function. The await keyword pauses the execution of main() until kernel.invoke() completes its asynchronous task. This is why we needed asyncio and async def main().
  • sk.ContextVariables(name="Alice"): We create a ContextVariables object and set the name variable to “Alice”. This is passed to the function, which then uses it in the prompt.
  • When no name is provided, the {{ $name }} in the prompt will be empty, and the LLM will fall back to its general greeting.

Finally, to run our main() async function, we need to add the following lines at the end of your sk_greeting.py file:

if __name__ == '__main__':
    asyncio.run(main())

Run this script: python sk_greeting.py. You should see output similar to:

Kernel initialized with OpenAI service.
GreetingSkill created.

Invoking SayHello with a name...
Result with name: Hello Alice! How can I help you today?

Invoking SayHello without a name...
Result without name: Hello there! How may I assist you?

(Note: Exact LLM output may vary slightly, but the core greeting logic should be consistent.)

Congratulations! You’ve successfully created and invoked your first Semantic Kernel semantic function. You’ve turned a prompt into a reusable, callable piece of your application.

Project 2: Orchestrating with Planners - A Goal-Oriented Assistant

Now, let’s elevate our application by using a Planner. We’ll create a simple scenario: an assistant that can answer a question and then create a summary of that answer. This requires two distinct steps, which a Planner can orchestrate.

We’ll define two skills:

  1. GeneralKnowledgeSkill: A semantic function to answer questions.
  2. SummarizationSkill: A semantic function to summarize text.

Then, we’ll use a SequentialPlanner to chain them.

Create a new Python file, say sk_planner_assistant.py.

Step 1: Setup Kernel and Services (Same as before)
import os
import asyncio
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.planners.sequential_planner import SequentialPlanner

async def main(): # Wrapped in async main
    kernel = sk.Kernel()

    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        raise ValueError("OPENAI_API_KEY environment variable not set.")

    kernel.add_service(
        OpenAIChatCompletion(service_id="chat-gpt", ai_model_id="gpt-3.5-turbo", api_key=api_key)
    )

    print("Kernel initialized with OpenAI service.")
Step 2: Define Skills (Semantic Functions)

Now, let’s define our two semantic functions. Add this inside your main() function.

    # Step 2.1: Define GeneralKnowledgeSkill
    general_knowledge_prompt = """
You are a helpful assistant. Answer the following question concisely.

Question: {{ $input }}
"""

    general_knowledge_function = kernel.create_function_from_prompt(
        prompt=general_knowledge_prompt,
        function_name="AnswerQuestion",
        plugin_name="GeneralKnowledgeSkill",
        description="Answers a general knowledge question concisely."
    )
    print("GeneralKnowledgeSkill created.")

    # Step 2.2: Define SummarizationSkill
    summarization_prompt = """
Summarize the following text in one brief sentence.

Text: {{ $input }}
"""

    summarization_function = kernel.create_function_from_prompt(
        prompt=summarization_prompt,
        function_name="SummarizeText",
        plugin_name="SummarizationSkill",
        description="Summarizes the given text into a single, brief sentence."
    )
    print("SummarizationSkill created.")

Explanation:

  • Both functions expect an {{ $input }} variable, which is a standard way for SK functions to receive their primary input.
  • Notice the description for each. These are vital hints for the Planner! The Planner reads these descriptions to understand what each tool can do and how they relate to the overall goal.
Step 3: Create and Invoke the Sequential Planner

Now for the exciting part: using the Planner! Add this inside your main() function.

    # Step 3: Create a Sequential Planner
    # The planner needs to know about all available skills to create a plan.
    planner = SequentialPlanner(kernel)

    # Define the user's high-level goal
    user_goal = "What is the capital of France? Then summarize that answer in one sentence."
    print(f"\nUser Goal: {user_goal}")

    # Create a plan to achieve the goal
    print("Creating plan...")
    plan = await planner.create_plan(goal=user_goal)
    print("Plan created successfully!")
    print(f"Plan:\n{plan.model_dump_json(indent=2)}") # View the generated plan

    # Execute the plan
    print("\nExecuting plan...")
    result = await plan.invoke(kernel)
    print(f"Final Result: {result}")

Explanation:

  • planner = SequentialPlanner(kernel): We instantiate the SequentialPlanner, passing it our kernel. The planner will automatically inspect all skills registered with the kernel.
  • user_goal: This is the natural language request we give to the planner.
  • plan = await planner.create_plan(goal=user_goal): The planner analyzes the goal and the available skills (based on their descriptions) to generate a sequence of function calls.
  • plan.model_dump_json(indent=2): This is super helpful! It shows you the actual JSON plan generated by the LLM, detailing which functions will be called in what order.
  • result = await plan.invoke(kernel): Once the plan is created, we execute it using the kernel. The kernel manages the flow of context between each step of the plan.

Finally, to run our main() async function, add this at the end of your sk_planner_assistant.py file:

if __name__ == '__main__':
    asyncio.run(main())

Run this script: python sk_planner_assistant.py.

You should see output similar to:

Kernel initialized with OpenAI service.
GeneralKnowledgeSkill created.
SummarizationSkill created.

User Goal: What is the capital of France? Then summarize that answer in one sentence.
Creating plan...
Plan created successfully!
Plan:
{
  "state": {},
  "steps": [
    {
      "function": "GeneralKnowledgeSkill.AnswerQuestion",
      "args": {
        "input": "What is the capital of France?"
      }
    },
    {
      "function": "SummarizationSkill.SummarizeText",
      "args": {
        "input": "{{$GeneralKnowledgeSkill.AnswerQuestion}}"
      }
    }
  ]
}

Executing plan...
Final Result: Paris is the capital of France.

(Again, LLM output may vary, but the structure should be correct.)

Notice the plan output: it correctly identified GeneralKnowledgeSkill.AnswerQuestion first, and then used the output of that function ({{$GeneralKnowledgeSkill.AnswerQuestion}}) as the input for SummarizationSkill.SummarizeText. This is the power of Planners! They enable dynamic, goal-oriented workflows without you having to hardcode every step.

Mini-Challenge: Extend the Planner with a New Skill

You’ve seen how Semantic Kernel uses skills and planners. Now, it’s your turn to add a new capability!

Challenge: Modify the sk_planner_assistant.py script to include a new Native Function skill. This skill should:

  1. Be named WordCountSkill.
  2. Contain a native function called CountWords.
  3. The CountWords function should take a string as input and return the number of words in that string as a string.
  4. Update the user_goal to: “What is the capital of Japan? Then summarize that answer, and finally, tell me how many words are in the summarized answer.”

Hint:

  • Native functions are defined as regular Python methods within a class. You then use the @kernel_function decorator to expose them to Semantic Kernel and provide a description.
  • You’ll add this class to the kernel using kernel.add_plugin(plugin=sk.KernelPlugin.from_object(YourClass(), plugin_name="YourSkillName")).
  • Remember to provide a clear description for your CountWords function within the class for the Planner to understand it.

What to observe/learn:

  • How easily new capabilities (skills) can be added to the Kernel.
  • How the Planner automatically incorporates new tools into its planning process if the goal requires them and the descriptions are clear.
# Example of how to define a native skill class (integrate this into your solution!)
from semantic_kernel.functions import kernel_function

class WordCountSkill:
    @kernel_function(description="Counts the number of words in the input text and returns the count as a string.")
    def CountWords(self, input: str) -> str:
        words = input.split()
        return str(len(words))

# To add this to your kernel (inside your main() async function):
# word_count_plugin = kernel.add_plugin(WordCountSkill(), plugin_name="WordCountSkill")

Common Pitfalls & Troubleshooting

  1. Vague Skill Descriptions: Planners rely heavily on the description you provide for each function. If a description is too vague or inaccurate, the Planner might fail to select the correct function or create an illogical plan.

    • Solution: Be explicit and precise in your function descriptions. Clearly state what the function does, its inputs, and its outputs. Think of it as writing documentation for an intelligent agent.
  2. Planner Over-planning or Under-planning: Sometimes the Planner might create a plan that’s too complex for a simple task, or it might miss obvious steps. This often relates to the LLM model used by the planner and the quality of skill descriptions.

    • Solution:
      • Refine skill descriptions.
      • Experiment with different LLM models for the planner (e.g., a more capable model like GPT-4 might produce better plans).
      • For very specific or critical workflows, consider using a more controlled orchestration pattern (like directly invoking functions) rather than relying solely on the Planner for complex decision-making.
  3. Context Window Limitations: As you chain many functions or pass large amounts of data, you might hit the context window limits of the underlying LLM. This can lead to truncated inputs or errors.

    • Solution:
      • Design skills to be concise and process only necessary information.
      • Implement summarization steps for large inputs or outputs between functions.
      • Consider using external memory solutions (like vector databases) for long-term context that doesn’t need to be in the active LLM context.
  4. API Key/Environment Variable Issues: Forgetting to set OPENAI_API_KEY or AZURE_OPENAI_API_KEY (or similar for other providers) is a very common starting error.

    • Solution: Always double-check that your environment variables are correctly set and that your terminal/IDE has loaded them. Restarting your terminal often helps.

Summary

Phew! We’ve covered a lot in this chapter. Semantic Kernel offers a robust, modular, and enterprise-friendly approach to building AI applications. Here are the key takeaways:

  • The Kernel is the central orchestrator, managing skills, context, and LLM integrations.
  • Semantic Functions turn LLM prompts into reusable, parameterized functions, treating “prompts as code.”
  • Native Functions allow you to integrate traditional business logic and external tools, treating “tools as code.”
  • Skills (Plugins) are logical groupings of related semantic and native functions, promoting modularity.
  • Context Variables provide the memory and state management, allowing information to flow between functions.
  • Planners are powerful AI components that take a high-level goal and automatically generate a sequence of skill invocations to achieve it, enabling dynamic orchestration.
  • Asynchronous Programming (async/await) is fundamental to Semantic Kernel’s Python implementation, enabling efficient handling of LLM calls.

Semantic Kernel excels in scenarios where you need to integrate LLM capabilities deeply into existing applications, leverage your existing codebase, and maintain high levels of control and modularity. In the next chapter, we’ll shift our focus to another exciting framework, exploring different paradigms for building intelligent agents.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.