Semantic Kernel

The Semantic Kernel Masterclass

From Script to System. A "Zero to Hero" guide to building enterprise-grade agents with Microsoft's Semantic Kernel.

0

The Paradigm Shift: From Chatbots to Agents

To master Semantic Kernel, we must first understand the fundamental shift in AI application architecture. Traditional "Chatbots" are linear: Input → LLM → Output. They can talk, but they cannot do.

True Agents require "Hands". An enterprise agent needs to modify databases, send emails, and call APIs. Semantic Kernel is Microsoft's answer to this challenge. It is not just a wrapper around OpenAI; it is a Dependency Injection Container for AI capabilities, designed to mix Native Code (C#, Python) with Semantic Code (Prompts).

Concept: The Kernel as the Operating System

Think of the Kernel as the OS. The LLM is the CPU (processing unit), and your Plugins are the Drivers that allow the CPU to talk to the hardware (APIs/DBs). The Kernel binds them all together.

graph LR subgraph "Traditional Chatbot" A[User] --> B[Prompt] --> C[LLM] --> D[Text Response] end subgraph "Semantic Kernel Agent" E[User] --> F((Kernel)) F --> G[Planner] G -->|Selects| H["Plugin A (Email)"] G -->|Selects| I["Plugin B (CRM)"] H --> F I --> F F --> J[Action & Response] end style F fill:#bfdbfe,stroke:#2563eb,stroke-width:2px style G fill:#fef08a,stroke:#eab308,stroke-width:2px

1. The Kernel Object

The Kernel is your central unit of work. You initialize it once and attach everything your agent needs to it.

app/foundations.py
import asyncio
import semantic_kernel as sk
from semantic_kernel.connectors.ai.google.google_ai import GoogleAIChatCompletion

async def main():
    # 1. Initialize the Kernel (The container)
    kernel = sk.Kernel()

    # 2. Add the Brain (LLM Service)
    kernel.add_service(
        GoogleAIChatCompletion(
            service_id="gemini",
            ai_model_id="gemini-1.5-pro",
            api_key="YOUR_API_KEY"
        )
    )

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

2. Prompts are Functions

In SK, we stop treating prompts as "magic strings" scattered across your code. We treat them as Semantic Functions. They have inputs, outputs, and configuration, just like a Python function. This makes them reproducible and testable.

# Creating a function from a prompt template
summarize = kernel.create_function_from_prompt(
    function_name="Summarize",
    plugin_name="Writer",
    prompt="Summarize this text in 3 bullet points: {{$input}}"
)

# Call it exactly like a native function (inside async main)
result = await kernel.invoke(summarize, input="Semantic Kernel is...")
print(result)
1

The Doer (Plugins & Native Functions)

Goal: Give the AI "hands" to interact with the real world (APIs and Code).

3. Native Plugins (The Bridge)

Plugins are how you make your existing Python code visible to the LLM. You use the @kernel_function decorator to add metadata that the LLM uses to understand what your code does.

app/plugins.py
from semantic_kernel.functions import kernel_function
from typing import Annotated

class MathPlugin:
    @kernel_function(
        description="Adds two numbers together",
        name="Add"
    )
    def add(
        self, 
        number1: Annotated[float, "The first number"], 
        number2: Annotated[float, "The second number"]
    ) -> float:
        return number1 + number2

# Import the plugin into the kernel
kernel.add_plugin(MathPlugin(), plugin_name="Math")

4. Auto-Function Calling (The Magic)

Once plugins are registered, you don't need to manually tell the LLM to call them. You set FunctionChoiceBehavior.Auto(), and the kernel handles the negotiation (generating the tool call, executing the code, and feeding the result back).

from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior

# Configure execution settings
settings = kernel.get_prompt_execution_settings_from_service_id("gemini")
settings.function_choice_behavior = FunctionChoiceBehavior.Auto()

# Now, if you ask a math question, SK automatically calls your Python code
result = await kernel.invoke_prompt(
    prompt="How much is 50 plus 25?",
    settings=settings
)
# Result: 75 (calculated by code, not guessed by LLM)

5. State Management

AI models are stateless. To have a conversation, you must manage the ChatHistory. SK provides a robust object to store user and assistant messages, which you pass into every invoke call.

from semantic_kernel.contents import ChatHistory

# 1. Create a History Object
history = ChatHistory()
history.add_system_message("You are a helpful assistant.")

# 2. Add User Message
history.add_user_message("My name is Vinoth.")

# 3. Invoke Kernel with History
# The Kernel sends the WHOLE history to the LLM
result = await kernel.invoke_prompt(
    prompt=None, # Not needed if using history
    chat_history=history
)

