One of the most powerful features of Semantic Kernel is the ability to nest kernel functions - calling one function from within another. This technique allows developers to create complex workflows by breaking down tasks into smaller, reusable units.
In this article, we'll build a Smart Shopping Assistant that demonstrates function nesting through a practical e-commerce scenario.
What is Function Nesting?
Function nesting involves calling one kernel function from within another. This creates a hierarchical structure where:
- Higher-level functions orchestrate the workflow
- Lower-level functions perform specific tasks
- The Kernel is passed as a parameter to enable cross-plugin communication
+---------------------------+
| ShoppingAssistantPlugin | <-- Orchestrator (calls other plugins)
+---------------------------+
|
+---> PricingPlugin.CalculateDiscount()
|
+---> InventoryPlugin.CheckStock()
|
+---> PricingPlugin.CalculateFinalPrice()
Benefits of Function Nesting
| Benefit Description |
| Modularity | Each plugin handles a specific domain |
| Reusability | Functions can be used independently or composed |
| Testability | Individual plugins can be tested in isolation |
| Maintainability | Changes in one plugin don't affect others |
| Clarity | Complex logic is broken into understandable pieces |
The Scenario: Smart Shopping Assistant
Our shopping assistant will:
- Check product inventory
- Determine customer discount based on membership level
- Calculate the final price
- Process the order
Implementation
Step 1: Define the Inventory Plugin
public class InventoryPlugin
{
private readonly Dictionary<string, int> _stock = new()
{
["laptop"] = 15,
["phone"] = 50,
["tablet"] = 8,
["headphones"] = 100
};
[KernelFunction("check_stock")]
[Description("Checks if a product is available in stock")]
public StockResult CheckStock(string productName, int quantity)
{
Console.WriteLine($"[Inventory] Checking stock for: {productName}");
var normalizedName = productName.ToLower();
if (!_stock.TryGetValue(normalizedName, out int available))
{
return new StockResult(false, 0, "Product not found");
}
bool isAvailable = available >= quantity;
string message = isAvailable
? $"Available: {available} units in stock"
: $"Insufficient stock: only {available} units available";
return new StockResult(isAvailable, available, message);
}
}
public record StockResult(bool IsAvailable, int AvailableQuantity, string Message);
Step 2: Define the Pricing Plugin
public class PricingPlugin
{
private readonly Dictionary<string, decimal> _prices = new()
{
["laptop"] = 999.99m,
["phone"] = 699.99m,
["tablet"] = 449.99m,
["headphones"] = 149.99m
};
[KernelFunction("get_base_price")]
[Description("Gets the base price for a product")]
public decimal GetBasePrice(string productName)
{
Console.WriteLine($"[Pricing] Getting base price for: {productName}");
return _prices.TryGetValue(productName.ToLower(), out decimal price)
? price
: 0m;
}
[KernelFunction("calculate_discount")]
[Description("Calculates discount percentage based on membership level")]
public int CalculateDiscount(string membershipLevel)
{
Console.WriteLine($"[Pricing] Calculating discount for: {membershipLevel}");
return membershipLevel.ToLower() switch
{
"gold" => 20,
"silver" => 10,
"bronze" => 5,
_ => 0
};
}
[KernelFunction("calculate_final_price")]
[Description("Calculates the final price after discount")]
public decimal CalculateFinalPrice(decimal basePrice, int quantity, int discountPercent)
{
var subtotal = basePrice * quantity;
var discount = subtotal * discountPercent / 100;
var finalPrice = subtotal - discount;
Console.WriteLine($"[Pricing] Subtotal: ${subtotal}, Discount: ${discount}, Final: ${finalPrice}");
return finalPrice;
}
}
Step 3: Define the Shopping Assistant Plugin (Orchestrator)
This is where function nesting happens - the orchestrator calls functions from other plugins:
public class ShoppingAssistantPlugin
{
[KernelFunction("process_order")]
[Description("Processes a complete order by checking inventory, applying discounts, and calculating final price")]
public async Task<string> ProcessOrder(
Kernel kernel,
string productName,
int quantity,
string membershipLevel)
{
Console.WriteLine($"\n{'=',-50}");
Console.WriteLine($"Processing order: {quantity}x {productName} for {membershipLevel} member");
Console.WriteLine($"{'=',-50}\n");
// Step 1: Check inventory (NESTED CALL)
var stockResult = await kernel.InvokeAsync<StockResult>(
nameof(InventoryPlugin),
"check_stock",
new KernelArguments
{
["productName"] = productName,
["quantity"] = quantity
});
if (!stockResult.IsAvailable)
{
return $"""
ORDER FAILED
Product: {productName}
Reason: {stockResult.Message}
""";
}
// Step 2: Get base price (NESTED CALL)
var basePrice = await kernel.InvokeAsync<decimal>(
nameof(PricingPlugin),
"get_base_price",
new KernelArguments { ["productName"] = productName });
// Step 3: Calculate discount (NESTED CALL)
var discountPercent = await kernel.InvokeAsync<int>(
nameof(PricingPlugin),
"calculate_discount",
new KernelArguments { ["membershipLevel"] = membershipLevel });
// Step 4: Calculate final price (NESTED CALL)
var finalPrice = await kernel.InvokeAsync<decimal>(
nameof(PricingPlugin),
"calculate_final_price",
new KernelArguments
{
["basePrice"] = basePrice,
["quantity"] = quantity,
["discountPercent"] = discountPercent
});
return $"""
ORDER CONFIRMED
================
Product: {productName}
Quantity: {quantity}
Unit Price: ${basePrice}
Membership: {membershipLevel}
Discount: {discountPercent}%
Final Price: ${finalPrice}
Stock Status: {stockResult.Message}
""";
}
}
Step 4: Register Plugins and Execute
using Microsoft.SemanticKernel;
var kernel = Kernel.CreateBuilder().Build();
// Register all plugins
kernel.ImportPluginFromType<InventoryPlugin>();
kernel.ImportPluginFromType<PricingPlugin>();
kernel.ImportPluginFromType<ShoppingAssistantPlugin>();
// Process an order - this single call triggers multiple nested function calls
var result = await kernel.InvokeAsync<string>(
nameof(ShoppingAssistantPlugin),
"process_order",
new KernelArguments
{
["productName"] = "laptop",
["quantity"] = 2,
["membershipLevel"] = "gold"
});
Console.WriteLine(result);
Execution Flow
When ProcessOrder is called, the following nested calls occur:
ProcessOrder("laptop", 2, "gold")
|
+---> InventoryPlugin.CheckStock("laptop", 2)
| Returns: StockResult(true, 15, "Available...")
|
+---> PricingPlugin.GetBasePrice("laptop")
| Returns: 999.99
|
+---> PricingPlugin.CalculateDiscount("gold")
| Returns: 20
|
+---> PricingPlugin.CalculateFinalPrice(999.99, 2, 20)
Returns: 1599.98
Expected Output
==================================================
Processing order: 2x laptop for gold member
==================================================
[Inventory] Checking stock for: laptop
[Pricing] Getting base price for: laptop
[Pricing] Calculating discount for: gold
[Pricing] Subtotal: $1999.98, Discount: $399.996, Final: $1599.984
ORDER CONFIRMED
================
Product: laptop
Quantity: 2
Unit Price: $999.99
Membership: gold
Discount: 20%
Final Price: $1599.984
Stock Status: Available: 15 units in stock
Key Takeaways
- Kernel as Parameter: The orchestrator receives
Kernel as a parameter to call other functions - Plugin Isolation: Each plugin handles its own domain (inventory, pricing)
- Composition: Complex workflows are built by composing simple functions
- Type Safety: Return types are strongly typed using generics (
InvokeAsync<T>)
Static vs Dynamic Orchestration
| AspectStatic (This Example)Dynamic (AI-Driven) |
| Flow Control | Developer-defined | AI decides |
| Predictability | Deterministic | Non-deterministic |
| Use Case | Business rules | Natural language |
| Function Calls | Explicit in code | Auto-selected by LLM |
Function nesting is a form of Static AI Orchestration (manual planning) where the developer explicitly defines which functions to call and when. This provides full control over the execution flow.
When to Use Function Nesting
- Business workflows with defined steps
- Order processing pipelines
- Data validation chains
- Report generation
- Any scenario requiring predictable, deterministic execution