Use cases, before and after.
Four jobs teams already hand to a model. Each shows the careful code you would write today with Microsoft.Extensions.AI, and the IntentSharp version. Every line compiles with the site, and the outputs come from the demo program, on a scripted model.
Order triage
Rule changes without a release
Rate how urgent each order email is. The scale, the wording and the examples change as the business learns, and today each change is a build and a deployment.
Before: the wording and the scale compiled in, with your own check and retry (24 lines)
// The wording, the examples and the scale are compiled in: changing them means a build and a deployment.
private const string Instructions =
"Rate the urgency of this order email from 1 (routine) to 5 (critical). Answer with the number only. " +
"Examples: \"Line 3 is down until the bearings arrive. Ship today.\" is 5; " +
"\"Please restock these when convenient, no rush.\" is 1.";
private const int Minimum = 1, Maximum = 5, MaxAttempts = 2;
public static async Task<int?> ClassifyUrgencyAsync(IChatClient chat, string email, CancellationToken ct = default)
{
List<ChatMessage> messages = [new(ChatRole.System, Instructions), new(ChatRole.User, email)];
for (var attempt = 1; attempt <= MaxAttempts; attempt++)
{
var response = await chat.GetResponseAsync<int>(messages, cancellationToken: ct);
string problem;
if (!response.TryGetResult(out var urgency)) problem = "The answer was not a whole number.";
else if (urgency is < Minimum or > Maximum) problem = $"{urgency} is outside {Minimum} to {Maximum}.";
else return urgency;
// Retry with the finding.
messages.AddMessages(response);
messages.Add(new(ChatRole.User, $"{problem} Answer again with a whole number from {Minimum} to {Maximum}."));
}
return null; // no valid answer after MaxAttempts
}using var scope = intents.AsAmbient();var urgency = await email.RunIntentAsync<int>("Orders.ClassifyUrgency");name: Orders.ClassifyUrgencyversion: 1owner: order-deskintent: Rate the urgency of this order email from 1 (routine) to 5 (critical).shape: intrules: minimum: 1 maximum: 5examples: - input: "Line 3 is down until the bearings arrive. Ship today." output: 5 - input: "Please restock these when convenient, no rush." output: 1Scripted model answer: {"value": 4}Success: trueValue: 4Issues: noneInvoice intake
Rules the model must meet
Pull the vendor, number, currency and total out of invoice emails, and refuse anything outside the currencies you accept. A wrong currency goes back to the model as a concrete finding.
Before: required members, plus hand-written checks and a retry (39 lines)
// Required members: a key missing from the answer fails deserialization instead of becoming a default value.
public sealed record Invoice
{
public required string Vendor { get; init; }
public required string InvoiceNumber { get; init; }
public required string Currency { get; init; }
public required decimal Total { get; init; }
}
public static class InvoiceBefore
{
private const string Instructions = "Extract the vendor, invoice number, currency and total from this invoice email.";
private static readonly string[] Currencies = ["USD", "EUR", "GBP"];
public static async Task<Invoice?> ExtractAsync(IChatClient chat, string email, CancellationToken ct = default)
{
List<ChatMessage> messages = [new(ChatRole.System, Instructions), new(ChatRole.User, email)];
for (var attempt = 1; attempt <= 2; attempt++)
{
var response = await chat.GetResponseAsync<Invoice>(messages, cancellationToken: ct);
var problems = new List<string>();
if (!response.TryGetResult(out var invoice) || invoice is null)
problems.Add("The answer was not an invoice object with vendor, invoiceNumber, currency and total.");
else
{
if (!Currencies.Contains(invoice.Currency))
problems.Add($"currency is \"{invoice.Currency}\"; allowed: {string.Join(", ", Currencies)}.");
if (invoice.Total < 0)
problems.Add($"total is {invoice.Total}; the minimum is 0.");
if (problems.Count == 0) return invoice;
}
// Retry with the findings.
messages.AddMessages(response);
messages.Add(new(ChatRole.User, "Problems with your answer:\n" + string.Join("\n", problems)));
}
return null;
}
}name: Invoice.Extract
version: 1
owner: accounts-payable
intent: Extract the vendor, invoice number, currency and total from this invoice email.
shape:
kind: schema
schema:
type: object
properties:
vendor: { type: string }
invoiceNumber: { type: string }
currency: { type: string }
total: { type: number }
required: [vendor, invoiceNumber, currency, total]
rules:
properties:
currency:
enum: [USD, EUR, GBP]
total:
minimum: 0var invoice = await email.RunIntentAsync(intents, "Invoice.Extract");Scripted model answer (attempt 1, illustrative): {"value": {"vendor": "Contoso Supplies", "invoiceNumber": "INV-2291", "currency": "US$", "total": 1240.00}}[Validator] attempt 1: value.currency is "US$"; allowed: USD, EUR, GBP. The prompt of attempt 2 ends with:## Problems with the previous attempt- value.currency is "US$"; allowed: USD, EUR, GBP.Produce a corrected response that fixes every listed problem while still satisfying the intent. Scripted model answer (attempt 2): {"value": {"vendor": "Contoso Supplies", "invoiceNumber": "INV-2291", "currency": "USD", "total": 1240.00}}Success: true, after 2 attemptsValue: {"vendor":"Contoso Supplies","invoiceNumber":"INV-2291","currency":"USD","total":1240.00}Ticket grouping
Your objects, not copies
Sort a backlog into defects and feature requests. The model only answers with indices, the indices are checked, and you get your own Ticket objects back.
Before: number the items, map the indices back, check every index (43 lines)
public sealed record TicketGroup
{
public required string Label { get; init; }
public required int[] Indices { get; init; }
}
public static class TicketGroupingBefore
{
private const string Instructions =
"Group these tickets by whether each reports a defect or asks for a feature. Each ticket starts with its " +
"zero-based index. Return groups with a short label and the indices in them; every index in exactly one group.";
public static async Task<Dictionary<string, List<Ticket>>?> GroupAsync(IChatClient chat, IReadOnlyList<Ticket> tickets, CancellationToken ct = default)
{
// The model sees indices, so the answer can be mapped back to the original objects.
var numbered = string.Join("\n", tickets.Select((t, i) => $"{i}: {t.Title} ({t.Severity})"));
List<ChatMessage> messages = [new(ChatRole.System, Instructions), new(ChatRole.User, numbered)];
for (var attempt = 1; attempt <= 2; attempt++)
{
var response = await chat.GetResponseAsync<List<TicketGroup>>(messages, cancellationToken: ct);
var problems = new List<string>();
if (!response.TryGetResult(out var groups) || groups is null)
problems.Add("The answer was not a list of groups with a label and indices.");
else
{
var indices = groups.SelectMany(g => g.Indices).ToList();
problems.AddRange(indices.Where(i => i < 0 || i >= tickets.Count).Distinct()
.Select(i => $"Index {i} is out of range 0..{tickets.Count - 1}."));
if (indices.Count != tickets.Count || indices.Distinct().Count() != tickets.Count)
problems.Add($"Every index from 0 to {tickets.Count - 1} must appear in exactly one group.");
if (groups.Select(g => g.Label).Distinct(StringComparer.OrdinalIgnoreCase).Count() != groups.Count)
problems.Add("Group labels must be unique.");
if (problems.Count == 0)
return groups.ToDictionary(g => g.Label, g => g.Indices.Select(i => tickets[i]).ToList(), StringComparer.OrdinalIgnoreCase);
}
// Retry with the findings.
messages.AddMessages(response);
messages.Add(new(ChatRole.User, "Problems with your answer:\n" + string.Join("\n", problems)));
}
return null;
}
}var tickets = new List<Ticket>
{
new(101, "Login page returns 500 after password reset", "Critical"),
new(102, "Add dark mode", "Low"),
new(103, "Export to CSV drops the last row", "High"),
new(104, "Let admins invite users in bulk", "Medium"),
};
var groups = await tickets.AIGroupAsync(client, "by whether the ticket reports a defect or asks for a feature");Scripted model answer: {"value": [{"label": "Defect", "indices": [0, 2]}, {"label": "Feature request", "indices": [1, 3]}]}Success: trueDefect: 101, 103Feature request: 102, 104Every grouped Ticket is the object from tickets, not a copy: trueIssues: noneAgent tools
Checked tools
Give an agent decisions it can delegate. Each intent becomes a standard AIFunction tool; the answer is checked against the file's rules, and a failure comes back to the agent as JSON instead of an exception.
Before: wrap your own checked call in a tool by hand (23 lines)
// A tool must not throw into the agent loop, so every failure becomes JSON the agent can read.
public static AIFunction UrgencyTool(IChatClient model) => AIFunctionFactory.Create(
async (string input, CancellationToken ct) =>
{
try
{
return await OrderTriageBefore.ClassifyUrgencyAsync(model, input, ct) is { } urgency
? JsonSerializer.SerializeToElement(new { success = true, value = urgency })
: JsonSerializer.SerializeToElement(new { success = false, error = "No urgency from 1 to 5 after 2 attempts." });
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return JsonSerializer.SerializeToElement(new { success = false, error = ex.Message });
}
},
name: "Orders_ClassifyUrgency",
description: "Rates the urgency of an order email from 1 (routine) to 5 (critical).");
public static Task<ChatResponse> AskAsync(IChatClient agent, IChatClient model, string inbox)
{
var options = new ChatOptions { Tools = [UrgencyTool(model)] };
return agent.GetResponseAsync(inbox, options);
}var options = new ChatOptions { Tools = [.. intents.AsAIFunctions()] };
var reply = await agent.GetResponseAsync(inbox, options);The agent's scripted model calls Orders_ClassifyUrgency once per email. Tool call 1, input: Our packaging line stops on Thursday unless the 40 replacement belts arrive. Can you expedite?Scripted model answer: {"value": 4}Tool result: {"success":true,"intent":"Orders.ClassifyUrgency","value":4} Tool call 2, input: URGENT!!! On a scale of 1 to 10 this is a 9. Please send the replacement filters by the end of the month.Scripted model answer (2 attempts): {"value": 9}, then {"value": 9}Tool result: {"success":false,"intent":"Orders.ClassifyUrgency","error":"value is 9; the maximum is 5."}