Building AI Agents with Semantic Kernel and .NET 8
The .NET ecosystem has quietly become a strong platform for building AI-powered applications, and Semantic Kernel sits at the center of that story. Developed by Microsoft, Semantic Kernel is an open-source SDK that lets you integrate large language models into your .NET applications with native support for function calling, plugins, and agentic workflows.
With .NET 8 bringing performance improvements and Semantic Kernel reaching a mature state, it is a good time for C# developers to start building AI agents. I will walk through building an agent that can reason about tasks, call functions automatically, and maintain conversational context.
What is Semantic Kernel?
Semantic Kernel is an orchestration SDK that sits between your application code and LLM providers like OpenAI, Azure OpenAI, or Hugging Face models. It provides abstractions for prompt management, function calling, memory, and planning.
Unlike simple API wrappers, Semantic Kernel treats LLM interactions as a first-class part of your application architecture, integrating naturally with dependency injection, configuration, and the patterns .NET developers already know.
Setting up a .NET 8 project
Create a new console application and add the required packages:
dotnet new console -n SemanticKernelAgent
cd SemanticKernelAgent
dotnet add package Microsoft.SemanticKernel --version 1.18.1
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
Initialize user secrets for your API key:
dotnet user-secrets init
dotnet user-secrets set "OpenAI:ApiKey" "your-api-key-here"
Building the kernel
The Kernel is the central object in Semantic Kernel. It manages AI services, plugins, and execution:
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
var apiKey = configuration["OpenAI:ApiKey"]
?? throw new InvalidOperationException("OpenAI API key not configured.");
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion("gpt-4o", apiKey);
var kernel = builder.Build();
Creating plugins with native functions
Plugins are how you give your agent capabilities beyond text generation. In Semantic Kernel, a plugin is simply a class with methods decorated with the KernelFunction attribute:
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class WeatherPlugin
{
[KernelFunction("get_current_weather")]
[Description("Gets the current weather for a specified city")]
public string GetCurrentWeather(
[Description("The city name, e.g. 'London'")] string city)
{
// In production, this would call a real weather API
var weatherData = new Dictionary<string, (int Temp, string Condition)>
{
["London"] = (18, "Partly cloudy"),
["New York"] = (25, "Sunny"),
["Tokyo"] = (30, "Humid"),
["Sydney"] = (14, "Rainy"),
};
if (weatherData.TryGetValue(city, out var data))
{
return quot;Weather in {city}: {data.Temp}C, {data.Condition}";
}
return quot;Weather data not available for {city}";
}
}
public class DateTimePlugin
{
[KernelFunction("get_current_datetime")]
[Description("Gets the current date and time")]
public string GetCurrentDateTime()
{
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss (dddd)");
}
[KernelFunction("get_days_until")]
[Description("Calculates the number of days until a specified date")]
public string GetDaysUntil(
[Description("Target date in yyyy-MM-dd format")] string targetDate)
{
if (DateTime.TryParse(targetDate, out var target))
{
var days = (target - DateTime.Today).Days;
return days >= 0
? quot;{days} days until {targetDate}"
: quot;{targetDate} was {Math.Abs(days)} days ago";
}
return "Invalid date format. Please use yyyy-MM-dd.";
}
}
The Description attributes are essential. The LLM reads these descriptions to decide when and how to call each function. Clear, specific descriptions lead to more accurate function invocation.
Registering plugins and enabling auto-invocation
Register your plugins with the kernel and configure automatic function calling:
kernel.Plugins.AddFromType<WeatherPlugin>();
kernel.Plugins.AddFromType<DateTimePlugin>();
var executionSettings = new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
Temperature = 0.7,
MaxTokens = 1024,
};
The AutoInvokeKernelFunctions setting is what makes this an agent rather than a simple chatbot. When the LLM determines it needs information from a plugin, Semantic Kernel automatically calls the appropriate function and feeds the result back to the LLM, which then incorporates it into its response. This loop can happen multiple times in a single interaction.
Building a conversational agent loop
Now bring everything together in a conversational loop that maintains chat history:
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage(
"You are a helpful assistant with access to weather data and date/time " +
"utilities. Use your available tools when the user asks questions that " +
"require real-time information. Be concise and helpful.");
Console.WriteLine("AI Agent ready. Type 'exit' to quit.\n");
while (true)
{
Console.Write("You: ");
var userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput) ||
userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
chatHistory.AddUserMessage(userInput);
var response = await chatCompletionService.GetChatMessageContentAsync(
chatHistory,
executionSettings,
kernel);
chatHistory.AddAssistantMessage(response.Content ?? string.Empty);
Console.WriteLine(quot;Agent: {response.Content}\n");
}
When a user asks "What is the weather like in London and how many days until Christmas?", the agent will automatically call both get_current_weather and get_days_until, receive the results, and compose a natural response incorporating both pieces of information.
Adding a more complex plugin
Let us add a plugin that demonstrates how agents can work with structured data:
public class TaskManagerPlugin
{
private readonly List<TaskItem> _tasks = [];
[KernelFunction("add_task")]
[Description("Adds a new task to the task list")]
public string AddTask(
[Description("The task description")] string description,
[Description("Priority: low, medium, or high")] string priority = "medium")
{
var task = new TaskItem
{
Id = _tasks.Count + 1,
Description = description,
Priority = priority,
CreatedAt = DateTime.Now,
IsCompleted = false,
};
_tasks.Add(task);
return quot;Task #{task.Id} added: '{description}' (priority: {priority})";
}
[KernelFunction("list_tasks")]
[Description("Lists all current tasks with their status")]
public string ListTasks()
{
if (_tasks.Count == 0) return "No tasks found.";
var taskList = _tasks.Select(t =>
quot;#{t.Id} [{(t.IsCompleted ? "Done" : t.Priority)}] {t.Description}");
return string.Join("\n", taskList);
}
[KernelFunction("complete_task")]
[Description("Marks a task as completed by its ID")]
public string CompleteTask(
[Description("The task ID number")] int taskId)
{
var task = _tasks.FirstOrDefault(t => t.Id == taskId);
if (task is null) return quot;Task #{taskId} not found.";
task.IsCompleted = true;
return quot;Task #{taskId} marked as completed: '{task.Description}'";
}
}
public class TaskItem
{
public int Id { get; set; }
public required string Description { get; set; }
public string Priority { get; set; } = "medium";
public DateTime CreatedAt { get; set; }
public bool IsCompleted { get; set; }
}
Register this as a singleton so state persists across the conversation:
var taskManager = new TaskManagerPlugin();
kernel.Plugins.AddFromObject(taskManager, "TaskManager");
Now the agent can manage a task list through natural conversation: "Add a high priority task to review the pull request" or "Show me all my tasks" or "Mark task 2 as done."
Handling errors gracefully
In production, you should handle cases where function calls fail or the LLM produces unexpected results:
try
{
var response = await chatCompletionService.GetChatMessageContentAsync(
chatHistory,
executionSettings,
kernel);
chatHistory.AddAssistantMessage(response.Content ?? string.Empty);
Console.WriteLine(quot;Agent: {response.Content}\n");
}
catch (HttpOperationException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
Console.WriteLine("Rate limited. Please wait a moment and try again.\n");
}
catch (Exception ex)
{
Console.WriteLine(quot;An error occurred: {ex.Message}\n");
}
Conclusion
Semantic Kernel brings AI agent capabilities to .NET in a way that feels natural for C# developers. The plugin model maps directly to familiar patterns -- classes and methods become tools the agent can invoke. Auto-invocation handles the loop of LLM reasoning and function calling. And because it is built on .NET 8, you get the performance and type safety the platform is known for. Start with simple plugins, observe how the LLM decides to use them, and gradually build more capable agents as you gain confidence in the patterns.