Vision
Setup
-
Create project
mkdir Vision
cd Vision
dotnet new console --framework net9.0 -
Add packages
dotnet add package Azure.AI.Inference --prerelease
dotnet add package Azure.AI.OpenAI --prerelease
dotnet add package Microsoft.Extensions.AI --prerelease
dotnet add package Microsoft.Extensions.AI.AzureAIInference --prerelease
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Hosting -
Open Code in Rider
rider . -
DI Setup
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Azure;
using Azure.AI.Inference;
using ChatRole = Microsoft.Extensions.AI.ChatRole;
var hostBuilder = Host.CreateApplicationBuilder(args);
hostBuilder.Configuration.AddUserSecrets<Program>();
var client = ChatClientFactory.CreateLlama4GitHubModelChatClient(hostBuilder.Configuration);
// Setup DI
hostBuilder.Services
.AddChatClient(client)
.UseFunctionInvocation()
// .UseLogging()
;
hostBuilder.Services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Trace);
});
var app = hostBuilder.Build();
var chatClient = app.Services.GetRequiredService<IChatClient>(); -
Create a Client Factory
public static class ChatClientFactory
{
public static IChatClient CreateLlama4GitHubModelChatClient(IConfiguration configuration)
{
var endpoint = new Uri("https://models.github.ai/inference");
var model = "meta/Llama-4-Maverick-17B-128E-Instruct-FP8";
var token = configuration["gitHubToken"] ?? throw new InvalidOperationException("Token is not set in configuration.");
var credential = new AzureKeyCredential(token);
var client = new ChatCompletionsClient(
endpoint,
credential
);
return client.AsIChatClient(model);
}
public static IChatClient CreateLlama4FoundryChatClient(IConfiguration configuration)
{
var endpoint = new Uri("https://dotnet-superpowers-aif.services.ai.azure.com/models");
var model = "Llama-4-Maverick-17B-128E-Instruct-FP8";
var token = configuration["foundryToken"] ?? throw new InvalidOperationException("Token is not set in configuration.");
var credential = new AzureKeyCredential(token);
var client = new ChatCompletionsClient(
endpoint,
credential
);
return client.AsIChatClient(model);
}
}
Creating a factory method allows you to easily switch between different models or providers (e.g. Ollama, GitHub Models, Azure AI Foundary, or Open AI) in the future without changing the main application logic.
GitHub Model
Switch back to slides and show the "two options" to getting an LLM token.
-
Navigate to the GitHub Fine-Grained Tokens and create a token with
models:readscope. -
Add this to secrets
{
"gitHubToken: "YOUR_GITHUB_TOKEN"
} -
Navigate to models and show 'Use this model'
Add Images
-
Create
Imagesfoldermkdir Images -
Copy images
cp -r /Users/daniel/Code/Work/SSW/dotnet-9-superpowers-2025/07-AI/dotnet-ai-building-blocks/Vision/images/* ./Images/ -
Set the image properties in Rider to 'Copy always'
<ItemGroup>
<None Update="Images\image1.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Images\image2.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Images\image3.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Images\image4.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Images\image5.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
Basic Image Recognition
-
Add the following code to
Program.cs// Multi-Modality (images)
var message = new ChatMessage(ChatRole.User, "What is in this image?");
var dir = Path.Combine(AppContext.BaseDirectory, "images");
var file = Directory.GetFiles(dir, "*.jpg").OrderBy(name => name).First();
byte[] imageBytes = File.ReadAllBytes(file);
message.Contents.Add(new DataContent(imageBytes, "image/jpeg"));
var response = await chatClient.GetResponseAsync([message]);
Console.WriteLine($"Response: {response.Text}");
Webcam Surf Report
-
Comment out what we just added
-
Extract information from camera images
// Surf Report
var dir = Path.Combine(AppContext.BaseDirectory, "images");
var files = Directory.GetFiles(dir, "*.jpg").OrderBy(f => f);
foreach (var file in files.Take(1))
{
Console.WriteLine($"Processing file: {file}");
var name = Path.GetFileNameWithoutExtension(file);
var message = new ChatMessage(ChatRole.User, $"""
Extract information from this camera image {name}.
If you detect waves that can be surfed, the wave conditions are excellent.
If you cannot detect any waves, the wave conditions are poor.
If you detect any dangerous conditions, please report the reason.
""");
var bytes = File.ReadAllBytes(file);
message.Contents.Add(new DataContent(bytes, "image/jpeg"));
// Text response
var response = await chatClient.GetResponseAsync([message]);
Console.WriteLine(response.Text);
} -
Run the application
-
Create
SurfCamResultclass SurfCamResult
{
public WaveConditions Status { get; set; }
public int SurferCount { get; set; }
public string DangerReason { get; set; } = "";
public enum WaveConditions
{
Excellent,
Good,
Fair,
Poor
}
} -
Replace the Text Response with a Structured Response
// Structured Response
var response = await chatClient.GetResponseAsync<SurfCamResult>([message]);
if (response.TryGetResult(out var result))
{
Console.WriteLine($"Wave Conditions: {result.Status}, Surfer Count: {result.SurferCount}, Danger Reason: {result.DangerReason}");
Console.WriteLine("");
} -
Remove the
Take(1)from the loop -
Run the application again