# 4. Save Assistant Response back to History
history.add_assistant_message(str(result))
2

The Knower (RAG & Memory)

Goal: Give the AI a "brain" to remember facts and company policies.

6. Embeddings 101

Embeddings convert text into vectors (lists of numbers). Similar meanings are mathematically close in this vector space. To do RAG (Retrieval Augmented Generation), you first need an Embedding Service.

# Use precise import path for safety
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_embedding import GoogleAITextEmbedding

kernel.add_service(
    GoogleAITextEmbedding(service_id="embedding", ai_model_id="embedding-001")
)

7. Vector Databases & RAG

SK allows you to write memory records to a store (Volatile for testing, Qdrant/Pinecone for production) and retrieve them. RAG is simply the patterns of: Search Memory → Paste into Prompt → Ask LLM.

graph LR A[User Query] --> B[Embed Query] B --> C[(Vector DB)] C -->|Retrieve Matches| D[Context] D --> E[Construct Prompt] E --> F((LLM)) F --> G[Answer] style C fill:#fef08a,stroke:#eab308,stroke-width:2px style F fill:#bfdbfe,stroke:#2563eb,stroke-width:2px
from semantic_kernel.memory import VolatileMemoryStore, SemanticTextMemory

# Setup Memory
memory = SemanticTextMemory(storage=VolatileMemoryStore(), embeddings_generator=kernel.get_service("embedding"))

# Save a fact
await memory.save_information(collection="HR_Policies", id="info1", text="Refunds are only allowed within 30 days.")

# Search (The 'Retrieval' in RAG)
results = await memory.search("HR_Policies", "Can I get a refund?", limit=1)
print(results[0].text)

8. The RAG Plugin

To make this "Agentic", we wrap the search logic in a Plugin. Now the LLM can decide when it needs to look up the policy.

class PolicyPlugin:
    def __init__(self, memory):
        self.memory = memory

    @kernel_function(description="Searches company policies for answers", name="SearchPolicy")
    async def search_policy(self, query: str) -> str:
        results = await self.memory.search("HR_Policies", query, limit=1)
        return results[0].text if results else "No policy found."

# Add to kernel
kernel.add_plugin(PolicyPlugin(memory), plugin_name="Policy")
3

The Planner (Autonomy)

Goal: Stop hard-coding flows. Let the AI decide the sequence of steps.

9. The Problem with "If/Else"

Hard-coding logic fails when you scale to 50+ plugins. You can't write an if statement for every possible user combination. Instead, we use a Planner.

How it works: The Planner specifically asks the LLM "What should I do next given these tools?" The LLM replies with a function call Plan, the Kernel executes it, and this loop continues until the user's goal is met.

10. Planners (The Orchestrator)

A Planner is a special agent that takes a user goal (e.g., "Plan a travel itinerary") and looks at all available plugins to generate a multi-step plan.

from semantic_kernel.planners import FunctionCallingStepwisePlanner

# Create the planner
planner = FunctionCallingStepwisePlanner(service_id="gemini")

# Give it a complex goal
goal = "Check my calendar for tomorrow and book a meeting room if I'm free."

# The planner figures out it needs:
# 1. CalendarPlugin.GetEvents()
# 2. Logic check
# 3. ComponentPlugin.BookRoom()
result = await planner.invoke(kernel, goal)
Note: Effective use of Planners relies heavily on Goal-Oriented Design. Your plugin descriptions must be crystal clear about what they do and when to use them.
4

The Professional (Production Engineering)

Goal: Make the system robust, safe, and observable.

11. Filters (Hooks/Middleware)

In production, you never let an LLM execute code blindly. Filters allow you to runs logic before function execution (e.g., permission checks) and after (e.g., logging).

from semantic_kernel.filters import Filter, AutoFunctionInvocationContext

class SafeGuardFilter(Filter):
    async def on_auto_function_invocation(self, context, next):
        # NOTE: 'on_auto_...' triggers only when the LLM effectively *chooses* a tool.
        # Use 'on_function_invocation' if you want to catch manual calls too.
        
        # PRE-HOOK: Check for dangerous parameters
        if "delete_database" in context.function.name:
            raise SecurityError("Action Blocked: Database deletion is not allowed.")
        
        # Execute the function
        await next(context)
        
        # POST-HOOK: Log the result
        print(f"Function {context.function.name} executed successfully.")

# Register the filter
kernel.add_filter(SafeGuardFilter())

12. Observability

