Let's Create Our First Agent Using .NET
In the previous section we wrote an Agent by hand and saw how much manual work that is. Now we will build the same thing with a library that does the heavy lifting for us.
In this lesson you will create your first real Agent: Alfred, who runs entirely on your computer and can call C# methods to answer questions.
What is Microsoft.Extensions.AI?
Section titled “What is Microsoft.Extensions.AI?”Microsoft.Extensions.AI is Microsoft’s library for working with LLMs in .NET. It is small, but it covers exactly the things we wrote ourselves in the previous lesson:
| In the Dummy Agent we wrote it ourselves | With Microsoft.Extensions.AI |
|---|---|
Ollama.cs: requests to the model with HttpClient |
IChatClient: one interface for every LLM provider |
ChatMessage.cs: our own record for one message |
ChatMessage: a ready-made type with the same name |
| A tool description written by hand into the system prompt | AIFunctionFactory: makes a tool out of an ordinary C# method |
Tools.Run and the while loop |
UseFunctionInvocation(): runs the whole cycle automatically |
There is also one difference under the surface. Our dummy Agent was a JSON Agent: we asked the model to write Action: {...} in its text and parsed it ourselves. The library uses function calling (see Actions): the model returns the tool call as structured data, so there is no format to get wrong.
Let’s build Alfred
Section titled “Let’s build Alfred”We will need Ollama and the model from the previous lesson. Make sure Ollama is running before you start.
1. Create the project
Section titled “1. Create the project”Open a terminal and run:
dotnet new console -n FirstAgentcd FirstAgentdotnet add package Microsoft.Extensions.AIdotnet add package OllamaSharpThe first two commands create the project, and the other two add the two NuGet packages. OllamaSharp is the client for Ollama. It implements the IChatClient interface, so the rest of the code does not depend on Ollama. If you ever switch model providers, you change one line.
Open the FirstAgent folder in your editor. We will work with three files.
2. Write the tool
Section titled “2. Write the tool”Create a file AlfredTools.cs. It holds Alfred’s only tool: a method that returns the current time in a given timezone.
using System.ComponentModel;
public static class AlfredTools{ [Description("Gets the current local time in a given timezone.")] public static string GetCurrentTime( [Description("An IANA timezone name, for example 'Europe/Sofia'.")] string timezone) { try { TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(timezone); DateTime now = TimeZoneInfo.ConvertTime(DateTime.UtcNow, zone);
return $"The current time in {timezone} is {now:yyyy-MM-dd HH:mm:ss}"; } catch (TimeZoneNotFoundException) { return $"Unknown timezone '{timezone}'."; } }}Three things make a method a good tool:
- A
[Description]attribute on the method and on every parameter. That is everything the model knows about the tool (see Tools). - Clear types for the parameters and the result.
- Errors are returned as text. If the model passes an invalid timezone, it will read the message as an Observation and may try again.
3. Create the Agent
Section titled “3. Create the Agent”Open Program.cs, delete its contents and put the following:
using Microsoft.Extensions.AI;using OllamaSharp;
const string Model = "qwen3.5:9b";
const string SystemPrompt = "You are Alfred, a helpful and polite AI assistant. " + "Use your tools when they can help and never invent their results.";
IChatClient model = new OllamaApiClient(new Uri("http://localhost:11434"), Model);
IChatClient agent = new ChatClientBuilder(model) .UseFunctionInvocation() .Build();
var options = new ChatOptions{ Temperature = 0, Tools = [AIFunctionFactory.Create(AlfredTools.GetCurrentTime)],};
List<ChatMessage> history =[ new(ChatRole.System, SystemPrompt), new(ChatRole.User, "What time is it in Tokyo right now?"),];
ChatResponse response = await agent.GetResponseAsync(history, options);
AgentLog.PrintSteps(response);Console.WriteLine($"Alfred: {response.Text}");The code has four parts:
- The model.
OllamaApiClientis the connection to the local model, exposed through theIChatClientinterface. It takes the place of ourOllama.cs. - The Agent.
UseFunctionInvocation()wraps the model and adds the Thought → Action → Observation loop. When the model asks for a tool, the library calls it, returns the result and asks the model again, until it gives a final answer. To keep it from spinning forever, the loop has a limit of 40 rounds per request. - Tools.
AIFunctionFactory.Createreads the method name, the[Description]attributes and the parameter types and passes them to the model. - The conversation.
historyis the list of messages: the system prompt and one question.GetResponseAsyncsends it to the Agent and returns the answer.
The line AgentLog.PrintSteps(response) does not compile yet, because that method does not exist. We will write it in the next step.
4. Show the cycle steps
Section titled “4. Show the cycle steps”The library runs the whole cycle inside GetResponseAsync. To see what happened, we will print the steps the Agent went through. Create a file AgentLog.cs:
using System.Text.Json;using Microsoft.Extensions.AI;
public static class AgentLog{ public static void PrintSteps(ChatResponse response) { foreach (AIContent content in response.Messages.SelectMany(m => m.Contents)) { switch (content) { case TextReasoningContent thought: Console.WriteLine($" Thought: {thought.Text}"); break; case FunctionCallContent call: string arguments = JsonSerializer.Serialize(call.Arguments); Console.WriteLine($" Action: {call.Name}({arguments})"); break; case FunctionResultContent result: Console.WriteLine($" Observation: {result.Result}"); break; } } }}response.Messages holds everything the Agent produced on the way to the answer. Each step of the cycle arrives as a separate .NET type:
| Step | Type in Microsoft.Extensions.AI |
|---|---|
| Thought | TextReasoningContent |
| Action | FunctionCallContent |
| Observation | FunctionResultContent |
| Final answer | TextContent (also available via response.Text) |
5. Start the program
Section titled “5. Start the program”In the terminal, in the FirstAgent folder, run:
dotnet runoutput:
Thought: The user is asking for the current time in Tokyo. I need to use the GetCurrentTime function with the appropriate timezone for Tokyo. Tokyo is in the Asia/Tokyo timezone. Action: GetCurrentTime({"timezone":"Asia/Tokyo"}) Observation: The current time in Asia/Tokyo is 2026-11-12 20:32:31 Thought: The function returned the current time in Tokyo as 2026-11-12 20:32:31. I should present this information clearly to the user.Alfred: The current time in Tokyo is November 12, 2026 at 8:32:31 PM (20:32:31).On your machine the time and the exact words will differ, but the steps will be the same. Here is what happened inside GetResponseAsync:
- The library sent the conversation to the model together with the description of
GetCurrentTime. - The model asked for the tool with the argument
Asia/Tokyo. That is theActionline. - The library called the real method
AlfredTools.GetCurrentTimeand returned the result to the model. That is theObservationline. - The model read the result and wrote the final answer.
This is the same cycle as in the Dummy Agent Library, but in our code there is no stop, no parsing and no while.
6. Turn it into a conversation
Section titled “6. Turn it into a conversation”For now Alfred answers one question written in the code. In Program.cs replace everything from the List<ChatMessage> history line downward with the following:
List<ChatMessage> history = [new(ChatRole.System, SystemPrompt)];
Console.WriteLine("Alfred is ready. Ask a question (or press Enter to exit).");
while (true){ Console.Write("\nYou: "); string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) { break; }
history.Add(new ChatMessage(ChatRole.User, input));
ChatResponse response = await agent.GetResponseAsync(history, options); history.AddMessages(response);
AgentLog.PrintSteps(response); Console.WriteLine($"Alfred: {response.Text}");}This while is not the Agent’s loop. It only reads questions from the console. The important lines are the two with history: the model remembers nothing between requests, so we append every question and everything the Agent answered to the list.
Run dotnet run and ask Alfred three questions:
Alfred is ready. Ask a question (or press Enter to exit).
You: What time is it in Tokyo right now? Thought: The user is asking for the current time in Tokyo. I need to use the GetCurrentTime function with the appropriate timezone for Tokyo. Tokyo is in the Asia/Tokyo timezone. Action: GetCurrentTime({"timezone":"Asia/Tokyo"}) Observation: The current time in Asia/Tokyo is 2026-11-12 20:33:16 Thought: The function returned the current time in Tokyo. I should present this information clearly to the user.Alfred: The current time in Tokyo is 8:33 PM on November 12, 2026 (20:33:16).
You: And in Sofia? Thought: The user is asking for the current time in Sofia. I need to use the GetCurrentTime function with the timezone "Europe/Sofia" since Sofia is in Bulgaria which uses the Europe/Sofia timezone. Action: GetCurrentTime({"timezone":"Europe/Sofia"}) Observation: The current time in Europe/Sofia is 2026-11-12 13:33:43 Thought: The function returned the current time in Sofia as 2026-11-12 13:33:43. I should present this information clearly to the user, converting it to a more readable format if needed.Alfred: The current time in Sofia is 1:33 PM on November 12, 2026 (13:33:43).
You: Hello, who are you? Thought: The user is asking me who I am. I should introduce myself as Alfred, a helpful and polite AI assistant. This doesn't require any tool calls - it's just a conversational response.Alfred: Hello! I'm Alfred, your helpful and polite AI assistant. How can I help you today?Notice two things:
- The second question is only “And in Sofia?”. Alfred understands it because together with it he receives the whole conversation from
history. - The third question needs no tool, so Alfred answers directly, without an Action. The model decides for itself when to use tools.
Your turn
Section titled “Your turn”Alfred has only one tool. The best way to learn is to give him more. To start, add the Calculator method from the Tools section to the AlfredTools class:
[Description("Multiply two integers.")]public static int Calculator(int a, int b) => a * b;and register it in Program.cs:
Tools =[ AIFunctionFactory.Create(AlfredTools.GetCurrentTime), AIFunctionFactory.Create(AlfredTools.Calculator),],Ask “What is 123 times 456?” and you will see Action: Calculator({"a":123,"b":456}), followed by Observation: 56088.
After that try your own ideas: a tool that reads a file, searches the web with HttpClient, or checks the weather through a public API.
Congratulations, you built your first Agent! 🎉
It is normal at the start for it to be a little slow or to make mistakes. In the next Units we will learn how to build better Agents.
In the next section the final quiz for Unit 1 awaits you.