
Semantic Kernel
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
semantic-kernel is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- semantic-kernel
- AI & Agent Building
- AI-coding skill
Semantic Kernel by the numbers
- 17 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,861 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill semantic-kernelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Semantic Kernel for .NET
Trigger On
- adding AI-driven prompts, plugins, or orchestration to a .NET app
- reviewing kernel construction, service registration, or plugin usage
- building function-calling patterns with LLMs
- migrating older Semantic Kernel code to current APIs
Documentation
- Semantic Kernel Overview
- Plugins and Functions
- Agent Functions
- GitHub Repository
- Microsoft Agent Framework
References
- patterns.md - Plugin patterns, function calling patterns, multi-agent patterns, prompt templates, and RAG patterns
- anti-patterns.md - Common Semantic Kernel mistakes and how to avoid them
Core Concepts
| Concept | Description |
|---|---|
| Kernel | Central orchestrator for AI services and plugins |
| Plugin | Collection of functions exposed to the LLM |
| Function | Native C# method or prompt template |
| Chat Completion | LLM service for generating responses |
| Memory | Vector storage for semantic search |
Workflow
1. Build the Kernel with required services 2. Create Plugins with well-described functions 3. Configure Function Calling for automatic tool use 4. Handle Responses and manage conversation state 5. Test and Observe AI behavior with logging 6. For Semantic Kernel dotnet-1.77.0 and later, keep OpenAPI plugin server URL validation enabled by default unless a trusted migration path requires a temporary exception, and use the updated Microsoft Agent Framework 1.0-compatible migration samples when moving SK agent code to Agent Framework.
Kernel Setup
Basic Configuration
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4",
endpoint: config["AzureOpenAI:Endpoint"]!,
apiKey: config["AzureOpenAI:ApiKey"]!);
// Or OpenAI
builder.AddOpenAIChatCompletion(
modelId: "gpt-4",
apiKey: config["OpenAI:ApiKey"]!);
var kernel = builder.Build();With Dependency Injection
builder.Services.AddKernel()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4",
endpoint: config["AzureOpenAI:Endpoint"]!,
apiKey: config["AzureOpenAI:ApiKey"]!);
// Register plugins
builder.Services.AddSingleton<WeatherPlugin>();
builder.Services.AddSingleton<OrderPlugin>();
// In your service
public class AiService(Kernel kernel)
{
public async Task<string> ChatAsync(string message)
{
var response = await kernel.InvokePromptAsync(message);
return response.ToString();
}
}Plugin Patterns
Creating a Plugin
public class WeatherPlugin
{
[KernelFunction]
[Description("Gets the current weather for a specified city")]
public async Task<string> GetWeather(
[Description("The city name, e.g., 'Seattle'")] string city,
[Description("Temperature unit: 'celsius' or 'fahrenheit'")] string unit = "celsius")
{
// Call actual weather API
var weather = await _weatherService.GetCurrentAsync(city);
return $"Weather in {city}: {weather.Temperature}° {unit}, {weather.Condition}";
}
[KernelFunction]
[Description("Gets the weather forecast for the next N days")]
public async Task<string> GetForecast(
[Description("The city name")] string city,
[Description("Number of days (1-7)")] int days = 3)
{
var forecast = await _weatherService.GetForecastAsync(city, days);
return FormatForecast(forecast);
}
}Plugin Best Practices
| Practice | Why It Matters |
|---|---|
Clear [Description] | LLM uses this to decide when to call |
| Specific parameter names | Helps LLM map user intent |
| Idempotent functions | Safe to retry on failures |
| Return meaningful strings | LLM needs to understand results |
| Validate inputs | LLM may hallucinate parameters |
Function Calling
Automatic Function Calling
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
kernel.Plugins.AddFromObject(new WeatherPlugin(), "Weather");
kernel.Plugins.AddFromObject(new OrderPlugin(), "Orders");
var result = await kernel.InvokePromptAsync(
"What's the weather in Seattle and do I have any pending orders?",
new KernelArguments(settings));Manual Function Selection
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Required(
[kernel.Plugins["Weather"]["GetWeather"]])
};Chat Completion Patterns
Multi-Turn Conversation
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddSystemMessage("You are a helpful assistant.");
history.AddUserMessage(userMessage);
var response = await chatService.GetChatMessageContentAsync(
history,
executionSettings: new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
},
kernel: kernel);
history.AddAssistantMessage(response.Content!);Streaming Response
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
history, executionSettings, kernel))
{
Console.Write(chunk.Content);
}Multi-Agent Plugin Isolation
// WRONG - agents share plugins
var sharedKernel = Kernel.CreateBuilder().Build();
sharedKernel.Plugins.AddFromObject(new AllPlugins());
var agent1 = new ChatCompletionAgent { Kernel = sharedKernel };
var agent2 = new ChatCompletionAgent { Kernel = sharedKernel };
// Both agents have same plugins!
// CORRECT - isolated kernels
var kernel1 = CreateKernelForAgent1();
kernel1.Plugins.AddFromObject(new WeatherPlugin());
var kernel2 = CreateKernelForAgent2();
kernel2.Plugins.AddFromObject(new OrderPlugin());
var agent1 = new ChatCompletionAgent { Kernel = kernel1 };
var agent2 = new ChatCompletionAgent { Kernel = kernel2 };Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
Vague [Description] | LLM won't call at right time | Be specific and actionable |
| Sharing kernel across agents | Plugin leakage | Clone or create new kernels |
| No input validation | Hallucinated parameters | Validate and return errors |
| Using deprecated Planners | Removed in favor of function calling | Use FunctionChoiceBehavior |
| Ignoring logging | Can't debug AI decisions | Enable Semantic Kernel logging |
Error Handling
[KernelFunction]
[Description("Places an order for a product")]
public async Task<string> PlaceOrder(
[Description("Product ID")] string productId,
[Description("Quantity (1-100)")] int quantity)
{
// Validate inputs
if (string.IsNullOrEmpty(productId))
return "Error: Product ID is required";
if (quantity < 1 || quantity > 100)
return "Error: Quantity must be between 1 and 100";
try
{
var order = await _orderService.CreateAsync(productId, quantity);
return $"Order {order.Id} placed successfully for {quantity} units";
}
catch (ProductNotFoundException)
{
return $"Error: Product '{productId}' not found";
}
}Testing Plugins
[Fact]
public async Task GetWeather_ReturnsFormattedWeather()
{
var mockWeatherService = new Mock<IWeatherService>();
mockWeatherService.Setup(w => w.GetCurrentAsync("Seattle"))
.ReturnsAsync(new Weather { Temperature = 20, Condition = "Sunny" });
var plugin = new WeatherPlugin(mockWeatherService.Object);
var result = await plugin.GetWeather("Seattle", "celsius");
Assert.Contains("20°", result);
Assert.Contains("Sunny", result);
}Microsoft Agent Framework
For complex multi-agent scenarios, consider microsoft-agent-framework:
- Multi-agent orchestration
- Agent-to-agent communication
- Enterprise patterns
Deliver
- kernel setup with clear service and plugin composition
- AI features that fit naturally into the existing .NET app
- observable and testable function-calling behavior
- proper plugin isolation for multi-agent scenarios
Validate
- plugins have clear, specific descriptions
- function calling works as expected
- AI flows are logged and debuggable
- input validation prevents hallucination issues
- kernel instances are properly scoped
- deprecated APIs are not used
{
"version": "1.1.0",
"category": "AI",
"package_prefix": "Microsoft.SemanticKernel"
}
Semantic Kernel Anti-Patterns
Plugin Anti-Patterns
Vague Function Descriptions
The LLM relies on descriptions to decide when to call functions. Vague descriptions lead to incorrect or missed function calls.
// BAD: Vague description
[KernelFunction]
[Description("Does something with orders")]
public async Task<string> ProcessOrder(string orderId) { ... }
// GOOD: Specific, actionable description
[KernelFunction]
[Description("Retrieves the current status and details of an order by its order ID")]
public async Task<string> GetOrderStatus(
[Description("The unique order identifier, e.g., 'ORD-12345'")] string orderId) { ... }Missing Parameter Descriptions
Without parameter descriptions, the LLM guesses parameter formats and values.
// BAD: No parameter descriptions
[KernelFunction]
[Description("Searches for products")]
public async Task<string> SearchProducts(string query, int limit, string category) { ... }
// GOOD: Clear parameter descriptions with examples and constraints
[KernelFunction]
[Description("Searches the product catalog")]
public async Task<string> SearchProducts(
[Description("Search keywords, e.g., 'wireless headphones'")] string query,
[Description("Maximum results to return (1-50, default 10)")] int limit = 10,
[Description("Product category filter: electronics, clothing, home, or 'all'")] string category = "all") { ... }Stateful Plugin Classes
Mutable state in plugins causes race conditions and unpredictable behavior.
// BAD: Mutable state
public class CartPlugin
{
private List<CartItem> _items = new(); // Shared across all calls!
[KernelFunction]
public string AddItem(string productId, int quantity)
{
_items.Add(new CartItem(productId, quantity));
return "Added to cart";
}
}
// GOOD: Inject state management
public class CartPlugin
{
private readonly ICartService _cartService;
public CartPlugin(ICartService cartService) => _cartService = cartService;
[KernelFunction]
[Description("Adds a product to the user's shopping cart")]
public async Task<string> AddItem(
Kernel kernel,
[Description("Product ID")] string productId,
[Description("Quantity (1-99)")] int quantity)
{
var userId = kernel.Data["userId"]?.ToString();
if (userId is null) return "Error: User not authenticated";
await _cartService.AddItemAsync(userId, productId, quantity);
return $"Added {quantity} x {productId} to your cart";
}
}Functions That Return Raw Objects
The LLM cannot interpret complex objects; always return formatted strings.
// BAD: Returns complex object
[KernelFunction]
public async Task<Order> GetOrder(string orderId)
{
return await _orderService.GetAsync(orderId);
}
// GOOD: Returns formatted, interpretable string
[KernelFunction]
[Description("Gets order details including status, items, and total")]
public async Task<string> GetOrder(
[Description("Order ID")] string orderId)
{
var order = await _orderService.GetAsync(orderId);
if (order is null) return $"Order {orderId} not found";
var items = string.Join("\n", order.Items.Select(i => $" - {i.Name} x{i.Quantity}: ${i.Price:F2}"));
return $"""
Order: {order.Id}
Status: {order.Status}
Date: {order.CreatedAt:yyyy-MM-dd}
Items:
{items}
Total: ${order.Total:F2}
""";
}No Input Validation
The LLM may hallucinate parameter values. Always validate inputs.
// BAD: No validation
[KernelFunction]
public async Task<string> TransferFunds(string fromAccount, string toAccount, decimal amount)
{
await _bankService.TransferAsync(fromAccount, toAccount, amount);
return "Transfer complete";
}
// GOOD: Comprehensive validation
[KernelFunction]
[Description("Transfers funds between accounts")]
public async Task<string> TransferFunds(
[Description("Source account number")] string fromAccount,
[Description("Destination account number")] string toAccount,
[Description("Amount to transfer (positive number)")] decimal amount)
{
// Validate account format
if (!AccountNumber.TryParse(fromAccount, out _))
return $"Invalid source account format: {fromAccount}";
if (!AccountNumber.TryParse(toAccount, out _))
return $"Invalid destination account format: {toAccount}";
if (fromAccount == toAccount)
return "Source and destination accounts must be different";
// Validate amount
if (amount <= 0)
return "Transfer amount must be positive";
if (amount > 10000)
return "Transfer amount exceeds single-transaction limit of $10,000";
try
{
var result = await _bankService.TransferAsync(fromAccount, toAccount, amount);
return $"Transferred ${amount:F2} from {fromAccount} to {toAccount}. Reference: {result.ReferenceId}";
}
catch (InsufficientFundsException)
{
return "Transfer failed: Insufficient funds in source account";
}
}Kernel Anti-Patterns
Shared Kernel Across Agents
Sharing a kernel instance causes plugin leakage between agents.
// BAD: Shared kernel
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(...)
.Build();
kernel.Plugins.AddFromObject(new AdminPlugin()); // All agents get admin!
var supportAgent = new ChatCompletionAgent { Kernel = kernel };
var salesAgent = new ChatCompletionAgent { Kernel = kernel };
// GOOD: Isolated kernels per agent
Kernel CreateAgentKernel(AgentRole role)
{
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(...)
.Build();
switch (role)
{
case AgentRole.Support:
kernel.Plugins.AddFromObject(new SupportPlugin());
break;
case AgentRole.Sales:
kernel.Plugins.AddFromObject(new SalesPlugin());
break;
}
return kernel;
}Not Disposing Kernels
Kernels hold resources that should be cleaned up, especially in request-scoped scenarios.
// BAD: Kernel never disposed
public class ChatController
{
public async Task<string> Chat(string message)
{
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(...)
.Build();
return (await kernel.InvokePromptAsync(message)).ToString();
}
}
// GOOD: Use DI with proper scoping
public class ChatController
{
private readonly Kernel _kernel;
public ChatController(Kernel kernel) => _kernel = kernel;
public async Task<string> Chat(string message)
{
return (await _kernel.InvokePromptAsync(message)).ToString();
}
}
// Registration
services.AddKernel()
.AddAzureOpenAIChatCompletion(...);Hardcoded API Keys
Never hardcode credentials in code.
// BAD: Hardcoded credentials
builder.AddAzureOpenAIChatCompletion(
"gpt-4",
"https://myresource.openai.azure.com",
"sk-abc123xyz..."); // Exposed in source control!
// GOOD: Configuration-based
builder.AddAzureOpenAIChatCompletion(
config["AzureOpenAI:DeploymentName"]!,
config["AzureOpenAI:Endpoint"]!,
new DefaultAzureCredential()); // Or use Key VaultFunction Calling Anti-Patterns
Using Deprecated Planners
The old Planner APIs have been removed. Use FunctionChoiceBehavior instead.
// BAD: Deprecated planner (removed in SK 1.x)
var planner = new StepwisePlanner(kernel);
var plan = await planner.CreatePlanAsync("...");
// GOOD: Modern function calling
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
await kernel.InvokePromptAsync("...", new KernelArguments(settings));Not Handling Function Call Loops
Without limits, function calling can loop indefinitely.
// BAD: Unbounded function calling
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
// GOOD: Set maximum iterations
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
options: new FunctionChoiceBehaviorOptions
{
AllowConcurrentInvocation = false,
AllowParallelCalls = true
})
};
// Also handle in chat loop
var maxIterations = 10;
var iteration = 0;
while (iteration++ < maxIterations)
{
var response = await chatService.GetChatMessageContentAsync(history, settings, kernel);
if (!response.Items.OfType<FunctionCallContent>().Any())
break;
// Process function calls...
}Exposing Dangerous Functions Without Safeguards
Some functions need additional authorization checks.
// BAD: Dangerous function without safeguards
[KernelFunction]
[Description("Deletes a user account")]
public async Task<string> DeleteUser(string userId)
{
await _userService.DeleteAsync(userId);
return "User deleted";
}
// GOOD: Safeguards and confirmation
[KernelFunction]
[Description("Initiates user account deletion - requires confirmation")]
public async Task<string> RequestUserDeletion(
Kernel kernel,
[Description("User ID to delete")] string userId)
{
var currentUser = kernel.Data["currentUserId"]?.ToString();
// Can only delete own account via this function
if (userId != currentUser)
return "You can only request deletion of your own account";
// Create pending deletion, don't actually delete
var token = await _userService.CreateDeletionRequestAsync(userId);
return $"Deletion request created. Confirm within 24 hours using token: {token}";
}Chat and Conversation Anti-Patterns
Unbounded Chat History
Chat history grows unbounded, causing token limits and cost issues.
// BAD: Never truncates history
public class ChatService
{
private readonly ChatHistory _history = new();
public async Task<string> Chat(string message)
{
_history.AddUserMessage(message);
var response = await _chatService.GetChatMessageContentAsync(_history);
_history.AddAssistantMessage(response.Content!);
return response.Content!;
}
}
// GOOD: Managed history with truncation
public class ChatService
{
private const int MaxHistoryMessages = 20;
private readonly ChatHistory _history = new();
public async Task<string> Chat(string message)
{
_history.AddUserMessage(message);
// Truncate old messages (keep system message)
while (_history.Count > MaxHistoryMessages + 1)
{
var firstNonSystem = _history.Skip(1).First();
_history.Remove(firstNonSystem);
}
var response = await _chatService.GetChatMessageContentAsync(_history);
_history.AddAssistantMessage(response.Content!);
return response.Content!;
}
}Ignoring System Messages
System messages set behavior expectations. Missing them causes inconsistent responses.
// BAD: No system message
var history = new ChatHistory();
history.AddUserMessage("Help me with my order");
// GOOD: Clear system context
var history = new ChatHistory();
history.AddSystemMessage("""
You are a helpful customer service agent for Contoso Electronics.
You can help with: order status, returns, product questions.
You cannot: process payments, access financial data, make promises about pricing.
Always be polite and concise.
""");
history.AddUserMessage("Help me with my order");Not Handling Streaming Errors
Streaming responses can fail mid-stream.
// BAD: No error handling for streaming
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(history))
{
Console.Write(chunk.Content);
}
// GOOD: Proper error handling
var fullResponse = new StringBuilder();
try
{
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(history))
{
fullResponse.Append(chunk.Content);
await outputStream.WriteAsync(chunk.Content);
}
history.AddAssistantMessage(fullResponse.ToString());
}
catch (HttpOperationException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
await outputStream.WriteAsync("\n[Rate limited - please try again shortly]");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Streaming failed after: {PartialResponse}", fullResponse.ToString());
throw;
}Testing Anti-Patterns
Testing Against Live LLM
Unit tests should not depend on external LLM services.
// BAD: Tests hit live API
[Fact]
public async Task Chat_ReturnsResponse()
{
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion("gpt-4", Environment.GetEnvironmentVariable("OPENAI_KEY")!)
.Build();
var response = await kernel.InvokePromptAsync("Say hello");
Assert.NotEmpty(response.ToString());
}
// GOOD: Mock the chat service
[Fact]
public async Task Plugin_ReturnsExpectedFormat()
{
var mockWeather = new Mock<IWeatherService>();
mockWeather.Setup(w => w.GetCurrentAsync("Seattle"))
.ReturnsAsync(new WeatherData { Temp = 55, Condition = "Cloudy" });
var plugin = new WeatherPlugin(mockWeather.Object);
var result = await plugin.GetWeather("Seattle", "fahrenheit");
Assert.Contains("55", result);
Assert.Contains("Cloudy", result);
}Testing Only Happy Paths
Test error conditions and edge cases.
// Test error handling
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData("invalid-id-format")]
public async Task GetOrder_InvalidId_ReturnsError(string orderId)
{
var plugin = new OrderPlugin(_mockOrderService.Object);
var result = await plugin.GetOrder(orderId);
Assert.StartsWith("Error:", result);
}
[Fact]
public async Task GetOrder_ServiceThrows_ReturnsGracefulError()
{
_mockOrderService.Setup(s => s.GetAsync(It.IsAny<string>()))
.ThrowsAsync(new TimeoutException());
var plugin = new OrderPlugin(_mockOrderService.Object);
var result = await plugin.GetOrder("ORD-123");
Assert.Contains("temporarily unavailable", result.ToLower());
}Observability Anti-Patterns
No Logging
Without logging, debugging AI behavior is impossible.
// BAD: No visibility
await kernel.InvokePromptAsync(prompt);
// GOOD: Comprehensive logging
builder.Services.AddLogging(logging =>
{
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Debug);
logging.AddFilter("Microsoft.SemanticKernel", LogLevel.Debug);
});
// Log function invocations
kernel.FunctionInvocationFilters.Add(new LoggingFilter(_logger));
public class LoggingFilter : IFunctionInvocationFilter
{
private readonly ILogger _logger;
public LoggingFilter(ILogger logger) => _logger = logger;
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
_logger.LogInformation(
"Invoking {Plugin}.{Function} with args: {Args}",
context.Function.PluginName,
context.Function.Name,
JsonSerializer.Serialize(context.Arguments));
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
_logger.LogInformation(
"Completed {Plugin}.{Function} in {Elapsed}ms. Result: {Result}",
context.Function.PluginName,
context.Function.Name,
sw.ElapsedMilliseconds,
context.Result?.ToString()?.Truncate(200));
}
}Not Tracking Token Usage
Token usage impacts cost and can hit limits unexpectedly.
// GOOD: Track token usage
var response = await chatService.GetChatMessageContentAsync(history, settings, kernel);
if (response.Metadata?.TryGetValue("Usage", out var usage) == true)
{
var usageData = usage as ChatCompletionUsage;
_telemetry.TrackMetric("PromptTokens", usageData?.PromptTokens ?? 0);
_telemetry.TrackMetric("CompletionTokens", usageData?.CompletionTokens ?? 0);
_telemetry.TrackMetric("TotalTokens", usageData?.TotalTokens ?? 0);
}Semantic Kernel Patterns
Plugin Patterns
Single Responsibility Plugin
Each plugin should focus on one domain or capability.
// Good: focused plugin
public class InventoryPlugin
{
private readonly IInventoryService _inventory;
public InventoryPlugin(IInventoryService inventory) => _inventory = inventory;
[KernelFunction]
[Description("Checks if a product is in stock")]
public async Task<string> CheckStock(
[Description("Product SKU")] string sku)
{
var quantity = await _inventory.GetQuantityAsync(sku);
return quantity > 0
? $"Product {sku} is in stock ({quantity} available)"
: $"Product {sku} is out of stock";
}
[KernelFunction]
[Description("Reserves inventory for an order")]
public async Task<string> ReserveStock(
[Description("Product SKU")] string sku,
[Description("Quantity to reserve")] int quantity)
{
var result = await _inventory.ReserveAsync(sku, quantity);
return result.Success
? $"Reserved {quantity} units of {sku}"
: $"Failed to reserve: {result.Reason}";
}
}Stateless Plugin Design
Keep plugins stateless; inject dependencies for external state.
// Good: stateless with injected dependencies
public class CustomerPlugin
{
private readonly ICustomerRepository _customers;
private readonly ILogger<CustomerPlugin> _logger;
public CustomerPlugin(ICustomerRepository customers, ILogger<CustomerPlugin> logger)
{
_customers = customers;
_logger = logger;
}
[KernelFunction]
[Description("Looks up customer information by email")]
public async Task<string> LookupCustomer(
[Description("Customer email address")] string email)
{
_logger.LogInformation("Looking up customer: {Email}", email);
var customer = await _customers.FindByEmailAsync(email);
return customer is not null
? $"Customer: {customer.Name}, ID: {customer.Id}, Status: {customer.Status}"
: "Customer not found";
}
}Contextual Plugin with Kernel Arguments
Pass runtime context through KernelArguments.
public class AuthorizedPlugin
{
[KernelFunction]
[Description("Gets user-specific data")]
public async Task<string> GetUserData(
Kernel kernel,
[Description("Data type to retrieve")] string dataType)
{
// Access context from kernel arguments
if (!kernel.Data.TryGetValue("userId", out var userId))
return "Error: User context not available";
var data = await _dataService.GetAsync(userId.ToString()!, dataType);
return data ?? "No data found";
}
}
// Usage
var args = new KernelArguments
{
["userId"] = currentUser.Id
};
await kernel.InvokePromptAsync("Get my recent orders", args);Function Calling Patterns
Conditional Function Exposure
Expose different functions based on context.
public class ConditionalPluginBuilder
{
public void ConfigureKernel(Kernel kernel, UserContext context)
{
// Always available
kernel.Plugins.AddFromObject(new InfoPlugin(), "Info");
// Role-based exposure
if (context.Roles.Contains("Admin"))
{
kernel.Plugins.AddFromObject(new AdminPlugin(), "Admin");
}
if (context.Roles.Contains("Support"))
{
kernel.Plugins.AddFromObject(new SupportPlugin(), "Support");
}
// Feature-flag based
if (_featureFlags.IsEnabled("BetaFeatures"))
{
kernel.Plugins.AddFromObject(new BetaPlugin(), "Beta");
}
}
}Required Function Forcing
Force specific function calls when needed.
// Force weather lookup for weather-related queries
public async Task<string> HandleWeatherQuery(Kernel kernel, string query)
{
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Required(
[kernel.Plugins["Weather"]["GetCurrentWeather"]])
};
var result = await kernel.InvokePromptAsync(query, new KernelArguments(settings));
return result.ToString();
}Function Result Post-Processing
Process function results before returning to conversation.
public class ProcessingPlugin
{
[KernelFunction]
[Description("Searches products and formats results")]
public async Task<string> SearchProducts(
[Description("Search query")] string query,
[Description("Maximum results (1-20)")] int maxResults = 5)
{
var products = await _productService.SearchAsync(query, maxResults);
if (!products.Any())
return "No products found matching your search.";
var sb = new StringBuilder();
sb.AppendLine($"Found {products.Count} products:");
foreach (var p in products)
{
sb.AppendLine($"- {p.Name} (${p.Price:F2}) - {p.ShortDescription}");
}
return sb.ToString();
}
}Multi-Agent Patterns
Kernel Factory for Agent Isolation
Create isolated kernels per agent to prevent plugin leakage.
public class AgentKernelFactory
{
private readonly IConfiguration _config;
private readonly IServiceProvider _services;
public AgentKernelFactory(IConfiguration config, IServiceProvider services)
{
_config = config;
_services = services;
}
public Kernel CreateForRole(AgentRole role)
{
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
_config["AzureOpenAI:DeploymentName"]!,
_config["AzureOpenAI:Endpoint"]!,
_config["AzureOpenAI:ApiKey"]!);
var kernel = builder.Build();
// Role-specific plugins
switch (role)
{
case AgentRole.CustomerSupport:
kernel.Plugins.AddFromObject(
_services.GetRequiredService<CustomerPlugin>(), "Customer");
kernel.Plugins.AddFromObject(
_services.GetRequiredService<TicketPlugin>(), "Tickets");
break;
case AgentRole.Sales:
kernel.Plugins.AddFromObject(
_services.GetRequiredService<ProductPlugin>(), "Products");
kernel.Plugins.AddFromObject(
_services.GetRequiredService<PricingPlugin>(), "Pricing");
break;
case AgentRole.TechnicalSupport:
kernel.Plugins.AddFromObject(
_services.GetRequiredService<DiagnosticsPlugin>(), "Diagnostics");
kernel.Plugins.AddFromObject(
_services.GetRequiredService<KnowledgeBasePlugin>(), "KB");
break;
}
return kernel;
}
}Agent Handoff Pattern
Coordinate between specialized agents.
public class AgentRouter
{
private readonly Dictionary<string, ChatCompletionAgent> _agents;
private readonly Kernel _routingKernel;
public async Task<AgentResponse> RouteQuery(string userMessage)
{
// Determine best agent
var classification = await ClassifyIntent(userMessage);
if (!_agents.TryGetValue(classification.AgentType, out var agent))
{
agent = _agents["general"];
}
// Execute with selected agent
var history = new ChatHistory();
history.AddUserMessage(userMessage);
var response = await agent.InvokeAsync(history);
return new AgentResponse
{
Agent = classification.AgentType,
Content = response.Content,
Confidence = classification.Confidence
};
}
private async Task<IntentClassification> ClassifyIntent(string message)
{
var result = await _routingKernel.InvokePromptAsync(
$"Classify this message into one of: support, sales, technical, general. Message: {message}");
// Parse and return classification
return IntentClassification.Parse(result.ToString());
}
}Shared Memory with Isolated Execution
Share memory across agents while keeping function execution isolated.
public class MultiAgentOrchestrator
{
private readonly ISemanticTextMemory _sharedMemory;
private readonly AgentKernelFactory _kernelFactory;
public async Task<string> ProcessWithContext(
string query,
AgentRole role,
string conversationId)
{
// Retrieve relevant context from shared memory
var memories = await _sharedMemory.SearchAsync(
collection: conversationId,
query: query,
limit: 5);
// Create isolated kernel for this agent
var kernel = _kernelFactory.CreateForRole(role);
// Build context-aware prompt
var contextBuilder = new StringBuilder();
contextBuilder.AppendLine("Relevant context:");
foreach (var memory in memories)
{
contextBuilder.AppendLine($"- {memory.Metadata.Text}");
}
contextBuilder.AppendLine($"\nUser query: {query}");
// Execute with isolated plugins but shared context
var response = await kernel.InvokePromptAsync(contextBuilder.ToString());
// Store response in shared memory for other agents
await _sharedMemory.SaveInformationAsync(
collection: conversationId,
text: response.ToString(),
id: Guid.NewGuid().ToString());
return response.ToString();
}
}Agent Group Chat Pattern
Coordinate multiple agents in a group conversation.
public class AgentGroupChat
{
private readonly List<ChatCompletionAgent> _agents;
private readonly ChatHistory _sharedHistory;
private readonly ITerminationStrategy _termination;
public async IAsyncEnumerable<AgentMessage> RunAsync(string initialPrompt)
{
_sharedHistory.AddUserMessage(initialPrompt);
var turnCount = 0;
while (!await _termination.ShouldTerminateAsync(_sharedHistory, turnCount))
{
foreach (var agent in _agents)
{
var response = await agent.InvokeAsync(_sharedHistory);
_sharedHistory.AddMessage(new ChatMessageContent(
AuthorRole.Assistant,
response.Content)
{
AuthorName = agent.Name
});
yield return new AgentMessage
{
AgentName = agent.Name,
Content = response.Content
};
if (await _termination.ShouldTerminateAsync(_sharedHistory, turnCount))
break;
}
turnCount++;
}
}
}Prompt Template Patterns
Parameterized Templates
Use Handlebars or Prompty templates for reusable prompts.
// prompts/analyze-sentiment.prompty
/*
---
name: AnalyzeSentiment
description: Analyzes text sentiment
authors:
- Team
model:
api: chat
parameters:
temperature: 0.3
---
system:
You are a sentiment analysis assistant. Analyze the sentiment of the given text.
Respond with: positive, negative, or neutral, followed by a confidence score.
user:
Text to analyze: {{$text}}
*/
// Usage
var function = kernel.CreateFunctionFromPromptyFile("prompts/analyze-sentiment.prompty");
var result = await kernel.InvokeAsync(function, new() { ["text"] = customerFeedback });Template Composition
Compose complex prompts from smaller templates.
public class PromptComposer
{
private readonly Kernel _kernel;
public async Task<string> ComposeAnalysis(AnalysisRequest request)
{
// Step 1: Summarize
var summary = await _kernel.InvokeAsync(
_kernel.Plugins["Prompts"]["Summarize"],
new() { ["content"] = request.Content });
// Step 2: Extract entities
var entities = await _kernel.InvokeAsync(
_kernel.Plugins["Prompts"]["ExtractEntities"],
new() { ["content"] = request.Content });
// Step 3: Final analysis combining results
var analysis = await _kernel.InvokeAsync(
_kernel.Plugins["Prompts"]["FinalAnalysis"],
new()
{
["summary"] = summary.ToString(),
["entities"] = entities.ToString(),
["originalContent"] = request.Content
});
return analysis.ToString();
}
}Memory and RAG Patterns
Scoped Memory Collections
Organize memory by scope for better retrieval.
public class ScopedMemoryService
{
private readonly ISemanticTextMemory _memory;
public async Task SaveAsync(string scope, string content, Dictionary<string, string> metadata)
{
var collection = $"{scope}-knowledge";
await _memory.SaveInformationAsync(
collection: collection,
text: content,
id: Guid.NewGuid().ToString(),
additionalMetadata: string.Join(";", metadata.Select(kv => $"{kv.Key}={kv.Value}")));
}
public async Task<IEnumerable<string>> SearchAsync(string scope, string query, int limit = 5)
{
var collection = $"{scope}-knowledge";
var results = await _memory.SearchAsync(collection, query, limit);
return results.Select(r => r.Metadata.Text);
}
}
// Usage
await memoryService.SaveAsync("product", productDocs, new() { ["category"] = "electronics" });
var context = await memoryService.SearchAsync("product", "wireless headphones");RAG-Enhanced Function
Combine retrieval with function execution.
public class RagEnhancedPlugin
{
private readonly ISemanticTextMemory _memory;
private readonly string _collection;
[KernelFunction]
[Description("Answers questions using the knowledge base")]
public async Task<string> AnswerFromKnowledge(
Kernel kernel,
[Description("The user's question")] string question)
{
// Retrieve relevant documents
var results = await _memory.SearchAsync(_collection, question, limit: 3);
var context = string.Join("\n\n", results.Select(r => r.Metadata.Text));
if (string.IsNullOrEmpty(context))
return "I don't have information about that in my knowledge base.";
// Generate answer with context
var prompt = $"""
Answer the question based only on the following context:
{context}
Question: {question}
If the context doesn't contain enough information, say so.
""";
var answer = await kernel.InvokePromptAsync(prompt);
return answer.ToString();
}
}