Skip to main content

Vision

Setup

  1. Create project

    mkdir Vision
    cd Vision
    dotnet new console --framework net9.0
  2. 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
  3. Open Code in Rider

    rider .
  4. 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>();
  5. 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);
    }
    }
info

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

Presenter

Switch back to slides and show the "two options" to getting an LLM token.

  1. Navigate to the GitHub Fine-Grained Tokens and create a token with models:read scope.

  2. Add this to secrets

    {
    "gitHubToken: "YOUR_GITHUB_TOKEN"
    }
  3. Navigate to models and show 'Use this model'

Add Images

  1. Create Images folder

    mkdir Images
  2. Copy images

    cp -r /Users/daniel/Code/Work/SSW/dotnet-9-superpowers-2025/07-AI/dotnet-ai-building-blocks/Vision/images/* ./Images/
  3. 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

  1. 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

  1. Comment out what we just added

  2. 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);
    }
  3. Run the application

  4. Create SurfCamResult

    class SurfCamResult
    {
    public WaveConditions Status { get; set; }
    public int SurferCount { get; set; }
    public string DangerReason { get; set; } = "";

    public enum WaveConditions
    {
    Excellent,
    Good,
    Fair,
    Poor
    }
    }
  5. 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("");
    }
  6. Remove the Take(1) from the loop

  7. Run the application again