Generative AI for Beginners Generative AI Techniques Created: 26 Mar 2026 Updated: 26 Mar 2026

Text Completions and Chat Conversations with Microsoft.Extensions.AI

In this lesson you will learn the fundamentals of interacting with AI models in .NET using the Microsoft.Extensions.AI library. We start with the simplest possible interaction — a single text completion — and build up to a fully conversational chat application that remembers context across turns.

Part 1 — Text Completion (One-Shot Interaction)

A text completion is a fire-and-forget request: you send one prompt and receive one response. No conversation history is maintained.

This pattern is ideal for tasks such as:

  1. Summarization
  2. Classification
  3. One-time text transformations

Creating the Client

First, create an IChatClient. This is the unified abstraction that works with any supported AI provider (OpenAI, Azure OpenAI, Ollama, etc.).

IChatClient chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
CallPurpose
new OpenAIClient(apiKey)Opens a connection to OpenAI
.GetChatClient("gpt-4o-mini")Selects the deployment / model
.AsIChatClient()Wraps it behind the standard IChatClient interface

Simple Completion

var singleResponse = await chatClient.GetResponseAsync(
"List three benefits of reading books regularly in one sentence each.");
Console.WriteLine(singleResponse.Text);

Practical Example — Book Genre Classification

Instead of writing classification logic ourselves, we describe the task in natural language and let the model do the work:

StringBuilder classifyPrompt = new();
classifyPrompt.AppendLine("Classify each book description into a genre. Output: Number, Genre, One-word reason.");
classifyPrompt.AppendLine();
classifyPrompt.AppendLine("Book 1: A detective in 1920s Chicago hunts a serial killer while battling his own demons.");
classifyPrompt.AppendLine("Book 2: Two astronauts stranded on Mars must engineer an escape using only salvaged parts.");
classifyPrompt.AppendLine("Book 3: A young baker discovers her grandmother's recipes hold the key to an ancient family curse.");

var classifyResponse = await chatClient.GetResponseAsync(classifyPrompt.ToString());
Console.WriteLine(classifyResponse.Text);

Expected output:

Book 1: Mystery/Noir — Detective
Book 2: Science-Fiction — Survival
Book 3: Fantasy — Curse

Part 2 — From Completion to Conversation

The Problem: AI Has No Memory

Every call to GetResponseAsync is independent. The model does not retain anything from previous calls:

// First call
await chatClient.GetResponseAsync("My favorite genre is science fiction.");

// Second call — the model has already forgotten
var forgetResponse = await chatClient.GetResponseAsync("What is my favorite genre?");
// Response: "I don't know your favorite genre."

The Solution: Manage History Yourself

To give the AI memory, maintain a List<ChatMessage> and send the full history with every request:

List<ChatMessage> history = new();
history.Add(new ChatMessage(ChatRole.User, "My favorite genre is science fiction."));

var remember1 = await chatClient.GetResponseAsync(history);
history.AddMessages(remember1);

history.Add(new ChatMessage(ChatRole.User, "What is my favorite genre?"));
var remember2 = await chatClient.GetResponseAsync(history);
// Response: "Your favorite genre is science fiction."

The AddMessages extension method extracts the assistant's reply from the ChatResponse and appends it to the history list so subsequent requests include full context.

Visualizing the Flow

Request 1: [User: "My favorite genre is science fiction."]
Response 1: "Great choice! Science fiction is wonderful."

Request 2: [User: "My favorite genre is science fiction.",
Assistant: "Great choice! ...",
User: "What is my favorite genre?"]
Response 2: "Your favorite genre is science fiction."

Part 3 — The Three Chat Roles

Every message in a conversation carries one of three roles:

RoleSet ByPurpose
SystemDeveloperSets the AI's behavior, personality, and boundaries
UserEnd userQuestions, commands, or input
AssistantAI modelGenerated responses

The System Role — Your Control Mechanism

The system message is placed at the very start of the conversation. It runs once but influences every response that follows.

List<ChatMessage> librarianChat = new()
{
new ChatMessage(ChatRole.System,
"You are a knowledgeable librarian named Mira. " +
"Recommend books based on the reader's mood and preferences. " +
"Keep answers short — at most three sentences. " +
"If asked about non-book topics, politely steer the conversation back to books.")
};

What this system message achieves:

  1. Persona: "librarian named Mira"
  2. Task: "Recommend books based on mood"
  3. Style: "at most three sentences"
  4. Boundary: "steer back to books"
librarianChat.Add(new ChatMessage(ChatRole.User,
"I feel adventurous today. What should I read?"));
var librarianResponse = await chatClient.GetResponseAsync(librarianChat);
Console.WriteLine($"Mira: {librarianResponse.Text}");

// Follow-up — Mira remembers the adventurous mood
librarianChat.Add(new ChatMessage(ChatRole.User,
"Do you have something similar but set in space?"));
var followUp = await chatClient.GetResponseAsync(librarianChat);
Console.WriteLine($"Mira: {followUp.Text}");

Part 4 — Building a Complete Chat Application

Combining everything above, we can build an interactive book advisor in a simple while loop:

List<ChatMessage> conversation = new()
{
new ChatMessage(ChatRole.System,
"You are a friendly book advisor. Help users discover their next great read. " +
"Ask about their mood, preferred genres, and past favorites to give personalized suggestions. " +
"Be concise and enthusiastic.")
};