Debugging AI can be hard. SK integrates with standard logging to show you the "Thinking Process"—tokens used, latency, and the exact prompt sent to the model.

import logging

# Set up standard Python logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

# Now, when you run the kernel, you will see logs like:
# [INFO] Function SearchPolicy execution time: 0.45s
# [INFO] Rendered Prompt: "You are a helpful assistant..."
# [INFO] Usage: 154 tokens
5

The Architect (Multi-Agent Systems)

Goal: Building complex ecosystems where agents work together.

13. The Manager-Worker Pattern

As systems grow, a single Kernel can become overwhelmed with too many plugins (confusing the LLM). The solution is to split tasks into specific Kernels: a "Manager" that triages requests, and "Workers" (specialized Kernels) that execute them.

# Conceptual: A Manager Agent deciding which 'worker' to call
@kernel_function(description="Delegates task to a coder or writer")
async def manager_delegate(self, task: str) -> str:
    if "code" in task:
        # Pass the task to the independent 'Coder Kernel'
        return await coder_kernel.invoke(..., input=task)
    elif "blog" in task:
        # Pass the task to the independent 'Writer Kernel'
        return await writer_kernel.invoke(..., input=task)
    return "I can handle this myself."

You can achieve this by having one Kernel invoke another via a plugin, or by using the new Agent Framework (experimental) which formalizes this hand-off.

Master Enterprise Template

This is the "Golden Scaffold." It consolidates everything we have learned—Plugins, Filters, Auto-Invocation, and Memory—into a single, production-ready Python script. You can copy-paste this to start any new enterprise agent project.

sk_agent_template.py v1.0.0
import asyncio
import os
from dotenv import load_dotenv # Added for safety
import semantic_kernel as sk
from semantic_kernel.connectors.ai.google.google_ai import GoogleAIChatCompletion # Consistent with tutorial
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents import ChatHistory
from semantic_kernel.filters import Filter, AutoFunctionInvocationContext

load_dotenv() # Load API keys

# --- 1. PLUGINS (The Hands) ---
class WebSearchPlugin:
    @kernel_function(description="Searches the web for information", name="search")
    def search(self, query: str) -> str:
        return f"Results for '{query}' from the web."

# --- 2. FILTERS (The Guardrails) ---
class ApprovalFilter(Filter):
    async def on_auto_function_invocation(self, context, next):
        # Enterprise Feature: Human-in-the-loop simulation
        print(f"\n[System] Agent wants to call: {context.function.name}")
        if input("[Admin] Allow this action? (y/n): ").lower() == "y":
            await next(context)
        else:
            raise Exception("Action blocked by user.")

# --- 3. MAIN APP ---
async def main():
    # A. Initialize Kernel
    kernel = sk.Kernel()
    
    # B. Add AI Service (Gemini Example)
    # Note: easily swappable for OpenAIChatCompletion
    kernel.add_service(
        GoogleAIChatCompletion(
            service_id="gemini", 
            ai_model_id="gemini-1.5-flash", 
            api_key=os.getenv("GOOGLE_API_KEY")
        )
    )
    # Generic Open AI example for reference:
    # kernel.add_service(
    #     OpenAIChatCompletion(service_id="gpt-4", ai_model_id="gpt-4", api_key=os.getenv("OPENAI_API_KEY"))
    # )
    
    # C. Add Plugins
    kernel.add_plugin(WebSearchPlugin(), plugin_name="Web")
    
    # D. Add Filters
    kernel.add_filter(ApprovalFilter())

    # E. Execution Settings (The Brain)
    settings = kernel.get_prompt_execution_settings_from_service_id("gemini")
    settings.function_choice_behavior = FunctionChoiceBehavior.Auto()

    # F. Chat Loop (The Memory)
    history = ChatHistory()
    history.add_system_message("You are an enterprise assistant.")
    
    print("--- Semantic Kernel Enterprise Agent Started ---")
    while True:
        user_input = input("User: ")
        if user_input == "exit": break
        
        history.add_user_message(user_input)
        
        # G. The Loop (Kernel handles parsing, tool calling, and response)
        result = await kernel.invoke_prompt(
            prompt=None,
            chat_history=history,
            settings=settings
        )
        
        print(f"Agent: {result}")
        history.add_assistant_message(str(result))

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

Appendix: Best Practices

  • Prompt Engineering for Agents: Don't just describe inputs. Describe the purpose of the function. "Use this to calculate tax" is better than "Takes a number."
  • Security: Never use exec() based plugins in production. Strictly type your inputs.
  • Async Design: Agent tasks can be slow. Build your UX to handle asynchronous updates.