The Semantic Kernel Masterclass
From Script to System. A "Zero to Hero" guide to building enterprise-grade agents with Microsoft's Semantic Kernel.
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.
1. The Kernel Object
The Kernel is your central unit of work. You initialize it once and attach everything
your agent needs to it.
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)
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.
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))
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.
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")
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.
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)
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
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.
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.