while (true)
{
Console.Write("You: ");
var input = Console.ReadLine();

if (string.IsNullOrWhiteSpace(input) ||
input.Equals("quit", StringComparison.OrdinalIgnoreCase))
break;

conversation.Add(new ChatMessage(ChatRole.User, input));

var chatResponse = await chatClient.GetResponseAsync(conversation);
conversation.AddMessages(chatResponse);

Console.WriteLine($"Advisor: {chatResponse.Text}\n");
}

Key points:

  1. The conversation list persists across the loop.
  2. Every user message is added before the API call.
  3. Every assistant response is added after the call via AddMessages.
  4. The system message shapes every response.

Summary

ConceptKey Takeaway
Text CompletionSingle prompt, single response. Great for one-off tasks like classification.
ConversationMaintain a List<ChatMessage> to give the AI memory.
System RoleControls behavior, persona, and boundaries. Set once, affects all responses.
User / Assistant RolesUser provides input; Assistant provides responses. Both stored in history.

Full Example

using System.Text;
using Microsoft.Extensions.AI;
using OpenAI;

namespace MicrosoftAgentFrameworkLesson.ConsoleApp;

public static class TextCompletionsAndChatDemo
{
public static async Task RunAsync()
{
// 1. Retrieve API key securely from environment
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine("Please set the OPEN_AI_KEY environment variable.");
return;
}

// 2. Create an IChatClient via Microsoft.Extensions.AI
IChatClient chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();

// ──────────────────────────────────────────────
// PART 1 — Text Completion (one-shot interaction)
// ──────────────────────────────────────────────
Console.WriteLine("═══ Part 1: Text Completion ═══\n");

var singleResponse = await chatClient.GetResponseAsync(
"List three benefits of reading books regularly in one sentence each.");
Console.WriteLine(singleResponse.Text);

// Practical example: classify book genres from descriptions
Console.WriteLine("\n═══ Part 1b: Book Genre Classification ═══\n");

StringBuilder classifyPrompt = new();
classifyPrompt.AppendLine("Classify each book description into a genre. Output: Number, Genre, One-word reason.");
classifyPrompt.AppendLine();
classifyPrompt.AppendLine("Book 1: A detective in 1920s Chicago hunts a serial killer while battling his own demons.");
classifyPrompt.AppendLine("Book 2: Two astronauts stranded on Mars must engineer an escape using only salvaged parts.");
classifyPrompt.AppendLine("Book 3: A young baker discovers her grandmother's recipes hold the key to an ancient family curse.");

var classifyResponse = await chatClient.GetResponseAsync(classifyPrompt.ToString());
Console.WriteLine(classifyResponse.Text);

// ──────────────────────────────────────────────
// PART 2 — Conversation (AI has no built-in memory)
// ──────────────────────────────────────────────
Console.WriteLine("\n═══ Part 2: Conversation Memory ═══\n");

// Without history — the model forgets
await chatClient.GetResponseAsync("My favorite genre is science fiction.");
var forgetResponse = await chatClient.GetResponseAsync("What is my favorite genre?");
Console.WriteLine($"Without history → {forgetResponse.Text}");

// With history — the model remembers
List<ChatMessage> history = new();
history.Add(new ChatMessage(ChatRole.User, "My favorite genre is science fiction."));

var remember1 = await chatClient.GetResponseAsync(history);
history.AddMessages(remember1);

history.Add(new ChatMessage(ChatRole.User, "What is my favorite genre?"));
var remember2 = await chatClient.GetResponseAsync(history);
history.AddMessages(remember2);
Console.WriteLine($"With history → {remember2.Text}");

// ──────────────────────────────────────────────
// PART 3 — The Three Chat Roles (System, User, Assistant)
// ──────────────────────────────────────────────
Console.WriteLine("\n═══ Part 3: System Role — Librarian Persona ═══\n");

List<ChatMessage> librarianChat = new()
{
new ChatMessage(ChatRole.System,
"You are a knowledgeable librarian named Mira. " +
"Recommend books based on the reader's mood and preferences. " +
"Keep answers short — at most three sentences. " +
"If asked about non-book topics, politely steer the conversation back to books.")
};

librarianChat.Add(new ChatMessage(ChatRole.User,
"I feel adventurous today. What should I read?"));

var librarianResponse = await chatClient.GetResponseAsync(librarianChat);
librarianChat.AddMessages(librarianResponse);
Console.WriteLine($"Mira: {librarianResponse.Text}");

// Follow-up — Mira remembers the mood
librarianChat.Add(new ChatMessage(ChatRole.User,
"Do you have something similar but set in space?"));

var followUp = await chatClient.GetResponseAsync(librarianChat);
Console.WriteLine($"Mira: {followUp.Text}");

// ──────────────────────────────────────────────
// PART 4 — Interactive Chat Loop
// ──────────────────────────────────────────────
Console.WriteLine("\n═══ Part 4: Interactive Book Advisor Chat ═══");
Console.WriteLine("(type 'quit' to exit)\n");

List<ChatMessage> conversation = new()
{
new ChatMessage(ChatRole.System,
"You are a friendly book advisor. Help users discover their next great read. " +
"Ask about their mood, preferred genres, and past favorites to give personalized suggestions. " +
"Be concise and enthusiastic.")
};

while (true)
{
Console.Write("You: ");
var input = Console.ReadLine();

if (string.IsNullOrWhiteSpace(input) ||
input.Equals("quit", StringComparison.OrdinalIgnoreCase))
break;

conversation.Add(new ChatMessage(ChatRole.User, input));

var chatResponse = await chatClient.GetResponseAsync(conversation);
conversation.AddMessages(chatResponse);

Console.WriteLine($"Advisor: {chatResponse.Text}\n");
}

Console.WriteLine("\nHappy reading!");
}
}


Share this lesson: